24 Ocak 2014 Cuma

Decorator Tasarım Kalıbı

Decorator Tasarım Kalıbı ve Kullanımı

Burada Decorator tasarım kalıbınının kullanımına yönelik örnek bir çalışma yapılmıştır. Öncelikle örnek bir uygulama verilmiş ve bu örnek uygulamadaki yetersizlikler belirtilerek, daha ideal bir tasarımın Decorator tasarım kalıbı kullanarak nasıl yapılabileceği anlatılmıştır.

Bu örnek hazırlanırken “Head First Design Patterns” kitabında verilen içecek örneğinden esinlenilmiştir. [1]



Kahve Dükkanı Uygulaması

"Kahve Dükkanı" isimli kahve ve çeşitli içecekler satan bir firmanın yönetimi için geliştirilen uygulamanın tasarımı aşağıdaki şekilde verilmiştir. 



Firmanın işlerini geliştirmesiyle birlikte, sundukları içecek seçeneklerine göre yazılımı geliştirmek gerekmektedir. Yeni gereksinimlere göre, kahve ve benzeri içecekler süt, karamel, krema, mocha gibi ekstra tatlarla sunulabilmektedir. Sunulan bu ekstra tatlara göre de içecek fiyatlarına ekstra ücret eklenmektedir. Mevcut yazılıma bu ihtiyaca cevap verecek şekilde yeni eklentiler yapılması için çalışma başlatılmıştır.
Gereksinim: İceceklerin aşağıdaki ekstra tatlarla, ekstra ücretleri eklenerek servis edilebilmesinin sağlanması.
  • Süt: 0.10 TL
  • Karamel: 0.10 TL
  • Krema: 0.15 TL
  • Mocha: 0.20 TL

Öncelikle kalıtım esasına uygun olarak, bu eklentilerin yeni içecek olarak sisteme tanımlanması düşünülmüştür. Bunun için, SutluEspresso, KaramelliEspresso, KremaliEspresso, MochaliEspresso, SutluKaramelliEspresso ve bunun gibi yeni sınıflar eklenmesi düşünülmüştür. Ancak bu yöntem, her bir içecek için 4 yeni sınıf, toplamda 5 içecek için 20 yeni sınıf eklenmesi anlamına geldiği için tercih edilmemiştir. Ayrıca bu yöntemde, her yeni içecek ve ekstra tat için eklenecek sınıf sayısının üssel olarak artacak olması sıkıntı yaratmıştır. Bu tip bir uygulamanın bakım ve yönetimi imkansız hale gelecektir. Bu yöntemden vazgeçildikten sonra daha akılcı bir dizayn oluşturulmaya çalışılmış ve ilk dizayn oluşturulmuştur.

İlk Dizayn

Yeni dizaynda ana Icecek sınıfı üzerine boolean değişkenler eklenerek içeceğin sütlü, karamelli, kremalı ya da mochalı olup olmadığının bu değişkenlerle kontrol edilmesi tasarlanmıştır. Bu şekilde kalıtım ilişkisiyle her bir içeceğin hangi ekstra tadı içerip içermediği kontrol edilebilecektir.

Ek fiyatlandırma için ise ana Icecek sınıfındaki fiyatlandir() metodu yeni eklenen boolean değişkenlere göre ek fiyatı hesaplayacaktır. Alt sınıflar ise kendi fiyatlandir() metodlarında kendi hesaplarına ek olarak, üst sınıfın metodunu da super anahtar kelimesi ile çağıracaklar ve bu ikisinin sonucunu toplayacaklardır.
public abstract class Icecek {

   protected String aciklama = "Icecek";
   private boolean sutlu;
   private boolean karamelli;
   private boolean kremali;
   private boolean mochali;

   public String getAciklama() { return aciklama; }

   public double fiyatlandir(){
      int ekFiyat = 0;
      if(sutlu){
         ekFiyat += 0.10;
      }
      if(karamelli){
         ekFiyat += 0.10;
      }
      if(kremali){
         ekFiyat += 0.15;
      }
      if(mochali){
         ekFiyat += 0.20;
      }
      return ekFiyat;
   }

   // getter ve setter metodlari
   // ...
}

public class Espresso extends Icecek {

   public Espresso() { aciklama = "Espresso"; }

