Wzorce projektowe – Dekorator

W jednym z poprzednich wpisów opisywałem wzorzec projektowy Obserwator, tym razem postanowiłem przećwiczyć zastosowanie Dekoratora.

Wzorzec Dekorator należy do grupy wzorców strukturalnych. Na wstępie warto zaznaczyć, że jest on uważany jako alternatywa dla dziedziczenia, gdyż tak samo jak ono rozszerza funkcjonalności klasy podstawowej. Są jednak pewne fakty, które w wybranych rozwiązaniach przemawiają na korzyść wzorca (oczywiście nie zawsze). Przede wszystkim jego zastosowanie spełnia zasadę projektowania Open-Close, która mówi, że aplikacja powinna być otwarta na rozbudowę, a jednocześnie zamknięta na modyfikacje. Dodanie nowych zachowań nie powoduje konieczności zmiany klasy podstawowej. Co więcej, te nowe zachowania można dodawać do obiektu dekorowanego w trakcie działania programu, co nie jest możliwe w przypadku stosowania czystego dziedziczenia. Ma to całkiem spore znaczenie w przypadku aplikacji, które mogą być rozwijane w przyszłości (czyli w zasadzie wszystkich:)), bo na początku projektu nie można przewidzieć wszystkich możliwych kombinacji klasy bazowej i jej rozszerzeń.

Oczywiście wzorzec powinien być stosowany z umiarem, a nie wszędzie, gdzie istnieje taka możliwość. Choćby ze względu na fakt, że w przypadku dużej liczby dekoratorów, tworzonych jest wiele małych klas, co może wprowadzać zamęt w projekcie.

Jeśli wiemy, że w kodzie zaimplementowano wzorzec Dekorator, musimy poszukać przede wszystkim obiektu podstawowego (dekorowanego) oraz obiektów rozszerzających jego zachowanie (dekorujących). Klasa bazowa jest podstawą zarówno dla konkretnych obiektów dekorowanych, jak również dla dekoratorów. Implementacja może być oparta o dziedziczenie z klas abstrakcyjnych lub o interfejsy. Ten akapit może wydawać się lekko zagmatwany, dlatego odsyłam do diagramu klas.

CC0 https://pl.wikipedia.org/wiki/Dekorator_(wzorzec_projektowy)#/media/File:Decorator_classes_pl.svg
Diagram klas wzorca Dekorator
Źródło: pl.wikipedia.org

Ogólna idea tego wzorca opiera się na mechanizmach kompozycji oraz delegacji. Obiekt dekorujący zawiera obiekt dekorowany (kompozycja), natomiast dekorator deleguje wywołanie wybranej metody do kolejnego dekoratora lub do metody pochodzącej z klasy dekorowanej, po drodze dodając od siebie swoje trzy grosze.

Jak zwykle najtrudniejszą czynnością jest wymyślenie przykładu, na którym można coś pokazać, a to on dopiero wytłumaczy (mam nadzieję!) o co chodzi z tym Dekoratorem. W sieci i literaturze często przewijają się fragmenty kodu, gdzie różnymi dodatkami dekorowana jest pizza oraz wyliczana jest ceny kawy ze śmietanką i ekstra cukrem. Oczywistym przypadkiem zastosowania wzorca są również klasy z pakietu java.io, np. InputStream. Pójdę trochę w inną tematykę, jednak schemat będzie analogiczny do pizzy i kawy. Kiedyś za czasów mojej młodości, całkiem sporo jeździłem na rowerze, a że aktualnie sezon w pełni, będzie więc to temat przewodni przykładu.

