Temiz ve Anlaşılır Kod Yazma Sanatına Giriş

Bir projeye başladığımızda, belirli teknoloji bilgi birikimini bir kenarda bırakırsak esasında her seviye geliştirici bir noktaya kadar kod yazabilir. Bir işi yapmanın birden fazla yolu ve şekli olduğunu hepimiz biliyoruz.

Peki, kod yazmak için her yol mübah mı?

Aslında Temiz Kod dediğimiz kavramının amacı bize her yolun mübah olmadığını gösterip, doğru kod yazma sürecinin - ki burada bir daha vurgulayalım kod yazmaktan bahsediyoruz, mimari bir tasarımdan veya kullanılacak teknolojilerden değil - nasıl olması gerektiğini ve hangi yolları tercih etmemiz gerektiğini açıklamasıdır.

Günün sonunda hedefe ulaşıyorsak yolun ne önemi var denilebilir. Peki o hedefe daha kısa sürede ulaşmak mümkünse, yürüdüğümüz yolda daha az yıpranacaksak, aynı yolu yürüyecek kişiler için yolu güzelleştirebiliyorsak ve belki de en önemlisi, hedefe ulaştıktan sonra diğer hedeflere gitmemizi mümkün kılacak doğrultudaki yönlendirmeleri içerecek bir kılavuz daha iyi olmaz mı?

Biz iyi olacağına inandığımız için Robert C. Martin’in yazmış olduğu “Clean Code” kitabını okuyarak bölüm çıktılarını sizlerle paylaşmak istedik.

Bölüm 1: Temiz Kod

Temiz Kod, temiz ve anlaşılır kod yazmanın önemine odaklanan bir kavramdır. Bu kavram, yazılım geliştiricilerin profesyonel olarak işlerini yaparken en iyi uygulamaları benimsemelerini ve kodlarının okunabilir, bakımı kolay ve etkili olmasını sağlamalarını hedefler. Yazılım projelerinde kod, kaçınılmaz bir gerçektir. Bu nedenle, yazılım geliştiricilerin kod kalitesine önem vermesi ve sürekli olarak iyileştirmeye çalışması gerekmektedir.

Zaman kısıtlaması, işlerin fazlalığı, yönetim ve müşteri baskısı gibi nedenler göz önüne alınarak proje kötü, karmaşık, özensiz şekilde kodlanabilmektedir. Bu durumu dikkate alınmamasının nedeni ise ortaya çıkan ürünün çalışıyor oluşu ve sonradan tekrar dönüp bu durumun düzeltileceğine inanılmasıdır. Ancak sonraya ertelendiğinde bu genelde hiç bir zaman yapılmamaktadır. Yeni gelen gereksinimler ile birlikte de kod karmaşıklığı giderek artmaktadır. Bu kodda geliştirme yapmak ve yeni şeyler eklemek karmaşık çalılardan ve gizli tuzaklardan oluşan bir bataklıkta dolaşmaya benzemektedir. Kötü kod, projelerin başarısız olmasına, bakım maliyetlerinin artmasına ve geliştirme süreçlerinin yavaşlamasına neden olabilmektedir. Bu nedenle, yazılım geliştiricilerin kötü kodu önlemek için sürekli çaba göstermeleri önemlidir.

İyi kod nasıl bu kadar çabuk kötü koda dönüşüyor?

Gereksinimlerin orijinal tasarımı etkileyecek düzeyde değişmesi ve planlamanın işleri yetiştiremeyecek kadar sıkışık olması gibi durumlar planlamanın uygun yapılmadığının bir göstergesidir. Böyle bir durumda temiz kod yazımı ikinci plana atılarak sadece çalışabilir düzeyde bir program geliştirilmesine odaklanma eğilimi olabilmektedir. Ancak bu durum bir süre için işleri hızlandırmış görünse de yeni geliştirmeler geldikçe veya oluşan bir hata düzeltilmek istendiğinde öncesinde oluşan kod karmaşıklığından dolayı sürüm tarihleri uzayabilmektedir. Aslında bakıldığında karmaşık kod yazarak yalnızca teslim süresi gecikmiş olur. Hızlı gitmek için her zaman kod temiz yazılmalıdır. Bu noktada planlamanın doğru şekilde yapılması önemlidir. Yazılım geliştiriciler projenin planlamasında büyük sorumluluk almaktadır. Çoğu yönetici gerçek durumu duymak ister ve ona göre vaatler verilir. Aynı şekilde kullanıcılar gereksinimlerin sisteme nasıl uyacağını doğrulamak için yazılımcılardan yardım bekler. Onlara karşı dürüst olup durum açıkça ifade edilmelidir.

Temiz kod yazmak, titizlikle kazanılmış bir “temizlik” duygusu aracılığıyla uygulanan sayısız küçük tekniğin disiplinli kullanımını gerektirir. Bu “kod duygusu” anahtardır. Bu anahtar yalnızca kodun iyi mi yoksa kötü mü olduğunu görmemizi sağlamakla kalmaz, aynı zamanda bize kötü kodu temiz koda dönüştürmek için disiplinimizi uygulama stratejisini de gösterir. “Kod duygusu” olmayan bir programcı, dağınık bir modüle bakabilir ve karışıklığı fark edebilir, ancak bu konuda ne yapacağına dair hiçbir fikri olmayacaktır. “Kod anlayışına” sahip bir programcı dağınık bir modüle bakacak, seçenekleri ve varyasyonları görebilecektir. “Kod duygusu”, programcının en iyi varyasyonu seçmesine yardımcı olacak ve buradan oraya ulaşmak için dönüşümleri koruyan bir davranış dizisi çizmesine rehberlik edecektir. Kısacası, temiz kod yazan bir programcı, boş bir ekranı bir dizi dönüşümden geçirerek zarif bir şekilde kodlanmış bir sistem haline getirebilen bir sanatçıdır.

Temiz kod, aşağıdaki özelliklere sahip olmalıdır:

  1. Anlaşılır: Kod, okuyan kişi tarafından kolayca anlaşılabilir olmalıdır.
  2. Bakımı kolay: Kod, değiştirilmesi ve güncellenmesi kolay olmalıdır.
  3. Etkili: Kod, gereksinimleri karşılamalı ve performans açısından etkili olmalıdır.

“Temiz kod” kavramına ilişkin deneyimli programcılardan gelen görüşler:

Bjarne Stroustrup (C++ Mucidi):

  • Zarif ve verimli kodu vurgular.
  • Bağımlılıkların minimal düzeyde olması gerektiğini belirtir.
  • Hataların gizlenmesini zorlaştıracak şekilde mantığın basit olması gerektiğini ifade eder.
  • Yazılımcıları kodun ilkesini bozmaya teşvik etmemek için performansın optimale yakın olması gerektiğini vurgular.

Pragmatik Dave Thomas:

  • Kod bozulması için “kırık pencereler” metaforunu tanıtır. Bu metafora göre kırık pencereleri olan bir binanın penceresi kırıldığında kimse onu umursamaz, daha fazla penceresinin kırılmasına müsaade eder. Kötü kodda böyledir. Kod mevcutta kötü yazılmışsa insanlar kötü kod eklemekten rahatsız olmayacaktır.
  • Anlamlı adlar, net API’lar ve minimal bağımlılıkları vurgular.
  • Birim ve kabul testlerinin önemini vurgular.

Grady Booch (Nesne Tabanlı Analiz ve Tasarımın Yazarı):

  • Temiz kodu basit, doğrudan ve iyi yazılmış yazı gibi tanımlar.
  • Temiz kodun hiçbir zaman tasarımcısının niyetini gizlemediğini, bunun yerine net soyutlamaları ve basit kontrol satırlarının bulunduğunu ifade eder.

Michael Feathers (Miras Kodla Etkin Çalışmanın Yazarı):

  • Temiz kodun özen ve titiz dikkat gerektirdiğini belirtir.
  • Temiz kodun okunması, geliştirilmesi ve testler içermesi gerektiğini savunur.

Ron Jeffries (Ekstrem Programlamayı Uygulamanın Yazarı):

  • Temiz kodun önceliklerini, testler, tekrarlanma olmaması, anlatıcı olma ve minimal varlıkları içerir.
  • Soyutlama yoluyla tekrarlanmanın kırılmasını vurgular.

Ward Cunningham (Wiki, Fit ve XP’nin Mucidi):

  • Okunulan her kodun hemen hemen beklenilen gibi çıkması temiz kod üzerinde çalışıldığının bir göstergesi olduğunu vurgular. Böyle kodlarda her bir modülün bir sonrakine zemin hazırlayacağını ve yazılımcıya sonraki modülün nasıl yazıldığı hakkında fikir verdiğini ifade eder.
  • Güzel kodun dilin probleme özgü olduğunu savunur.

Robert C. Martin:

  • Temiz kod hakkındaki farklı programcıların görüşlerini sunar.
  • Programlama düşünce okullarını dövüş sanatlarıyla karşılaştırır. Dövüş sanatı öğrencileri ilk olarak sadece bir ustanın öğretilerini takip etmeye başlar ve sonrasında geliştikçe başka ustaların öğrencisi olup bilgilerini çeşitlendirirler. Bir süre sonra ise kendi okullarını açıp birikimlerini, tekniklerini farklı öğrenciler ile paylaşırlar. Robert C. Martin’de tecrübelerini, öğretilerini, sanatını uygulayış biçimini aktarmak için temiz koda yönelik bir kitap oluşturduğunu, bu öğretilerin takip edilmesi durumunda temiz ve profesyonel kod yazma sanatının öğrenebileceğini belirtir. Ancak tek doğru kaynağın kendisi olmadığını yazılım dünyasında başka ustaların da bulunduğu ve onların da farklı şeyler öğreteceğini vurgular.
  • Farklı bakış açılarından öğrenmeyi teşvik eder.

Aslında okumaya ve yazmaya harcanan zamanın oranı 10:1’in oldukça üzerindedir. Yeni kod yazarken sürekli eski kodlar okunur. Bu oran çok yüksek olduğu için kod yazmayı zorlaştırsa da okumanın kolay olması istenir. Elbette okumadan kod yazmanın bir yolu yoktur. Dolayısıyla okumayı kolaylaştırmak aslında yazmayı da kolaylaştırır. Çevreleyen kod okunmuyorsa yeni kod yazmak çok zordur. Yeni kodun yazılması, çevresindeki kodun ne kadar zor veya kolay okunduğuna bağlı olacaktır. Yani hızlı ilerlemek, işin çabuk bitirilmesi ve kodun yazılması kolay olması isteniyorsa kod okunur olmalıdır. Kodu iyi yazmak yeterli değildir. Kodun zaman içinde temiz tutulması gerekir. Zaman geçtikçe kodun çürüdüğü ve bozulduğu görülür. Dolayısıyla bu bozulmanın önlenmesinde aktif rol alınmalıdır. Amerika İzcileri’nin yazılım geliştiricilerinin uygulayabileceği basit bir kuralı vardır: Kamp alanını bulduğunuzdan daha temiz bırakın. Eğer kod teslim alındığından biraz daha temiz bir şekilde teslim edilirse, kod çürümez. Temizlemenin büyük bir şey olması gerekmez. Bir değişken adını daha anlaşılır olacak şekilde değiştirmek, biraz fazla büyük olan bir işlevi bölmek, küçük bir yinelemeyi ortadan kaldırmak veya bir bileşik if ifadesini temizlemek yapılabilir.

