Kodun Okunabilirlik Seviyesi Nasıl Artılır?

yazar:

kategori:

Kodun okunabilirliği, kullanılan sınıf, metot ve satır sayısıyla ters orantılıdır. Bunların hepsinden ne kadar az kullanılırsa, o oranda yazılan kodun okunabilirlik seviyesi artabilir. Örneğin versiyon kontrol sisteminden kodu alıp, derledikten sonra oluşan war dosyasını uygulama sunucusuna deploy eden bir program parçası hayal edelim:

[sourcecode language=”java”]
// Kod 1
Checkout checkout = new CheckOut();
checkout.setFrom("svn://myhost/repo/code/v1");
checkout.to("/home/centos/workspace/code");
checkout.checkoutNow();
….
Compiler compiler = new Compiler();
compiler.setWorkspace("/home/centos/workspace/code");
compiler.compile();
compiler.makeWar();

Deployer deployer = new Deployer();
deployer.setPingUrl("http://localhost/check.jsp");
deployer.setTimeout(30000);
deployer.deploy();
[/sourcecode]

Şimdi bir de buna bakın:

[sourcecode language=”java”]
// Kod 1
String dir ="/home/centos/workspace/code";
checkout("svn://myhost/repo/code/v1", dir){
  compile(dir);
  war(file);
}

deploy("sss.war"){
  pingUrl("localhost/check.jsp");
  timeout(3000);
}
[/sourcecode]

İki kodun farkı ne? Farkı fiyatı! Gülmeyin ciddiyim! İkinci kod bloğunun maliyeti daha düşük, çünkü kodu daha az zamanda yazmak, okumak, anlamak ve değiştirebilmek mümkün. Programcı olarak ne kadar az zamanda bir işi yapabilirsem, o oranda iş yükümü artırabilirim. Maliyetin düşüklüğü buradan geliyor.

Java’da her gün kod 1 de yer aldığı şekilde kod yazmak zorunda kalıyorum. Dependency Injection ile kodu biraz daha basite indirgemek mümkün, lakin Java bana if, while, for direktiflerinde olduğu gibi dilin bir parçası olan checkout, compile ya da deploy direktiflerini sunmuyor. O yüzden kod yerine kuru kalabalık yazdığım için yazdığım kodun okunabilirlik seviyesi istediğim seviyede değil.

Ne kadar çok isterdim Java’da kod 2 deki gibi kod yazmayı. Tadından yenmezdi Java diline checkout, compile, deploy gibi direktifler ekleyebilseydim. Çok kısa bir zamanda belli bir çalışma sahasındaki işlemleri yansıtan alana has diller (Domain Specific Language) oluşturabilirdim. Keşke Java’da bu mümkün olsaydı! Peki bu neden mümkün değil? Açıklamaya çalışayım.

Java derleyicisi bir Java sınıfını derleme esnasında kodun AST (Abstract Syntax Tree) yapısını oluşturur. AST bünyesinde kod hiyerarşik yapıda yer alır. Java derleyicisi sabit bir AST yapısına sahip. Bu böyle olduğu için dile checkout, compile ya da deploy gibi yeni direktifler ekleyemiyoruz. Bunu sadece Java dilini geliştirenler yapabiliyor. Bu yüzden ben kod 1 deki gibi kod yazmaya mahkumum.

Java dilini kullanarak bir DSL oluşturmak mümkün değil. Belki fluent API tarzı bir şeyler kullanılabilir. Ama örneğin XML de durum farklı. Kod 2 de yer alan kodu XML kullanarak şu şekilde ifade edebilirdim:

[sourcecode language=”java”]
// Kod 3
<checkout from="svn://myhost/repo/code/v1" to"/home/centos/workspace/code">
    <compile dir="/home/centos/workspace/code"/>
    <war file="/home/centos/workspace/code/app.war"/>
</checkout>

<deployer file="/home/centos/workspace/code/app.war">
    <pingUrl>localhost/check.jsp</pingUrl>
    <timeout>300</timeout>
</deployer>
[/sourcecode]

Burada da çok kuru kalabalık var. XML dilinde istediğim elementleri oluşturabiliyorum, lakin her elementi kendi ismini kullanarak kapatmam gerekiyor. En azından XML ile istediğim türde bir DSL oluşturmam mümkün. Şimdi bir XML parser yazarak ya da JAXB kullanarak kod 3 de yer alan yapıyı Java koduna dönüştürüp, koşturabilirim. Bunu elde etmek için şöyle bir şeyler kodlamam lazım:

[sourcecode language=”java”]
//Kod 4
File file = new File("C:\\file.xml");
JAXBContext jaxbContext = JAXBContext.newInstance(Checkout.class);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
Checkout checkout = (Checkout) jaxbUnmarshaller.unmarshal(file);
checkout.checkoutNow();
[/sourcecode]