Załóżmy, że do napisania jest aplikacja obliczająca wagę roweru. Wiadomo, waga sprzętu to istotna rzecz.. jak ktoś się ściga oczywiście. Niektórzy mawiają, że taniej jest odchudzić siebie niż inwestować w droższy jednoślad, ale jest to temat na oddzielną dyskusję i na pewno nie w tym miejscu. Wracając do tematu, chciałbym aby w mojej aplikacji użytkownik wybierał typ roweru, przykładowo rower do XC, enduro, miejski (CityBike) czy szosowy (RoadBike). Dla uproszczenia przyjmuję, że każdy typ ma swoją ściśle określoną wagę. W kolejnym kroku użytkownik dobiera akcesoria, jakie chciałby zainstalować w swoim rowerze, przykładowo dzwonek (BicycleBell), błotnik tylny (RearFender) czy koszyk na bidon (BottleCage). O ile taki błotnik może być zamontowany tylko w jednym egzemplarzu na każde koło, to przykładowo koszyki na bidon mogą być dwa. Chciałbym, aby mój program na swoim wyjściu podawał całkowitą wagę roweru wraz z wszystkimi wybranymi dodatkami.

Obiekt dekorowany

Z opisu jasno wynika, że obiektem dekorowanym w tym przykładzie jest rower (Bike), który posiada ściśle określony podzbiór (to jest założenie, bo pewnie za parę lat wymyślą jakiś nowy typ). Typy rowerów wpisałem na sztywno jako enum BikeType.

public enum BikeType {
    ROAD, XC, ENDURO, TREKKING, CITY, UNKNOWN;
}

Rower niech będzie opisany jako klasa abstrakcyjna Bike, która zawiera pole prywatne określające jego typ z publicznym akcesorem oraz deklarację metody getWeight(), której zadaniem jest zwrócenie wagi konkretnego egzemplarza roweru.

public abstract class Bike {
 
    protected BikeType type = BikeType.UNKNOWN;
 
    protected BikeType getType() {
        return this.type;
    }
 
    public abstract float getWeight();
}

W zasadzie do zaprezentowania wzorca wystarczą implementacje dwóch klas pochodnych po Bike, niech to będzie rower szosowy (RoadBike) oraz miejski (CityBike). Konstruktor obu klas ustawia odpowiedni typ roweru, natomiast implementacje metody abstrakcyjnej zwracają różne wartości reprezentujące wagę roweru (w [kg]).

public class RoadBike extends Bike {
 
    public RoadBike() {
        this.type = BikeType.ROAD;
    }
 
    @Override
    public float getWeight() {
        return 8.200f;
    }
}
 
public class CityBike extends Bike {
 
    public CityBike() {
        this.type = BikeType.CITY;
    }
 
    @Override
    public float getWeight() {
        return 12.300f;
    }
}

Dekoratory

Akcesoria jakie można dodatkowo zamontować w rowerze pełnią funkcję dekoratorów. Wszystkie, jakie przyjdą mi do głowy, wrzucę do jednego worka i opiszę wspólną nadklasą abstrakcyjną BikeAccessory. Aby nie tylko nazwa tej grupy świadczyła o tym, że są to akcesoria przeznaczone do rowerów, a nie np. gadżety to auta, również BikeAccessory dziedziczyć będzie po klasie Bike. Znaczy to tyle, że dekorator (akcesorium) będzie miał taką samą funkcjonalność (tutaj metodę zwracającą wagę) jak obiekt dekorowany (rower).

public abstract class BikeAccessory extends Bike {
 
    protected Bike bike;
}

Istotnym krokiem implementacji wzorca jest utworzenie klas dekoratorów (akcesoriów rowerowych), z konstruktorami umożliwiającymi przekazanie referencji do obiektu dekorowanego. Jest ona zachowana w polu bike, odziedziczonym z klasy BikeAccessory. Z tej właściwości pobierana jest waga oryginalnego roweru („gołego” lub udekorowanego wcześniejszymi akcesoriami), a następnie dodawana jest masa wybranego akcesorium.

Utworzyłem przykładowe komponenty, które można zamontować w rowerze: bagażnik (Carrier), dzwonek (BicycleBell), koszyk na bidon (BottleCage), tylny błotnik (RearFender) oraz przedni błotnik(FrontFender).

public class Carrier extends BikeAccessory {
 
    public Carrier(Bike bike) {
        this.bike = bike;
    }
 
    @Override
    public float getWeight() {
        return this.bike.getWeight() + 0.680f;
    }
}
 
public class BicycleBell extends BikeAccessory {
 
    public BicycleBell(Bike bike) {
        this.bike = bike;
    }
 
