Okunabilir Kod Sanatı — 2
Döngüleri ve Mantığı Basitleştirmek
Daha önceki yazımızda yüzeysel-seviye iyileştirmelerden bahsettik. Tek satır üzerinde kodun okunulabilirliği, fazla risk ve efor gerektirmeden, nasıl artırılır onu gördük.
- Bu yazı Art of Readable Code kitabından alıntıdır. Orjinal halini buradan alabilirsiniz.
- Yazının önceki bölümünü buradan bulabilirsiniz.
Bu yazımızda, döngüleri ve mantığı nasıl basitleştirebiliriz ona bakacağız.
Bunu da kodunuzun “zihinsel yükünü(mental baggage)” azaltarak yapmaya çalışacağız. Karmaşık bir döngü, kocaman bir ifade ya da çok sayıda değişken gördüğünüz her seferinde, bu sizin zihninize yük yükler. Bu “kolay anlaşılırın” tam tersidir. Kodda çok fazla zihinsel yük varsa, hatalar bulmak daha zor olmaya başlar, kodun değişimi daha zor olur ve çalışmak daha az zevkli olur.
Kontrol Akışını Kolay Okunabilir Yapmak
Kodun hiç koşulu ya da kontrol akışı olmasaydı, okuması çok kolay olurdu. Kodda ki zıplama ve dallanmalar kodu hızlıca karmaşık bir hale sokuyor.
Bütün koşullarınızı, döngülerinizi ve diğer kontrol akışlarınızı mümkün olduğunca “doğal” yapın. Okuyucu durup tekrar tekrar okuma gereği duymasın.
Koşullardaki Argüman Sırası
Hangi kod daha okunulabilir?
if (length >= 10)
ya da
if (10 <= length)
Çoğu programcı için ilki daha okunulabilirdir. Kodun doğallığının aynısı, dillerde de bu böyledir. Örneğin, “Eğer en az 18 yaşındaysanız” deriz. “18 yaş, sizin yaşınızdan küçük veya eşitse” demeyiz.
YODA Gösterimi: Kullanımı Faydalı
Bazı dillerde(C, C++ gibi) if şartının içinde bir atama yapmaya izin verilmiştir.
if (obj = NULL) ...
Programcı aslında şunu kastetmiştir.
if (obj == NULL) ...
Bu tür hataları önlemek için, argümanların yerini değiştirebilir. Bu şekilde kod yanlışlıkla tek eşittir(=) ile yazılmış olsa bile derlenmez.
if (NULL == obj) ...
YODA gösterimi, eskilerde kalsa bile bazen kullanışlı olabilir.
if/else Blok Sırası
if/else ifadesi yazarken, yerlerini değiştirmekte özgürsündür. Şöyle de yazabilirsiniz,
if (a == b) {
// Case One ...
} else {
// Case Two ...
}
Ya da,
if (a != b) {
// Case Two ...
} else {
// Case One ...
}
Genel olarak ilk kod yazma şeklini seçmenin şöyle iyi sebepleri olabilir;
- Negatif durum yerine positif durumla baş etmeyi tercih edersiniz. if(!debug) yerine if (debug) yazarsınız.
- Daha basit durumla ilk olarak başa çıkmayı tercih edersiniz.
- Daha ilginç veya şüpheli durumla ilk başta uğraşmayı tercih edersiniz.
Şöyle bir örnek verebiliriz,
if (!url.HasQueryParameter("expand_all")) {
response.Render(items);
//...
} else {
for (int i = 0; i < items.size(); i++) {
items[i].Expand();
}
//...
}
Bu kodu görünce beynimiz hemen expand_all durumunu düşünürüz. Birine, “pembe bir fil düşünme” demeye benzer. Bunun hakkında düşünmekten geri duramazsınız. Bu örneği şu şekilde yazmak daha şık olur,
if (url.HasQueryParameter("expand_all")) {
for (int i = 0; i < items.size(); i++) {
items[i].Expand();
}
//...
} else {
response.Render(items);
//...
}
Diğer tarafta, bazı durumlarda negatif durum daha basit ve tehlikeli olabilir. Bu yüzden ilk olarak baş etmek gerekir.
if not file:
# Log the error ...
else:
# ...
Üçlü(Ternary) Operatörü
Üçlü operatörü(<koşul> ? a : b), if (<koşul>) { a } else { b } gösteriminin kısa şeklidir. Okunabilirlik olarak ise tam tersi etki yapabilir. Şöyle bir örnekte üçlü operatörü daha okunabilirdir.
time_str += (hour >= 12) ? "pm" : "am";
Üçlü operatör ile yazmak istemeseydik, daha biraz gereksiz gibi gözüküyor.
if (hour >= 12) {
time_str += "pm";
} else {
time_str += "am";
}
Bununla birlikte, üçlü operatörler çabuk şekilde okunması zor hale gelebilir.
return exponent >= 0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent)
Bu durumda üçlü operatörü seçmemek daha mantıklı duruyor.
Amacımız kod satırını en aza indirmek yerine, daha iyi bir metrik olarak, insanların kodu anlama süresini en aza indirmek olmalıdır.
if (exponent >= 0){
return mantissa * (1 << exponent);
} else {
return mantissa / (1 << -exponent);
}
Varsayılan olarak, if/else kullanın. Sadece en basit durumlarda üçlü operatörü kullanın.
do/while Döngülerinden Kaçının
Birçok dilde, do {ifadeler} while(koşul) döngüsü vardır.
do {
if (node.getName().equals(name)) {
return true;
}
node = node.next();
} while (node != null);
do/while döngüsünde tuhaf olan şey, kod bloğu koşula göre çalıştırılabilir de, çalıştırılmayabilir de. Bu kafa karıştırıcı bir durumdur.
Neyse ki, tüm do/while döngüleri while döngüsü olarak yazılabilir.
C++ yaratan adam, Bjarne Stroustrup, bu konu hakkında şöyle der:
Deneyimime göre, do-ifadeleri hatalara ve kafa karışıklığına yol açar. Yukarıda görebileceğim şartları tercih ederim. Sonuç olarak, do-ifadeleri kullanmaktan kaçınırım.
Fonksiyondan Erken Dönün
Bazı programcılar, fonksiyonların birçok return ifadesi olmaması gerektiğine inanırlar. Bu bizce doğru değildir. Fonksiyondan erken dönmek gayet mantıklıdır. Örneğin;
public boolean Contains(String str, String substr){
if(str == null || substr == null) return false;
if(substr.equals("")) return true;
// ...
}
Baştaki bu çıkış için gerekli koruyucu ifadeler olmasa, fonksiyon çok da sağlam çalışmaz.
Modern dillerde bu çıkış işlemleri için daha karmaşık çözümler sunar. Bunlar;
İçiçe İfadeleri En Aza İndirmek
Derin bir şekilde içiçe geçmiş kodun anlaşılması zordur. Çok da zor olmayan bir örneği şöyle verebiliriz. Bunda bile insanın anlaması için iki kere kontrol etmesi gerekiyor.
if (user_result == SUCCESS) {
if (permission_result != SUCCESS) {
reply.WriteErrors("error reading");
reply.Done();
return;
}
reply.WriteErrors("");
} else {
reply.WriteErrors(user_result);
}
reply.Done();
Koyu kısım sonradan koda eklenmiş gibi duruyor. Kodu takip ederken, user_result ve permission_result gibi içiçe yer alan değişken değerlerini takip etmek gerekiyor. Bu da gittikçe zorlaşan bir durumdur.
Aslında kod şöyle olsaydı işimiz çok kolay olurdu. Kod da çok anlaşılır olurdu.
if (user_result == SUCCESS) {
reply.WriteErrors("");
} else {
reply.WriteErrors(user_result);
}
reply.Done();
Büyük ihtimalle koda sonradan yeni istek geldi. Programcı da en kolay nereye eklerim diye düşünüp SUCCESS içine yeni kodu eklemiş.
Kodda değişiklik yapacağın zaman taze bir perspektiften koda bakın. Biraz geri çekilip koda bütün olarak bakmak gerekiyor.
Şimdi kodu fonksiyondan erken dönme ile iyileştirelim. Bunu da, “hatalı durumlar” ile ilk başa çıkarak yapabiliriz.
if (user_result != SUCCESS) {
reply.WriteErrors(user_result);
reply.Done();
return;
}
if (permission_result != SUCCESS) {
reply.WriteErrors(permission_result);
reply.Done();
return;
}
reply.WriteErrors("");
reply.Done();
Yeni kodumuz sadece bir seviye içerde. Anlaşılması da çok daha kolay.
Döngüler İçindeki İçiçe İfadeleri Kaldırmak
Erken dönme tekniği her zaman uygulanabilir olmaz. Şu kod örneğine bakalım,
for (int i=0; i<results.size(); i++) {
if (results[i] != NULL) {
non_null_count++; if (results[i]->name != ""){
cout << "Considering candidate..."<<endl;
//...
}
}
}
Döngü içinde, continue ile erken dönme yöntemine benzer bir yöntem kullanabiliriz.
for (int i=0; i<results.size(); i++) {
if (results[i] != NULL) continue;
non_null_count++; if (results[i]->name == "") continue;
cout << "Considering candidate..."<<endl;
//...
}
Genelde continue ifadesi kafa karıştırıcıdır ama bu durumda kullanmak mantıklıdır.