13 Eylül 2011 Salı

Decorator Pattern (Bezeme Şablonu)


Tür: Yapısal Şablon
Diğer Adı: Sarmalayıcı (Wrapper)

Adında anlaşılacağı gibi bezeme şablonu ilişkilendirildiği (bu manada Ingilizce 'attach' fiili kullanılır) sınıfı dinamik olarak dekore eder. Bezeme şablonuna ayrıca sarmalıyıcı (Wrapper) da denir. Bu isimle daha çok karşılaşmak mümkündür çünkü C# dilinin kendi içinde doğal türlerin (int, float, double gibi) birer Wrapper sınıfları bulunur. Buradaki konsept her ne kadar farklı da olsa gerçek hayatta bir şablonun zaten tek ve nihayi bir hali yoktur.

Bezeme şablonuna alternatif olarak sınıf türetme düşünülebilir. Fakat bu durumda bir sınıfın alacağı durum önceden belirlenmiş (statik olarak) ve sınırlandırılmış olur. Bu genellikle karşılaşılan bir durumdur fakat bazı durumlar vardır ki dinamik olarak bezemek daha çok işimize gelebilir. Örneğin bir sınıfımıza ekleyebileceğimiz n tane yeteneğimiz (capability) olsun. Bu yeteneklerin bir yada bir kaçını destekleyen alt sınıflar elde etmek istersek n'in bütün kombinasyonlarını toplamamız gerekir. Fakat bu işi bezeme sınıfları ile yaparsak sadece n tane bezeme sınıfını türetmek yeterli olur.

Bezeme şablonu oldukça sık karşılaşılan bir şablondur. Son zamanlarda moda olan AOP (Aspect Oriented Programming) özellikle bezeme şablonu üzerine bina olmuştur. AOP'ta Interceptor denilen nesneler kendisi için özel olarak bezenmiş sınıflara sonradan raporlama gibi iş hayatında önemli özellikler katabilir.

Bezeme şablonu en çok katmanlı yapılarda kullanılır. Her sınıf bir katman teşkil eder. Katmanlı yapılara da en iyi örnek görsel uygulamalardır (GUI App - Graphic User Interface Application). İlk önce boş ekranı temsil eden bir sınıf olur. Bu sınıf bile tek başına görüntü oluşturabilir. Fakat çeşitli kütüphanelerde (.NET, Java Swift, Qt gibi) farklı olmakla birlikte çok benzer katmanlı görsel sınıflar vardır. Bu sınıflar hepsi aynı sınıftan türerler. Hepsinin kendini çizdirme metodları vardır. İstenilen çizdirme özelliği için var olan görsel nesne bu yeteneği temsil eden sınıf tarafından sarmalanır.


Burada GoF'un meşhur kitabı olan "Design Patterns: Elements of Reusable Object Oriented Software"dan alıntı. Burada görsel olarak katmanlamanın nasıl yapıldığı ve sonucun neye dönüştüğü gösterilmiştir. Aşağıda da bu yapının sınıf çizelgesi de vardır.


Burada VisualComponent ana sınıfımızdır. Genellikle soyut sınıf (abstract class) olur. TextView sınıfı sadece verilen yazıyı göstermekle yükümlüdür. Diğer Decorator'dan türeyen ScrollDecorator ve BorderDecorator sınıfları TextView sınıfını isteğe bağlı olarak bezer. İster biri, ikisi yada hiçbiri kullanılmayabilir.

Çünkü tüm sınıflar bir birinden bağımsızdırlar (decoupled, loose coupling çok önemli bir konudur ayrıca bahsedeceğim).

Ayrıca Java'daki dosya işleme sınıfları tamamen bezeme sınıfları üzerine kurulmuştur. 
public class FrequencyCountedInputStream extends FilterInputStream 
    implements Iterable<String> {
    // constructor

 @Override
 public int read() throws IOException {
  int ic = super.read();
  char c = (char)ic;
  
  if (c == ' ' || c == '\r' || c == '\n' || c == '\t') {
   word = word.trim();
   
   if (word == "")
    return ic;
   
   putWord(word);
   
   word = "";
  }
  else
   word += c;
    
  return ic;
 }
 
 @Override
 public int read(byte[] buffer, int offset, int length) throws 
        IOException {
  int result = super.read(buffer, offset, length);
  String word = "";
  char c;
  
  for (int i = offset; i < offset + result; ++i)
  {
   c = (char)buffer[i];
   
   if (c == ' ' || c == '\r' || c == '\n' || c == '\t') {
    word = word.trim();
    
    if (word == "")
     continue;
    
    putWord(word);
    
    word = "";
   }
   else
    word += c;
  }
  
  return result;
 }

 void putWord(String word) {
  Integer value = wordFrequencies.get(word);
  
  if (value == null)
   value = 0;
  
  wordFrequencies.put(word, value + 1);
 }
 
 // other functions
 
 String word;
 HashMap<String, Integer> wordFrequencies;
}