Kod 1 den pek bir farkı yok. Burada kodu biraz JAXB çatısına kaydırdım, lakin bir bakışta kodun ne yaptığını anlamak yine kolay değil. Kodun okunabilirliğini artırmak için bana başka bir yöntem lazım. Bunu nasıl yapabilirim? Java dilini unutalım. Sabit bir AST yapısına sahip olan dilleri kendi direktiflerimizi yansıtacak şekilde genişletmemiz mümkün değil. Game over!

XML örneğinden yola çıkarak, nereye kadar gidebileceğimize bir göz atalım. XML deki kuru kalabalığı çıkardıktan sonra, aşağıdaki gibi bir yapı geri kalmakta:

[sourcecode language=”java”]
// Kod 5
(checkout from="svn://myhost/repo/code/v1" to"/home/centos/workspace/code")
    (compile dir="/home/centos/workspace/code")
    ( war file="/home/centos/workspace/code/app.war"))
[/sourcecode]

Bu kodun artık aslında

[sourcecode language=”java”]
checkout("svn://myhost/repo/code/v1", dir){
  compile(dir);
  war(file);
}
[/sourcecode]

olduğunu görebiliyoruz.

Şimdi kodun veri (code as data) olma özelliğinden bahsetmek istiyorum. Kod 1 de yer alan kodu App.java isminde bir dosya içinde tutabiliriz. Kod bu dosya içinde olduğu sürece veri (data) konumundadır. Bu veri üzerinde istediğimiz her türlü işlemi yapabiliriz. Örneğin dosyayı açıp, kaç kez public, kaç kez final kelimelerinin kullanıldığını tespit edebiliriz. Bu işlemleri yaptığımız sürece kod veri konumundadır. Bu dosyayı Java derleyicisi ile derlediğimiz anda bu veri kod haline dönüşür. Demek oluyor ki Java’da kodun veriye, verinin koda dönüşmesi mümkün. Mümkün olmayan tek bir şey var, o da kod 2 de yer alan kodun App2.java isimli bir dosyada olmasına rağmen, bu kodu hemen Java kodu olarak koşturamıyor olmam. Demek oluyor ki Java’da kodun veri olması, verinin de kod olması geçerliliğini her zaman korumuyor. Bu sadece Java kodu için geçerli. Herhangi bir veriyi Java kodu olarak koşturamıyoruz.

Java’nın kod 2 de yer alan kodu koşturamamasının sebebi daha öncede belirttiğim gibi checkout gibi direktifleri tanımamasında ya da benim Java’yı bu direktifleri tanıyıp, kullanabilecek şekilde genişletemem de yatmakta. Eğer Java bünyesinde checkout, compile ve deploy gibi direktifler olsaydı, o zaman kod 2 nin yer aldığı App2.java dosyasını derleyip, koşturabilirdim ki o zaman Java’da kodun veri, verinin de kod olma filozofisi geçerli olurdu.

Verinin kod olarak koşturulabilmesi için kullandığım dili dinamik olarak yeni direktiflerle genişletebilmem gerekiyor. Bu aslında derleyici tarafından oluşturulan AST nesnesinin programcının istediği direktifleri kapsayabilmesi anlamına gelmektedir. Dinamik bir AST yapısı dili yeni direktiflerle genişletmeyi mümkün kılmaktadır. Örneğin Clojure dilinde bu mümkün. Bunun için Makro yapıları kullanılıyor.

Clojure Lisp kökenli bir dil. Lisp kelimesi LISt Processing kelimelerinin kısaltılmış halidir. Lisp bünyesinde veriler listeler halinde işlenir. Örneğin:

[sourcecode language=”java”]
// Kod 6
(1 2 3)
[/sourcecode]

Bir liste üzerinde işlem yapmak demek, bir fonksiyonu kullanmak ve liste içindeki verileri bu fonksiyona parametre olarak vermek demektir. Eğer bir liste içindeki verileri toplamak isteseydik, Clojure ya da LISP dilinde şöyle yazardık:

[sourcecode language=”java”]
// Kod 8
(+ 1 2 3)
[/sourcecode]

+ burada yer alan fonksiyondur ve 1 2 3 değerlerini toplamak için kullanılır. Görüldüğü gibi kullanılan fonksiyon listenin bir parçası haline gelmiştir. Bu sebepten dolayı Lisp ya da Clojure programlarında kod veri, veri de kod olabilmektedir.

Clojure (+ 1 2 3) şeklinde bir satır ile karşılaştığında bu satırı kod olarak değerlendirir ve işletir. Bu işlemin sonucu 6 dır.

Eğer (+ 1 2 3) bir dosya içinde işlenmek üzere bekleyen bir satır olsaydı, o taktirde (+ 1 2 3) işlenmeyi bekleyen veri (data) olurdu. Clojure bünyesinde bu veriyi doğrudan kod olarak şu şekilde işletmek mümkün.

