Wzorzec Dekorator

wpis w: Blog | 0

Kolejnym wzorcem z serii artykułów o wzorcach jest wzorzec dekorator. Dekorator jest jednym z wzorców strukturalnych. Wzorzec ten jest świetnym przykładem na to, żeby przedkładać kompozycje nad dziedziczeniem. Przyjrzyjmy się pierw definicji tego wzorca.

„Wzorzec Dekorator – umożliwia dynamiczne przydzielenie wybranemu obiektowi nowych zachowań. Dekoratory dają elastyczność podobną do tej, jaką daje dziedziczenie, jednak oferują znacznie lepszą funkcjonalność.”

Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra – „Rusz głową! Wzorce Projektowe”

Po zapoznaniu się wstępnie z definicją wzorca, jego działanie i zastosowanie najłatwiej będzie przedstawić na zasadzie przykładu. Załóżmy, że jesteśmy właścicielami nowo powstałej burgerowni w mieście. W swojej ofercie posiadamy kilka burgerów i początkowo pomysł jest taki, że każdy rodzaj burgera dziedziczy po abstrakcyjnej klasie Burger.

Pierwszy diagram klas burgerowni

Z czasem jednak zauważono, że klienci chcieliby mieć możliwość dodania własnych dodatków do burgerów. Dlatego zdecydowano się na wprowadzenie takich dodatków jak: dodatkowe mięso, dodatkowy ser, papryczki jalapeno, ogórek kiszony, frytki. Pomysł na rozbudowę aplikacji był prosty. Stworzyć nowe klasy które dziedziczą po klasie burger z konkretnymi dodatkami, czyli np. BurgerKlasykZFrytkami, BurgerKlasykZDodatkowymMiesem, BurgerKlasykZOgorkiemKiszonym itd. Jak łatwo można zauważyć ilość tych klas nagle stałą się bardzo duża a taki mechanizm wprowadziłby wiele problemów przy ewentualnej dodatkowej rozbudowie aplikacji np. gdybyśmy chcieli dodać kolejny dodatek musielibyśmy stworzyć znów o wiele więcej klas, gdyby nastąpiła zmiana ceny jednego dodatku wtedy w każdej klasie z tym dodatkiem należało by dokonać zmiany, a nie chcę tu już nawet wspominać o takiej możliwości, że klient może wybrać 2, 3 lub dowoloną ilość dodatkó a nie tylko jeden. Tutaj z pomocą przychodzi wzorzec dekorator. Działa on w ten sposób, że nas podstawowy obiekt, np BurgerKlasyk będzie dekorowany przez obiekty dodatkowe, np. dodatkowe mięso, frytki. Daje to nam możliwość swobodnego dodawania ilości dodatków oraz każdy dodatek jest osobną klasą. Ważne w tym jest do, że obiekty dekorujące muszą być tego samego rodzaju co obiekt dekorowany w tym przypadku muszą być klasą Burger. Wygląda to w ten sposób, że gdy będziemy chcieć stworzyć BurgeraKlasyka z dodatkowym mięsem i frytkami pierw stworzymy obiekt BurgerKlasy, następnie udekorujemy go frytkami oraz dodatkowym mięsem, a przy zliczaniu ceny pierw dodana zostanie cena zewnętrznego obiektu czyli mięsa potem frytek a na końcu podstawowego obiektu czyli BurgeraKlasyka. Przyjrzyjmy się diagramowi wzorca dekorator oraz nowemu diagramowi naszej burgerowni.

Diagram wzorca dekorator (źródło: Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra – „Rusz głową! Wzorce Projektowe” )
Diagram burgerowni opartej na wzorcu dekorator.

Teraz, gdy mamy już stworzony diagram klas można przejść do zaprogramowania naszej burgerowni.

  1. W pierwszym etapie stworzymy abstrakcyjną klasę Burger oraz dziedziczaca po niej klasie SkladnikDekorator. Klasa ta dziedziczy po klasie Burger, aby każdy nasz dodatek (obiekt dekorujący) był tego samego typu co podstawowy burger (obiekt dekorowany).
