Wzorzec dekorator

Zasada jednej odpowiedzialności

Wzorce projektowe – wzorzec dekorator.

W teraźniejszości występuje wiele reguł, wzorców i zasad programowania, którymi kierują się obecni programiści. Żeby ich kod był zrozumiały i jasny dla innych oraz wydajny posługują się oprócz zasad SOLID, tzw. wzorcami projektowymi. Wzorce projektowe (design patterns) są abstrakcyjnymi opisami rozwiązań programistycznych. Opisują rozwiązanie problemów, które często występują podczas tworzenia oprogramowania. Pokazują zależności pomiędzy klasami i obiektami oraz korzyści wynikające z tych zależności. Przedstawiają sposoby projektowania oraz zachowanie poszczególnych klas i obiektów. Celem stosowania wzorców projektowych jest ułatwienie tworzenia, a następnie modyfikacji i utrzymania kodu źródłowego. Występuje wiele wzorców projektowych jednym z nich jest wzorzec dekorator. W tym artykule opisze ten wzorzec oraz przedstawię przykład kodu programu przed zastosowaniem tego wzorca jak i po.

Wstęp

Wzorzec dekorator jest to wzorzec projektowy należący do grupy wzorców strukturalnych. Pozwala na dodanie nowej funkcji do istniejących klas dynamicznie podczas działania programu. Wzorzec dekoratora polega na opakowaniu oryginalnej klasy w nową klasę „dekorującą”. Zwykle przekazuje się oryginalny obiekt jako parametr konstruktora dekoratora, metody dekoratora wywołują metody oryginalnego obiektu i dodatkowo implementują nową funkcję. Dekoratory są alternatywą dla dziedziczenia. Dziedziczenie rozszerza zachowanie klasy w trakcie kompilacji, w przeciwieństwie do dekoratorów, które rozszerzają klasy w czasie działania programu. Wzorzec dekorator może posiadać wiele małych klas co może być minusem, jednak umożliwia tworzenie elastycznych projektów zgodnie z reguła otwarte-zamknięte. Wzorzec dekorator należy do grupy wzorców skatalogowanych przez Gang czworga. Poniżej przedstawię przykład dla wzorca dekorator, opisze jego zalety i wady.

Zastosowanie

W przypadku języka Java wzorzec projektowy dekorator jest dość często używany w bibliotece standardowej. Za przykład mogą tu posłużyć strumienie wykorzystywane przy operacjach na plikach. InputStream jest klasą abstrakcyjną, która posiada wiele dekoratorów, na przykład FileInputStream czy BufferedInputStream. Innym przykładem, również z języka Java, mogą być dekoratory kolekcji. Dekoratory te na przykład pozwalają na utworzenie kolekcji, która jest synchronizowana czy niemodyfikowalna. Collections zawiera szereg metod zaczynających się od synchronized albo unmodifiable, które tworzą instancje dekoratorów. W języku Python istnieje składnia, która pozwala na łatwe użycie dekoratorów. Można powiedzieć, że ten wzorzec projektowy został wbudowany w język. Notacja @dekorator pozwala dekorować zarówno klasy jak i funkcje. Przykładami dekoratorów dostępnych w bibliotece standardowej mogą być @property, @contextlib.contextmanager czy @functools.wraps.

Przykład niepoprawny

Mamy klasę abstrakcyjną, która reprezentuje to czym każda pizza będzie. Każda pizza musi posiadać dwie metody. Jedna z nich informuje nas o tym, ile Pizza będzie kosztować. Druga zwróci nazwę pizzy.

  1. public abstract class Pizza  
  2. {  
  3.     public abstract double CalculateCost();  
  4.   
  5.     public abstract string GetName();  
  6. } 

Na chwilę obecną mamy definicję małej pizzy.

  1. public class SmallPizza : Pizza  
  2. {  
  3.     public override double CalculateCost()  
  4.     {  
  5.         return 12.00;  
  6.     }  
  7.   
  8.     public override string GetName()  
  9.     {  
  10.         return "Small Pizza";  
  11.     }  
  12. }

Średniej pizzy:

  1. public class MediumPizza : Pizza  
  2. {  
  3.     public override double CalculateCost()  
  4.     {  
  5.         return 25.00;  
  6.     }  
  7.   
  8.     public override string GetName()  
  9.     {  
  10.         return "Medium";  
  11.     }  
  12. } 

oraz dużej pizzy:

  1. public class LargePizza : Pizza  
  2. {  
  3.     public override double CalculateCost()  
  4.     {  
  5.         return 50.00;  
  6.     }  
  7.   
  8.     public override string GetName()  
  9.     {  
  10.         return "Large Pizza";  
  11.     }  
  12. } 

Każda z nich ma swoją osobistą nazwę i cenę. Wszystko wydaje się w porządku, ale do czasu. Otóż na horyzoncie pojawił się cały zestaw innych dodatków do pizzy. Dodatki do pizzy lekko modyfikują istniejące już mechanizmy naszej pizzy. Problem jednak polega na tym, że istnieje dużo kombinacji dodatków. Mamy pizzę z szynką lub z pieczarkami. Mamy też pizzę, która zawiera oba te składniki. Widząc wszystkie te klasy widzimy, że robimy coś źle. Nie możemy mieć klasy do każdego możliwego przypadku. Musimy ten przypadek jakoś utworzyć dynamicznie.

  1. public class LargePizzaWithHamAndCheese : Pizza  
  2. {  
  3.     public override double CalculateCost()  
  4.     {  
  5.         return 52.00;  
  6.     }  
  7.   
  8.     public override string GetName()  
  9.     {  
  10.         return "LargePizzaWithHamAndCheese";  
  11.     }  
  12. }  