Buradaki FilterInputStream dosya işleme sınıfı bezeyicisinden başka birşey değildir. Bizim sınıfımız da açılmış dosyadaki kelimelerin ne sıklıkla geçtiklerini (frekanslarını) daha dosyayı okurken hashmap'a kaydeder. Ayrıca burada iki tane read fonksiyonu da kullanılmıştır çünkü az sonra kullanacağımız örnek read fonksiyonunun parametreli olanını çağırırken genel kullanımda parametresiz read çağrılır. Aşağıda ise sınıfımızı kullanan örneği yazcağım.

// imports and package info
public class CustomFileInputTest {

 @Override
 public static void main(String[] args) {
  String fileName;
  
  System.out.print("Enter file name: ");
  
  try {
   BufferedReader keyReader = new BufferedReader(
     new InputStreamReader(System.in));
   
   fileName = keyReader.readLine();
   
   InputStream in =
    new FrequencyCountedInputStream(
      new BufferedInputStream(
        new FileInputStream(fileName)
        )
      );
   
   BufferedReader fileReader = new BufferedReader(
     new InputStreamReader(in));
   
   String line;
   
   System.out.println("File : " + fileName + "\n");
   
   while ((line = fileReader.readLine()) != null)
    System.out.println(line);
   
   System.out.println("------ End of File -------\n");
   System.out.println("Words and their frequencies:\n");
   
   FrequencyCountedInputStream fcin = 
                         (FrequencyCountedInputStream)in;
   
   int i = 0;
   
   for (String word : fcin) {
    System.out.println(word + " - " 
     + fcin.getValue(word));
    i++;
   }
   
   System.out.println("\nNumber of words : " + i);
  }
  catch (IOException e) {
   e.printStackTrace();
  }
  
 }

}

Stream sadece dosya değil ayrıca veri sağlayan herhangi bir şey olabilir. Klavye'yi buna örnek olarak gösterebiliriz. Klavye'nin InputStream nesnesi System.in ile temsil edilir. InputStreamReader System.in'i kolay okunabilir hale getirir. BufferedReader ise klavyeden satır satır okumayı sağlar. Bu sınıflar bezeme sınıfı olmamasına rağmen çok benzer yapıdalardır. Sınıflara yeni özellikler katarlar ama tamamen başka sınıfa dönüştürürler. Bu kompozisyondur. 

İkinci kısımda ise BufferedInputStream dosyamızı bezer. Sonra bu bezenmiş nesneyi bizim yazdığımız FrequencyCountedInputStream daha çok bezer ve bu yine bir InputStream nesnesi olarak saklanabilir. Dolayısıyla biz aynı işlemleri bizim nesnemize de uygularsak dosyadan satır satır okuyabiliriz.

İlk önce dosyayı yazdırırız tam bu sırada arka planda frekanslar hesaplanır. Sonra da kelimeleri ve frekanslarını yazdırırız.

Şimdiye kadarki proje kodlarına buradan ulaşabilirisiniz.

NOT: HashMap'e kelime eklerken eğer o kelime varsa yeni kelime eklenmez yeni değer ile yenilenir. Bu da frekans saymak için tam da gerekli şeydir.

Bu yapının kötü yanı ise bir sürü bağımsız genişletme sınıfı düşünülüp aşırı derecede sınıf yazımına yol açabilir. Yani rutin bir iş için bile bir sınıfı 10 kere bezemek kullanışlı değildir. Bu bir tasarım mevzusudur.

Bazen gereksiz yere bezeme sınıfları düşünülebilir. Unutulmamalıdır ki bir birine tamamen bağlı kodları ayırmak mantıksız olabilir. Eğer ayrılan parçalar sonra başka bir şeyle birleşemeyecekse yada olmazsa olmaz ise sınıfı tek bir parça halinde yazmak gerekli olur.

Buna örnek olarak 3B koşu simulasyonunda koşucuyu temsil ederken, koşucunun hem animasyonunu, hem bir koşu pistindeki konumunun belirlenmesini ve kamera açılarının belirlenmesi farklı bezeme sınıflarına bölünse daha iyi gözükebilir fakat uygulamada bu yetenekler bir birlerinden ayrılamazlar. 

Bezeme şablonu ve strateji şablonu ikisi de bir sınıf nesnesini dinamik olarak değiştirmeyi hedefler. Fakat strateji şablonu davranışlarını değiştirirken bezeme şablonu tamamen yeni davranışlar ekler veya çıkarır.

1 yorum:

  1. Bu çocuk işini biliyor! Açıklamalarında sahip olduğun düzeni de özellikle takdir ediyorum, insan her iş üzerinde böyle çalışmalı.

    YanıtlaSil