public abstract class Burger {
    String nazwa = "burger";
    public String pobierzNazwe(){
        return nazwa;
    }
    public abstract double cena();
}

public abstract class SkladnikDekorator extends Burger{
    public abstract String pobierzNazwe();
}

2. Kolejno tworzymy poszczególne klasy burgerów oraz dodatków, które odpowiednio dziedziczą po klasie Burger oraz SkładnikDekorator

public class BurgerKlasyk extends Burger{
    public BurgerKlasyk(){
        nazwa = "Burger w klasycznym stylu";
    }

    @Override
    public double cena() {
        return 13;
    }
}
public class BurgerWloski extends Burger{
    public BurgerWloski(){
        nazwa = "Burger w wloskim stylu";
    }

    @Override
    public double cena() {
        return 15;
    }
}
public class BurgerGoralski extends Burger{
    public BurgerGoralski(){
        nazwa = "Burger w goralskim stylu";
    }

    @Override
    public double cena() {
        return 18;
    }
}
public class DodatkoweMieso extends SkladnikDekorator{
    Burger burger;
    public DodatkoweMieso(Burger burger){
        this.burger = burger;
    }
    public String pobierzNazwe(){
        return burger.pobierzNazwe() + " + dodatkowe mieso";
    }

    public double cena(){
        return burger.cena() + 3.50;
    }
}
public class DodatkowySer extends SkladnikDekorator{
    Burger burger;
    public DodatkowySer(Burger burger){
        this.burger = burger;
    }
    public String pobierzNazwe(){
        return burger.pobierzNazwe() + " + dodatkowy ser";
    }

    public double cena(){
        return burger.cena() + 1;
    }
}
public class OgorekKiszony extends SkladnikDekorator{
    Burger burger;
    public OgorekKiszony(Burger burger){
        this.burger = burger;
    }
    public String pobierzNazwe(){
        return burger.pobierzNazwe() + " + ogorek kiszony";
    }

    public double cena(){
        return burger.cena() + 1;
    }
}

3. Ostatnim etapem jest sprawdzenie naszego programu w naszej Burgerowni!

public class Burgerownia {
    public static void main (String[] args){
        Burger burger1 = new BurgerGoralski();
        System.out.println(">>> Klient pierwszy <<<");
        System.out.println(burger1.pobierzNazwe());
        System.out.println("Koszt: " + burger1.cena());

        Burger burger2 = new BurgerWloski();
        burger2 = new DodatkoweMieso(burger2);
        burger2 = new Jalapeno(burger2);
        System.out.println(">>> Klient drugi <<<");
        System.out.println(burger2.pobierzNazwe());
        System.out.println("Koszt: " + burger2.cena());

        Burger burger3 = new BurgerKlasyk();
        burger3 = new DodatkowySer(burger3);
        burger3 = new DodatkowySer(burger3);
        burger3 = new Frytki(burger3);
        System.out.println(">>> Klient trzeci <<<");
        System.out.println(burger3.pobierzNazwe());
        System.out.println("Koszt: " + burger3.cena());
    }
}

Podsumowując wzorzec dekorator :

  • obiekty dekorujące są tego samego typu co obiekty dekorowane wzorzec dekorator
  • jeden obiekt podstawowy może zostać udekorowany jeden lub więcej razy
  • przy założeniu, że dekorator jest tego samego typu co obiekt dekorowany, możemy przekazywać obiekt już „owinięty” (udekorowany), zawierający wszelkie dodatki łącznie z podstawą, zamiast oryginalnego (samej podstawy)
  • dekorator dodaje swoje własne zachowania przed delegowaniem do obiektu dekorowanego właściwego zadania i/lub po nim
  • obiekty mogą być dekorowane w dowolnym momencie (także w czasie działania programu)
  • wzorzec dekorator jest świetnym przykładem preferowania kompozycji nad dziedziczeniem
  • nadużywanie dekoratorów może w znacznym stopniu wpłynąć na zwiększenie złożoności kodu

Źródła:

  • Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra – „Rusz głową! Wzorce Projektowe”