Bölüm 2: İsimlendirme

Bu bölümde, değişkenler, fonksiyonlar, sınıflar ve diğer kod öğeleri için doğru isimleri seçmenin, kodunuzun okunabilirliğini ve bakımını önemli ölçüde iyileştirebileceği vurgulanmaktadır. İyi isim seçmek zaman alabilir, ancak uzun vadede daha çok zaman kazandırmaktadır. İsim koymak zor ve zaman alsa da amacı ortaya koymalıdır. Okunurluk artacağından bu durum aslında uzun vade de tasarruf sağlar. Bir değişkenin, fonksiyonun veya sınıfın adı tüm büyük sorulara cevap vermelidir. Kodu yazan kişiye neden var olduğunu, ne yaptığını ve nasıl kullanıldığını anlatmalıdır. Bir isim yorum gerektiriyorsa, o zaman isim amacını açıklamaz.

  • Yazılım geliştiriciler, kodun anlamını gölgeleyen yanlış ipuçları bırakmaktan ve yerleşik anlamları, kastettiğimiz anlamdan farklı olan kelimelerden kaçınılmalıdır. Örneğin “hp”, “aix” ve “sco”, “Unix” platformlarının veya türevlerinin adları oldukları için zayıf değişken adları olacaktır. Bir hipotenüs kodluyorsanız, “hp” iyi bir kısaltma gibi görünebilir ama bu yanıltıcı olabilir.
1
int d; //elapsed time in days (gün cinsinden geçen süre)
  • d değişkeni anlamlı bir isimlendirmeye sahip olmadığı için bu değişkeni okuyan kişiye günlerle ya da zaman ile alakalı bir anlam uyandırmamakta ve neden kullandığı anlaşılmamaktadır. Bu şekilde kullanılan değişkenler için anlamlı isimler seçilmelidir, yukarıda örnekte gösterilen değişken için aşağıdaki isimlendirmeler kullanılabilir:
1
2
3
4
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
  • Yazılan kodun amacının ne olduğu belli olmalıdır. Fonksiyonlar az satır içerse dahi ismi ve içerisindeki değişken isimleri anlaşılır olmadığı sürece, fonksiyonun ne için yazıldığını anlamak zaman alacaktır.
1
2
3
4
5
6
public List<int[]> getThem() {
    List<int[]> list1 = new ArrayList<int[]>();
    for (int[] x : theList)
        if (x[0] == 4) list1.add(x);
        return list1;
}

Örnekteki kodun anlaşılması zordur. Bir yazılım geliştirici bu örnekteki gibi isimlendirilmiş bir fonksiyonu gördüğünde aklında şu şekilde sorular oluşabilir:

  1. Listede ne tür şeyler var?

  2. Listedeki bir öğenin sıfırıncı alt simgesinin önemi nedir?

  3. 4 değerinin önemi nedir?

  4. Döndürülen listeyi nasıl kullanırım?

Yukarıdaki kodun düzenlenmiş hali:

1
2
3
4
5
6
7
8
public List<Cell> getFlaggedCells() {
  List<Cell> flaggedCells = new ArrayList<Cell>();
   for (Cell cell : gameBoard)
    if (cell.isFlagged())
     flaggedCells.add(cell);
    return flaggedCells;
}

Düzenlenmiş halinde kodun basitliği, kullanılan bellek miktarı aynı kalmasına rağmen kod daha okunur hale gelmiştir.

  • Yerleşik anlamları bizim kastettiğimiz anlamdan farklı olan kelimelerden kaçınmalıyız. Örneğin hp, aix ve sco, Unix platformlarının veya türevlerinin adları oldukları için zayıf değişken adları olacaktır. Bir hipotenüs kodluyor olsanız ve hp iyi bir kısaltma gibi görünse bile, bu yanıltıcı olabilir.

  • Bir liste olmadığı sürece “accountList” gibi bir isimlendirme kullanmakta yanıltıcı olabilir, “accountGroup”, “accounts” gibi isimlendirmeler tercih etmek daha doğru olacaktır.

  • Not: Gerçek bir liste türünde olsa dahi isimlendirmede liste olarak belirtmemek daha iyi olacaktır. Liste isimlendirmelerinde listler accountList gibi isimlerle tanımlanıyor. Ancak zaten bunun veri tipinin List olduğunu ve List gibi bir ifadeye ihtiyacın olmadığı söylenebilir. Accounts, accountGroup gibi tanımlamalar yapılması yeterlidir.

  • Birbirleri arasında küçük farklılıklar gösteren isimlendirmeleri kullanmaktan kaçınılmalıdır. XYZControllerForEfficientHandlingOfStrings - XYZControllerForEfficientStorageOfStrings gibi.

  • Kötü isim örneklerinden birisi de küçük L harfi ya da büyük O harfi kullanmaktır. Çünkü görünüşte bir ve sıfıra benzerler.

1
2
3
4
5
int a = l;
if (O == l)
    a = O1;
else
    l = 01;
  • Aynı ismi, aynı kapsamdaki (scope) 2 farklı objeye veremediğimizden, birini değiştirme yoluna gideriz. Sayı eklemek yeterli olmadığında, bunu objelerden bir tanesinin bir harfini eksilterek yaparız. Ancak, isimlerin farklı olması gerekiyorsa anlamları da farklı olmalıdır. a1, a2, a3 gibi isimlendirmeler kesinlikle anlamlı değildir. Bu tür isimler yazarın amacı hakkında en ufak bir ipucu bile vermemektedir. Burada a1 ve a2 yerine kaynak (source) ve hedef (destination) kullanılması çok daha anlamlıdır:
1
2
3
4
5
public static void copyChars(char a1[], char a2[]) {
 for (int i = 0; i < a1.length; i++) {
  a2[i] = a1[i];
 }
}
  • Gürültü oluşturan kelimeler gereksizdir. “Variable” kelimesini bir değişkenin adında, tablo kelimesi de bir tablo adında yer almamalıdır. “NameString” gibi isimlendirmelerin de kullanılması uygun değildir çünkü “Name” isimli değişkenin “float” tipinde olması beklenmemektedir, bu değişkenin “string” tipinde bir değişken olması beklendiği için böyle bir isimlendirmeye gerek yoktur. Örneğin “Product” adında sınıf varken “ProductInfo” veya “ProductData” isimli başka sınıflar varsa farklı bir anlam taşımayan sınıflar elde edilmiş olur. Benzer şekilde “Customer” sınıfı varken “CustomerObject” ismiyle yeni bir sınıf oluşturmakta gereksizdir. İngilizce ’de bulunan “A”, “an”, “the” gibi “data” ve “info” kelimeleri de anlamı belirsiz sözcüklerdir.

  • Kolayca telaffuz edilebilir ve ekibinizle konuşulabilen isimler seçilmelidir. Bu, kodunuz hakkında iletişim kurmayı kolaylaştırır.

1
2
int elpsd; // Kötü Örnek
int elapsedTime; // İyi Örnek
  • Kod tabanınızda arama yaparken kolayca bulunabilecek isimler kullanılmalıdır. Bu, belirli değişkenleri veya fonksiyonları bulmaya çalışırken zaman kazandırabilir.
1
2
int s; // Kötü Örnek (Arama yapılırken bulması zor)
int seconds; // İyi Örnek
  • Şu iki kodu karşılaştıralım:
1
2
3
4
5
6
7
8
9
10
11
12
13
for (int j = 0; j < 34; j++) {
    s += (t[j] * 4) / 5;
}

int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;

for (int j = 0; j < NUMBER_OF_TASKS; j++) {
    int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
    int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
    sum += realTaskWeeks;
}
  • “sum” fonksiyonun ismi çok kullanışlı bir isim değildir ancak en azından kod içerisinde arama yaparak bulunabilir. Benzer şekilde WORK_DAYS_PER_WEEK sabitinin kullanımlarını bulmak, 5 sayısını bulmaktan kat ve kat daha kolaydır.

  • Sınıf isimlendirmelerinde tanımlanacak sınıfın single responsibilty prensibine uygun olması ve isminin de bu single responsibility’i net bir biçimde yansıtıyor olması gerekir. İyi isimlendirilmiş sınıflar genelde isim olarak bir fiil değil bir isim alırlar.

1
2
3
4
5
6
7
8
//Kötü isimlendirme örnekleri                   //İyi isimlendirme örnekleri

class WebsSiteBO {...}                           class User {...}
class Utilty  {...}                              class Account {...}
class Common  {...}                              class QueryBuilder {...}
class MyFunctions  {...}                         class ProductRepository {...}
class JimmysObjects {...}
class Manager {...}
  • İyi isimlendirilmiş metodun ismini okunduğunda da metodun gövdesini dahi okumadan ne iş yaptığı anlaşılır ve kullanılabilir. Metotlar bir fonksiyonelliği yerine getirdiklerinden isimleri ‘fiil’ olmalıdır. Metotlar da sınıflar gibi single responsibilty prensibine uymalı ve sadece tek bir fonksiyonelliği yerine getirmelidir. Metot isimlendirmesinde And, If, Or, gibi bağlaçlar geçiyorsa metot birden fazla işi içerisinde barındırıyor mu diye düşünülmesi gerekir.
1
2
3
4
5
6
//Kötü isimlendirme örnekleri                   //İyi isimlendirme örnekleri

