Press "Enter" to skip to content

Wzorce projektowe – Dekorator

Wstęp i definicja

Wzorzec dekorator jest wzorcem należącym do grupy wzorców strukturalnych. Stosujemy go wtedy, kiedy chcemy dodać nowe funkcjonalności do istniejących już obiektów.

„Wzorzec dekorator pozwala na dynamiczne przydzielanie danemu obiektowi nowych zachowań. Dekoratory dają elastyczność podobną do tej,
jaką daje dziedziczenie, oferując jednak w zamian znacznie rozszerzoną funkcjonalność.”

Definicja zaczerpnięta z książki Rusz głową! Wzorce projektowe

Salon samochodowy

Wyobraźmy sobie sytuację, w której mamy do zaprogramowania prosty system do obsługi zamówień w salonie samochodowym. Aktualny system posiada już podstawową strukturę aplikacji, którą będziemy mogli spokojnie wykorzystać do zaprojektowania naszego nowego systemu. Klient aplikacji podesłał nam taką oto strukturę klas samochodów:

Struktura klas salonu samochodowego

Dodawanie nowych funkcjonalności

Posiadając już gotową strukturę, przedstawioną na obrazku wyżej, podczas dodawania nowej funkcjonalności, można pomyśleć o wykorzystaniu dziedziczenia. Nie byłoby w tym nic strasznego, gdyby nie fakt, że niektóre funkcjonalności chcielibyśmy wykorzystywać w różnych typach samochodów. Wyobraźmy sobie sytuację, w której do oferty musimy dodać nowe typy kół do wyboru, pakiet nagłośnienia, skórzane fotele i kilka innych. Korzystając z dziedziczenia, potrzebowalibyśmy do jednego pojazdu dopisać minimum 7 NOWYCH KLAS!!! (przy założeniu, że posiadamy do wyboru jeden rodzaj kół, jeden rodzaj nagłośnienia, jeden rodzaj foteli). Do 4 samochodów zatem musimy dodać 28 nowych klas, które praktycznie niczym się od siebie nie różnią. 3 nowe funkcjonalności dodały nam do projektu 28 nowych klas… Kiedy już to zrobiliśmy, klient decyduje, że chciałby dodać możliwość wyboru w pakiecie dodatkowym – czujniki parkowania. Wyobraźcie sobie nową strukturę takiego projektu, będzie ona bardzo zawiła, dlatego nie będę jej tutaj umieszczał.

Wykorzystanie wzorca dekorator

W takich sytuacjach, jak ta opisana wyżej, przychodzi nam z pomocą wzorzec dekorator, a oto jego struktura:

Schemat wzorca dekorator

Co nam daje zastosowanie tego wzorca?

Dzięki zastosowaniu tego wzorca, możemy oddzielić nowe funkcjonalności i opakowywać nimi istniejący już obiekt, dzięki czemu mamy zdecydowanie mniej klas w strukturze projektu oraz bardziej elastyczny projekt aplikacji. Wzorzec dekorator jest ściśle powiązany z typem dekorowanego obiektu, ponieważ dziedziczy po tej samej klasie, co obiekty konkretne, dzięki czemu zawsze zwracamy interesujący nas obiekt.

Nowa struktura aplikacji

Obiekty dekorujące (o ile są wykorzystywane) są zawsze na szczycie obiektów dekorowanych (obiekty dekorujące posiadają odwołania do obiektów dekorowanych):

Obiekty dekorujące i dekorowane

W sytuacji, w której chcemy do naszego zamówienia samochodu dodać niestandardowe koła, wystarczy, że udekorujemy nasz obiekt dekoratorem Wheels. Ale co zrobić w sytuacji, w której chcemy dostać dwa komplety kół? Odpowiedź jest bardzo prosta: Wystarczy udekorować nasz pojazd podwójnie Kołami.
W powyższym przykładzie, cena oraz opis naszego samochodu będą dynamicznie ulegały zmianie. Do ceny samochodu będzie dodawana cena niestandardowych foteli, potem kół – tak samo zadziała opis samochodu.

Podsumowanie

Wzorca dekorator używamy wszędzie tam, gdzie chcemy dodać do istniejącego obiektu dodatkową funkcjonalność w czasie działania programu. Ponieważ wzorzec ten jest ściśle powiązany z typem obiektu oryginalnego, cały czas mamy wrażenie, jakbyśmy operowali na tym samym obiekcie, który utworzyliśmy wcześniej. Kolejność dekorowania obiektu nie ma znaczenia, dlatego też dekoratory możemy dodawać dowolnie, według naszych upodobań, bądź wymagań. Na koniec wspomnę jeszcze o dwóch najważniejszych wadach tego wzorca: projekt naszej aplikacji może stać się bardzo trudny do zrozumienia, szczególnie dla osób, które nie znają tego wzorca projektowego oraz w projekcie powstaje duża ilość klas. Przykład prostej aplikacji jak zawsze znajduje się na githubie oraz w bibliotece java.io języka JAVA – są nimi wszystkie klasy obsługujące strumienie, np.

new LineNumberInputStream(new BufferedInputStream(new FileInputStream(file)));

Przykład uruchomienia aplikacji:

public class Startup {
    public static void main(String[] args) {
        Car corvette = new Corvette();
        System.out.println(corvette.price() + " " + corvette.getDescription());
        corvette = new Wheels(corvette);
        corvette = new Audio(corvette);
        System.out.println(corvette.price() + " " + corvette.getDescription());

        Car mustang = new Mustang();
        mustang = new Seats(mustang);
        mustang = new Audio(mustang);
        mustang = new Wheels(mustang);
        System.out.println("\n" + mustang.price() + " " + mustang.getDescription());
    }
}

Wyjście:

550000.0 Corvette
605000.0 Corvette, 22" wheels, bose audio

295000.0 Mustang, leatcher seats, bose audio, 22" wheels