    @Override
    public float getWeight() {
        return this.bike.getWeight() + 0.030f;
    }
}
 
public class BottleCage extends BikeAccessory {
 
    public BottleCage(Bike bike) {
        this.bike = bike;
    }
 
    @Override
    public float getWeight() {
        return this.bike.getWeight() + 0.065f;
    }
}
 
public class RearFender extends BikeAccessory {
 
    public RearFender(Bike bike) {
        this.bike = bike;
    }
 
    @Override
    public float getWeight() {
        return this.bike.getWeight() + 0.220f;
    }
}
 
public class FrontFender extends BikeAccessory {
 
    public FrontFender(Bike bike) {
        this.bike = bike;
    }
 
    @Override
    public float getWeight() {
        return this.bike.getWeight() + 0.200f;
    }
}

Działanie w akcji

Klasy dekorowane są już zaimplementowane (rowery), klasy dekoratorów (akcesoria) – również. Pora pokazać, jak to wygląda w działaniu. Chcę, aby rower miejski miał zamontowane oba błotniki, dzwonek oraz bagażnik, natomiast rower szosowy ma być wyposażony jedynie w koszyk na bidon. Na początku tworzę nowy obiekt klasy CityBike i przypisuje go do typu Bike, po czym wyświetlam jego podstawową wagę. Kolejny krok (montowanie dodatków) to meritum dekorowania. Dodawanie akcesoriów odbywa się na zasadzie tworzenia nowych obiektów, do których przekazywana jest wcześniejsza wersja roweru. Dekorowanie w tym przypadku zmienia zachowanie funkcji obiektu dekorowanego pobierającej wagę. Bez konieczności tworzenia niezliczonej liczby klas np. CityBikeWithBicycleBellAndFrontFender, CityBikeWithCarrier czy CityBikeWithBottageCageAndBicycleBell można w dowolny sposób konfigurować własny jednoślad. Każdy dekorator w elastyczny sposób modyfikuje zachowanie metody getWeight(), jednocześnie zapewniając możliwość montowania nowych akcesoriów (licznik, oświetlenie, itp.) bez konieczności zmiany klasy typu Bike i pochodnych.

public class DecoratorRunner {
 
    public static void main(String[] args) {
        System.out.println("Wzorzec projektowy Dekorator");
 
        Bike cityBike = new CityBike();
        System.out.println("Waga roweru miejskiego bez akcesoriów: " + String.format("%.4g", cityBike.getWeight()));
 
        cityBike = new Carrier(cityBike);
        cityBike = new RearFender(cityBike);
        cityBike = new FrontFender(cityBike);
        cityBike = new BicycleBell(cityBike);
 
        System.out.println("Waga roweru miejskiego z akcesoriami: " + String.format("%.4g", cityBike.getWeight()));
 
        Bike roadBike = new RoadBike();
        System.out.println("Waga roweru szosowego bez akcesoriów: " + String.format("%.4g", roadBike.getWeight()));
 
        roadBike = new BottleCage(roadBike);
 
        System.out.println("Waga roweru szosowego z akcesoriami: " + String.format("%.4g", roadBike.getWeight()));
    }
}

Analogicznie sprawa wygląda z rowerem szosowym, do roadBike typu Bike przypisany jest nowy obiekt typu RoadBike, który w dalszej kolejności jest dekorowany klasą BottleCage. Na wyjściu programu wyświetlane są masy poszczególnych rowerów w wersji podstawowej oraz po modyfikacjach. Jak widać, ciężar całości równy jest sumie wagi roweru i wszystkich dodanych akcesoriów.

Wzorzec projektowy Dekorator
Waga roweru miejskiego bez akcesoriów: 12,30
Waga roweru miejskiego z akcesoriami: 13,43
Waga roweru szosowego bez akcesoriów: 8,200
Waga roweru szosowego z akcesoriami: 8,265

Powyższy kod dostępny jest na GitHub.

Literatura obowiązkowa

Wzorce projektowe. Rusz głową! – po raz kolejny polecam
Class Formatter
Dekorator (wzorzec_projektowy)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *