Wzorce projektowe – Dekorator
Dekorator jest ciekawym wzorcem projektowym, który w pewnych okolicznościach może być niezwykle użyteczny. Należy do grupy wzorców strukturalnych i pozwalającym na dynamiczne rozszerzenie funkcjonalności danej klasy. Odbywa się to poprzez opakowanie klasy bazowej w inne klasy rozszerzające jej działanie.
Charakterystyka wzorca dekorator
Dekorator pozwala na rozbudowę istniejącej klasy, dodając lub zmieniając jej zachowania, bez potrzeby dziedziczenia. Wykorzystanie tego wzorca projektowego polega na opakowaniu klasy bazowej (dekorowanej) w klasę dekorującą.
Zaletą wykorzystania tego wzorca jest bardzo duża elastyczność poprzez rozbicie programu na wiele mniejszych klas, które mogą dynamicznie zmieniać działanie klasy bazowej. Aplikacja napisana w ten sposób pozwala na łatwe rozszerzanie o nowe funkcjonalności. Wadą jest rozbicie projektu na małe klasy, które często są do siebie bardzo podobne.
Wzorzec dekorator nie jest bardzo skomplikowany w zrozumieniu i użyciu. Poniższy diagram UML przedstawia jego podstawową strukturę. Podstawą jest wspólny interfejs, który jest implementowany zarówno przez klasę dekorowaną jak i wszystkie klasy dekorujące.
Przykład zastosowania wzorca dekorator – tworzenie kanapek
Najlepiej pokazać działanie wzorca na przykładzie. Stwórzmy prosty skrypt, którego zadaniem będzie robienie kanapek z wykorzystaniem różnych składników. W zależności od dobranych składników, cena za kanapkę będzie się różniła.
Zacznijmy od napisania kontraktu (interfejsu) naszej kanapki. W celach demonstracyjnych wystarczą nam dwie metody, które zwracają cenę i listę składników.
1 2 3 4 5 6 7 |
namespace DesignPatterns\Decorator\Sandwich; interface SandwichInterface { public function getCost(); public function getIngredients(); } |
Ok., teraz napiszmy naszą klasę bazową dla kanapki. Każda kanapka musi składać się z podstawowych składników, jakimi są: bułka, masło i majonez. Musimy również zainicjować cenę.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; class BasicSandwich implements SandwichInterface { private $ingredients = ['bułka', 'masło', 'majonez']; private $cost = 8.00; public function getCost() { return $this->cost; } public function getIngredients() { return $this->ingredients; } } |
Mając podstawowy model kanapki, możemy przystąpić do jej rozbudowy (dekorowania) o dodatkowe składniki. Każdy składnik piszemy, jako osobną klasę, w naszym przykładzie będą to salami, ser i warzywa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; class Salami implements SandwichInterface { private $ingredients = ['salami']; private $cost = 2.00; protected $sandwich; public function __construct(SandwichInterface $sandwich) { $this->sandwich = $sandwich; } public function getCost() { return $this->sandwich->getCost() + $this->cost; } public function getIngredients() { return array_merge($this->sandwich->getIngredients(), $this->ingredients); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; class Chees implements SandwichInterface { private $ingredients = ['ser']; private $cost = 1.50; protected $sandwich; public function __construct(SandwichInterface $sandwich) { $this->sandwich = $sandwich; } public function getCost() { return $this->sandwich->getCost() + $this->cost; } public function getIngredients() { return array_merge($this->sandwich->getIngredients(), $this->ingredients); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; class Vegetables implements SandwichInterface { private $ingredients = ['ogórek', 'pomidor', 'sałata']; private $cost = 2.50; protected $sandwich; public function __construct(SandwichInterface $sandwich) { $this->sandwich = $sandwich; } public function getCost() { return $this->sandwich->getCost() + $this->cost; } public function getIngredients() { return array_merge($this->sandwich->getIngredients(), $this->ingredients); } } |
Każda z klas dekorowanych implementuje interfejs kanapki. Ponadto w konstruktorze ustawia instancję kanapki, która następnie jest rozwijana przy pomocy implementowanych metod.
Skrypt jest gotowy do użycia, jednak porównując klasy dekorujące można zauważyć, że są one niemal identyczne. Łamie to świętą zasadę DRY – Don’t Repeat Yourself – Nie Powtarszaj Się. Powyższy kod można zrefaktoryzować upraszczając nieco jego strukturę. Zacznijmy od napisania abstrakcyjnej klasy, reprezentującej bliżej nieokreślony składnik naszej kanapki.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; abstract class SandwichAddition implements SandwichInterface { protected $ingredients = []; protected $cost = 0.00; protected $sandwich; protected function __construct(SandwichInterface $sandwich) { $this->sandwich = $sandwich; } public function getCost() { return $this->sandwich->getCost() + $this->cost; } public function getIngredients() { return array_merge($this->sandwich->getIngredients(), $this->ingredients); } } |
Następnie wystarczy jedynie rozszerzyć abstrakcyjny dodatek do kanapki o jego konkretne implementacje, w których wystarczy jedynie określić właściwości dla poszczególnych składników.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; use DesignPatterns\Decorator\Sandwich\SandwichAddition; class Salami extends SandwichAddition { public function __construct(SandwichInterface $sandwich) { parent::__construct($sandwich); $this->ingredients = ['salami']; $this->cost = 2.00; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; use DesignPatterns\Decorator\Sandwich\SandwichAddition; class Chees extends SandwichAddition { public function __construct(SandwichInterface $sandwich) { parent::__construct($sandwich); $this->ingredients = ['ser']; $this->cost = 1.50; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
namespace DesignPatterns\Decorator\Sandwich; use DesignPatterns\Decorator\Sandwich\SandwichInterface; use DesignPatterns\Decorator\Sandwich\SandwichAddition; class Vegetables extends SandwichAddition { public function __construct(SandwichInterface $sandwich) { parent::__construct($sandwich); $this->ingredients = ['ogórek', 'pomidor', 'sałata']; $this->cost = 2.50; } } |
Jak widzimy, nasz kod mocno się skrócił i uprościł. Przyszedł czas na przetestowanie przykładu i stworzenie konkretnej kanapki.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
use DesignPatterns\Decorator\Sandwich\BasicSandwich; use DesignPatterns\Decorator\Sandwich\Salami; use DesignPatterns\Decorator\Sandwich\Chees; use DesignPatterns\Decorator\Sandwich\Vegetables; // Basic Sandwich $sandwich = new BasicSandwich(); // Sandwich Additions $sandwich = new Salami($sandwich); $sandwich = new Chees($sandwich); $sandwich = new Vegetables($sandwich); // Above can be written in one line: // $sandwich = new Vegetables(new Salami(new Chees(new BasicSandwich()))); // Final Sandwich echo 'Wybrane składniki: '.implode(', ', $sandwich->getIngredients()); echo 'Koszt: '.$sandwich->getCost().' zł'; |
Najpierw tworzymy instancje klasy bazowej, czyli w powyższym przykładzie jest to klasa podstawowej kanapki. Następnie obudowujemy ją o dodatkowe składniki, przekazując aktualną wersje kanapki do kolejnej klasy dekorującej. Jako rezultat otrzymujemy w pełni skomponowaną kanapkę.
1 2 |
Wybrane składniki: bułka, masło, majonez, salami, ser, ogórek, pomidor, sałata Koszt: 14 zł |
Zmiana w klasy dekorującej spowoduje automatyczną aktualizację końcowej ceny oraz listy składników.
Tak napisany skrypt można rozbudowywać dalej o nowe składniki, które działają jako niezależne obiekty, mogące być dodawane lub odejmowane dynamicznie podczas działania aplikacji. Program zachowuje zasadę OCP (Open-Close Principle), czyli jest otwarty na rozszerzenia, ale zamknięty na modyfikację.
Powyższy przykład można pobrać tutaj: decorator-sandwich
Przykład na GitHub: https://github.com/molitorys/design-patterns/tree/master/src/Decorator/Sandwich
Podsumowanie
Wzorzec projektowy dekorator możemy użyć tam, gdy:
- istnieje konieczność dodawania nowych funkcjonalności do poszczególnych obiektów w sposób dynamiczny, bez wpływu na inne obiekty
- potrzebny jest mechanizm cofania wprowadzanych do obiektów zmian
- rozszerzanie funkcjonalnosci przez tworzenie kolejnych podklas jest niepraktyczne i powoduje zbyt duże zamieszanie w kodzie
Wykorzystanie omawianego wzorca daje większą elastyczność niż zwykłe dziedziczenie oraz pozwala na uniknięcie tworzenia przeładowancyh funkcjami klas. Jak zawsze są jakieś minusy, np. konieczność tworzenia wielu małych klas, ale zdecydowanie zalety przesłaniają wady. Zachęcam do zapoznania się z wzorcem dekorator.