Pytanie dla początkujących na temat wzoru dekoracyjnego

18

Czytałem artykuł programistyczny, w którym wspomniałem o wzorze dekoratora. Przez jakiś czas programowałem, ale nie miałem formalnego wykształcenia ani szkolenia, ale staram się poznać standardowe wzorce i tym podobne.

Poszukałem Dekoratora i znalazłem na nim artykuł w Wikipedii . Teraz rozumiem koncepcję wzoru Dekoratora, ale ten fragment był mnie trochę zdezorientowany:

Jako przykład rozważmy okno w systemie okienkowym. Aby umożliwić przewijanie zawartości okna, możemy dodać do niego odpowiednio poziome lub pionowe paski przewijania. Załóżmy, że okna są reprezentowane przez instancje klasy Window, i załóżmy, że ta klasa nie ma funkcji dodawania pasków przewijania. Możemy stworzyć podklasę ScrollingWindow, która je udostępnia, lub możemy stworzyć ScrollingWindowDecorator, który doda tę funkcjonalność do istniejących obiektów Windows. W tym momencie każde rozwiązanie byłoby w porządku.

Załóżmy teraz, że chcemy również możliwości dodawania ramek do naszych okien. Ponownie, nasza oryginalna klasa Window nie ma wsparcia. Podklasa ScrollingWindow stanowi teraz problem, ponieważ skutecznie stworzyła nowy rodzaj okna. Jeśli chcemy dodać obsługę granic do wszystkich okien, musimy utworzyć podklasy WindowWithBorder i ScrollingWindowWithBorder. Oczywiście problem ten nasila się z każdą dodaną nową funkcją. W przypadku rozwiązania dekoratora po prostu tworzymy nowy BorderedWindowDecorator - w czasie wykonywania możemy dekorować istniejące okna za pomocą ScrollingWindowDecorator lub BorderedWindowDecorator lub obu, w zależności od potrzeb.

OK, kiedy mówią, aby dodać obramowanie do wszystkich okien, dlaczego po prostu nie dodać funkcji do oryginalnej klasy Window, aby umożliwić tę opcję? Moim zdaniem podklasowanie służy jedynie do dodawania określonych funkcji do klasy lub zastępowania metody klasowej. Gdybym musiał dodać funkcjonalność do wszystkich istniejących obiektów, dlaczego po prostu nie zmodyfikowałbym nadklasy, aby to zrobić?

W artykule była inna linia:

Wzór dekoratora jest alternatywą dla podklas. Podklasowanie dodaje zachowanie podczas kompilacji, a zmiana wpływa na wszystkie instancje oryginalnej klasy; dekorowanie może zapewnić nowe zachowanie w czasie wykonywania dla poszczególnych obiektów.

Nie rozumiem, co mówią „... zmiana wpływa na wszystkie wystąpienia oryginalnej klasy” - jak podklasa zmienia klasę nadrzędną? Czy nie o to chodzi w podklasie?

Zakładam, że artykuł, podobnie jak wiele Wiki, po prostu nie jest napisany jasno. Widzę użyteczność Dekoratora w ostatnim wierszu - „... zapewniają nowe zachowanie w czasie wykonywania dla poszczególnych obiektów”.

Bez zapoznania się z tym wzorcem, gdybym musiał zmienić zachowanie w czasie wykonywania dla poszczególnych obiektów, prawdopodobnie wbudowałbym pewne metody w nadklasę lub podklasę, aby włączyć / wyłączyć to zachowanie. Pomóż mi naprawdę zrozumieć przydatność Dekoratora i dlaczego moje myślenie dla początkujących jest wadliwe?

Jim
źródło
doceniam edycję, Walter ... fyi, użyłem „facetów” nie do wykluczenia kobiet, ale jako nieformalne powitanie.
Jim
Edycja polegałaby jedynie na usunięciu powitania w ogóle. To nie jest standardowy SO / protokół SE używać jednego w kwestiach [Nie martw się, że nie, że to niegrzeczne, aby przejść prosto do pytania]
Farrell
fajne dzięki! po prostu oznaczył to jako „usuwające płeć” i nienawidzę nikogo, kto by pomyślał, że jestem mizoginistyczny czy coś w tym stylu !! :)
Jim
Używam ich mocno, aby uniknąć tych prawnych czynności proceduralnych zwanych „usługami”: medium.com/@wrong.about/…
Zapadlo

