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@propertywzorzec 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 wielkiefunkcja
upercase_decorator()zwraca napis (wielkimi literami), natomiast sam dekoratorcapitalize(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:
AbstractDecoratordziedziczy poAbstractWindow- ma to sens, bo okno z paskiem przewijania to dalej jest oknokonstruktor
AbstractDecoratorprzyjmuje i przechowuje obiekt klasyAbstractWindow.interfejs
AbstractWindowzakłada istnienie polax,yitd; 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()klasyAbstractWindowjest jednocześnie dekorowana@propertyoraz@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.