Tasarım Kalıpları - Decorator

Tasarım kalıplarının genel yaklaşımı iş kuralı değişse de birbirine benzer problemlere ortak bir çözüm sunmaktadır diyebiliriz. Peki Decorator kalıbı nasıl bir sorunu çözüyor ilk önce ona bakalım;

❓ Örnekle açıklarsak bir araba satın almaya karar verdiğiniz de standart bir paket seçimi yaptırılır ve ardından istediğiniz özellikler eklenerek nihai fiyat belirlenir. Burada ilk temel objeniz standart paket iken seçtiğiniz paketlere göre objeye yeni özellikler eklenerek fiyatı değişmektedir.

Yani soruna biraz daha yakından bakarsak elimizde seçeneklerin olduğu ama çalışma zamanına (runtime) kadar hangi özellikler ile çalışacağını bilmediğimiz bir durum bulunuyor.

Bu noktada özellikleri/yetkinlikleri sınıflara değil objelere vermek isteriz ki o sınıftan oluşan her obje aynı değil farklı yetkinlikler ile çalışabilsin. Örnekle ilişkilendirirsek temelde bir araba istiyoruz fakat bir araba dizel motor iken diğeri benzinli olabilir ikisi de araba sınıfından türemiştir ama farklı yetkinlikleri bulunur. Bu gibi durumlarda değişebilen özellikleri sınıflarda değil objelerde tutmak isteyebiliriz.

Yazılım dünyasında bir problemi çözmenin yüzlerce farklı yolu olabilir, bu problem için de çok farklı çözüm yolları tasarlayabiliriz. Genel yazılım prensiplerine (SOLID vb.) uygunluğuna göre bu çözümleri değerlendirip hangisini kullanacağımıza karar verebiliriz.

💡 Çözüm1:

Kalıtım (Inheritance) compile zamanda objelere yeni özellik kazandırır. Bir sınıf extend olduğu sınıfdan yeni özellikler alabilir fakat bu runtime da değiştirilemez. Sorunda belirttiğimiz örneğe bakarsak her araba modeli için farklı özelliklerin kombinasyonları olan yeni sınıflar oluşturabiliriz. O sınıflardan ürettiğimiz objeleri kullanabiliriz fakat buradaki sorun her yeni özellik için onlarca yeni sınıf ekleme ihtiyacı oluşacaktır. Buda ürün üzerinde bakım maliyetlerini çok fazla artırır.

💡 Çözüm2:

Sınıf üyesi olarak bu özellikleri saklamak isteyebiliriz. Örneğin Car diye bir sınıfım var renk seçeneği, jant modeli gibi birçok özellik seçimini bir değişken olarak saklayıp o değişkenlerin değerlerine göre özellikleri kullanmak isteyebilirim. Buradaki sorun ise yeni bir özellik daha geldiğinde mevcut kodda değişiklik yapma gereksinimi ortaya çıkar bu da Open-Closed Principle ine uygun değildir. Başka bir durumda bu her yeni özellik bu sınıftan oluşmuş alt sınıflara uygun olmayabilir. Örneğin bir araba modeli için sunroof geçerli bir özellik iken diğerine uygulanamıyor olabilir. Bu özelliklerin birden fazla alındığı durumda da yine aynı şekilde sınıf değişkeni olarak tutulduğunda çözüm sağlanamaz.

💡 Çözüm3:

Model diyagramı ile çözüm üzerinden gidelim. Tasarım başta belirttiğimiz objelere özellik ekleme üzerine oluşturulmuş. Decorator ler sarmalayacağı obje ile aynı tipte referans almaktadır ve bunlar üzerinden objelere yeni fonksiyonlar eklenebiliyor.

Örnek bir uygulama yapıp başta belirttiğimiz problemi bu pattern ile çözmeye çalışacağız. En üstte ortak metotları içeren bir abstract class oluşturduk. Bu sınıfta price metodu abstract halde, bundan türeyen bütün sınıfların bu metodu implement etmesini istiyoruz.

public abstract class Car {
String modelName = "Standard Package";

public String getModelName() {
return modelName;
}

public abstract double price();
}

Passat model bir araba için fiyatlandırma yaptığımızı düşünelim aşağıdaki gibi Car sınıfından extends ederek oluşturduk ve price metodunu override ettik, standart fiyat için değer döndürdük.

public class Passat extends Car{

public Passat(){
modelName = "Passat ";
}

@Override
public double price() {
return 10.0;
}
}

Sorunda belirttiğimiz gibi bir araba satın alırken ek olarak bir sürü opsiyon belirtebiliyoruz. Bunun içinde aşağıdaki gibi decoratorler yazabiliriz.

public class Paint extends CarDecorator {

private Car car;

public Paint(Car car){
this.car = car;
}

@Override
public double price() {
return car.price() + 0.2;
}

@Override
public String getModelName() {
return car.getModelName() + " + Red Color ";
}
}
/*********************************/public class Wheel extends CarDecorator {

private Car car;

public Wheel(Car car) {
this.car = car;
}

@Override
public double price() {
return car.price() + 0.6;
}

@Override
public String getModelName() {
return car.getModelName() + " + 17' ventura wheels ";
}
}

Şimdi yazdığımız kısmı test edelim bir tane Passat objesi oluşturup bu objeyi decoratorlere referans vererek ana objemizi sarmalıyoruz. Standart fiyat olan 10 üzerine + 0.2 renk özelliği +0.6 tekerlek özelliğini de ekleyerek 10.8 fiyat buluyoruz.

Car passat = new Passat();
passat = new Paint(passat);
passat = new Wheel(passat);
System.out.println(passat.getModelName()+ ", Price: " + String.format("%.2f", passat.price()) );
/* Çıktı; */
Passat + Red Color + 17' ventura wheels , Price: 10,80

En son çözüm ile birlikte yeni bir özellik geldiğinde mevcut sınıflarımızda bir değişiklik yapmadan yeni bir decorator sınıfı oluşturup bunu kullanmak yeterli olacak. Gelişime açık fakat değişime kapalı bir kod tasarlanmış oldu.

Car passat = new Passat();
passat = new Paint(passat);
passat = new Wheel(passat);
passat = new Sunroof(passat); // yeni özellik sunroof eklendi + 1.2
System.out.println(passat.getModelName()+ ", Price: " + String.format("%.2f", passat.price()) );
/* Çıktı; */
Passat + Red Color + 17' ventura wheels + Sunroof added , Price: 12,00

Şuana kadar Decorator tasarım kalıbı ile neler öğrendik;

  • Dinamik olarak uygulama içerisinde nesnelere yeni özellik kazandırmayı amaçlar.
  • Decorator onları çeviren obje ile aynı tiptedir. Model diyagramımıza bakarsak decorator de ana objede Component tipindedir.
  • Decorator sarmaladığı objeye eklenmeden önce veya sonra kendi işlemini yapabilir.

Aggregation or Composition instead of Inheritance ifadesini sıklıkla OOP içeren yazılarda görebilirsiniz. Decorator tasarım kalıbı da aslında bu prensibi kullandığı için kısaca değinebiliriz.

Kalıtım statik bir süreçtir yani var olan bir objeye çalışma zamanında yeni davranış ekleyemezsiniz. Sadece bu objeyi onunla aynı alt tipteki objelere referans edebilirsiniz bir nevi polimorfizm olarak düşünebilirsiniz. Eğer kalıtım kullanıyorsak objeye yeni bir özellik eklemek istediğimizde kodu değiştirip tekrar compile etmekten başka yolumuz yoktur.

Spring ve Java dünyasında da sıklıkla bu kalıbın kullanıldığı yerler mevcuttur. Head First Desing Patterns kitabında gördüğüm aşağıdaki örnek java io yapısında bu kalıbın nasıl kullanıldığını çok güzel açıklamış;

head-first-design

--

--