Wzorzec Dekorator (ang. Decorator) pochodzi z grupy wzorców strukturalnych.
Wykorzystuje się go do dynamicznego rozszerzania funkcjonalności wybranej klasy bez potrzebny jej modyfikacji.
Najprościej mówiąc, jest to alternatywa dla dziedziczenia klas. Zamiast dodawać nową funkcjonalność do klasy, tworzymy nową klasę (dekorującą), która wykorzystuje klasę bazową (klasa dekorowana) i dodatkowo wprowadza nową funkcjonalność, a to wszystko bez fizycznej ingerencji w klasę bazową, do której zresztą nie zawsze mamy dostęp (jeśli jest w postaci skompilowanej biblioteki).
Nie są to jakieś moje wymysły i wymyślanie sztucznych problemów na potrzeby artykułu. Jest to problem z którym zetknąłem się podczas pisania aplikacji.
Żeby nie teoretyzować za dużo, przytoczę życiowy przykład.
Przyjmijmy, że w naszej aplikacji wykorzystujemy taką o to strukturę danych:
class MessageType { int age; String fillName; }
Nasza aplikacja wyświetla te dane w kontrolce ListView, ale w sposób przez nas pożądany. Musimy więc dostarczyć klasę, która implementuje dodatkowo interfejs:
Osp::Ui::Controls::ICustomElement
Najprościej więc zmodyfikować naszą strukturę danych MessageType o implementację tego interfejsu:
class MessageType : public ICustomElement { public: int age; String fillName; virtual bool OnDraw(Osp::Graphics::Canvas &canvas, const Osp::Graphics::Rectangle &rect, Osp::Ui::Controls::ListItemDrawingStatus status); }
Nasza struktura oprócz tego, że posiada niezbędne informacje w postaci wieku i imienia z nazwiskiem, posiada także metodę, która jest odpowiedzialna za wyświetlenie jej w kontrolce ListView.
Gdzie leży więc problem? Jeśli struktura danych ma być wyświetlana tylko w jeden słuszny sposób, problemu nie ma. Jeśli natomiast te same dane chcemy wyświetlać na kilka rożnych sposobów, to mamy dwa (zapewne minimum) wyjścia.
Pierwszym wyjściem jest wyprowadzenie nowych klas dziedziczących po klasie MessageType i nadpisujących metodę OnDraw().
class RedMessageType : public MessageType { public: virtual bool OnDraw(Osp::Graphics::Canvas &canvas, const Osp::Graphics::Rectangle &rect, Osp::Ui::Controls::ListItemDrawingStatus status); }
class GreenMessageType : public MessageType { public: virtual bool OnDraw(Osp::Graphics::Canvas &canvas, const Osp::Graphics::Rectangle &rect, Osp::Ui::Controls::ListItemDrawingStatus status); }
Wyprowadziliśmy dwie klasy, które różnią się tylko metodą OnDraw(). Klasa RedMessageType jest odpowiedzialna za rysowanie elementu na czerwono, GreenMessageType zaś na zielono.
Niby wszystko fajnie, ale nasza wymyślona funkcja zwraca listę obiektów klasy MessageType i nie mamy możliwości jej zmiany, lub nie chcemy tworzyć jej klonów, które różniłyby się tylko tym, że raz zwracałaby listę obiektów klasy RedMessageType, a innym razem GreenMessageType – to nie ma sensu.
W moim przypadku stworzyłem jedną funkcję, która jest odpowiedzialna za pobieranie danych z Internetu oraz zwracanie ich w postaci listy obiektów klasy MessageType i nie chcę w nią już ingerować psując logikę aplikacji. Struktura danych ma zawierać tylko niezbędne dane, nie musi i nie powinna zawierać informacji o sposobie ich wyświetlania, bo nie powinno to ją interesować.
Doskonałym rozwiązaniem mojego problemu był właśnie wzorzec Decorator i jest to drugie rozwiązanie naszego (mojego) problemu.
Klasę MessageType pozostawiamy w takiej postaci:
class MessageType { public: int age; String fullName; }
Tworzymy zatem klasy dekorujące:
class RedMessageDecorator : public ICustomElement { private: MessageType *__pMessageType; public: RedMessageDecorator(MessageType *pMessage); virtual bool OnDraw(Osp::Graphics::Canvas &canvas, const Osp::Graphics::Rectangle &rect, Osp::Ui::Controls::ListItemDrawingStatus status); }
class GreenMessageDecorator : public ICustomElement { private: MessageType *__pMessageType; public: GreenMessageDecorator(MessageType *pMessage); virtual bool OnDraw(Osp::Graphics::Canvas &canvas, const Osp::Graphics::Rectangle &rect, Osp::Ui::Controls::ListItemDrawingStatus status); }
Stworzyliśmy dwie klasy odpowiedzialne za renderowanie danych dekorujące klasę MessageType. Obiekt klasy MessageType przekazujemy jako argument do konstruktora klasy dekorującej.
Klasa dekorująca np. RedMessageDecorator przechowuje wskaźnik do klasy dekorowanej i może na niej operować. Dodatkowo dostarcza metodę odpowiedzialną za rysowanie naszej struktury.
Implementacja klasy RedMessageDecorator:
RedMessageDecorator::RedMessageDecorator(MessageType *pMessage) { __pMessageType = pMessage; } bool RedMessageDecorator::OnDraw(Osp::Graphics::Canvas &canvas, const Osp::Graphics::Rectangle &rect, Osp::Ui::Controls::ListItemDrawingStatus status) { canvas.DrawText(Point(0, 0), this->__pMessageType->fullName); return true; }
Doszliśmy już do końca naszego pierwszego przykładu. Rozszerzyliśmy dynamicznie klasę bazową (dekorowaną) o jedną metodę OnDraw() bez modyfikacji tej pierwszej.
Wystarczy teraz postępować wg schematu:
- pobieramy dane w postaci np. listy, której elementami są obiekty typu MessageType,
- dekorujemy wszystkie elementy listy używając wybranej klasy dekorującej (RedMessageDecorator, GreenMessageDecorator),
- dodajemy nowy obiekt do kontrolki ListView.
Dodając element do ListView korzystamy z jej metody:
result AddElement(const Osp::Graphics::Rectangle &rect, int elementId, const ICustomElement &element);
Argument element będzie wymagał podania jednego z obiektów typu: RedMessageDecorator lub GreenMessageDecorator.
Tym sposobem odseparowaliśmy klasy odpowiedzialne za prezentację danych od klas reprezentujących tylko dane.
Jako, że tysiąc słów nie zastąpi jednego obrazka, wygenerowałem dwa schematy UML przedstawiające obie wersje rozwiązań.
Wracając do zagadnienia. W naszym przykładzie klasa MessageType nie udostępnia żadnych metod. Pamiętajmy, że zadaniem dekoratora jest także udostępnienie funkcjonalności klasy dekorowanej klasie dekorującej i jej rozszerzenie.
Warto więc stworzyć interfejs, który będzie implementowany przez klasę dekorowaną, a klasa dekorującą powinna tylko przez ten interfejs odwoływać się do klas dekorowanych.
Ostatni schemat UML jest po porostu zoptymalizowaną wersją poprzedniego schematu.
Implementację do postaci kodu zostawiam już Tobie.
Jeśli widzisz błąd semantyczny lub inny (także na schematach UML), bardzo proszę o powiadomienie. Zaprezentowane przykłady były pisane jak zwykle na tak zwanej kartce papieru i mogą się zdarzyć pomyłki.