Odpowiedzi:

13

Wzorzec dekoratora jest tym, który preferuje kompozycję zamiast dziedziczenia [kolejny paradygmat OOP, o którym warto się dowiedzieć]

Główną zaletą wzoru dekoratora - ponadklasowanie jest umożliwienie większej liczby opcji mieszania i dopasowywania. Jeśli masz na przykład 10 różnych zachowań, które może mieć okno, oznacza to - z podklasą - musisz utworzyć każdą inną kombinację, która nieuchronnie obejmie wiele ponownego użycia kodu.
Co się jednak stanie, gdy zdecydujesz się dodać nowe zachowanie?

Dzięki dekoratorowi po prostu dodajesz nową klasę, która opisuje to zachowanie i to wszystko - wzorzec pozwala na efektywne upuszczenie tego bez modyfikowania reszty kodu.
Dzięki podklasom masz koszmar na rękach.
Pytanie, które zadałeś, brzmiało: „w jaki sposób podklasa zmienia klasę nadrzędną?” To nie tak, że zmienia klasę nadrzędną; kiedy mówi instancję, oznacza to dowolny obiekt, który „utworzyłeś” (jeśli używasz Java lub C #, na przykład za pomocą newpolecenia). Odnosi się to do tego, że kiedy dodajesz te zmiany do klasy, nie masz wyboru, aby ta zmiana była dostępna, nawet jeśli tak naprawdę nie jest to potrzebne.

Alternatywnie, możesz umieścić całą jego funkcjonalność w jednej klasie z włączoną / wyłączoną za pomocą flag ... ale kończy się to jedną klasą, która staje się coraz większa wraz z rozwojem projektu.
Nie jest niczym niezwykłym, aby rozpocząć projekt w ten sposób i przekształcić wzór dekoratora, gdy osiągniesz skuteczną masę krytyczną.

Interesujący punkt, który należy zrobić: możesz dodać tę samą funkcjonalność wiele razy; więc możesz na przykład mieć okno z podwójną, potrójną lub dowolną ilością lub obramowaniami według potrzeb.

Głównym punktem tego wzorca jest umożliwienie zmian w czasie wykonywania: możesz nie wiedzieć, jak chcesz wyglądać okna, dopóki program nie uruchomi się, a to pozwala na łatwą modyfikację. To prawda, że ​​można tego dokonać poprzez podklasę, ale nie tak ładnie.
I wreszcie, umożliwia dodanie funkcji do klas, których edytowanie może nie być możliwe - na przykład w klasach zamkniętych / końcowych lub w innych interfejsach API

Farrell
źródło
2
Dzięki, Farrell - kiedy mówisz „Alternatywnie, możesz umieścić całą jego funkcjonalność w jednej klasie z włączoną / wyłączoną za pomocą flag ... ale kończy się to jedną klasą, która staje się coraz większa wraz z rozwojem projektu”. to sprawiło, że kliknąłem za mnie. Myślę, że część problemu polega na tym, że nigdy nie pracowałem nad tak dużą klasą, że dekorator miał dla mnie sens. Myśląc poważnie, zdecydowanie widzę korzyści ... dzięki!
Jim
Także część o zapieczętowanych klasach ma mnóstwo sensu ... dzięki!
Jim
3

Rozważ możliwości dodawania pasków przewijania i ramek z podklasami. Jeśli chcesz każdej możliwości, otrzymujesz cztery klasy (Python):

class Window(object):
    def draw(self):
        "do actual drawing of window"

class WindowWithScrollBar(Window):
    def draw(self):
        Window.draw(self)
        "do actual drawing of scrollbar"

class WindowWithBorder(Window):
    def draw(self):
        Window.draw(self)
        "do actual drawing of border"

class WindowWithScrollBarAndBorder(Window):
    def draw(self):
        WindowWithScrollBar.draw(self)
        WindowWithBorder.draw(self)

Teraz WindowWithScrollBarAndBorder.draw()okno zostanie narysowane dwukrotnie, a po raz drugi może zastąpić już narysowany pasek przewijania, zależnie od implementacji. Twój kod pochodny jest ściśle powiązany z implementacją innej klasy i musisz się tym przejmować za każdym razem, gdy zmieniasz Windowzachowanie klasy. Rozwiązaniem byłoby skopiowanie i wklejenie kodu z superklas do klas pochodnych i dostosowanie go do potrzeb klas pochodnych, ale każda zmiana w superklasie musi zostać ponownie wklejona i ponownie dostosowana, tak aby ponownie klasy pochodne były ściśle powiązane z klasą bazową (poprzez potrzebę dostosowania i skopiowania-wklejenia). Innym problemem jest to, że jeśli potrzebujesz jeszcze jednej właściwości, którą okno może mieć lub nie, musisz podwoić każdą klasę:

class Window(object):
    ...

class WindowWithGimmick(Window):
    ...

class WindowWithScrollBar(Window):
    ...

class WindowWithBorder(Window):
    ...

class WindowWithScrollBarAndBorder(Window):
    ...

class WindowWithScrollBarAndGimmick(Window):
    ...

class WindowWithBorderAndGimmick(Window):
    ...

class WindowWithScrollBarAndBorderAndGimmick(Window):
    ...

Oznacza to dla zestawu wzajemnie niezależnych cech f z | f | ponieważ jest liczbą funkcji, musisz zdefiniować 2 ** | f | zajęcia, np. jeśli masz 10 funkcji, otrzymasz 1024 ściśle powiązane klasy. Jeśli użyjesz Wzorca Dekoratora, każda funkcja ma swoją własną niezależną i luźno powiązaną klasę, a masz tylko 1 + | f | klasy (to 11 w powyższym przykładzie).

pillmuncher
źródło
Widzisz, moim myśleniem nie byłoby ciągłe dodawanie klas - byłoby dodanie funkcjonalności do oryginalnej (pod) klasy. więc w klasie Window (obiekt): masz scrollBar = true, border = false itp. Odpowiedź Farrell pokazuje mi, gdzie to może pójść nie tak - po prostu prowadząc do rozdętych klas i takie ... dzięki za odpowiedź, +1!
Jim
Cóż, +1, gdy mam do tego przedstawiciela!
Jim
2

Nie jestem ekspertem od tego konkretnego wzorca, ale widzę, że wzorzec Dekoratora można zastosować do klas, które mogą nie mieć możliwości modyfikacji lub podklasy (na przykład mogą nie być twoim kodem i być zapieczętowane ). W twoim przykładzie, co jeśli nie napisałeś klasy Window, a po prostu ją konsumujesz? Tak długo, jak klasa Window ma interfejs i programujesz przeciwko temu interfejsowi, twój Dekorator może używać tego samego interfejsu, ale rozszerzyć jego funkcjonalność.

Przykład, o którym wspominasz, jest tutaj właściwie schludny:

Rozszerzanie funkcjonalności obiektu można wykonać statycznie (w czasie kompilacji) za pomocą dziedziczenia, jednak może być konieczne dynamiczne rozszerzanie funkcjonalności obiektu (w czasie wykonywania), gdy obiekt jest używany.

Rozważ typowy przykład okna graficznego. Aby rozszerzyć funkcjonalność okna graficznego, na przykład dodając ramkę do okna, konieczne byłoby rozszerzenie klasy okna w celu utworzenia klasy FramedWindow. Aby utworzyć okno w ramce, konieczne jest utworzenie obiektu klasy FramedWindow. Jednak niemożliwe byłoby rozpoczęcie od zwykłego okna i rozszerzenie jego funkcjonalności w czasie wykonywania, aby stać się oknem w ramce.

http://www.oodesign.com/decorator-pattern.html

Dan Diplo
źródło