Wzorce projektowe: dekorator

Czasami przychodzi moment kiedy potrzebujemy istniejące w kodzie klasy uzupełnić o dodatkowe funkcjonalności jednocześnie nie psując już napisanego kodu. Przykładowo dodając logowanie każdej wywołanej metody w klasie, albo uzupełniając zapis do pliku o kompresję danych. Tego typu operacje możemy w prosty sposób dodać do już istniejącego kodu za pomocą wzorca dekorator, którym dzisiaj się zajmiemy.

Co to ten dekorator?

Na początek warto w ogóle wyjaśnić czym jest wzorzec dekoratora.

Otóż jest to wzorzec strukturalny, a więc taki, który opisuje w jaki sposób obiekt jest zbudowany.

Dekorator polega na opakowaniu bazowej klasy w klasę dekorującą. Wykorzystuje się do tego kompozycję. Wzorzec ten jest alternatywą dla dziedziczenia, które posiada szereg ograniczeń w tym zakresie. Przede wszystkim dekorator pozwala na dekorowanie w trakcie działania programu, a nie podczas kompilacji. Dodatkowo umożliwia „składanie” dekoratorów, a więc daje elastyczność w kwestii doboru zestawu nowych funkcjonalności.

Ważne w tym wzorcu jest to, że pierwotne zachowanie klasy, którą dekorujemy pozostaje bez zmian. Pozwala to zachować pełną kompatybilność z już istniejącym kodem i zapewnia transparentność naszego dekoratora.

Formalny diagram opisujący sposób budowy dekoratora prezentuje się następująco:

 

Dekorator
Diagram wzorca ‚dekorator’ – Wikipedia, CC0

Jednak jak to z diagramami bywa – nie są dla niektórych najczytelniejszą formą przedstawiania budowy. Dlatego postaram się opisać wzorzec również słownie.

Po kryjomu

Cała „magia” działania dekoratora opiera się o wykorzystanie tego samego interfejsu, z którego korzysta klasa, którą chcemy dekorować. I tutaj jest już pierwszy warunek wymagany przy tym wzorcu – istnienie interfejsu dla klasy bazowej.

Sam dekorator w najprostszej postaci możemy wykonać na jednej klasie, która po prostu zaimplementuje ten sam interfejs co klasa bazowa. Dodatkowo musi ona przyjmować w jakiś sposób klasę, którą dekorujemy – zazwyczaj jest to wykonywane za pomocą konstruktora.

Tak więc zakładamy, że mamy interfejs i implementującą go klasę, którą używamy w naszym kodzie i chcemy ją wzbogacić o nowe funkcje nie zmieniając jej implementacji. Dodajemy nową klasę, która również implementuje nasz interfejs. Klasa ta przyjmuje w konstruktorze obiekt, którego typem jest implementowany interfejs (dzięki temu nie ograniczamy się do dekorowania jednej klasy, ale całego zbioru. Jest to ogromna zaleta pozwalająca nakładać dekoratory jeden na drugi). Klasa dekorująca musi w metodach z interfejsu wywołać takie same metody z obiektu przekazanego w konstruktorze – pozwala to zachować pełną funkcjonalność klasy dekorowanej.

Mając tak przygotowaną konstrukcję możemy dodawać nowe funkcjonalności. Czy to dodając osobne metody, które użyjemy w naszym kodzie modyfikując za ich pomocą obiekt, który dostaniemy, albo dokładając efekty uboczne do już istniejących metod. Dobrym przykładem jest tutaj dodanie logowania wywołania metody z obiektu albo np. modyfikowanie danych przekazywanych do metod obiektu.

Przykładowa implementacja

Przykładowa implementacja napisana będzie w języku C#.

Na początek mamy nasz interfejs i klasę, którą będziemy dekorować:

Interfejs zawiera dwie metody, które są następnie implementowanie w klasie BaseClass. Teraz pora na dekorator:

Jak widać on również implementuje ten sam interfejs. Dodatkowo przyjmuje obiekt klas implementujących ten interfejs.

Następnie w implementacji metod wymaganych przez IBaseType oczywiście wywołujemy odpowiednie metody z przekazanego wcześniej obiektu. Dodatkowo jednak uzupełniamy ich działanie – w pierwszym przypadku dodajemy logowanie wywołania metody, zaś w drugim przypadku modyfikujemy przekazywaną wartość.

Teraz chcąc użyć naszego dekoratora możemy zrobić to w taki sposób:

Ja chcę więcej!

Wspominałem już, że jedną z właściwości wzorca dekorator jest możliwość nakładania różnych dekoratorów tego samego interfejsu na siebie. Jest to bardzo proste jednak warto to również pokazać. W tym celu dodajmy jeszcze jeden dekorator:

Zostawia on metodę FancyMethod() w spokoju jednak dodaje całkowicie nową.

Teraz możemy nałożyć oba dekoratory na nasz obiekt. W takim wypadku lekko zmodyfikowany przykład przedstawiony powyżej będzie prezentował się następująco:

Dodane zostało jedynie tworzenie nowego dekoratora, który w konstruktorze przyjął poprzedni dekorator zawierający bazowy obiekt. W razie konieczności moglibyśmy te dekoratory zamienić ze sobą miejscami albo dodawać kolejne.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *