Sansür, bir toplumun kendine olan güvensizliğini yansıtır ve otoriter rejimlerin belirgin bir özelliğidir.

--Potter Stewart

6.11.2010

Introduce Null Object


Tasarım örüntüleri yazılımın yönetilebilirliği ve genişletilebilirliği için ulaşılmak istenen hedeftir. Sadece bu hedefi anlayıp uygulamaya çalışmak yerine, hedefe nasıl ulaşıldığını anlamak daha önemlidir. Refactoring yöntemleri, sizi ideal hedefe ulaştıran tasarımsal evrimi temsil eder[1]. Bu yöntemlerden bir tanesi de Null Objecttir.

Uygulama geliştirme esnasında bir metot sorgusu sonucu dönen nesnenin null olup olmadığının kontrolünü yapmak sıklıkla başvurduğumuz kontrollerden birisidir. Bu kontrolü unuttuğumuz da ise vay halimize, o ünlü NullPointerException hatasıyla uygulamamızın göçmesi kaçınılmaz olur.

İş mantığımızın gereksinimleri sebebiyle bir nesne için bu kontrolü sıklıkla tekrarlayarak null olma durumunda kodun alternatif davranışlar sergilemesi gerekebilir. Bu durum kaynak kodumuzun fazlaca tekrarlı kontrol deyimi içermesine ve Martin Fowler’ın Refactoring[2] kitabındaki deyimiyle kodun kötü kokular yaymasına sebep olacaktır. Böyle bir durumda uygulanması gereken refactoring (ben “kod adam etme” demeyi seviyorum) yöntemi Null Object olmalıdır.

Bir Null Object temel olarak gerçek nesneyle aynı metot imzalarına sahiptir. Metotları ise null olma durumunda varsayılan değerleri döndürecek, alternatif davranışları sergileyecek şekilde gerçekleştirilir.

Metot imzaları aynı olacağı için Null Object ya gerçek nesneden kalıtım (inheritance) yoluyla türetilecek ya da gerçek nesneyle ortak bir interfaceden geliştirilecek (implement) şekilde tasarlanabilir.


Kalıtım yoluyla yaratılan Null Object sınıfının, null olma durumunda alternatif davranışı sergileyecek metotları override etmesi gerekmektedir. Atasınıfa yeni eklenen metotların alternatif davranış için override edilmesinin unutulması hatalı işleyişe sebep olabilir. Interface gerçekleştirilerek yaratılan Null Object sınıfında bu risk söz konusu olmaz. Ama bu sefer yaratılan yeni sınıf içeriğinde alternatif davranış sergilemek zorunda olmayan diğer metotların gövdelerinin de barındırması gerekir. 
Bir diğer sıkıntı da geliştirim aşamasında değişmeye meyilli interface tasarımlarıdır. Yeni metot eklendikçe, metot çıkartıldıkça ya da metot imzaları değiştikçe Null Object sınıfını da değiştirmek gerekir. Bu soruna Kenan Sevindik, blogunda yazdığı “Mockito ile Null Object”[3] yazısında ilginç bir çözüm getiriyor. Joshua Kerievsky “Refactoring to Patterns”[1] kitabında Null Object yönteminin temel amacının kodu basitleştirmenin yanında kaynak kod satır sayısını düşürmesi gerektiğini, en azından aynı düzeyde tutması gerektiğinin altını çiziyor. Kaynak kod satır sayısı artıyorsa uygulanmamasını  tavsiye ediyor.

Null Object örüntüsünün varlığı null kontrollerinin yapılmayacağını garanti etmez. Null Object örüntüsünün kullanıldığından habersiz bir başka geliştirici null kontrolleri yapmaya devam edebilir dahası null olma durumunda işletilecek kod parçalarını iş mantığına ekleyebilir. Null Object nesnesini ve gerçek nesneyi davranış barındırmayan Nullable gibi boş bir interfaceten gerçekleştirmek geliştiricilerin dikkatini çekerek bu gibi problemlerin önüne bir nebze de olsa geçebilir. Boş bir interfaceten gerçekleştirmek yerine isNull() metotu içeren bir interface de ilerde ihtiyaç duyulabilecek olası bir null kontrolü için faydalı olur:


public interface Nullable {
 boolean isNull();
}
public class Customer implements Nullable {
 boolean isNull(){
  return false;
 }

   Plan getPlan(){
          doMoreThings();
  }
}
public class NullCustomer extends Customer {
 boolean isNull(){
  return true;
 }
 
  Plan getPlan(){
    return Plan.emptyPlan();
  }
}


Kaynak koda müdahale edemediğimiz durumlarda sadece Null Object davranışı olmayan bir interfaceten gerçekleştirilerek null kontrolü instanceof ile yapılabilir:


