Dekorator (Decorator Pattern)

Omawiając programowanie w języku Python, pojęcie dekoratora jest dwuznaczne. Może bowiem oznaczać:

  • dekoratory języka Python, a więc pewne funkcje przyjmujące jako argumeny inne funkcje. Z takich dekoratorów korzystamy poprzedzając je ze znakiem @. Znanym Ci przykładem dekoratora, predefiniowanym w języku Python jest @property

  • wzorzec projektowy dekorator, który można zaimplementować w każdym obiektowym języku programowania.

Dekoratory w języku Python

Dekorator to w zasadzie funkcja, której argumentem jest inna funkcja, która …. zwraca jeszcze inną funkcję. Jako przykład niech posłuży funkcja, która zamienia w tekście małe litery na wielkie. W pierwszym, bardzo prostym przykładzie, funkcja dekorowana greetings() jest bezargumentowa i zwraca napis Hello!. Dekoratorem jest funkcja capitalize(func), która:

  • przyjmuje jako argument dowolną funkcję (argument func)

  • deklaruje i zwraca funkcję dekorującą upercase_decorator() - jako zmienną!

  • funkcja dekorująca wywołuje func; zakłada że wynik jest napisem i zamienia w nim małe litery na wielkie

  • funkcja upercase_decorator() zwraca napis (wielkimi literami), natomiast sam dekorator capitalize(func) zwraca funkcję dekorującą

Sam moment dekorowania funkcji greetings() odbywa się w linii 12. Znajduąca się tam instrukcja uppercase_greetings = capitalize(greetings) równoważna jest napisaniu @capitalize nad definicją funkcji greetings(). Po drobnych zmianach nasz dekorator działa też dla funkcji z argumentami:

W tym przypadku linia 12 zastąpiona została instrukcją @capitalize, którą dodano w linii 8. Poniższy przykład pokazuje, że dekoratory można zagnieżdżać - w liniach 14 i 15 wpisano dwie dekoracje, które są równoważne instrukcji intro_uppercase_greetings = intro(capitalize(greetings)) :

Powyżej opisałem tylko część możliwości dekoratorów, które mogą np. przyjmować swoje własne argumenty.

Note

Dekoratory w Pythonie umożliwiają łatwą modyfikację działania programu. Załóżmy że mając funkcję greetings() tworzysz nową funkcję uppercase_greetings(). Napisanie nowej funkcji, która zamienia małe litery na wielkie, jest znacznie mniej skomplikowana, niż tworzenie dekoratora. Ale chcąc zmienić działanie programu musisz w całym kodzie zamieniać wywołanie greetings() na uppercase_greetings() albo na odwrót. Stosując dekoratory musisz tylko zakomentować lub odkomentować jedną linijkę (np. linię 15 w powyższym programie)

Wzorzec dekorator

Dekoratory w Pythonie znacznie ułatwiają dostosowaywanie działania programu do chwilowych potrzeb. Mają jednak poważną wadę: nie są dynamiczne. Dekorator musi być dodany w czasie pisania programu, a nie w czasie jego uruchomienia. Dodatkowo, udekorowana funkcja greetings() pozostanie udekorowana na zawsze, tzn każde jej wywołanie będzie w wariancie uppercase. Aby dynamicznie modyfikować działanie kodu, potrzebujesz wzorca projektowego dekorator.

Wzorzec dekorator omówimy na przykładzie okienek aplikacji. Takie okienko może mieć:

  • belkę tytułową

  • guziki min - max

  • menu

  • pasek przewijania: poziomy, pionowy lub obydwa

  • guzik np. “OK”

Może, ale nie musi. Generalnie programista, tworząc interfejs graficzny aplikacji, powinien być przygotowany na wszystkie mozliwości: okno z belką ale bez menu, okno z menu ale bez przewijania itp. Frontalny atak na ten problem - napisanie oddzielnej klasy na każdy typ okienka - jest skazany na porażkę. Poszczególne klasy trzeba tworzyć dynamicznie. Dynamiczna modyfikacja zachowania obiektu możliwa jest jedynie przez zawieranie.

Wzorzec dekorator wymaga zdefinowania klasy dekorowanej, oraz klas - dekoratorów, które odpowiedzialne są za wprowadzanie modyfikacji. W powyższych przykładach elementem dekorowanym była funkcja name_yourself(), dekoratorami zaś capitalize() oraz intro(). W tym przypadku klasą dekorowaną będzie okno w wersji podstawowej - absolutne minimum, bez menu pasków i bez ikonek. Dodatki będą dekoracjami.

Zacznijmy od interfesów: AbstractWindow definiuje funkcjonalność, jaką udostępniać musi każde okienko naszej aplikacji; klasa AbstractDecorator zaś definiuje dekoracje. Zauważ, że:

  • AbstractDecorator dziedziczy po AbstractWindow - ma to sens, bo okno z paskiem przewijania to dalej jest okno

  • konstruktor AbstractDecorator przyjmuje i przechowuje obiekt klasy AbstractWindow.

  • interfejs AbstractWindow zakłada istnienie pola x, y itd; dekorator sam z siebie nie wie, jakie jest położenie okna - odczytuje te wartości z obiektu, który przechowuje.

  • zauważ też, że metoda x() klasy AbstractWindow jest jednocześnie dekorowana @property oraz @abstractmethod

 4from visualife.core import HtmlViewport
 5
 6
 7class AbstractWindow(ABC):
 8
 9    @abstractmethod
