Udekoruj swoje obiekty!

Dekorator (ang. Decorator) to jeden z podstawowych wzorców projektowych należących do grupy wzorców strukturalnych. Umożliwia on przydzielenie danemu obiektowi nowych zachowań w sposób dynamiczny, a przy tym nie korzysta z mechanizmu dziedziczenia, dzięki czemu gwarantuje dużą elastyczność i rozszerzoną funkcjonalność. Nadanie nowych cech odbywa się poprzez „opakowanie” obiektu bazowego obiektem (lub obiektami) dekorującym, który jest tego samego typu, co obiekt dekorowany. Diagram UML tegoż wzorca prezentuje się następująco:

Diagram UML klas wzorca dekorator
Rys. 1. Diagram UML klas wzorca dekorator

Uwagę czytelnika najprościej przykuć pisząc o pysznym jedzeniu. Mimo że nie jest to blog kulinarny, przenieśmy się na chwilę do nowej burgerowni w mieście, której właściciele w przypływie kreatywnej myśli, postanowili nadać nazwę „DecoBurger”. Początkowy diagram klas systemu zamówień w lokalu dla 3 burgerów prezentował się następująco:

Początkowy diagram klas DecoBurger
Rys. 2. Początkowy diagram UML klas DecoBurger’a

Wraz z rosnącą popularnością DecoBurger’a zaczęło odwiedzać coraz więcej klientów, wśród których przybywało także tych bardziej wymagających. Wymusiło to na właścicielach podjęcie decyzji o wprowadzeniu możliwości dodawania składników do poszczególnych burgerów, takich jak ekstra bekon, ser czy cebula. Następstwem tej decyzji była konieczność przebudowy systemu. Pierwszą myślą było utworzenie nowych klas, gdzie każda reprezentowałaby burgera wraz odpowiednim dodatkiem/dodatkami. Diagram UML prezentowałby się następująco:

Diagram UML po wprowadzeniu możliwości dodawania składników do burgerów
Rys. 3. Diagram UML po wprowadzeniu możliwości dodawania składników do burgerów

Już na pierwszy rzut oka widać, że nie byłoby to dobre i przejrzyste rozwiązanie. Prowadziłoby ono do utworzenia ogromnej ilości nowych klas. Dodatkowo taki kod byłby problematyczny w rozbudowie i mało elastyczny. Łatwo wyobrazić sobie sytuację, w której wprowadzenie przez właścicieli lokalu nowego dodatku do burgera powodowałoby konieczność utworzenia kolejnych kilku klas. Innym przykładem może być zmiana ceny jakiegokolwiek dodatku, w efekcie czego zaistniałaby konieczność zmiany kodu w kilku klasach. I wtedy wchodzi on… cały na biało. Mowa oczywiście o wzorcu dekorator, którego zastosowanie poskutkuje uproszczeniem kodu:

Diagram UML po wprowadzeniu możliwości dodawania składników do burgerów z zastosowanie wzorca dekorator
Rys. 4. Diagram UML po wprowadzeniu możliwości dodawania składników do burgerów z zastosowanie wzorca dekorator

Kodowanie rozwiązania rozpocznijmy od definicji klasy Burger:

public abstract class Burger {
    String opis = "Burger nieznany";

    public String pobierzOpis() {
        return opis;
    }

    public abstract double obliczCene();
}

Następna w kolejne jest klasa abstrakcyjna SkładnikDekorator:

public abstract class SkładnikDekorator extends Burger {
    public abstract String pobierzOpis();
}

Po utworzeniu klas bazowych można zabrać się za implementację poszczególnych burgerów:

public class Binaryburger extends Burger {

    public Binaryburger() {
        opis = "Binaryburger";
    }
    
    @Override
    public double obliczCene() {
        return 10.10;
    }
}

, a także dodatków (dekoratorów):

public class Bekon extends SkładnikDekorator {

    Burger burger;

    public Bekon(Burger burger) {
        this.burger = burger;
    }

    @Override
    public String pobierzOpis() {
        return burger.pobierzOpis() + ", Bekon";
    }

    @Override
    public double obliczCene() {
        return burger.obliczCene() + 1.20;
    }
}

Kod kolejnych burgerów i dodatków różni się jedynie ceną i opisem, dlatego ich utworzenie pozostawiam czytelnikom w ramach treningu 👨‍💻.

Nie pozostaje nic innego jak przetestować nasze rozwiązanie. W tym celu zostanie utworzona klasa testowa o nazwie Decoburger:

public class Decoburger {

    public static void main(String[] args) {
        Burger burger1 = new Binaryburger();
        wypiszOpis(burger1);

        Burger burger2 = new Cheeseburger();
        burger2 = new Bekon(burger2);
        wypiszOpis(burger2);

        Burger burger3 = new Royalburger();
        burger3 = new Ser(burger3);
        burger3 = new Cebula(burger3);
        burger3 = new Bekon(burger3);
        wypiszOpis(burger3);
    }

    static void wypiszOpis(Burger burger) {
        System.out.println(burger.pobierzOpis() + ", koszt: " + burger.obliczCene() + " zł");
    }
}

Wynik uruchomienia kodu prezentuje się następująco:

Binaryburger, koszt: 10.1 zł
Cheeseburger, Bekon, koszt: 10.6 zł
Royalburger, Ser, Cebula, Bekon, koszt: 13.1 zł

W omawianym przykładzie zastosowanie wzorca dekorator spowodowało znaczny wzrost elastyczności kodu i zwiększyło łatwość jego utrzymania i rozbudowy. Stało się tak również dzięki wykorzystaniu reguły projektowania otwarte/zamknięte. Co więcej, takie rozwiązanie umożliwia dodawanie nowych zachowań w trakcie działania programu, poprzez wykorzystanie kompozycji oraz delegacji. Wzorzec ten stanowi silną alternatywę dla dziedziczenia w kontekście funkcjonalności. Należy jednak mieć na uwadze, że nadużywanie dekoratorów prowadzi do wzrostu złożoności kodu.

Cały kod z wpisu znajduje się na moim Githubie.

Źródła literaturowe:
[1] Eric Freeman, Elisabeth Freeman, Kathy Sierra, Bert Bates: Wzorce projektowe. Rusz głową!

2 thoughts on “Udekoruj swoje obiekty!”

  1. Hi! Do you knoѡ іf they make any plugins to help with SEO?
    I’m trying to get my blog to rank for some targeted ҝeywords but Ι’m not seeing very good gaіns.

    If you know of any please sһare. Appreciate it!

Zostaw komentarz

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