Kod 1. Kod programu bez użycia wzorca

Duża pizza z szynką i serem kosztuje 52 złote. Co, jeśli jednak zwiększę cenę sera. To znaczy, że muszę zmieniać tę cenę w tylu klasach równocześnie. Dlaczego jednak nie mieć samej klasy, która by reprezentowała tylko ser i wtedy bym ustalał cenę tylko w jednym miejscu. Tutaj z pomocą przychodzi wzorzec dekorator.

Przykład poprawny

Klasa abstrakcja reprezentująca podstawę Pizzy nie uległa zmianie.

  1. public abstract class Pizza  
  2. {  
  3.     public abstract double CalculateCost();  
  4.     public abstract string GetName();  
  5. } 

To samo tyczy się wszystkich rozmiarów

  1. public class LargePizza : Pizza  
  2. {  
  3.     public override double CalculateCost()  
  4.     {  
  5.         return 50.00;  
  6.     }  
  7.   
  8.     public override string GetName()  
  9.     {  
  10.         return "Large Pizza";  
  11.     }  
  12. }  
  13. public class MediumPizza : Pizza  
  14. {  
  15.     public override double CalculateCost()  
  16.     {  
  17.         return 25.00;  
  18.     }  
  19.   
  20.     public override string GetName()  
  21.     {  
  22.         return "Medium";  
  23.     }  
  24. }  
  25. public class SmallPizza : Pizza  
  26. {  
  27.     public override double CalculateCost()  
  28.     {  
  29.         return 12.00;  
  30.     }  
  31.   
  32.     public override string GetName()  
  33.     {  
  34.         return "Small Pizza";  
  35.     }  
  36. }

Tutaj jednak siedzi serce naszego wzorca. Oto klasa implementująca wzorzec dekoratora. Dekorator musi dziedziczyć po klasie abstrakcyjnej lub interfejsie reprezentującej wszystkie możliwe klasy dekorujące. Dekorator też może być dekorowany dlatego on też dziedziczy, powinien w sobie posiadać pole dostępne tylko dla klas dziedziczących. Jak widzisz metody  odwołują się do tej pizzy z pola. Pole to zostanie uzupełnione w konstruktorze. Czyli definicję pizzy przesyłamy do konstruktora. Jak zaraz zobaczysz daje to nam możliwość stworzenia stosów różnych dekoratorów, które będą wywoływany kolejne definicję.

  1. public class PizzaDecorator : Pizza  
  2. {  
  3.   
  4.     //obiekt, który będzie dekorowany  
  5.     protected Pizza _pizza;  
  6.   
  7.     public PizzaDecorator(Pizza pizza)  
  8.     {  
  9.         _pizza = pizza;  
  10.     }  
  11.     public override double CalculateCost()  
  12.     {  
  13.         return _pizza.CalculateCost();  
  14.     }  
  15.   
  16.     public override string GetName()  
  17.     {  
  18.         return _pizza.GetName();  
  19.     }  
  20. } 

Na razie wszystko jeszcze nie jest jasne, bo sam dekorator tylko jest podstawką, na której bazie utworzymy główne bardziej określone mechanizmy dekoratorów.

Oto nasz pierwszy serowy dekorator.

  1. public class CheeseDecorator : PizzaDecorator  
  2. {  
  3.     public CheeseDecorator(Pizza pizza)   
  4.         : base(pizza)  
  5.     {  
  6.   
  7.     }  
  8.   
  9.     public override double CalculateCost()  
  10.     {  
  11.         return base.CalculateCost() + 2.15;  
  12.     }  
  13.   
  14.     public override string GetName()  
  15.     {  
  16.         return base.GetName() + " Cheese";  
  17.     }  
  18.  

Dekorator serowy wykonuje metody z poprzednich klas dziedziczących dodając od siebie swoją nazwę oraz cenę.

  1. class Program  
  2. {  
  3.     static void Main(string[] args)  
  4.     {  
  5.         Pizza largePizza =  new LargePizza();  
  6.   
  7.         largePizza = new CheeseDecorator(largePizza);  
  8.   
  9.         Console.WriteLine("{0:C2}", largePizza.CalculateCost());  
  10.   
  11.         Console.ReadKey();  
  12.     }  
  13. }

Kod 2. Kod programu z użyciem wzorca

Użycie dekoratora jest proste. Tworzę sobie dużą pizzę i do dekoratora serowego przekazuję swoją dużą pizzę. Można w ten sposób stworzyć jeszcze inne składniki pizzy.

Podsumowanie

Dziedziczenie jest formą rozszerzania klas, ale niekoniecznie musi być najlepszym sposobem na elastyczne projekty. Tworząc program warto zwracać uwagę aby możliwe było rozszerzanie zachowań bez konieczności modyfikacji istniejącego kodu. Taki efekt uzyskamy wykorzystując kompozycję i delegację. Wzorzec projektowy dekorator stanowi poważną alternatywę dla dziedziczenia pod względem dodawania nowych zachowań. Ważną informacją jest to, że dekoratory są przezroczyste dla klientów danego składnika tak długo, jak długo funkcjonowanie klienta nie jest uzależnione od rzeczywistej implementacji danego składnika.

Podsumowując, wzorzec dekorator posiada wiele zalet choćby zapewnia większa elastyczność niż statyczne dziedziczenie, pozwala uniknąć tworzenia przeładowanych funkcjami klas na wysokich poziomach hierarchii. Posiada również wady: interfejs dekoratora musi być dokładnie taki sam jak klasy dekorowanej, przy użyciu tego wzorca może powstać wiele małych obiektów. Po lekturze tego artykułu wiesz czym jest wzorzec dekorator. Znasz przykładowy sposób jego implementacji, jakie posiada zalety i wady.