Czytasz jeden z artykułów opisujących wzorce projektowe. jeżeli interesuje Cię ten temat zapraszam Cię do lektury pozostałych artykułów, które powstały w ramach tej serii – wzorce projektowe. W zrozumieniu artykułu przyda Ci się wiedza dotycząca podstaw UML’a.
Problem do rozwiązania
Wyobraź sobie restaurację, w której możesz zjeść pizzę. Właściciel restauracji daje Ci do wyboru 10 różnych dodatków. Możesz skomponować pizzę samodzielnie używając dostępnych dodatków. Każdy z dodatków ma swoją cenę i może być użyty wyłącznie jeden raz. Właściciel restauracji mógłby wypisać wszystkie kombinacje z tych 10 dodatków. Menu miałoby wtedy 1023 pozycje, 1024 jeżeli wliczymy Margharitę… Trochę dużo ;).
Właściciel podszedł do sprawy inaczej. przez cały czas daje Ci dowolność w wyborze dodatków, jednak wycenia każdy z nich jako osobną pizzę. Na przykład pizza z szynką, pizza z bazylią, pizza z mozzarellą i tak dalej. Następnie pozwala Ci łączyć ze sobą te pizze w dowolny sposób. Na przykład pizza bez żadnych dodatków kosztuje 15zł. Pizza z szynką kosztuje o 7 zł więcej niż pizza bazowa. Pizza z bazylią kosztuje o 2 zł więcej niż pizza bazowa.
Dzięki takiemu podejściu w menu znajduje się 11 pozycji. Cena pizzy bez dodatków i cena każdego dodatku określona jako cena pizzy bazowej + X zł. Można powiedzieć, iż właściciel restauracji użył wzorca dekoratora do opracowania cennika1.
Podobne problemy występują w projektach informatycznych. Zdarzają się sytuacje, w których trzeba rozszerzyć działanie pewnego obiektu. Możliwych rozszerzeń jest wiele, jeszcze więcej jest kombinacji tych rozszerzeń. Z pomocą w rozwiązaniu tego problemu przychodzi wzorzec projektowy dekorator (ang. decorator2).
Wzorzec dekorator
Diagramy klas
Istnieje wiele możliwości implementacji tego wzorca projektowego. Diagram klas poniżej pokazuje najprostszą z nich:
DecoratorA i DecoratorB dekorują klasę Component. Dekoratory zawierają instancję klasy Component.
Często ten wzorzec projektowy przedstawiany jest w bardziej skomplikowany sposób:
W tym przypadku dekoratory mają wspólnego przodka, abstrakcyjną klasę Decorator. Sam komponent, który jest dekorowany także jest klasą abstrakcyjną, która posiada swoje konkretne implementacje. Na diagramie wyżej jest to ConcreteComponent.
Nie są to jedyne możliwe wersje implementacji tego wzorca. Przykładem innej implementacji może być użycie interfejsów w miejscu klasy komponentu. Inną modyfikacją może być użycie kompozycji w miejscu agregacji. Obie zmiany nie wpływają znacząco na implementację tego wzorca projektowego.
Wzorzec projektowy dekorator pozwala na wielokrotne rozszerzenie funkcjonalności obiektu poprzez „nakładanie” na siebie dekoratorów.
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Przykładowa implementacja dekoratora
Zacznę od pizzy bazowej:
Ot, zwykła klasa, która reprezentuje podstawową pizzę. Posiada metodę getPrice, która zwraca jej cenę.
Poniżej możesz zobaczyć jeden z dekoratorów. W tym przypadku jest to pizza z mozzarellą:
PizzaWithMozzarella w konstruktorze przyjmuje jako parametr instancję klasy Pizza, którą opakowuje. Następnie używa jej do obliczenia ceny pizzy z mozzarellą dodając do ceny pizzy bazowej cenę sera.
W tym przypadku klasa Pizza odpowiada klasie Component z diagramu UML, a klasa PizzaWithMozzarella reprezentuje DecoratorA.
Poniżej możesz zobaczyć użycie dekoratorów w praktyce. Opakowując kolejne pizze w dekoratory otrzymuję coraz bardziej skomplikowane pozycje. Dzięki takiemu podejściu mogę łączyć dodatki w dowolny sposób:
Dodatkowe rozważania
Zalety
Jedną z często polecanych praktyk w programowaniu obiektowym jest preferowanie kompozycji przed dziedziczeniem. Wzorzec projektowy dekorator jest flagowym przykładem użycia tej reguły. Takie podejście pozwala na dynamiczne rozszerzanie funkcjonalności obiektu bez potrzeby kompilacji kodu.
Niewątpliwą zaletą dekoratora jest możliwość dowolnego łączenia istniejących dekoratorów. Każdy z nich będzie opakowywał kolejny obiekt nie mając świadomości, iż jest kolejnym dekoratorem w kolejce. Jest to istotne w przypadku gdy istnieje kilka dodatkowych funkcjonalności, które powinna zawierać rozszerzana klasa.
Wady
Interfejs dekoratora musi być dokładnie taki sam jak klasy dekorowanej. W niektórych językach programowania (na przykład w Javie) może prowadzić to do klas, które mają sporo metod, których implementacja polega na przekazaniu wywołania do dekorowanego obiektu (jeśli dekorator implementuje interfejs). Tę wadę można rozwiązać stosując dziedziczenie3.
Dekorator często jest „płaską klasą”. Rozszerza on dekorowaną klasę o jedną, podstawową funkcjonalność. Prowadzić to może do sytuacji, w której system zawiera wiele niewielkich klas. W sytuacji gdy zwykle używa się stałego zbioru dekoratorów użycie standardowego dziedziczenia może ograniczyć tę liczbę.
Przykłady użycia wzorca dekorator
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ć, iż 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.
Zadanie do wykonania
Chociaż klasy reprezentujące pizze z dodatkami spełniają swoje zadanie mogą być ulepszone. Zwróć uwagę, iż klasy te są do siebie bardzo podobne. Duplikacja kodu jest zła, zrefaktoryzuj kod w taki sposób aby usunąć tę duplikację. Spróbuj rozwiązać ten problem używając bardziej skomplikowanej wersji dekoratorów z drugiego diagramu UML.
Jak zwykle zachęcam Cię do samodzielnego rozwiązania zadania, w ten sposób nauczysz się najwięcej. Możesz też porównać swoje rozwiązanie z przykładowym.
Dodatkowe materiały do nauki
Niezmiennie, we wszystkich artykułach z serii poświęconej wzorcom projektowym polecam książkę Design Patterns – Gamma, Helm, Johnson, Vlissides. jeżeli miałbym polecić wyłącznie jedno źródło to poprzestałbym na tej książce.
Warto także rzucić okiem do polskiej i angielskiej Wikipedii gdzie znajdziesz artykuły dotyczące tego wzorca projektowego.
Zachęcam Cię też do zajrzenia do kodu źródłowego, którego użyłem w tym artykule.
Podsumowanie
Po lekturze tego artykułu wiesz czym jest wzorzec dekorator. Znasz przykładowy sposób jego implementacji. Masz też zestaw materiałów dodatkowych, które pozwolą Ci spojrzeć na temat z innej strony. Po rozwiązaniu zadania wiesz jak zaimplementować ten wzorzec samodzielnie. Innymi słowy udało Ci się właśnie poznać kolejny wzorzec projektowy. Gratulacje! ;)
Jeśli artykuł przypadł Ci do gustu proszę podziel się nim ze znajomymi. Dzięki temu pozwolisz mi dotrzeć do nowych Czytelników, za co z góry dziękuję. jeżeli nie chcesz pomiąć kolejnych artykułów dopisz się do samouczkowego newslettera i polub Samouczka Programisty na Facebooku.
Do następnego razu!
-
Ten przykład jest trochę naciągany. Sam dodatek nie jest pizzą, ale pizza z dodatkiem już tak. Jest to coś najbliższego światu rzeczywistemu co jest „dekoratorem” i powinno być łatwe do zrozumienia. ↩
-
Inną nazwą tego wzorca projektowego, z którą możesz się spotkać jest wrapper. ↩
-
Takie podejście może wydłużać hierarchię dziedziczenia, sam preferuję użycie interfejsów jeżeli hierarchia dziedziczenia jest dość długa. ↩