public List<User> Getprocesses {...}               public List<User> GetRegisteredUsers {...}
public int pending  {...}                          public Boolean IsValidSubmission {...}
public void start  {...}                           public void SendEmail
public void SaveAndPublish  {...}                      
  • Ara yüzlerin isimlerinin başına “I” harfi eklemek yaygın bir pratiktir, ancak çok fazla dikkat dağıtmakta ve bilgi vermek konusunda da çok yardımcı olmamaktadır. Eğer ara yüz ya da gerçekleştirim sınıflarından birini belirtmemiz gerekiyorsa, gerçekleştirim sınıfını seçmeliyiz; “ShapeFactoryImpl” adında beton (Concrete) sınıfı isimlendirmeyi tercih etmek daha iyi bir yaklaşım olabilir. Ara yüz sınıfını belirtmekten çok daha iyidir.

  • Sınıf isimleri “Customer”, “WikiPage”, “Account” ya da “AddressParser” gibi isimlerden/isim tamlamalarından oluşmalıdır. Sınıf isimleri asla bir fiil olmamalıdır.

  • Metot isimleri “postPayment”, “deletePage” ya da “save” gibi fiillerden/fiil tamlamalarından oluşmalıdır. Erişimci (getters), mutatör (setters) ya da doğrulayıcı (predicate) (true/false değerleri dönen “isEmpty” fonksiyonu gibi) metotların başlarına, Java standartlarına göre “get”, “set”, “is” eklenmelidir.

  • İki farklı amaç için aynı kelimeyi kullanmaktan ya da aynı amaçlar için farklı kelimeleri kullanmaktan kaçınılmalıdır. Örneğin “Controller”, “Manager” ya da “Driver” kelimelerini aynı kapsamda farklı sınıflar için kullanmak iyi bir kullanım örneği değildir. Bir tanesi seçilmeli ve onunla devam edilmelidir. Örneğin, “add” isimli bir metodun daha önce yazılmış ve iki değeri birbiri ile birleştirdiğini (concat) düşünelim. Bir listeye kayıt ekleyen bir fonksiyon yazılmak istendiğinde “add” yerine “insert” veya “append” gibi farklı bir isimlendirme yapmak daha uygun olacaktır.

  • “firstName”, “lastName”, “Street”, “houseNumber”, “city”, “state” ve “zipcode” isimli değişkenlerimizin olduğunu düşünelim. Birlikte kullanıldığında bir adresin detaylarını ifade ettiği çabuk anlaşılabilmektedir. Ancak sadece “state” değişkenini görürsek, yine de adrese ait olduğunu düşünebilir miyiz?

  • Önekler (prefix) kullanarak bağlam (context) sağlayabilirsiniz; “addrsFirstName”, “addrLastName”, “addrState” vb. Okuyucular bu değişkenlerin daha büyük bir yapının parçası olduğunu anlayabileceklerdir. Elbette daha iyi bir çözüm Address isimli bir sınıf oluşturmaktır.

Bu yönergeleri izleyerek, daha kolay okunabilir, anlaşılır ve bakımı yapılabilecek kodlar yazabilirsiniz. Bu sadece size değil, aynı zamanda ekip arkadaşlarınıza ve kodunuz üzerinde çalışacak gelecekteki geliştiricilere de fayda sağlayabilecektir.

Bölüm 3: Fonksiyonlar

Fonksiyonlar bir programın içindeki belirli bir görevi yerine getirmek için tasarlanmış kod bloklarıdır. Kodun yeniden kullanılabilirliğini artırır ve programın mantığını modüler hale getirirler.

Fonksiyonlar Kısa Olmalıdır

Fonksiyonlar mümkün olduğunca kısa ve öz olmalı, 20 satırdan uzun olmamalıdır. Yazdığımız her bir fonksiyonun tek bir işlevi yerine getirmesine ve sorumluluklarının net bir şekilde belirtilmiş olmasına dikkat etmeliyiz. Fonksiyonları isimlendirmek genellikle zordur fakat isimleri belirlerken fonksiyon isimlerinin açıklayıcı ve tek başına anlaşılabilir olmasına özen göstermeliyiz. Akış sırasına göre her fonksiyon bizleri kullanılan bir sonraki fonksiyona yönlendirmelidir.

Bloklar ve Girinti

Karar ve döngü ifadelerinde bulunan çok fazla girinti ve blok sayısı kodun okunabilirliğini önemli ölçüde azaltabilmektedir. Bu yüzden ürettiğimiz kodlarda “IF”, “ELSE” ve “WHILE” gibi döngü ve karar ifadelerinin bir satır olmasına dikkat etmeliyiz. Tek bir satırda bir fonksiyon çağırısı yapıldığında, çağrılan fonksiyonun açıklayıcı bir adı olmasını ve kodu okuyan kişinin bloklar arasında kaybolmasını engelleyebiliriz. Fonksiyonları eklerken kullandığımız girinti seviyesi kodu okuyan kişinin blokları doğru şekilde takip edebilmesi için çok büyük önem taşımaktadır bu yüzden fonksiyonlarımızda girinti seviyesi bir veya ikiden fazla olmamalıdır.

Tek Bir Şey Yapmalı

Fonksiyonları tasarlarken temel dikkat etmemiz gereken kurallardan bir tanesi de fonksiyonların tek bir iş yapması ve bunu iyi yapmasıdır. Bir fonksiyon içinde farklı mantıksal adımlar bulunuyorsa, bunları ayrı alt fonksiyonlara ayırmak genellikle en iyi yaklaşımdır. Örneğin, mevcut bir fonksiyonun belirli bir kısmını anlamlı bir isimle yeni bir fonksiyona taşıyabileceğinizi fark ederseniz, bu genellikle orijinal fonksiyonun aşırı geniş kapsamlı olduğuna ve yeniden düzenlenmesi gerektiğine işaret eder.

Bu kurallara dikkat ettiğimizde hem yazdığımız fonksiyonların tek bir sorumluluğa sahip olmasını hem de kodu daha okunaklı ve sürdürülebilir hale getirilmesini sağlayabiliriz. Bu saydığımız özellikler ile yukarıda bahsedilen kurallara göre fonksiyonlarımızı tasarlamanın bize sağladığı avantajlar şunlardır:

  1. Okunabilirlik: Tek bir işlemi gerçekleştiren fonksiyonlar, kodun anlaşılmasını ve okunmasını kolaylaştırır.
  2. Yeniden kullanılabilirlik: İyi tasarlanmış fonksiyonlar, başka yerlerde de kullanılabilir ve kod tekrarının önüne geçer.
  3. Test edilebilirlik: Tek bir işlemi gerçekleştiren fonksiyonlar, test etmesi daha kolaydır ve hataların tespit edilmesini hızlandırır.
  4. Bakım ve güncelleme: Fonksiyonlar tek bir işlemi gerçekleştirdiğinde, değişiklikler yapmak ve hataları düzeltmek daha kolay hale gelir.

Yukarıda listelenen başlıklar göz önünde bulundurulduğunda kodun kalitesi ve projenin başarılı bir şekilde tamamlanmasına önemli ölçüde etki edebileceğini görebiliriz.

Fonksiyon Başına Bir Soyutlama Düzeyi

Daha önceki başlıklarda fonksiyonlarımızın tek bir şey yapması ve bu işlemi de iyi yapması gerektiğinden bahsetmiştik. Bu durumu sağlamak için fonksiyon içinde kullandığımız ifadelerin aynı düzeyde soyutlandığına dikkat etmeliyiz. Farklı seviyelerdeki soyutlamaya sahip ifadelerin, tek bir fonksiyon içinde kullanılması kodun okunabilirliğini azaltabileceği gibi anlaşılabilirliğini de önemli ölçüde zorlaştırabilir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String generateReport(List<Data> dataList) {  
    StringBuilder report = new StringBuilder();
    // High-level abstraction  
    report.append(getHeader());
    // Intermediate-level abstraction  
    for (Data data : dataList) {
        report.append(data.getName()) .append(": ")
            .append(data.getValue())
            .append("\n");  
    }
    // High-level abstraction  
    report.append(getFooter());
    return report.toString();  
}

Aşağıdaki örnekte görülen “getDataLines” isimli yeni bir fonksiyon oluşturularak, “generateReport” fonksiyonunda bulunan orta seviyeli soyutlamaya sahip ifadeleri birbirinden ayırabiliriz. Bu işlem sonrasında “generateReport” fonksiyonunun soyutlama düzeyinde tutarlı olmasını sağlamış oluruz. Bu değişikler yapıldıktan sonra başlıkların, veri satırlarının ve alt bilgi alanlarının ayrı fonksiyonlarda oluşturulduğunu gözlemleyebiliriz. Ayrı fonksiyonlarda yönetilen bu işlemler kolay bir şekilde güncellenebilir ya da düzenlenebilirler. Bu da kodun daha okunabilir ve kolay yönetilebilir olmasını sağlar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String generateReport(List<Data> dataList) {
    StringBuilder report = new StringBuilder();
    report.append(getHeader());  
    report.append(getDataLines(dataList));  
    report.append(getFooter());
    return report.toString();  
}
private String getDataLines(List<Data> dataList) {  
    StringBuilder dataLines = new StringBuilder();  
    for (Data data : dataList) {
        dataLines.append(data.getName())  
            .append(": ")
            .append(data.getValue())  
            .append("\n");
    }
    return dataLines.toString();  
}

Kodu Yukarıdan Aşağıya Okuma: Adımlama Kuralı

Bir önceki başlıkta fonksiyonların soyutlama düzeylerinin karmaşık olmaması ve tutarlı bir şekilde bulunması gerektiğinden bahsetmiştik. Fonksiyonlarımızı farklı seviyelerdeki soyutlama düzeylerine göre ayırırken adımlama kuralını kullanabiliriz.

Adımlama kuralı, kodun daha düzenli ve mantıklı bir yapıya sahip olmasını sağlayarak, kodu okuyan kişinin daha kolay anlamasına yardımcı olur. Bu kurala göre, kodlar üst düzey soyutlamadan başlayarak, daha alt düzeylere doğru adım adım ilerlemelidir. Bu sayede, her bir paragraf veya kod bloğu, bir önceki bloğun daha detaylı bir açıklaması niteliğinde olacaktır. Bu yapı, kodun daha modüler ve takip edilebilir olmasını sağlayacaktır.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ReportGenerator {
    public String generateReport(List<Data> dataList) {
        StringBuilder report = new StringBuilder();
        report.append(getHeader());  
        report.append(getDataLines(dataList));  
        report.append(getFooter());
        return report.toString();  
    }
    private String getHeader() {
        return "=== Report Header ===\n";
    }
    private String getDataLines(List<Data> dataList) {  
        StringBuilder dataLines = new StringBuilder();  
        for (Data data : dataList) {
            dataLines.append(data.getName())  
                .append(": ")
                .append(data.getValue())  
                .append("\n");
        }
        return dataLines.toString();  
    }
    private String getFooter() {
        return "=== Report Footer ===\n";
    }  
}

Switch İfadeleri

Switch ifadeleri projelerde tercih edilen bir karar ifadesidir. Fakat Switch ifadesi yapısı gereğiyle birden fazla işi yapma eğilimindedir. Bu yüzden okunabilirliği azaltabilir ve bakımı zorlaştırabilirler. Kodu daha okunabilir ve bakımı kolay bir hale getirmeyi hedeflediğimiz için Switch ifadeleri kullandığımız yerleri dikkat etmeliyiz, gerekiyorsa kullanmamaya özen göstermeliyiz.

Bazı durumlarda Switch ifadelerini kullanmak zorunda kalabiliriz, bu gibi durumlarda bu ifadelerin düşük seviyeli bir sınıfta bulunduğundan ve tekrarlanmadığından emin olmalıyız. Saydığımız bu durumlardan emin olunabilmesi için çok biçimlilik (polymorphism) kullanılabilir.

