Wstęp
Wzorzec projektowy dekorator pozwala na dodawanie nowych funkcji do istniejących klas w sposób dynamiczny. Dekoratory opakowują obiekt główny dodając do niego pożądane zachowania. Wzorzec ten przydatny jest jako zastępstwo klasycznego dziedziczenia i przydaje się kiedy mamy do czynienia z wieloma kombinacjami różnych właściwości.
Przykład
Weźmy pod lupę przykład Pizzerii. W menu najczęściej znajduje się kilkanaście kompozycji przygotowanych przez restauratora, a dodatkowo pojawia się pozycja z dowolnie skomponowanymi przez klienta składnikami. Pomyślmy teraz co by się stało, jeśli chciałby zamiast tego wypisać wszystkie możliwe kombinacje, powiedzmy 6 składników? Daje nam to 720 różnych kombinacji! Jeśli zażyczyłby on sobie programu obsługującego jego menu i poszlibyśmy tą drogą, stworzylibyśmy ogromna i chaotyczną sieć klas. Z pomocą przychodzi tutaj wzorzec dekorator. Możemy stworzyć bazową, abstrakcyjną klasę ( lub interfejs) Pizza i następnie dekorować ją wybranymi składnikami i dodawać koszt każdego z nich do ogólnej ceny.
public abstract class Pizza {
String description = "Not yet a pizza";
public String getDescription(){
return description;
}
public abstract double price();
}
public abstract class PizzaDecorator extends Pizza{
public abstract String getDescription();
}Kolejnym krokiem jest stworzenie bazowych pizz, średniej i dużej, które będą dekorowane składnikami.
public class MediumPizza extends Pizza {
public MediumPizza() {
description = "Średnia pizza";
}
@Override
public double price() {
return 25.50;
}
}
public class LargePizza extends Pizza{
public LargePizza(){
description = "Duża pizza";
}
@Override
public double price() {
return 32.0;
}
}I na koniec tworzymy składniki spośród których można wybierać.
public class Ham extends PizzaDecorator{
Pizza pizza;
public Ham(Pizza pizza) {
this.pizza = pizza;
}
@Override
public double price() {
return pizza.price() + 2.5;
}
@Override
public String getDescription() {
return pizza.getDescription() + ", szynka";
}
}
public class Cheese extends PizzaDecorator {
Pizza pizza;
public Cheese(Pizza pizza) {
this.pizza = pizza;
}
@Override
public double price() {
return pizza.price() + 2.0;
}
@Override
public String getDescription() {
return pizza.getDescription() + ", ser";
}
}Teraz możemy stworzyć naszą bazową Pizzę i dodać do niej składniki.
public class Main {
public static void main(String[] args) {
Pizza pizza1 = new LargePizza();
pizza1 = new Ham(pizza1);
pizza1 = new Cheese(pizza1);
System.out.println(pizza1.getDescription() + " " + pizza1.price() + " zł");
Pizza pizza2 = new MediumPizza();
pizza2 = new Cheese(pizza2);
System.out.println(pizza2.getDescription() + " " + pizza2.price() + " zł");
}
}Wynikiem wywołania powyższego kodu będzie :
Duża pizza, szynka, ser 36.5 zł
Średnia pizza, ser 27.5 zł
Podsumowanie
Jak widzimy zastosowanie wzorca dekorator daje nam dużo możliwości. Jest bardzo dobrą alternatywą dla dziedziczenia, zapewnia nam łatwą drogę do zachowania zasady otwarty/zamknięty, pomaga dodawać nowe funkcjonalności bez naruszania struktury programu. Jednak trzeba uważać i nie nadużywać dekoratorów, ponieważ powodują one szybki rozrost programu w wyniku dodawania dużej ilości małych klas, co może doprowadzić do niepotrzebnego zwiększenia złożoności kodu.