10    def draw(self, viewport): pass
11
12    @property
13    @abstractmethod
14    def x(self): pass
15
16    @property
17    @abstractmethod
18    def y(self): pass
19
20    @property
21    @abstractmethod
22    def w(self): pass
23
24    @property
25    @abstractmethod
26    def h(self): pass
27
28
29class AbstractDecorator(AbstractWindow):
30
31    def __init__(self, decorated: AbstractWindow):
32        self._decorated = decorated
33
34    @abstractmethod
35    def draw(self, viewport): pass
36
37    @property
38    def x(self): return self._decorated.x
39
40    @property
41    def y(self): return self._decorated.y
42
43    @property
44    def w(self): return self._decorated.w
45
46    @property
47    def h(self): return self._decorated.h

Następnie tworzymy minimalne okienko, które później będziemy dekorować:

 1class SimpleWindow(AbstractWindow):
 2    def __init__(self, x, y, w, h):
 3        self.__x, self.__y = x, y
 4        self.__w, self.__h = w, h
 5        self.fill = "white"
 6        self.stroke = "black"
 7
 8    @property
 9    def x(self): return self.__x
10
11    @property
12    def y(self): return self.__y
13
14    @property
15    def w(self): return self.__w
16
17    @property
18    def h(self): return self.__h
19
20    def draw(self, viewport):
21        viewport.rect("", self.x, self.y, self.w, self.h, fill=self.fill, stroke=self.stroke, stroke_width=1)

Metoda draw() rysuje najprostsze możliwe okienko, czyli pusty prostokąt. Nastepnie tworzymy dekoratory; każdy z nich wywołuje draw() z obiektu, który sam zawiera. Oczywiście kolejność rysowania jest istotna: dekorator dodający cień najpierw rysuje cień a potem okienko, dekorator dodający menu najpierw rysuje okienko a potem dopiero menu.

 1class TitlebarWindow(AbstractDecorator):
 2
 3    def __init__(self, a_window, bar_height=10):
 4        super().__init__(a_window)
 5        self.bar_height = bar_height
 6
 7    def draw(self, viewport):
 8        # --- draw the simple window
 9        self._decorated.draw(viewport)
10        # --- and draw the decoration - a title bar and the title itself
11        viewport.rect("", self.x, self.y, self.w, self.bar_height, stroke_width=1)
12        viewport.text("", self.x+30, self.y+8, "window title", stroke_width=0, fill="white", text_anchor="start")
13
14
15class DropShadow(AbstractDecorator):
16
17    def __init__(self, a_window):
18        super().__init__(a_window)
19
20    def draw(self, viewport):
21        # --- draw the shadow first
22        viewport.rect("", self.x+5, self.y+5, self.w, self.h, fill="lightgray", stroke_width=0)
23        # --- and draw the main window
24        self._decorated.draw(viewport)
25
26
27class MenuBar(AbstractDecorator):
28
29    def __init__(self, a_window, menu_items):
30        super().__init__(a_window)
31        self.__items = menu_items
32
33    def draw(self, viewport):
34        # --- draw the simple window
35        self._decorated.draw(viewport)
36        # --- add the menu
37        for i, el in enumerate(self.__items):
38            viewport.text("", self.x+i*40+10, self.y+20, el, stroke_width=0, fill="black", text_anchor="start")
39
40
41class MacButtons(AbstractDecorator):
42
43    def __init__(self, a_window):
44        super().__init__(a_window)
45
46    def draw(self, viewport):
47        # --- draw the simple window
48        self._decorated.draw(viewport)
49        # --- and draw the decoration - a title bar and the title itself
50        viewport.circle("", self.x+5, self.y+5, 3, fill="red", stroke_width=1, stroke="darker")
51        viewport.circle("", self.x+15, self.y+5, 3, fill="yellow", stroke_width=1, stroke="darker")
52        viewport.circle("", self.x+25, self.y+5, 3, fill="green", stroke_width=1, stroke="darker")
53
54
55def build_window(x, y, if_shadow, if_title, if_menu, if_icons):
56    w = SimpleWindow(x, y, 150, 80)
57    if if_shadow:
58        w = DropShadow(w)
59    if if_title:
60        w = TitlebarWindow(w)
61    if if_icons:
62        w = MacButtons(w)
63    if if_menu:
64        w = MenuBar(w, ["File", "Edit", "Help"])
65    return w

Znajdująca się na końcu powyższego kodu funkcja build_window() tworzy okienko o zadanych dekoracjach. Poniżej możesz się sam przekonać, że działanie programu jest dynamiczne. Kliknij na białym polu poniżej, aby utworzyć okienko. Wygląd okna zależy od tego, jakie dekoracje wybierzesz w menu po prawej.

Wzorzec dekorator jest podobny do wzorca strategia. Generalnie różnica między nimi jest taka, że strategia służy do dynamicznego zmieniania działania obiektu, podczas gdy dekorator służy do tworzenia różnorodnych wariantów danej klasy.