public interface Null { }
public class NullCustomer extends Customer implements Null{
//...
}
//...
if(customer instanceof Null) { }
//...



Null Object davranışı ve durumu değişmeyecek bir sınıf olduğu için çok fazla Null Object nesnesi yaratılma durumunda Singleton tasarım örüntüsü kullanılarak her yeni çağrıda yeni null sınıf yaratılmasının önüne de geçilebilir.

Artıları/Eksileri:
+ Null kontrolü tekrarları yapmadan, null hatalarının önüne geçer.
+ Null kontrollerinin sayısını en aza indirerek kodu basitleştirir.
- Sadece bir kaç null kontrolü kullanıldığı durumlarda kodu karmaşıklaştırır.
- Null Object gerçekleştirimi yapıldığından haberi olmayan bir geliştirici gereksiz null kontrolleri yazabilir.
- Yönetilebilirliği zorlaştırır. Gerçek sınıfa eklenen yeni metotlar için Null Object sınıfı içinde override işlemi yapılmalıdır.



Bir örnekle konuya açıklık getirelim:

class SecmenKutugu {
 Vatandas vatandas;
 Vatandas getVatandas(){
  return vatandas;
 }
}
class Vatandas{
 public long getTCNo() { 
  //..
  }
 public Adres getAdres() { 
  //.. 
  }
 public Muhtar setMuhtar { 
  //..
  }
}

class Adres {
 public int getSokakNo() { 
  //.. 
  }
 // ...
}




Vatandas vatandas = secmenKutugu.getVatandas();
if(vatandas != null) vatandas.setMuhtar(Muhtar.getInstance());
//...
long tcNo;
if(vatandas == null) tcNo = 0;
else tcNo = vatandas.getTCNo();
//...
int sokakNo;
if(vatandas == null) sokakNo = 0;
else sokakNo = vatandas.getAdres().getSokakNo();
//..

Yukardaki örnekteki gibi tekrarlı null kontrolleri yapan kodumuz olsun. Bu kodu adam etmeye null testi yapmaya imkan sağlayan Null Object yaratmakla başlayalım. Böylelikle bahsettiğimiz gibi diğer geliştiricilerin de Null Object kullanıldığına dair dikkatini çekebiliriz:


interface Nullable {
 boolean isNull();
}
class Vatandas implements Nullable{
 
 static Vatandas newNull() {
                return new NullVatandas();
        }
        boolean isNull(){
  return false;
 }
 //...
}
class NullVatandas extends Vatandas {
 boolean isNull(){
  return true;
 }
}
Null değer dönebilecek yerlerde yarattığımız yeni Null Object nesnesini döndürelim:
class SecmenKutugu {
 Vatandas vatandas;
 Vatandas getVatandas(){
  return (vatandas == null)? Vatandas.newNull(): vatandas;
 }
}
Null kontrollerinin yapıldığı her bir iş mantığı için Null Object sınıfına null olma durumunda işletilecek alternatif metot kodunu ekleyelim ve null kontrollerini kaldıralım. Ayrıca her değişiklikte kodun davranışının değişmediğini test etmekte yarar var. Tüm değişikliklerin sonunda kodumuz şu şekilde olacak:
class NullVatandas extends Vatandas {
 public long getTCNo() {
  return 0;
 }
 public Muhtar setMuhtar(Muhtar muhtar) { }
 public Adres getAdres() {
  return Adres.newNull();
 }
}
class NullAdres extends Adres {
 public int getSokakNo(){
  return 0;
 }
 //..
}
 

Vatandas vatandas = secmenKutugu.getVatandas();
vatandas.setMuhtar(Muhtar.getInstance());
//...
long tcNo = vatandas.getTCNo();
//...
int sokakNo = vatandas.getAdres().getSokakNo();
//..

Yukarıdaki örnekte Adres sınıfı için de bir
Null Object yazma gerekliliğimiz dikkatinizi çekmiştir. Sınıfın başka bir sınıfa delege ettiği metotlar için null sınıfın da metotu delege edeceği yeni null sınıflar yazılır.



Null Object yöntemi nesnenin null davranışını modellemek için kullanılabileceği gibi aynı zamanda nesnenin varsayılan davranış sergileyecek farklı senaryoları için de (Örk: NullVatandas, UnknownVatandas, PrivateVatandas vb.) kullanılabileceğini belirterek konumuzu sonlandıralım.


Kaynaklar:
1. Joshua Kerievsky - Refactoring to Patterns
2. Martin Fowler - Refactoring: Improving the Design of Existing Code
3. Kenan Sevindik - http://ksevindik.blogspot.com/2010/01/mockito-ile-null-object.html

Hiç yorum yok:

Yorum Gönder