Çok biçimlilik, nesne yönelimli programlamada kullanılan bir tekniktir ve farklı sınıfların aynı ara yüzü veya yöntemi uygulamasına olanak tanır. Bu sayede, “Switch” ifadelerinin yerine geçebilir. Çok biçimlilik kodun daha esnek ve yeniden kullanılabilir olmasını sağlar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
interface Shape {
 void draw();  
}
class Rectangle implements Shape {
 public void draw() {
   System.out.println("Drawing rectangle...");  
 }
}
class Circle implements Shape {
 public void draw() {
   System.out.println("Drawing circle...");
 }  
}
class Triangle implements Shape {
 public void draw() {
   System.out.println("Drawing triangle...");
 }
}
class DrawingApp {
 private Shape shape;
 public DrawingApp(Shape shape) {
   this.shape = shape;
 }
 public void drawShape() {
   shape.draw();  
 }
}
public class Main {
 public static void main(String[] args) {
   DrawingApp app1 = new DrawingApp(new Rectangle());
   app1.drawShape();
   DrawingApp app2 = new DrawingApp(new Circle());  
   app2.drawShape();
   DrawingApp app3 = new DrawingApp(new Triangle());
   app3.drawShape();
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DrawingApp {
 private Shape shape;
 public DrawingApp(Shape shape) {
   this.shape = shape;
 }
 public void drawShape() {
   switch(shape):
     case Rectangle:
       System.out.println("Drawing rectangle...");
       break;
     case Circle:
       System.out.println("Drawing circle...");
       break;
     case Triangle:
       System.out.println("Drawing triangle...");
       break;
 }
}
public class Main {
 public static void main(String[] args) {
   DrawingApp app1 = new DrawingApp(new Rectangle());
   app1.drawShape();
   DrawingApp app2 = new DrawingApp(new Circle());
   app2.drawShape();
   DrawingApp app3 = new DrawingApp(new Triangle());
   app3.drawShape();
 }
}

Switch ifadeleri, aşağıdaki koşullar sağlandığında kullanılması hoş görülebilir:

  1. Sadece bir kez görünürler.
  2. Çok biçimli nesneler oluşturmak için kullanılırlar.
  3. Kalıtımın arkasına gizlenirler.

Yukarıda koşulların zihnimizde canlanabilmesi için şu örneği düşünebiliriz. Farklı şekillerin alanını hesaplamak için bir “Switch” ifadesi düşünelim. Bu durumda, her şekil için ayrı bir durum kullanmak yerine, her şekil sınıfının alanını hesaplamak için ortak bir ara yüz (bu ara yüz içerisine hesaplaAlan() adında bir fonksiyon kullanılabilir.) sağlayan bir üst sınıf kullanılarak çok biçimlilik (polymorphism) kullanılabilir. Bu şekilde, “Switch” ifadesi yerine, çok biçimli nesneler kullanarak kodun daha esnek ve takip edilebilir olmasını sağlayabiliriz.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Bird {  
    private BirdType type;
    public double getSpeed() {  
        switch (type) {
            case EUROPEAN:
                return getBaseSpeed();
            case AFRICAN:
                return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
            case NORWEGIAN_BLUE:
                return isNailed ? 0 : getBaseSpeed(voltage);
            default:
                throw new RuntimeException("Should be unreachable");
        }  
    }
}

Özetlememiz gerekirse, “Switch” ifadesi kodun “tek bir şey” yapma kuralına yapısal olarak uygun olmadığı için kullanımını en aza indirgemek kodun daha esnek, anlaşılır ve yeniden kullanılabilir olmasını sağlayacaktır. “Switch” ifadelerinin kullanımını azaltabilmek için nesne yönelimli programlamanın en önemli başlıklarından birisi olan çok biçimlilik prensibini kullanabiliriz.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public abstract class Bird {
    public abstract double getSpeed();
}
public class EuropeanBird extends Bird {  
    @Override
    public double getSpeed() {
        return getBaseSpeed();  
    }
}
public class AfricanBird extends Bird {  
    private int numberOfCoconuts;
@Override
    public double getSpeed() {
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;  
    }
}
public class NorwegianBlueBird extends Bird {  
    private boolean isNailed;
    private double voltage;
    @Override
    public double getSpeed() {
        return isNailed ? 0 : getBaseSpeed(voltage);  
    }
}
public class BirdFactory {
    public Bird createBird(BirdType birdType) {
        if (birdType == BirdType.EUROPEAN)) {  
            return new EuropeanBird();
        } else if (birdType == BirdType.AFRICAN)) {  
            return new AfricanBird();
        } else if (birdType == BirdType NORWEGIAN_BLUE)) {  
            return new NorwegianBlueBird();
        } else {  
            return null;
        }  
    }
}

Açıklayıcı İsimler Kullanın

Fonksiyon isimlerini seçmek bazen çok zor olabilir. Fonksiyon isimlerini belirlerken dikkat edebileceğimiz bazı prensipler bulunmaktadır. Bunlardan bir tanesi Ward’ın prensibidir. Ward’ın prensibi, temiz kod yazarken küçük ve amacına odaklanmış fonksiyonlar için anlamlı isimler seçmenin önemini vurgulamaktadır. Fonksiyon adlarının iyi isimlendirilmesi, fonksiyon adları arasındaki tutarlılık, kodların okunabilirliğini ve anlaşılabilirliğini arttırarak, yazılım geliştirme sürecini daha verimli hale getirmektedir. İsimlendireceğimiz fonksiyon ne kadar küçük ve amacına odaklı olursa, açıklayıcı ve anlamlı bir isim seçmek o kadar kolaylaşır. Fonksiyonların büyüdükçe ve soyutluk seviyesi karmaşıklaştıkça kodların okunabilirliğinin azalacağından bahsetmiştik. Benzer bir yaklaşım fonksiyon isimlerinde de bulunmaktadır. Fonksiyon isimleri için uzun isimler kullanmaktan kaçınılmalıdır.

Fonksiyon isimleri kısaltılmaya çalışılırken bazı durumlarda anlaşılması zor ve karmaşık isimler oluşturulabilmektedir. Bu tarz durumlarda, uzun tanımlayıcı isimler, kısa ama anlaşılması zor veya karmaşık isimlerden daha anlaşılır olabilmektedir. Açıklayıcı isimler seçmek zihnimizdeki tasarımı ortaya çıkarır ve onu geliştirmenize yardımcı olur. Birden fazla farklı isimlendirmeli, denemeli ve her biri ile birlikte kodu tekrar okuyarak en anlamlı olanı tercih etmeliyiz. Seçtiğimiz fonksiyon isimleri kodumuzda kullandığımız diğer fonksiyon isimleri ile tutarlı olmalıdır. Fonksiyon isimleri için aynı isimler, kelime grupları, sıfatlar ve filleri tercih edebiliriz.

Fonksiyon isimlerini seçmek uzun vakitler alabilir. Fonksiyonlar için anlamlı isimler seçmek için harcanan vakit kayıp olarak görülmemelidir. Bulduğumuz anlamlı fonksiyon isimlerinin ileride kodun bakım süresini kısaltacağını hatırlayabiliriz.

Fonksiyon Argümanları

Temiz kod yazma sanatında, fonksiyonların okunabilirliği ve anlaşılabilirliği büyük önem taşıdığından bahsetmiştik. Fonksiyon argümanlarının sayısı, fonksiyonların karmaşıklığını, kullanılabilirliğini ve okunabilirliğini doğrudan etkiler. Bu nedenle, fonksiyonların parametre olarak alacağı argümanların sayısını belirlerken dikkatli olunmalıdır.

Bir fonksiyonun hiçbir argümana ihtiyaç duymaması en iyi durumdur. Bu durum bizlere bu fonksiyonun bağımsız olduğunu ve dış etkenlerden etkilenmediğini gösterir. Bu durumda bulunan fonksiyonlar test edilebilmesi ve kullanılması en kolay olanladır.

Bir argümanı olan fonksiyonlar, hiçbir argümana ihtiyaç duymayan fonksiyonlardan sonra tercih edilmesi gereken ve genellikle kullanımı kolay, anlaşılması zor olmayan fonksiyonlardır. Bu fonksiyonlar, genellikle bir girdi alarak belirli bir işlemi gerçekleştirirler.

İki argümanı olan fonksiyonlar, bir önceki metinde bahsedilen hiçbir parametre almayan ya da bir parametre alan fonksiyonlara göre farklı kombinasyonlar oluşturabileceği için test edilmesi diğer iki fonksiyon tipine göre daha zordur. Ancak, iki argüman mantıklı bir şekilde bir arada kullanılması durumunda, bu tür fonksiyonların kullanılması da kodun anlaşılabilirliğini önemli derecede etkilemeyecektir.

Üç argümanı olan fonksiyonlar, iki argümanı olan fonksiyonlarda bahsedilen zorlukları benzer şekilde içermekte ve oluşabilecek kombinasyon sayısını daha fazla arttıracağı için iki argümanı olan fonksiyonlara göre kodu daha karmaşık ve test edilmesi daha zor bir hale getirmektedir. Eğer üç argümanı bir arada kullanmanın gerçekten gerekli olduğunu düşünülüyorsa, bu durum gözden geçirilmeli ve fonksiyonun daha basit hale getirilebilmesi için alternatif çözümler aranmalıdır.

Üçten fazla argümana sahip fonksiyonlar, çok özel durumlar dışında tercih edilmemelidir. Bu tür fonksiyonlar, üç argümana sahip ve iki argümana sahip fonksiyonlarda bulunan benzer problemlere sahiptir. Oluşabilecek kombinasyon sayısı daha fazla artacağı için kodun okunabilirliği ve test edebilirliği önemli ölçüde düşecektir. Eğer bir fonksiyon üç veya daha fazla argümana ihtiyaç duyuyorsa, fonksiyonun görevleri değerlendirilmeli ve yönetilebilir küçük parçalara bölünmelidir.

Bayrak Argümanları (Flag Arguments)

Bayrak argümanları, fonksiyonlara parametre olarak geçirilen “boolean” değerlerdir ve fonksiyonun davranışını değiştirmek için kullanılır. Bu tür argüman alan fonksiyonlar birden fazla görevi olabileceği için kodun okunabilirliğini ve anlaşılabilirliğini azaltır. Yazının başlarında bahsettiğimiz gibi iyi tasarlanmış bir fonksiyonun sadece yerine getirmesi gerektiği tek bir sorumluğu vardır (Single Responsibility Principle). Bayrak argümanları kullanmak bir fonksiyona birden fazla sorumluluk yükleyebileceği için kullanmaktan kaçınılmalıdır.

Bayrak argümanları kullanmak yerine farklı görevleri yerine getiren ayrı fonksiyonlar oluşturmak bu durumu yönetmek için daha iyi bir tercihtir. Bu şekilde kodunuzu okuyan kişinin yazılan fonksiyonu okuduğunda parametre olarak kullanılan “boolean” parametre değerinin ne için kullanıldığını araştırmasına ve vakit kaybetmesini engellemiş olursunuz. Bu durumda kodunuzun daha anlaşılır ve bakımını daha kolay hale gelmesine önemli derecede yardımcı olacaktır.

1
2
3
4
5
6
7
public void printData(boolean isCSVFormat) {  
    if (isCSVFormat) {
        // CSV formatında veriyi yazdır  
    } else {
        // Başka bir formatla veriyi yazdır
    }  
}

Bu fonksiyon, bayrak argümanı “isCSVFormat” kullanarak veriyi farklı formatlarda yazdırmaktadır.

Bunun yerine, iki ayrı fonksiyon kullanarak daha temiz bir kod elde edilebilir:

1
2
3
4
5
6
7
public void printDataAsCSV() {
    // CSV formatında veriyi yazdır
}
 
public void printDataInOtherFormat() {
    // Başka bir formatla veriyi yazdır  
}

Sonuç olarak, bayrak argümanları kullanmaktan kaçınarak, kodun okunabilirliğini ve bakımını kolaylaştırılabilirsiniz.

Yaygın Olarak Kullanılan Tek Parametreli Fonksiyonlar

Tek parametre alan fonksiyonlarda, fonksiyon isimlerini seçerken açık bir ayrım yapılmasını sağlayacak isimler tercih edilmelidir. Kullanılan fonksiyon isimleri kendi aralarında tutarlı olmalıdır. Örneğin, parametre olarak aldığı “StringBuffer” tipinde olan “pageText” objesi üzerinde değişiklik yapan “void includeSetupPageInto(StringBuffer pageText)” fonksiyonunu düşünelim. Bu fonksiyon parametre olarak verilen “pageText” objesi üzerindeki alanları değiştirebileceğinden, bu fonksiyon çağrıldıktan sonraki bölümlerde “pageText” objesi kullanılan bölümlerde hataya veya kafa karışıklığına sebep olabilir. Bu tarz aldığı obje üzerinde değişiklik yapan fonksiyonlarda, fonksiyonun ismi yapılan işlemi çok net bir şekilde açıklamalıdır.

Tek parametre alan bir fonksiyon eğer aldığı argüman değerini değiştirecekse genellikle bunu çıktı değeri olarak döndürmesi tercih edilmelidir. Bu kodun anlaşılabilirliğini ve takip edilebilirliği için önemli bir fayda sağlayacaktır. Örneğin bu fonksiyonu düşünebiliriz:

1
StringBuffer transform(StringBuffer in)

Bu fonksiyon aldığı “in” parametresi üzerinde değişiklik yaparak bu değeri döndürür.

1
void transform(StringBuffer out)

İkinci örnekte parametre ile gönderilen “out” objesi üzerinde değişiklik yapılır ve bu fonksiyon çağrıldıktan sonra parametreden gelen “out” değeri değişikliğe uğramış şekilde kullanılır. Birinci örnekteki fonksiyon, ikinci örnekteki fonksiyondan daha iyidir. Çünkü birinci örnekteki fonksiyon parametredeki argüman üzerinde değişiklik yaptıktan sonra değeri döndürür, bu dönüşüm ikinci örnekteki dönüşüme göre daha kolay takip edilebilmektedir.

İki Argümanlı Fonksiyonlar

Bir önceki başlıklarda fonksiyon argümanlarının kodun okunabilirliğini ve bakımı için önemlerinden bahsetmiştik. İki argümanı olan fonksiyonlar kullanılırken nelere dikkat edilmesi gerektiğinden bahsedeceğiz.

İdeal olarak, fonksiyonlar mümkün olduğunca az sayıda argüman almalıdır. Bunun nedeni, argüman sayısı arttıkça fonksiyonun karmaşıklığının ve anlaşılmasının zorlaşmasıdır. Bununla birlikte, bazı durumlarda iki argüman alan fonksiyonların kullanılması kaçınılmazdır ve dikkatli bir şekilde uygun durumlarda kullanılabilirler.

İki argümanlı fonksiyonlar kullanılırken dikkat edilmesi gereken bazı noktalar şunlardır:

  1. İyi adlandırma: Fonksiyon adı ve argümanlarının adları, fonksiyonun ne yaptığını ve argümanların nasıl kullanıldığını açıkça belirtmelidir. Bu, kodun okunabilirliğini artırır ve hataların önlenmesine yardımcı olur.
  2. Fonksiyonun tek bir görevi olmalı: Fonksiyonlar, Tek bir görevi yerine getirmelidir. Eğer bir iki argümana sahip fonksiyon birden fazla görev yapıyorsa, bu görevleri ayrı fonksiyonlara bölmek daha iyidir.
  3. Argümanların sırası: Fonksiyonun nasıl çalışacağı hakkında kullanacak kişiye bir bilgi verdiği için kullanım sırası önemlidir. Örneğin bir çıkarma fonksiyonu düşünelim. “a” değerinden “b” değerini çıkarıyorsa, ilk parametrenin “b” ve ikinci parametrenin “a” olması kullanıcıda kafa karışıklığı oluşturabilir ve okunabilirliği azaltabilir.
1
2
3
4
Integer subtract(Integer b, Integer a){ 
    // Parametlerinin sırası kafa karışıklığına sebep olabilir.
    return a - b;
}

Yukarıda bahsedilen kuralların uygulandığı basit bir örnek olarak aşağıdaki fonksiyonu düşünebiliriz.

1
2
3
public int add(int a, int b) {
    return a + b;
}

Üç Argümanlı Fonksiyonlar

Bir önceki başlıkta iki argümana sahip fonksiyonların kodun okunabilirliğini ve bakımı için önemlerinden bahsetmiştik. Üç argümanı olan fonksiyonlar kullanılırken nelere dikkat edilmesi gerektiğinden bahsedeceğiz.

Üç veya daha fazla argüman alan fonksiyonlar, karmaşıklığı ve anlaşılmasının zorluğunu artırır. Bu nedenle, üç argümanı olan fonksiyonları kullanmaktan kaçınmak ve fonksiyonları daha basit hale getirmek önemlidir. Eğer bir fonksiyonun üç argümana ihtiyacı varsa, bu argümanları gruplamak veya fonksiyonu daha küçük fonksiyonlara bölmek düşünülebilir.

Üç argümana sahip fonksiyonların kullanımı azaltmak için aşağıdaki ipuçları takip edilebilir:

  1. Nesne kullanarak argümanları gruplama: Eğer fonksiyonun alması gereken argümanlar mantıksal olarak bir arada bulunuyorsa, bu argümanlar bir nesne içinde gruplanarak fonksiyonun karmaşıklığı azaltılabilir. Bu, fonksiyonun sadece bir argüman almasını sağlar ve kodun okunabilirliğini artırır.
  2. Fonksiyonları bölmek: Eğer bir fonksiyon birden fazla görev yapıyorsa ve bu nedenle üç veya daha fazla argüman alıyorsa, bu görevleri ayrı fonksiyonlara bölmek daha iyidir. Bu, her fonksiyonun sadece tek bir görevi yerine getirmesini sağlar ve kodun anlaşılabilirliğini artırır.
1
2
3
public void createReport(String title, String author, String content) {  
    // Rapor oluşturma işlemleri
}

Bu fonksiyon, üç argüman alarak bir rapor oluşturmaktadır. Bunun yerine, bu argümanlar bir nesne içinde gruplanarak fonksiyonun karmaşıklığı azaltılabilir:

1
2
3
4
5
6
7
8
9
10
class Report {
    String title;
    String author;
    String content;
    // Constructor ve diğer metodlar
}
 
public void createReport(Report report) {
    // Rapor oluşturma işlemleri  
}

Sonuç olarak üç veya daha fazla parametre alan fonksiyonların karmaşıklığı arttırması ve kodun okunabilirliği düşürmesinden dolayı kullanmaktan kaçınılmalıdır. Yukarıda belirtilen ipuçları takip edilerek daha küçük ve anlamlı fonksiyonlara bölünebilirler.

Argüman Nesneleri

Fonksiyonlara geçirilen birden fazla argümanın tek bir nesne içinde gruplandığı bir tasarım prensibidir. Bu prensip, fonksiyonların daha az sayıda argüman almasını sağlayarak, kodun okunabilirliğini ve anlaşılabilirliğini artırır. Bir fonksiyonun birden fazla argümanı varsa ve bu argümanlar mantıksal olarak bir arada bulunuyorsa, bu argümanları bir nesne içinde gruplamak daha iyidir. Bu, fonksiyonun sadece bir argüman almasını sağlar ve kodun okunabilirliğini artırır.

1
2
3
public void createEmployee(String firstName, String lastName, String address, String phoneNumber) {
    // Çalışan oluşturma işlemleri
}

Bu fonksiyon, dört argüman alarak bir çalışan oluşturmaktadır. Bunun yerine, bu argümanlar bir nesne içinde gruplanarak fonksiyonun karmaşıklığı azaltılabilir.

1
2
3
4
5
6
7
8
9
10
11
class EmployeeInfo {
    String firstName;
    String lastName;
    String address;
    String phoneNumber;
// Constructor ve diğer metodlar
}
 
public void createEmployee(EmployeeInfo employeeInfo) {
    // Çalışan oluşturma işlemleri
}

Bu yaklaşım, fonksiyonun sadece bir argüman almasını sağlar ve kodun okunabilirliğini artırır. Ayrıca, “EmployeeInfo” nesnesi, ileride daha fazla alan eklemeye veya değiştirmeye ihtiyaç duyulduğunda kolayca güncellenebilir.

Fiiller ve İsimler

Bir fonksiyon için iyi bir isim seçmek, fonksiyonun ve değişkenlerinin amacını açıklamak ve o fonksiyonu kullanacak kişilerin fonksiyonun içeriğini anlamalarında büyük anlamda yardımcı olacaktır. Tek argümanı olan fonksiyonlarda, fonksiyon ve argüman çok iyi bir fiil ve isim ikilisi olacak şekilde seçilmelidir.

Örneğin write(name) imzasına sahip bir fonksiyon oldukça açıklayıcıdır.

Hatta writeField(name) bize name’in bir alan (field) olduğunu ve bir yerlere yazılacağını söyler.

İkinci bir örnek olarak assertEquals metodu parametrelerinin sırası önemli olduğundan genellikle karıştırılmaktadır, assertExpectedEqualsActual (expected, actual) olarak yazılarak bu fonksiyonu kullanacak kişiye parametrelerin nasıl beklendiği açık bir şekilde belirtilebilir ve daha okunaklı olabilirdi. Bu yazım şekli, argümanların sırasını hatırlamanın zorunluluğunu ortadan kaldırır.

Yan Etkisi (Side Effects) Olmayan Metotlar Yazılmalıdır

Yan etkisiz fonksiyonlar, fonksiyonun dışındaki herhangi bir durumu değiştirmeyen ve sadece girdi argümanlarına bağlı olarak bir sonuç üreten fonksiyonlardır. Yan etkisiz fonksiyonlar, kodun daha tahmin edilebilir, test edilebilir ve anlaşılabilir olmasına yardımcı olur.

Yan etkisiz fonksiyonlar kullanırken dikkat edilmesi gerekilen bazı noktalar şunlardır:

  1. Global değişkenleri değiştirmemek: Fonksiyonlar, global değişkenlerin değerlerini değiştirmemelidir. Bu, fonksiyonun davranışının dış etkenlerden bağımsız olmasını sağlar ve kodun daha kolay test edilmesine olanak tanır.
  2. Girdi argümanlarını değiştirmemek: Fonksiyonlar, girdi argümanlarının değerlerini değiştirmemelidir. Bu, fonksiyonun sadece girdi argümanlarına bağlı olarak bir sonuç üretmesini sağlar ve kodun daha anlaşılır olmasına yardımcı olur.

Yan etki örneği olarak aşağıdaki kodu inceleyelim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class UserValidator {
 
    private Cryptographer cryptographer;
 
    public boolean checkPassword(String userName, String password) {
 
        User user = UserGateway.findByName(userName);
 
        if (user != User.NULL) {
 
            String codedPhrase = user.getPhraseEncodedByPassword();
 
            String phrase = cryptographer.decrypt(codedPhrase, password);
 
            if ("Valid Password".equals(phrase)) {
 
                Session.initialize();
 
                return true;
 
            }
 
        }
 
        return false;
 
    }
 
}

Kullanıcı adı ve şifre arası eşleşme kontrolü yapan klasik bir algoritma örneği. Burada yan etki olarak göreceğimiz durum, şifre ve kullanıcı adı eşleştiğinde yeni bir oturum başlatılıyor. Burada sıkıntılı olan birkaç durumu sıralamak gerekirse:

  1. Fonksiyonumuz temelde iki iş yapıyor ki bu bizim istemediğimiz bir durum. Fonksiyonların tek bir iş yapmasını bekleriz.
  2. Fonksiyon isminden sadece şifre kontrolü yapılacağını anlayabiliyoruz ancak fonksiyon içerisinde yeni bir oturum başlatıyor. İsimlendirme de problemlidir.
  3. Fonksiyon içerisinde yeni bir oturum başlattığı için mevcutta bulunan oturum bilgileri kaybolabilir.

Bu gibi durumlar geçici bağlanma (temporal coupling) olarak adlandırılır. Şifre kontrolü yeni bir oturum başlatacağı için, yani kontrol işlemi yeni oturum başlatmayla bağlı olduğu için bu kontrolümüzü sadece oturum başlatmak güvenliyken çağırabiliriz. Bu gibi durumlardan kaçınmak, başka çözümler aramak gereklidir ancak mecbur kalıyorsak, fonksiyonun iki iş yaptığını isminde belirterek örneğin, checkPasswordAndInitializeSession, şeklinde kullanmak daha kullanıcı dostu bir yol sunar.

Çıktı Argümanları

Şimdiye kadar argümanları girdiler olarak ele aldık. Ancak çıktı argümanlarından da bahsetmemiz gerekir. Bir örnek üzerinden gidecek olursak aşağıdaki fonksiyon çağrısını inceleyelim:

1
sonucKismiEkle(a);

Burada a argümanının eklenecek sonuç mu veya sonuç kısmının eklenmesi gereken bir nesne mi olduğunu anlayamıyoruz. Eğer a sonuç ise girdi argümanıdır, eğer sonucun ekleneceği bir nesne ise çıktı argümanıdır. Fonksiyon imzasına bakmadan bu karışıklığı aşmamız mümkün değildir.

1
public void sonucKismiEkle(Deneme deneme)

Fonksiyon imzasına baktığımızda bir deneme nesnesine sonuç kısmının eklendiğini görüyoruz, yani a bir çıktı argümanı. Çıktı argümanları bazen gerekli gibi gözükse de aslında nesneye yönelimli dillerin bize sunduklarıyla onlardan kaçına biliriz. Örneğin doğru bir tasarımla aşağıdaki çağrıyı yapalım;

1
deneme.sonucKismiEkle();

Herhangi bir girdi veya çıktı argümanına sahip değiliz. Tam da istediğimiz gibi.

Sonuç olarak, çıktı argümanlarından karışıklığa yol açtığı ve argümanın durumunu değiştirdiği için kaçınılmalıdır. Eğer fonksiyonunuzun bir şeyin durumunu değiştirmesini istiyorsanız bunu sadece sahip olduğu nesnenin durumunu değiştirecek şekilde yapmasını sağlamalısınız.

Komut ve Sorgu Ayırma

Fonksiyonlar ya sizin bir komutunuza bağlı olarak bir iş yapmalı ya da bir sorunuza cevap vermelidir. Örneğin setAge(uncleBob, 26) ve isExists(uncleBob).

Aynı şekilde fonksiyonlar ya bir objenin durumunu değiştirmelidir ya da o objeye ait bilgi dönmelidir. İki işi birden yapmak karmaşıklığa yol açar.

Örneğimizdeki setAge() fonksiyonu çalışma şeklinin şöyle olduğunu düşünelim. Önce gelen kişi nesnesinin var olup olmadığına bakıyor, eğer varsa o kişinin yaş bilgisine gelen değeri koyup true dönüyor, kişiyi bulamadığında da false dönüyor. Sonuç olarak iki iş birden yapmış oluyor. Bunun yerine aşağıdaki gibi komut ve sorgu kısımlarını ayırarak daha doğru bir kullanımı hedeflemeliyiz.

1
2
3
4
5
6
7
8
9
if (isExists(uncleBob)) {
 
    setAge(uncleBob, 26);
 
    . . .
 
}
 
. . .

Exception Yakalamayı Hata Kodu Dönmeye Tercih Edin

Bir market alışveriş uygulamasını basitçe düşünelim. Kişi nesnemiz olsun ve uygulamada bir kişiye ait sadece faturalar ve talepler kaydedilsin. Uygulamaya katılan bir kişi çıkma talebiyle bir istek oluşturdu ve bizimde buna istinaden bu kişi ve ilişkili tüm kayıtları silmemiz gerekiyor. Bunun için yazılmış aşağıdaki kodu inceleyelim.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (kisiServis.sil(kisi) == E_OK) {
 
    if (faturaKaydiServis.kisiyeAitTumunuSil(kisi.getTc()) == E_OK) {
 
        if (talepServis.kisiyleIliskiliTumunuSil(kisi.getId()) == E_OK) {
 
            logger.log("kisi ve ilişkili tüm kayıtlar silindi");
 
        } else {
 
            logger.log("talep kayıtları silinemedi");
 
        }
 
    } else {
 
        logger.log("fatura kayıtları silinemedi");
    }
 
} else {
 
    logger.log("kişi silme işlemi başarısız");
 
    return E_ERROR;
 
}

Üç aşamada silme işlemi gerçekleşiyor ve eğer sistem başarılı olursa E_OK koduyla bunu bildiriyor. Eğer bir hata alırsak da bu durumu kaydediyoruz. Gördüğünüz gibi iç içe girift if else koşulları oluştu ki baştan beri kaçınmamız gerektiğinden bahsettik. Ayrıca hata kodlarının bir enum sınıfından sağlandığını düşünelim. Ve bu sınıf birçok yerde kullanılsın veya bir kütüphane bize bu sınıfı sağlasın. Eklenen her yeni hata kodu için kendi kodumuzu güncellememiz gerekebilir. Bu da bize ekstra iş yükü getireceğinden aslında hata kodlarından neden kaçınmamız gerektiğine dair bir başka örnektir.

Peki hata kodu dönmektense exception kullanmayı deneyelim:

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
 
    kisiServis.sil(kisi);
 
    faturaKaydiServis.kisiyeAitTumunuSil(kisi.getTc());
 
    talepServis.kisiyleIliskiliTumunuSil(kisi.getId());
 
} catch (Exception e) {
 
    logger.log(e.getMessage());
 
}

Bu sefer de try catch kullanımında hata yakalama işveliyle kodun silme işlemi birbirine karıştı ve kod yapısında karmaşıklığa sebep olmuştur. Bu yüzden try ve catch bloklarının içini ayrı fonksiyonlara çıkmak bu karışıklığı giderebilir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void sil (Kisi kisi){
 
    try {
 
        kisiVeIliskiliTumKayitlariSil(kisi);
 
    } catch (Exception e) {
 
        logError(e);
 
    }
 
}

private void kisiVeIliskiliTumKayitlariSil (Kisi kisi) throws Exception {
 
    kisiServis.sil(kisi);
 
    faturaKaydiServis.kisiyeAitTumunuSil(kisi.getTc());
 
    talepServis.kisiyleIliskiliTumunuSil(kisi.getId());
 
}

private void logError(Exception e) {
    logger.log(e.getMessage());
}

Görüldüğü üzere artık sil fonksiyonu kendi içinde sadece hata yakalama işlevini görür. Öte yandan kisiVeIliskiliTumKayitlariSil fonksiyonuysa bizim bir kişiye ait tüm silme işlemlerimizi yapacak hale geldi. Hata yakalama bu fonksiyon özelinde görmezden gelinip sadece silme işlemine odaklanılabilir.

Hata Yakalama Tek Bir İş Kavramına Dahildir

Fonksiyonların tek bir iş yapması gerektiğinden bahsedip çeşitli örnekler vermiştik. Hata yakalama da bunlardan biridir. Dolayısıyla eğer bir fonksiyon try blokuna sahipse bu, fonksiyon imzasından hemen sonra gelmelidir, yani en tepede olmalıdır ve catch/finally bloklarından sonra herhangi bir ekstra işlem bulunmamalıdır.

Kendini Tekrar Etmeme Prensibi (Don’t Repeat Yourself DRY)

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Her bilgi parçasının bir sistem içerisinde tek, açık ve yetkili bir temsili olmalıdır.

Andy Hunt ve Dave Thomas, The Pragmatic Programmer kitabında DRY prensibini bu şekilde tanımlamış. Bu prensip yazılım kalıplarının tekrarlanmasını ve bunun sonucu oluşabilecek karmaşıklığın önüne geçilip fazlalıkların elimine edilmesini amaçlar. Bizlere sürdürülebilirlik, okunabilirlik, tekrar kullanılabilirlik gibi avantajlar sağlamasının yanında aslında test yükümüzü de hafifletir. Birçok başka prensip ve pratik, kimisi tamamen kimisi de bir parçası olarak, kendini tekrar etmeme prensibini sağlamayı amaçlar. Örnek olarak Component Oriented Programming veya Aspect Oriented Programming verilebilir.

Özetlemek gerekirse; temiz kod geliştirme prensiplerine uygun olarak, programlama esnasında fonksiyonların nasıl ele alınması gerektiğini, isimlendirmesinden uzunluğuna, yazımından argümanlarına, argümanlarının sayısına göre değerlendirilmesine, detaylandırılmasına kadar bu ve benzeri diğer çeşitli yönleriyle bölümümüzde açıklamaya çalıştık. Bunlara uyduğumuzda fonksiyonlarımız kısa, doğru isimlendirilmiş ve iyi organize edilmiş olacaktır. Bölümde ele alınan kurallar, tavsiyeler veya genel bir ifade ile yaklaşımlar zamanla kod yazdıkça oturacak kavramlardır. Birden hepsini uygulayamayabiliriz ancak her kod yazımımızda kendi değerlendirmemizi bu kurallar çerçevesinde yaparak belirli alışkanlıklar kazanabiliriz. Usta programcılar geliştirdikleri sistemleri kod parçaları olarak değil, anlatılacak hikâyeler olarak ele alırlar dolayısıyla bu bölümde ele aldığımız kurallar ve yaklaşımlar bizlerin de hikâyesini daha iyi ifade edebilmesi için elzemdir.

Bölüm 4: Yorumlar

Yorumların yazılmasının amacı, yazılımcının kod ile iyi ifade edemediği durumları anlatmak istemesidir. Anlatılmak istenen durumun yorumsuz ifade edilemediğinde kodda kusur vardır. Yazılımcı yorum yazmaya başladıysa kodu gözden geçirerek yorum yazmak yerine başka yollar olup olmadığına bakmalıdır.

Yazılan yorumların üzerinden zaman geçtikçe, yorumlar yazıldığı koddan o kadar uzak ve yanlış olmaktadır. Çünkü yazılımcılar yorumları güncellemezler. Yanlış yorumlar yanlış yönlendirebilirler. Bu nedenle hiç yorum yazılmasa daha iyidir. Kod okunarak gerçek ve doğru bilgiye ulaşılabilinir.

Yorum Yerine Kod ile Anlatma

  1. Kodun kendisi açıklayıcı olmalıdır: İyi yazılmış kod, ne yaptığını ve neden yapıldığını anlatmalıdır. Bu, kodun okunabilir ve anlaşılır olması için uygun değişken isimleri, fonksiyon isimleri ve sınıf isimleri kullanmayı gerektirir.
  2. Yorumlar yerine kod kullanılmalıdır: Yorumlar yerine, kodun kendisi ile açıklama yapmaya çalışılmalıdır. Yorumlar zamanla güncellenmeyebilir ve kodla uyumsuz hale gelebilir. Kodun kendisi, ne yapıldığını ve neden yapıldığını açıklamak için yeterli olmalıdır.
  3. Yorumlar sadece gerçekten gerekli olduğunda kullanılmalıdır: Yorumlar, sadece kodun anlaşılmasına yardımcı olacak ve karmaşıklığı azaltacak durumlarda kullanılmalıdır. Örneğin, algoritmanın karmaşık bir kısmını açıklamak veya dış kaynaklardan alınan kod parçalarını belirtmek için yorumlar kullanılabilir.
  4. Yorumlar doğru ve güncel tutulmalıdır: Eğer yorumlar kullanılacaksa, kodla uyumlu ve güncel tutulmalıdır. Yanıltıcı veya eski yorumlar, kodun anlaşılmasını zorlaştırabilir ve hatalara yol açabilir.

İyi Yorumlar

Yasal Yorumlar

Yasal yorumlar, genellikle telif hakkı ve lisans bilgilerini içeren yorumlardır. Bu tür yorumlar, kodun başında yer alır. Kodun kullanımı, dağıtımı ile ilgili kısıtlamaları ve koşulları belirtir. Yasal yorumlar, kodun sahibinin haklarını korumaya yardımcı olur ve kodun kullanımına ilişkin yasal sorumlulukları açıklar.

1
2
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.

Yasal yorumların güncel tutulması gerekir. Ayrıca, yasal yorumların kodun işleyişine dair bilgi vermediği ve kodun anlaşılabilirliğini artırmadığı gözlemlenmiştir. Bu nedenle, yasal yorumların dışında, kodun anlaşılabilirliğini ve bakımını kolaylaştırmak için bu tarz yorumların mümkün olduğunca az ve etkili kullanılması önerilir. Tüm şartlar ve koşulları yorum içine koymaktansa, mümkünse standart bir lisans ya da dış bir dokümana referans tercih edilmelidir.

Bilgilendirici Yorumlar

Kodun daha iyi anlaşılmasına yardımcı olmak için yazılırlar. Bu tür yorumlar, kodun işlevselliğini, kullanım amacını ve bazen de içerdiği algoritmanın temel mantığını açıklamaya çalışır. İyi yazılmış bilgilendirici yorumlar, kodun okunabilirliğini ve bakımını kolaylaştırırlar. Ancak aşağıdaki bilgilendirici yorumlar gereksizdir.

Aşağıdaki örnek incelenecek olursa abstract bir metodun dönüş değerini açıklıyor. Fonksiyon ismini responderBeingTested şeklinde değiştirerek yorum gereksiz hale getirilebilirdi.

1
2
// Returns an instance of the Responder being tested.
protected abstract Responder responderInstance();

Diğer bir örnek:

1
2
// format matched kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile("\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

Yorum yazmak yerine eğer kod, tarihlerin ve zamanların formatını dönüştüren bir sınıfa taşınsaydı çok daha açık ve temiz olabilirdi.

Bilgilendirici yorumları yazarken dikkat edilmesi gereken temel noktalar:

  1. Kısa ve öz olunmalıdır: Yorumlar, kodun işlevselliğini ve amacını açıklarken, gereksiz detaylardan kaçınılmalıdır. Yorumlar ne kadar kısa ve öz olursa, okuyan kişinin anlaması o kadar kolay olacaktır.
  2. Güncel tutulmalıdır: Kod değişiklikleri yapıldıkça, yorumların da güncellenmesi önemlidir. Eski ve güncel olmayan yorumlar, kodun anlaşılmasını zorlaştırabilir ve yanıltıcı olabilir.
  3. Teknik terimler açıklanmalıdır: Kod içinde kullanılan teknik terimler ve kısaltmalar, yorumlarda açıklanmalıdır. Bu, okuyucunun kodu daha hızlı ve kolay anlamasına yardımcı olacaktır.
  4. Kodun niçin yazıldığı belirtilmelidir: Yorumlar, kodun niçin yazıldığını ve hangi problemleri çözmeye yönelik olduğunu açıklamalıdır. Bu, okuyan kişi kodun amacını ve işlevselliğini daha iyi kavramasına yardımcı olacaktır.

Açıklayıcı Yorumlar

Belirsiz bir argümanın veya dönüş değerinin anlamını daha okunabilir hale getirmek için çeviri yapmak yararlı olabilir. Genel olarak, argüman veya dönüş değerlerinin kendisini anlaşılır kılmak daha iyidir; ancak dışarıdan kullandığımız kütüphanenin bir parçası olduğunda veya değiştiremediğimiz bir kodda olduğunda, açıklayıcı yorum yazmak faydalı olabilir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void testCompareTo() throws Exception {
  WikiPagePath a = PathParser.parse("PageA"); 
  WikiPagePath ab = PathParser.parse("PageA.PageB"); 
  WikiPagePath b = PathParser.parse("PageB"); 
  WikiPagePath aa = PathParser.parse("PageA.PageA"); 
  WikiPagePath bb = PathParser.parse("PageB.PageB"); 
  WikiPagePath ba = PathParser.parse("PageB.PageA");
  assertTrue(a.compareTo(a) == 0); // a == a 
  assertTrue(a.compareTo(b) != 0); // a != b
  assertTrue(ab.compareTo(ab) == 0);  // ab == ab
  assertTrue(a.compareTo(b) == -1); // a < b
  assertTrue(aa.compareTo(ab) == -1);   // aa < ab 
  assertTrue(ba.compareTo(bb) == -1); // ba < bb
  assertTrue(b.compareTo(a) == 1); // b > a
  assertTrue(ab.compareTo(aa) == 1);  // ab > aa 
  assertTrue(bb.compareTo(ba) == 1);// bb > ba
}

Örnekte, yorumların kodun anlaşılmasına yardımcı olduğu gözükmektedir. Ancak, yorumların yanlış olma riski de vardır. Örneği inceleyerek doğru olduklarını doğrulamanın ne kadar zor olduğunu gözlemlenebilir. Bu durum, açıklamanın neden gerekli olduğunu ve neden riskli olduğuna örnek gösterilebilir. Bu tür yorumlar yazmadan önce, daha iyi bir yol olup olmadığına dikkatlice karar verilmelidir ve ardından doğru olduklarından emin olmak için dikkatli bir şekilde kontrol edilmelidir. Açıklayıcı yorumlar, kodun anlaşılabilirliğini artırarak, yazılım geliştirme sürecini daha verimli hale getirebilir. Ancak, doğru ve güncel olduklarından emin olmak için düzenli olarak kontrol edilmesi gerekir.

Yazılım geliştirme sürecinde, bazen diğer programcıları belirli sonuçlar hakkında uyarmak faydalı olabilir. Bu tür uyarılar, kodun daha sağlıklı çalışmasına katkıda bulunabilir ve gelecekteki hataları önlemeye yardımcı olabilir. Testin uzun süre çalışabileceği durumlar olabilir. Bu durumlarda diğer geliştiricileri uyarmak faydalı olabilir:

1
2
3
4
5
6
7
8
9
// Çalıştırmadan önce biraz zamanınızın olduğundan emin olun.
public void _testWithReallyBigFile() {
    writeLinesToFile(10000000);
    response.setBody(testFile);
    response.readyToSend(this);
    String responseString = output.toString();
    assertSubString("Content-Length: 1000000000", responseString);
    assertTrue(bytesSent > 1000000000);
}

Günümüzde, test durumunu @Ignore özelliği ile devre dışı bırakarak ve uygun bir açıklama ekleyerek kapatabiliriz:

1
@Ignore("Çalıştırmak için çok uzun sürer")

TODO Yorumları

Yazılımcının şu anda yapamayacağı ancak daha sonra yapılması gereken işlerini belirtir. TODO kullanım amaçlarından biri kullanımdan kaldırılması planlanan bir özelliği silmek için hatırlatıcı olabilir. Diğer amacı başka bir kişinin bir sorunu incelemesi için bir talep olabilir. Planlanan bir duruma bağlı olarak yapılması gereken bir değişikliği hatırlatma amacıyla da kullanılabilir. TODO yorumları, kötü kodu sistemde bırakma bahanesi olmamalıdır.

1
2
3
4
//TODO-MdM these are not needed
// We expect this to go away when we do the checkout model protected VersionInfo makeVersion() throws Exception
{
return null; }

Kodunuzun TODO’larla dolu olmasını istemeyiz. Bu nedenle, düzenli olarak kodumuzu dolaşmalı ve mümkün olanları kaldırmalıyız.

Ayrıntıları İle Açıklamak İçin Yazılan Yorumlar (Amplification)

Yorumların kodun anlamını ve amacını vurgulamak için kullanılması gerektiği anlamına gelir. Yani, yorumlar kodun ne yaptığını değil, neden ve nasıl yapıldığını açıklamalıdır. Bu, kodun daha anlaşılır ve bakımı kolay hale gelmesine yardımcı olur.

1
2
3
4
5
6
String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting 
// spaces that could cause the item to be recognized
// as another list.
new ListItemWidget(this, listItemContent, this.level + 1); 
return buildList(text.substring(match.end()));

Halka Açık API’lar da Javadocs Yorumları