   public double fiyatlandir() {
      return 1.99 + super.fiyatlandir();
   }
}

Yapılan dizaynın UML diyagramı aşağıda gösterilmiştir.



Bu dizaynın örnek bir kullanımı aşağıda verilmiştir:
public class KahveDukkani {

   public static void main(String args[]) {

      Icecek icecek1 = new Espresso();
      System.out.println(icecek1.getAciklama()
            + " $" + icecek1.fiyatlandir());

      Icecek icecek2 = new Neskafe();
      icecek2.setMochali(true);
      icecek2.setKaramelli(true);
      System.out.println(icecek2.getAciklama()
            + " $" + icecek2.fiyatlandir());

      Icecek icecek3 = new FiltreKahve();
      icecek3.setKaramelli(true);
      icecek3.setSutlu(true);
      System.out.println(icecek3.getAciklama()
            + " $" + icecek3.fiyatlandir());

   }
}

İlk Dizayn Kusurları


İlk dizayn özellikle kalıtım ile çözme fikri ile kıyaslandığında, ilk bakışta düzgün bir tasarım gibi görünse de incelendiğinde aşağıdaki sıkıntılar tespit edilmiştir.
  • Mevcut dizayn çifte kremalı, çifte karamelli gibi müşterinin taleplerini desteklememektedir. Bu durum müşteri memnuniyetsizliğine yol açabilecek ciddi bir problemdir.
  • Her ekstra tat, her içeceğe uygun olmayabilir. Örneğin buzlu çaya süt ya da mocha katmanın bir anlamı yoktur. Buna rağmen BuzluCay sınıfı ana sınıftaki isSutlu(), setSutlu(), isMochali(), setMochali() gibi metodları da kalıtımla miras almış olur.
  • Ekstra tatlardaki fiyat değişimleri nedeniyle Icecek sınıfındaki fiyatlandir() metod kodunu değiştirmemiz gerekecektir.
  • Yeni bir ekstra tat tanımı için, Icecek sınıfına yeni boolean değişkenler, yeni getter ve setter metodları eklemek gerekecek ve fiyatlandir() metod kodunu değiştirmemiz gerekecektir.
  • Alt sınıfların üst sınıftan override ettikleri fiyatlandir() metodunda hesap yaparken, üst sınıftaki fiyatlandir() metodunu super ile çağırması hem bir yinelenen kod durumu oluşturmakta hem de hatalara neden olabileceği için sakıncalı görünmektedir. Yazılımcı her bir alt sınıfta aynı eklemeyi yapacağı için, bu eklemeyi yapmayı unutabilir ya da toplama yerine çıkarma işareti koyma gibi hatalar yapabilir. Bunun yerine şablon metod oluşturma (form template method) çözümüne gidilebilir. fiyatlandir() metodu ikiye yarılarak esktraUcret() ve icecekUcreti() gibi iki parçaya ayrılabilir. ekstraUcret() metodu yine ust sınıfta tanımlanırken, icecekUcreti() metodu üst sınıfta abstract olarak tanımlanır ve gerçeklenmesi alt sınıfa bırakılır. Bu çözum aşağıda kabaca gösterilmiştir:
public double fiyatlandir(){
   return icecekUcreti() + ekstraUcret();
}

public double ekstraUcret(){

   int ekFiyat = 0;

   if(sutlu)
      ekFiyat += 0.10;
   if(karamelli)
      ekFiyat += 0.10;
   if(kremali)
      ekFiyat += 0.15;
   if(mochali)
      ekFiyat += 0.20;

   return ekFiyat;
}

public abstract double icecekUcreti();
  • Sınıflardaki aciklama değişkeni, içecek hakkında bilgiler sunmaktadır. Mevcut dizaynda, bu açıklamalar içeceğe katılan ekstra tatlar hakkında bilgi vermemektedir. Bunu sağlamak için fiyatlandir() metodunda yapıldığı gibi her bir içeceğin sütlü, karamelli, kremalı ya da mochalı olup olmadığı kontrol edilip bu özellikler açıklanmaya eklenmelidir. Tabi ki bu da kodun bakımı için daha önceden açıklanan sıkıntıların burada da söz konusu olmasına yol açacaktır.
  • Söz konusu tasarım “Sınıflar eklentiye açık, fakat değişikliğe kapalı olmalıdır” prensibine uygun değildir. Fiyat değişimi, eskstra tat eklenmesi gibi değişikler mevcut sınıflardaki kodların değişmesi ihtiyacına yol açmaktadır.

Yeni Tasarım

İlk tasarımdaki eksiklikler göze alındığında yeni bir tasarım geliştirilmesi ihtiyacı doğmuştur. Bir dizayn ilkesi olarak değişen kısımları bulup, onları sarmalamalıdır. Bu nedenle, yukarıdaki tasarımda olduğu gibi ekstra tatları içeceğin bir özelliği olarak tutmak yerine ayrı bir soyutlamada tutmak gerekmektedir.

Gereksinimler gözden geçirildiğinde görülmektedir ki, içeceğe ekstra bir tat eklendiğinde ortaya yine bir içecek çıkmaktadır. Yani asıl yaptığımız bir içecek oluşturmak, ona çeşitli ekstra tatlar ekleyerek içeceği süslemek ve ortaya yeni bir içecek çıkarmaktır. Bu gereksinimi sağlamak için “Decorator” tasarım kalıbı uygun görünmektedir.



Decorator tasarım kalıbında amaç, nesnelere çalışma anında (dinamik olarak) ek sorumluluklar atamaktır. Bu kalıbın bazı özellikleri şunlardır:
  • Decorator sınıfı, etkilemek istenen sınıfın türünde tanımlanır.
  • Bir ya da birden fazla Decorator kullanarak, etkilenmek istenen obje saramalanabilir.
  • Decorator sınıfı da üst sınıfın tipinde olduğu için, bir üst sınıf gibi davranır.
  • Nesneler dinamik olarak Decorator ile sarmallanarak nesneye yeni özellikler kazandırılabilir.
  • Decorator sınıfının operation() metodu kendi ekleyeceği işlemleri tanımladıktan sonra ya da önce, sarmalladığı nesnenin operation nesnesini çağırır.

Yeni tasarımda Icecek sınıfı Component rolündedir. Espresso, FiltreKahve, KafeinsizKahve, Nescafe ve BuzluCay sınıfları ise ConcreteComponent rolündedirler. Decorator rolünde ise EkstraTatDecorator isimli bir sınıf tanımlanmıştır. Sut, Karamel, Krema ve Mocha ise EkstraTatDecoraton'dan türemiş ConcreteDecorator sınıflarıdır.

Sonuçta ortaya çıkan tasarım aşağıdaki şekilde verilmiştir.



Decorator tasarım kalıbına uygun olarak tanımlanan EkstraTatDecorator ve örnek bir alt sınıfı aşağıda verilmiştir.
public abstract class EkstraTatDecorator extends Icecek {

   protected Icecek icecek;

   public EkstraTatDecorator(Icecek icecek) {
      this.icecek = icecek;
   }

   public abstract String getAciklama();
}

public class Karamel extends EkstraTatDecorator {

   public Karamel(Icecek icecek) { super(icecek); }

   public String getAciklama() {
      return icecek.getAciklama() + ", Karamel";
   }

   public double fiyatlandir() {
      return .10 + icecek.fiyatlandir();
   }
}
Görüldüğü üzere Karamel sınıfı fiyatlandırma hesabı yaparken önce kendi ücretini eklemekte, daha sonra eklendiği içeceğin ücretini toplayarak sonucu dönmektedir. Bu şekilde rekursif mantığa benzer şekilde, her decorator bu işlemi tekrarlamakta ve en son bir componente gelindiğinde sonuç fiyatlandırması hesaplanmış olmaktadır.



Bu dizaynın örnek bir kullanımı aşağıda verilmiştir:
public class KahveDukkani {
   public static void main(String args[]) {

      Icecek icecek1 = new Espresso();
      System.out.println(icecek1.getAciklama()
            + " $" + icecek1.fiyatlandir());

      Icecek icecek2 = new Neskafe();
      icecek2 = new Mocha(icecek2);
      icecek2 = new Mocha(icecek2);
      icecek2 = new Karamel(icecek2);
      System.out.println(icecek2.getAciklama()
            + " $" + icecek2.fiyatlandir());

      Icecek icecek3 = new FiltreKahve();
      icecek3 = new Krema(icecek3);
      icecek3 = new Mocha(icecek3);
      icecek3 = new Karamel(icecek3);
      System.out.println(icecek3.getAciklama()
            + " $" + icecek3.fiyatlandir());
   }
}


Kaynak Kodlar

Dizaynın son halinin kaynak kodları sınıf sınıf aşağıda verilmiştir:

public class KahveDukkani {
   public static void main(String args[]) {

      Icecek icecek1 = new Espresso();
      System.out.println(icecek1.getAciklama()
            + " $" + icecek1.fiyatlandir());

      Icecek icecek2 = new Neskafe();
      icecek2 = new Mocha(icecek2);
      icecek2 = new Mocha(icecek2);
      icecek2 = new Karamel(icecek2);
      System.out.println(icecek2.getAciklama()
            + " $" + icecek2.fiyatlandir());

      Icecek icecek3 = new FiltreKahve();
      icecek3 = new Krema(icecek3);
      icecek3 = new Mocha(icecek3);
      icecek3 = new Karamel(icecek3);
      System.out.println(icecek3.getAciklama()
            + " $" + icecek3.fiyatlandir());
   }
}

public abstract class Icecek {
   protected String aciklama = "Icecek";

   public String getAciklama() {
      return aciklama;
   }

   public abstract double fiyatlandir();
}

public class Espresso extends Icecek {

   public Espresso() {
      aciklama = "Espresso";
   }

   public double fiyatlandir() {
      return 1.99;
   }
}

public class FiltreKahve extends Icecek {

   public FiltreKahve() {
      aciklama = "Filtre Kahve";
   }

   public double fiyatlandir() {
      return .89;
   }
}

public class KafeinsizKahve extends Icecek {

   public KafeinsizKahve() {
      aciklama = "Kafeinsiz Kahve";
   }

   public double fiyatlandir() {
      return 1.05;
   }
}

public class Neskafe extends Icecek {

   public Neskafe() {
      aciklama = "Neskafe";
   }

   public double fiyatlandir() {
      return .99;
   }
}

public class BuzluCay extends Icecek {

   public BuzluCay() {
      aciklama = "Buzlu Cay";
   }

   public double fiyatlandir() {
      return 2.99;
   }
}

public abstract class EkstraTatDecorator extends Icecek {

   protected Icecek icecek;

   public EkstraTatDecorator(Icecek icecek) {
      this.icecek = icecek;
   }

   public abstract String getAciklama();
}

public class Sut extends EkstraTatDecorator {

   public Sut(Icecek icecek) { super(icecek); }

   public String getAciklama() {
      return icecek.getAciklama() + ", Sut";
   }

   public double fiyatlandir() {
      return .10 + icecek.fiyatlandir();
   }
}

public class Karamel extends EkstraTatDecorator {

   public Karamel(Icecek icecek) { super(icecek); }

   public String getAciklama() {
      return icecek.getAciklama() + ", Karamel";
   }
   public double fiyatlandir() {
      return .10 + icecek.fiyatlandir();
   }
}

public class Krema extends EkstraTatDecorator {
   public Krema(Icecek icecek) { super(icecek); }

   public String getAciklama() {
      return icecek.getAciklama() + ", Krema";
   }

   public double fiyatlandir() {
      return .15 + icecek.fiyatlandir();
   }
}

public class Mocha extends EkstraTatDecorator {

   public Mocha(Icecek icecek) { super(icecek); }

   public String getAciklama() {
      return icecek.getAciklama() + ", Mocha";
   }

   public double fiyatlandir() {
      return .20 + icecek.fiyatlandir();
   }
}

Kaynaklar


[1] Elisabeth Freeman, Eric Freeman, Bert Bates, and Kathy Sierra. 2004. Head First Design Patterns. O' Reilly & Associates, Inc..c

[2] Alan Shalloway and James Trott. 2004. Design Patterns Explained: A New Perspective on Object-Oriented Design (2nd Edition) (Software Patterns Series). Addison-Wesley Professional.

[3] Yunus Emre Selcuk, YTÜ Nesneye Dayalı Tasarım ve Modelleme Yüksek Lisanas Ders Notları, 2013



1 yorum:

Selman dedi ki...

Eline sağlık : )

Bu arada Head First Design Patterns kitabına biraz bakmıştım. Çok güzel anlatımlara, görsellere ve örneklere sahip. Okumak lazım