Koşullu ifadelerin mantıkları projeniz/programınız büyüdükçe giderek daha karmaşık hale gelir. Bu durumla mücadele edebileceğimiz oldukça fazla refactoring tekniği bulunmaktadır.
Karmaşık bir koşulunuz var (if-then
/else
veya switch
) bu durumda projedeki diğer geliştiriciler bu koşullu ifadeyi anlamak için zaman kaybedecektir.
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
}
else {
charge = quantity * summerRate;
}
Koşulun karmaşık kısımlarını ayrı yöntemlere ayırabilirsiniz. Bu okunurluğu arttıracaktır.
if (isSummer(date)) {
charge = summerCharge(quantity);
}
else {
charge = winterCharge(quantity);
}
Bir kod parçası ne kadar uzun olursa anlaşılması da o kadar zor olur. Kodunuz, koşullarla doldurulduğunda anlaşılması daha da gü. hale gelir:
-
then
bloğundaki kodun ne yaptığını bulmakla meşgulken ilgili koşulun ne olduğunu unutabilirsiniz. Ki uzun koşulları ifadelerde bu çok olağan bir durumdur. -
else
kısnını ayrıştırmakla meşgulken, kodunthen
kısmının ne yaptığını unutursunuz.
-
Koşullu kodu açıkça adlandırılmış yöntemlere çıkararak, kodda daha sonra değişiklik veya bakımını yapacak olan kişinin (örneğin, iki ay sonraki siz) hayatını kolaylaştırırsınız.
-
Bu refactoring tekniği aynı zamanda koşullardaki kısa ifadeler için de geçerlidir.
isSalaryDay()
işlevi, tarihleri karşılaştırmaya yönelik koddan çok daha güzel ve daha açıklayıcıdır.
-
Koşulu, Extract Method. tekniği aracılığıyla ayrı bir yönteme çıkarın.
-
then
veelse
blokları için işlemi tekrarlayın.
Aynı sonuca veya eyleme yol açan birden fazla koşulunuz olduğu durumlarda bazı sorunla karşılaşabilirsiniz. Sonuçta koşullu ifadeniz yanlış koşulda takılabilir.
double disabilityAmount() {
if (seniority < 2) {
return 0;
}
if (monthsDisabled > 12) {
return 0;
}
if (isPartTime) {
return 0;
}
// Compute the disability amount.
// ...
}
Tüm bu koşul cümlelerini tek bir ifadede birleştirin.
double disabilityAmount() {
if (isNotEligibleForDisability()) {
return 0;
}
// Compute the disability amount.
// ...
}
Kodunuz aynı eylemleri gerçekleştiren birçok alternatif operatör içerir. Operatörlerin neden bölündüğü belli bile olmayacaktır. Anlaması ise oldukça güçleşecektir.
Birleştirmenin temel amacı, daha fazla netlik sağlamak için koşulu ayrı bir yönteme çıkarmaktır.
-
Yinelenen kontrol akış kodunu, ortadan kaldırır. Böylelikle daha net bir kod oluşur. Aynı amaca sahip yani aynı değeri döndüren birden fazla koşulu birleştirmek, tek bir eyleme yol açan yalnızca tek bir karmaşık kontrol yaptığınızı göstermenize yardımcı olur.
-
Tüm operatörleri birleştirerek artık bu karmaşık ifadeyi, koşulun amacını açıklayan bir isimle yeni bir yöntemde izole edebilirsiniz. Böylelikle diğer geliştiriciler yöntemin adına bakarak bu koşulun ne yaptığını anlayabilir.
Refactoring yapmadan önce, yalnızca değerleri döndürmek yerine, koşullu ifadelerin herhangi bir yan etkisi (side effects) olmadığından veya herhangi bir değeri/değişkeni değiştirmediğinden emin olun. Bir koşulun sonuçlarına göre bir değişkende bir değişiklik olduğu gibi, operatörün içinde yürütülen kodda yan etkiler gizleniyor olabilir. Tüm bunlara refactoring sırasında dikkat etmelisiniz.
- Koşullu ifadeleri
and
veor
kullanarak tek bir ifadede birleştirin. Konsolidasyon sırasında genel bir kural olarak:- İç içe geçmiş koşullar
and
kullanılarak birleştirilir. - Ardışık koşul cümleleri
or
ile birleştirilir.
- İç içe geçmiş koşullar
- Operatör koşullarında Extract Methods tekniği uygulayın ve yönteme, ifadenin amacını yansıtan bir ad verin.
Bir koşullunun tüm dallarında aynı kod bulunabilir, bu durum kodu okuyan kişinin koşullu ifadenin amacını anlamasını zorlaştırır.
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.98;
send();
}
Kodu koşulun dışına taşıyın.
if (isSpecialDeal()) {
total = price * 0.95;
}
else {
total = price * 0.98;
}
send();
Bir koşulun tüm dallarında yinelenen kod bulunur; bu, genellikle koşulun dalları içindeki kodun evriminin bir sonucudur. Takım gelişimi buna katkıda bulunan bir faktör olabilir.
Kod tekrarını azaltır.
-
Duplicate edilmiş kod koşullu ifadelerin başındaysa, kodu koşullu dallanmadan önceki bir yere taşıyın.
-
Eğer kod dalların sonunda çalıştırılıyorsa, onu koşuldan dışarı çıkarın. Sonrasında koşullu ifadelerin sonra yerleştirin.
-
Yinelenen kod dalların içinde rastgele yerleştirilmişse, sonraki kodun sonucunu değiştirip değiştirmediğine bağlı olarak ilk önce kodu dalın başına veya sonuna taşımayı deneyin.
-
Uygunsa ve duplicate edilmiş kod bir satırdan uzunsa, Extract Methods tekniği kullanmayı deneyin. Böylelikle kod daha temiz hale gelir.
Birden çok boolean ifadesi için kontrol bayrağı görevi gören bir boolean değişkeniniz varsa bu durum kodun okunmasını zorlaştırabilir.
Değişken yerine break
, continue
ve return
anahtar kelimelerini kullanın. Böylelikle koşullu ifadeye bakan herkes aynı durumu hızlıca anlayabilir.
Modern programlama dillerinde, döngülerdeki ve diğer karmaşık yapılardaki kontrol akışını değiştirmek için özel operatörlerimiz olduğundan, bu tarz kullanımlar kodunuzu daha temiz gösterecektir. Ekstra bir bayrak eklemek, kodu karmaşık kılar.
break
: Döngüyü durdururcontinue
: Geçerli döngünün yürütülmesini durdurur ve bir sonraki yinelemede döngü koşullarını kontrol etmeye giderreturn
: Tüm fonksiyonun yürütülmesini durdurur ve operatörde verilmişse sonucunu döndürür.
Kontrol bayrağı kodu genellikle kontrol akışı operatörleriyle yazılan koddan çok daha hantaldır.
-
Döngüden veya geçerli yinelemeden çıkışa neden olan kontrol bayrağına değer atamasını bulun.
-
Eğer bu bir döngüden çıkışsa, bunu
break
ile değiştirin; Bu bir yinelemeden çıkışsacontinue
veya bu değeri işlevden döndürmeniz gerekiyorsareturn
değerlerini kullanın. -
Kontrol bayrağıyla ilişkili kalan kodu ve kontrolleri kaldırın.
Bir grup iç içe geçmiş koşul cümleniz var ve kod yürütmenin normal akışını belirlemek zor olması kod içerisinde karmaşıklık oluşturur. Sonradan projeye dahil olan bir kişi belkide aynı amaca hizmet edeceğini anlayamayacağı için kod tekrarına sebep olabilir.
public double getPayAmount() {
double result;
if (isDead){
result = deadAmount();
}
else {
if (isSeparated){
result = separatedAmount();
}
else {
if (isRetired){
result = retiredAmount();
}
else{
result = normalPayAmount();
}
}
}
return result;
}
Tüm özel kontrolleri ve uç durumları (edge cases) ayrı maddelere ayırın ve bunları ana kontrollerin önüne yerleştirin. İdeal olarak, birbiri ardına gelen düz bir koşul listesine sahip olmalısınız.
public double getPayAmount() {
if (isDead){
return deadAmount();
}
if (isSeparated){
return separatedAmount();
}
if (isRetired){
return retiredAmount();
}
return normalPayAmount();
}
Her bir iç içelik düzeyinin girintileri, acı ve keder yönünü sağa doğru işaret eden bir ok oluşturur:
if () {
if () {
do {
if () {
if () {
if () {
...
}
}
...
}
...
}
while ();
...
}
else {
...
}
}
Kod yürütmenin normal akışı hemen belli olmadığından, her koşulun ne yaptığını ve nasıl yaptığını anlamak zordur. Bu koşullar, her bir koşulun, genel yapıyı optimize etmeye yönelik herhangi bir düşünce olmaksızın geçici bir önlem olarak eklendiği, karışık bir evrimi gösterir.
Durumu basitleştirmek için özel durumları, yürütmeyi hemen sonlandıran ve koruma hükümleri doğruysa boş değer döndüren ayrı koşullara ayırın. Aslında buradaki göreviniz yapıyı düz (flat) hale getirmektir. Böylelikle koşullu ifade daha anlaşılabilir olur.
Kodu yan etkilerden (side effects) kurtarmaya çalışın; Separate Query from Modifier tekniği bu amaç için yararlı olabilir. Bu çözüm aşağıda açıklanan reshuffling için gerekli olacaktır.
-
Bir hatanın çağrılmasına veya yöntemden bir değerin anında döndürülmesine yol açan tüm koruma cümlelerini ayırın. Bu koşulları yöntemin başına yerleştirin. Böylelikle kod tüm koşulları takip etmeden erkenden işlemini sonlandırabilir.
-
Yeniden düzenleme tamamlandıktan ve tüm testler başarıyla tamamlandıktan sonra, aynı istisnalara veya döndürülen değerlere yol açan koruma cümleleri için Consolidate Conditional Expression tekniğini kullanıp kullanamayacağınıza bakın.
Nesne türüne veya özelliklerine bağlı olarak çeşitli eylemleri gerçekleştiren bir koşulunuz olduğunu varsayalım. Koşullu ifade zamanla uzadıkça uzayabilir, bu durum da anlaşılmasını güçlendirir.
class Bird {
// ...
double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException("Should be unreachable");
}
}
Koşulun dallarıyla eşleşen alt sınıflar oluşturun. Bunlarda, paylaşılan bir yöntem oluşturun ve kodu, koşulun karşılık gelen dalından ona taşıyın. Daha sonra koşullu ifadeyi, ilgili yöntem çağrısıyla değiştirin. Sonuç olarak, nesne sınıfına bağlı olarak polimorfizm yoluyla uygun uygulamaya ulaşılacaktır.
abstract class Bird {
// ...
abstract double getSpeed();
}
class European extends Bird {
double getSpeed() {
return getBaseSpeed();
}
}
class African extends Bird {
double getSpeed() {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
double getSpeed() {
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}
// Somewhere in client code
speed = bird.getSpeed();
Bu refactoring tekniği, kodunuz aşağıda listelenenlere göre değişen çeşitli görevleri gerçekleştiren, operatörler içeriyorsa yardımcı olabilir:
- Uyguladığı nesnenin veya arayüzün sınıfı
- Bir nesnenin alanının değeri
- Bir nesnenin yöntemlerinden birinin çağrılmasının sonucu
Yeni bir nesne özelliği veya türü ortaya çıkarsa, tüm benzer koşullardaki kodu aramanız ve eklemeniz gerekecektir. Dolayısıyla, bir nesnenin tüm yöntemlerine dağılmış birden fazla koşulu varsa, bu tekniğin faydası katlanarak artar.
-
Bu teknik, Söyle-Sorma (Tell-Don’t-Ask) ilkesine bağlıdır: Bir nesneye durumu hakkında soru sormak ve ardından buna dayalı eylemler gerçekleştirmek yerine, nesneye ne yapması gerektiğini basitçe söylemek ve nasıl yapılacağına kendisinin karar vermesine izin vermek çok daha kolaydır.
-
Yinelenen kodu kaldırır. Neredeyse aynı olan birçok koşuldan kurtulursunuz. Daha temiz bir kod source elde edilir.
-
Yeni bir yürütme varyantı eklemeniz gerekiyorsa tek yapmanız gereken mevcut koda dokunmadan yeni bir alt sınıf eklemektir (Açık/Kapalı Prensibi).
Refactoring'e Hazırlık
Bu refactoring tekniği için alternatif davranışları içerecek hazır bir sınıf hiyerarşisine sahip olmalısınız. Eğer böyle bir hiyerarşiniz yoksa bir tane oluşturun. Diğer teknikler bunun gerçekleşmesine yardımcı olacaktır:
-
Replace Type Code with Subclasses: Belirli bir nesne özelliğinin tüm değerleri için alt sınıflar oluşturulacaktır. Bu yaklaşım basittir ancak nesnenin diğer özellikleri için alt sınıflar oluşturamayacağınız için daha az esnektir.
-
Replace Type Code with State/Strategy: Belirli bir nesne özelliği için bir sınıf ayrılacak ve özelliğin her değeri için ondan alt sınıflar oluşturulacaktır. Geçerli sınıf, bu türdeki nesnelere referanslar içerecek ve yürütmeyi onlara devredecektir.
Aşağıdaki adımlar, hiyerarşiyi zaten oluşturduğunuzu varsayarak listelenmiştir.
** Refactoring Adımları**
-
Koşul başka eylemleri de gerçekleştiren bir yöntemdeyse, Extract Methods tekniğini uygulayın.
-
Her hiyerarşi alt sınıfı için, koşulluyu içeren yöntemi yeniden tanımlayın ve karşılık gelen koşullu dalın kodunu bu konuma kopyalayın.
-
Bu dalı koşuldan silin.
-
Koşullu boşalıncaya kadar değiştirmeyi tekrarlayın. Daha sonra koşulluyu silin ve yöntemin özetini bildirin.
Bazı yöntemler gerçek nesneler yerine null
değerini döndürdüğünden, kodunuzda null
için birçok kontrol bulunur. Bu kontroller bir süre sonra tekrarlı koda ve karmaşıklığa neden olacaktır.
if (customer == null) {
plan = BillingPlan.basic();
}
else {
plan = customer.getPlan();
}
null
yerine, varsayılan davranışı sergileyen null bir nesne döndürün.
class NullCustomer extends Customer {
boolean isNull() {
return true;
}
Plan getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}
// Replace null values with Null-object.
customer = (order.customer != null) ?
order.customer : new NullCustomer();
// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
Düzinelerce null
kontrolü, kodunuzu daha uzun ve daha çirkin hale getirir.
- Koşullu ifadelerden kurtulmanın bedeli yeni bir sınıf daha yaratmaktır. Ama bu sınıfı oluşturmaya duruma göre deyecektir.
-
Söz konusu sınıftan
null
nesne rolünü üstlenecek bir alt sınıf oluşturun. -
Her iki sınıfta da, boş bir nesne için
true
değerini ve gerçek bir sınıf içinfalse
değerini döndürenisNull()
yöntemini oluşturun. -
Kodun gerçek bir nesne yerine
null
değerini döndürebileceği tüm yerleri bulun. Kodu, boş bir nesne döndürecek şekilde değiştirin. -
Gerçek sınıfın değişkenlerinin
null
ile karşılaştırıldığı tüm yerleri bulun. Bu kontrolleriisNull()
çağrısıyla değiştirin.- Bir değer
null
değerine eşit olmadığında orijinal sınıfın yöntemleri bu koşullar altında çalıştırılıyorsa, bu yöntemleri null sınıfında yeniden tanımlayın ve koşulunelse
kısmındaki kodu buraya ekleyin. Daha sonra tüm koşulu silebilirsiniz ve farklı davranışlar polimorfizm yoluyla uygulanacaktır. - Eğer işler o kadar basit değilse ve yöntemler yeniden tanımlanamıyorsa, boş bir değer durumunda gerçekleştirilmesi gereken işlemleri,
null
nesnenin yeni yöntemlerine basitçe ayıklayıp çıkaramayacağınıza bakın. Varsayılan olarak işlemler olarakelse
'deki eski kod yerine bu yöntemleri çağırın.
- Bir değer
Kodun bir kısmının doğru çalışması için belirli koşulların veya değerlerin doğru olması gerekir. Bu olağan bir durumdur, ama kodu daha temiz hale getirebilecekken neden bu şekilde ilerleyesiniz ki?
double getExpenseLimit() {
// Should have either expense limit or
// a primary project.
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit :
primaryProject.getMemberExpenseLimit();
}
Bu varsayımları belirli assertion kontrolleriyle değiştirin.
double getExpenseLimit() {
Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit:
primaryProject.getMemberExpenseLimit();
}
Kodun bir kısmının, örneğin bir nesnenin mevcut durumu veya bir parametrenin veya yerel değişkenin değeri hakkında bir şeyler varsaydığını varsayalım. Genellikle bu varsayım, bir hata durumu dışında her zaman geçerli olacaktır.
İlgili assertionları ekleyerek bu varsayımları açık hale getirin. Yöntem parametrelerindeki tür ipuçlarında olduğu gibi, bu assertionlar kodunuz için canlı belgeler görevi görebilir. Böylelikle bu kısımda çalışacak başka bir geliştirici olduğunda anlama sırasında vakit kaybetmeyecektir.
Kodunuzun nerede assertionlara ihtiyaç duyduğunu görmek için bir kılavuz olarak, belirli bir yöntemin çalışacağı koşulları açıklayan yorumları kontrol edin.
Bir varsayım doğru değilse ve bu nedenle kod yanlış sonuç veriyorsa, büyük sonuçlara yol açabilir. Bu durumda, veri bozulmasına yol açmadan yürütmeyi durdurmak daha iyidir. Bu aynı zamanda programın testini gerçekleştirmenin yollarını tasarlarken gerekli testi yazmayı ihmal ettiğiniz anlamına da gelir.
-
Bazen bir hata, basit bir assertiondan daha uygundur. İstisnanın gerekli sınıfını seçebilir ve geri kalan kodun bunu doğru şekilde işlemesini sağlayabilirsiniz.
-
Bir istisna ne zaman basit bir assertiondan daha iyidir? İstisna, kullanıcının veya sistemin eylemlerinden kaynaklanabiliyorsa ve bu durmu istisnayla (exception) halledebilirsiniz. Öte yandan, sıradan isimsiz ve işlenmemiş istisnalar temel olarak basit assertionlara eşdeğerdir; onları ele almazsınız ve bunlar yalnızca hiçbir zaman meydana gelmemesi gereken bir program hatasının sonucu olarak ortaya çıkar.
-
Bir koşulun varsayıldığını gördüğünüzde emin olmak için bu koşula bir assertion ekleyin.
-
İddiayı eklemek programın davranışını değiştirmemelidir.
-
Kodunuzdaki her şey için assertionlar kullanarak aşırıya kaçmayın. Yalnızca kodun doğru çalışması için gerekli olan koşulları kontrol edin. Belirli bir assertion yanlış olsa bile kodunuz normal şekilde çalışıyorsa, assertionu güvenle kaldırabilirsiniz.