Javadoc, Java programlama dilinde yazılan kodların belgelendirilmesi için kullanılan bir yorumlama sistemidir. Javadoc yorumları, kodun işlevselliğini ve kullanımını açıklamak için özel biçimlendirilmiş yorumlardır. Bu yorumlar, daha sonra otomatik olarak HTML formatında API belgeleri oluşturmak için kullanılabilir. API’leri, dış kullanıcıların ve geliştiricilerin uygulamanızın veya kütüphanenizin işlevselliğine erişmesine izin veren arayüzlerdir. Bu nedenle, bu API’lerin doğru ve eksiksiz bir şekilde belgelendirilmesi önemlidir. Bu belgelendirme Javadocs ile yapılır. JavaDocs yorumlarını hazırlarken dikkat edilmesi beklenen başlıklar:

  1. Açıklamalar: API’nin ne yaptığını ve nasıl kullanılması gerektiğini açıklayan genel açıklamalar eklenmelidir.
  2. Parametreler: API’nin alacağı tüm parametrelerin açıklamalarını ve beklenen değer türleri belirtilmelidir.
  3. Dönüş değerleri: API’nin döndüreceği değerlerin açıklamalarını ve türleri belirtilmelidir.
  4. Hatalar ve istisnalar: API’nin ne tür hatalar ve istisnalar fırlatabileceği belirtilmelidir.

Kötü Yorumlar

Gereksiz veya Anlamsız Yorumlar (Mumbling)

“Mumbling” terimi, kodun anlaşılmasını zorlaştıran ve okuyucunun kafasını karıştıran gereksiz veya anlamsız yorumlara işaret eder. Bu tür yorumlar, kodun okunabilirliğini ve bakımını olumsuz etkiler. Bu yorumlardan kaçınmak için aşağıdaki başlıklar dikkate alınabilir:

  1. Yorum yazmadan önce, kodun kendisinin anlaşılır ve açık olup olmadığını kontrol edilmelidir. Eğer kod zaten anlaşılır ve açıksa, yorum yazmaya gerek yoktur.
  2. Yorumlar kısa ve öz tutulmalıdır. Uzun ve karmaşık yorumlar, okuyucunun dikkatini dağıtabilir ve kodun anlaşılmasını zorlaştırabilir.
  3. Yorumlar güncel tutulmalıdır. Kod değişiklikleri yapıldığında, ilgili yorumları da güncellemeyi unutulmamalıdır. Eski ve güncel olmayan yorumlar, kodun anlaşılmasını zorlaştırır ve hatalara yol açabilir.

Gereksiz Yorumlar

Kodun anlaşılabilirliğine katkıda bulunmayan ve genellikle kodun işlevini tekrarlayan yorumlardır. Bu tür yorumlar, kodun okunabilirliğini azaltabilir ve zamanla güncel olmayan bilgiler içerebilir.

1
2
3
4
// Bu işlem iki sayıyı toplar
public int topla(int a, int b) {
    return a + b;
}

Örnekte, yorum gereksizdir çünkü topla fonksiyonunun ne yaptığını zaten açıkça anlaşılmaktadır.

Burada ise Apache Tomcat’in kaynak kodlarından alınmış, tamamıyla gereksiz ve faydasız bir yorum örneği var. Bu yorumlar sadece karmaşaya ve belirsizliğe sebep olur, belgelemeye ise hiçbir faydası olmaz:

1
2
3
4
5
6
7
8
9
10
public abstract class ContainerBase implements Container, Lifecycle,Pipeline, MBeanRegistration, Serializable {    /**
     * The processor delay for this component.
     */
    protected int backgroundProcessorDelay = -1;    /**
     * The lifecycle event support for this component.
     */
    protected LifecycleSupport lifecycle = new LifecycleSupport(this);    /**
     * The container event listeners for this Container.
     */
    protected ArrayList listeners = new ArrayList();.....

İyi bir kod, yorumlara gerek kalmadan kodun amacını ve işlevini anlatmalıdır. Yalnızca kodun işleyişini veya amacını açıklamak için yorum kullanılmalıdır. Eğer kodunuz zaten bu bilgileri açıkça sunabiliyorsa, yorum eklemeye gerek yoktur.

Zorunlu Yorumlar

Her fonksiyonun bir Javadoc’u ya da her değişkenin bir yorumu olması gerektiğini söyleyen kurallar yanlıştır. Örneğin şu koddaki yorumlar mevcut koda hiçbir şey katmaz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * @param title             The title of the CD
 * @param author            The author of the CD
 * @param tracks            The number of tracks on the CD
 * @param durationInMinutes The duration of the CD in minutes
 */
public void addCD(String title, String author, int tracks, int durationInMinutes) {
    CD cd = new CD();
    cd.title = title;
    cd.author = author;
    cd.tracks = tracks;
    cd.duration = duration;
    cdList.add(cd);
}

Yanıltıcı Yorumlar

Yazılım geliştirme sürecinde kullanılan yorumların bazen yanıltıcı olabilmektedir. Yanıltıcı yorumlar, kodun anlaşılmasını zorlaştırarak ve hatalara yol açarak, yazılım geliştirme sürecini olumsuz etkileyebilir. Bu tür yorumlar, genellikle aşağıdaki durumlarla ilişkilendirilir:

  1. Yanlış bilgi içeren yorumlar: Yorumlar, kodun işleyişine dair yanlış veya eksik bilgi sağlayabilir. Bu durum, kodu okuyan ve anlamaya çalışan kişilerin yanlış varsayımlarda bulunmasına neden olabilir.
  2. Eski yorumlar: Kodun sürekli güncellenmesi ve değiştirilmesi durumunda, yorumlar da güncellenmeli ve kodun mevcut durumunu yansıtmalıdır. Eski yorumlar, kodun eski sürümüne dair bilgi sağlayarak, mevcut durumu anlamayı zorlaştırabilir.

Noise Yorumlar

Kötü yorumların yazılım kodlarında anlamsız veri çokluğuna yol açabilmektedir. Zamanla bu yorumlar göz ardı edilmektedir. Bu tür yorumlar, kodun anlaşılabilirliğine katkıda bulunmaz ve bazen kodun yanıltıcı olmasına neden olabilirler. Yorumların yerine kodu temizlemeye ve yapıyı iyileştirmeye odaklanmak daha iyi bir yaklaşımdır.

1
2
/** The day of the month. */
private int dayOfMonth;

Bir fonksiyon veya değişkenin kullanılabileceği durumda yorum kullanmamalıdır. Anlamsız yorumlardan kaçınmak için aşağıda yazılanlar dikkate alınabilir.

  1. Yorumlar kullanmak yerine, kodun anlaşılabilirliğini artırmak için fonksiyon ve değişken isimleri kullanılabilir.
  2. Kodun okunabilirliğini artırmak için, yorumları gereksiz kılacak şekilde kodu yeniden düzenlemek önemlidir.
  3. Kod yazarken, önce yorum yazmak ve ardından yorumu karşılayacak kodu yazmak yerine, kodun kendisinin açıklayıcı olmasını sağlamak daha iyi olabilir.
1
2
3
// does the module from the global list <mod> depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))

Yukarıdaki kod metodlara bölünerek yorum yazmadan anlaşılacak hale getirilebilir.

1
2
3
ArrayList moduleDependees = smodule.getDependSubsystems(); 
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))

İşaretçi Yorumlar

Kaynak dosyasındaki belirli bir konumu işaretlemek genellikle gereksizdir ve düzeni bozar. İşaretçi olarak kullanılan yorumlar, özellikle sonunda eğik çizgi dizisi bulunanlar, karmaşıklığa neden olabilir. Çok nadiren ve sadece önemli fayda sağladığında kullanmak, dikkat çekici ve etkili olmasını sağlar. Aşırı kullanmak, onların göz ardı edilmesine neden olur.

1
// Actions //////////////////////////////////

Kapama Parantezlerine Eklenen Yorumlar

Bazen yazılımcılar kapama parantezlerine özel yorumlar eklerler, aşağıdaki örnekte olduğu gibi. Uzun fonksiyonlarda bir anlam ifade etse de küçük fonksiyonlarda sadece kalabalık sağlar. Bu nedenle, eğer bir kapama parantezine yorum ekleniyorsa, bunun yerine fonksiyonu kısaltmak denenebilir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class WC {
    public static void main(String[] args) {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String line;
        int lineCount = 0;
        int charCount = 0;
        int wordCount = 0;
        try {
            while ((line = in.readLine()) != null) {
                lineCount++;
                charCount += line.length();
                String words[] = line.split("\\W");
                wordCount += words.length;
            } //while
            System.out.println("wordCount = " + wordCount);
            System.out.println("lineCount = " + lineCount);
            System.out.println("charCount = " + charCount);
        } // try
        catch (IOException e) {
            System.err.println("Error:" + e.getMessage());
        } //catch
    } //main
}

Kapatılmış Kodlar

Kod içerisinde bazı satırlar yoruma alınıp silinmez. Aşağıda örnekleri gösterilmektedir.

1
2
3
4
5
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(),formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));

Apache Commans‘dan alınan örnek:

1
2
3
4
5
6
7
8
9
10
11
12
this.bytePos = writeBytes(pngIdBytes, 0);
//hdrPos = bytePos;
writeHeader();
writeResolution();
//dataPos = bytePos;
if(writeImageData()) {
    writeEnd();
    this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos);
} else {
    this.pngBytes = null;
}
return this.pngBytes;

Sonuç

Temiz kod yazılım geliştiricilerin profesyonel olarak işlerini yaparken en iyi uygulamaları benimsemelerini ve kodlarının okunabilir, bakımı kolay ve etkili olmasını sağlamalarını hedefler. Temiz kod yazmak için yazılım geliştiricilerin kod kalitesine önem vermesi ve sürekli olarak iyileştirmeye çalışması gerekmektedir. Yazılım geliştiricilerinin temiz kod yazmasını sağlamak için Robert C. Martin’in yazmış olduğu Clean Code kitabının ilk dört bölüm çıktılarını paylaşmak istedik. İlk bölümde temiz kod ile ilgili bilgiler verilerek yazarların temiz kod hakkında düşüncelerine yer verilmiştir. İkinci bölümde, değişkenler, fonksiyonlar, sınıflar ve diğer kod öğeleri için doğru isimleri seçmenin önemi vurgulanmıştır. Üçüncü bölümde, fonksiyonların yazılırken nelere dikkat edilmesi gerektiği ile bilgiler paylaşılmıştır. Dördüncü bölümde, kod içerisinde yorum yazmanın iyi ve kötü pratikleri örnekler ile açıklanmıştır.

Kaynakça

  • Clean Code: A Handbook of Agile Software Craftsmanship, Martin, R.C., 2009, Pearson Education, Bölüm 1, Bölüm 2, Bölüm 3 & Bölüm 4
  • [https://udemy.com/course/writing-clean-code/], Maximillian Schwarzmüller
  • The pragmatic programmer from journeyman to master, Andy Hunt & Dave Thomas, 2000, Addison-Wesley