[sourcecode language=”java”]
// Kod 9
(eval
 ‘(+ 1 2 3))
[/sourcecode]

Aynı şeyi Java’da yapmak isteseydik:

[sourcecode language=”java”]
//Kod 10

String line = "(+ 1 2 3)";
List<Integer> numbers = new ArrayList<Integer>();
Pattern p = Pattern.compile("(\\d+)");
Matcher m = p.matcher(line);
while (m.find()) {
  numbers.add(Integer.parseInt(m.group()));
}
int result = 0;
if (line.charAt(1) == ‘+’) {
  for (Integer integer : numbers) {
  result += integer;
}
System.out.println(result);
[/sourcecode]

Java veri olarak kayıtlı olan (+ 1 2 3) satırını kod olarak değerlendirip, koşturamamaktadır, çünkü veriyi kod olarak algılama yeteneğine sahip değildir.

Şimdi kod 3 de yer alan XML örneğine tekrar göz atalım. Kuru XML kalabalığını çıkartıktan sonra elimizde aşağıdaki gibi bir yapı kalmıştı:

[sourcecode language=”java”]
// Kod 11
(checkout from="svn://myhost/repo/code/v1" to"/home/centos/workspace/code")
    (compile dir="/home/centos/workspace/code")
    ( war file="/home/centos/workspace/code/app.war"))
[/sourcecode]

Bu neredeyse bir LISP fonksiyonudur. Bu fonksiyonun kısaltılmış hali şu şekilde:

[sourcecode language=”java”]
// Kod 12
(checkout (from toDir)
    (compile toDir)
    (war file))
[/sourcecode]

Toplama örneğinde olduğu gibi checkout burada bir fonksiyon ismidir. from ve to isminde iki parametresi mevcuttur. Fonksiyon bünyesinde compile isminde bir fonksiyon koşturulmaktadır. Aynı şekilde checkout fonksiyonu bünyesinde war ismindeki fonksiyon koşturulmaktadır.

Clojure bünyesinde de checkout, compile ya da war isminde fonksiyon ya da direktifler bulunmuyor. Bu şekilde kod 12 de yer alan veriyi kod olarak koşturmak mümkün değil. Ama bir makro oluşturarak, Clojure derleyicisinin bu direktif ya da fonksiyonları tanımasını sağlayabilirim.

Clojure checkout, compile ve war direktiflerini tanırsa, kod 12 de yer alan veriyi kod olarak koşturabilir. Bu amaçla bu fonksiyonların isimlerini taşıyan makrolar oluşturmamız gerekiyor.

[sourcecode language=”java”]
// Kod 13
(defmacro checkout (from to)
    ‘(block
        (print stdout tab "Checking out from : "
            ~(head from) endl)
        (print stdout tab "to Dir: " ~to endl endl)))

[/sourcecode]

Makroları program yazan programlar olarak düşünebiliriz. Bu bir nevi meta programlama türüdür. Verinin kod olarak algılanabilmesi için dili dinamik olarak genişletmek için kullanılırlar.

Kod 13 de yer alan örnekte makroya parametre olarak gönderilen from ve to kod olarak algılanıp, koşturulmaz. Daha ziyade defmacro bu verileri kullanarak checkout isminde yeni bir fonksiyon oluşturur ve makroya veri olarak gönderilen from ve to değerlerini bu fonksiyona parametre olarak verir. Checkout ismini taşıyan bu yeni metodun yapası makro bünyesinde programlanır.

Kodun okunabilirlik seviyesinin kullanılan dilin ne oranda değişikliğe izin verdiğiyle orantılı olduğu gördük. Java Lisp gibi dinamik bir yapıda olmasa da, bu Java dilinde okunabilirlik seviyesinin yüksek olduğu kodlar yazamayacağımız anlamına gelmiyor. Clean Code tekniklerini uyguladığımız sürece yazdığımız kod okunur kalacaktır.


EOF (End Of Fun)
Özcan Acar


Yorumlar

“Kodun Okunabilirlik Seviyesi Nasıl Artılır?” için bir yanıt

  1. Hocam bundan yaklaşık bir yıl kadar önce bir entegrasyon yaparken şu şekilde bir kod yazmıştım.

    createMHRSThread(service)
    .setMethodType(mmtRandevuCetvelOzetDetaySorgulama)
    .setInParams(ip).run();

    İşin ilginç tarafı ben bu kodu yazdığım esnada fluent API’den bi haberdim.
    Yine bundan birkaç ay öncesi fluent API ile ilgili bir makale okuduğumda ise,yukarıda ki yazdığım kod aklıma geldi.

    Demek ki ; Aklın yolu bir. 🙂