Wzorzec projektowy Decorator
Definicja wzorca Decorator
Wzorzec Decorator (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ść.
Przed zastosowaniem wzorca
Jako przykład niech posłuży nam klasa abstrakcyjna Vehicle opisująca ogólne parametry pojazdu.
public abstract class Vehicle {
protected String description = "";
public String getDescription() {
return description;
}
public abstract double getPrice();
}
Klasa ta posiada pole description przechowujące opis pojazdu oraz dwie metody getDescription() oraz getPrice(), które zwracają odpowiednio opis oraz cenę pojazdu.
Utwórzmy teraz podklasę klasy abstrakcyjnej Vehicle, reprezentującą konkretny typ samochodu, np. Golf III.
public class GolfIII extends Vehicle {
public GolfIII() {
description = "Golf III";
}
@Override
public double getPrice() {
return 10000.0;
}
}
Następnie utwórzmy obiekt tej klasy i wyświetlmy jego opis oraz cenę.
public class MainDecorator {
public static void main(String[] args) {
Vehicle golf3 = new GolfIII();
System.out.println("Vehicle : " + golf3.getDescription());
System.out.println("Price: " + golf3.getPrice() + " zł");
}
}
Póki co wszystko działa bez problemu, jednak problem pojawia się w momencie, gdy chcielibyśmy mieć taki sam samochód lecz z innym wyposażeniem, np. z radiem i elektrycznymi lusterkami. Musimy stworzyć kolejną podklasę klasy abstrakcyjnej Vehicle.
public class GolfIIIRadioElectricMirrors extends Vehicle {
public GolfIIIRadioElectricMirrors() {
description = "Golf III, radio, electric mirrors";
}
@Override
public double getPrice() {
return 10800.0;
}
}
A co jeśli teraz chcielibyśmy kolejną wersję Golfa, np. z radiem, elektrycznymi lusterkami i klimatyzacją? Kolejna klasa? A jeśli chcielibyśmy jeszcze do tego podgrzewane fotele? Jeszcze jedna klasa?
Jak widać z każdym dodatkiem do podstawowej wersji samochodu wiąże się konieczność implementacji kolejnej klasy, co prowadzi do powstania bardzo dużej ilości klas i powielania kodu, a całość staje się ciężka w utrzymaniu i rozbudowie. I tutaj z pomocą przychodzi wzorzec Decorator.
Po zastosowaniu wzorca
Wykorzystując wzorzec Decorator możemy stworzyć każdy dodatek jako osobną podklasę i “dekorować” nim wersję podstawową pojazdu. Aby tego dokonać musimy stworzyć kolejną klasę abstrakcyjną rozszerzającą klasę Vehicle, z której to powstaną klasy opisujące dodatki do pojazdu.
Stwórzmy zatem przykładową klasę abstrakcyjną ElementDecorator rozszerzającą klasę Vehicle. Klasa ta posiada tylko jedną metodę abstrakcyjną getDescription(), która musi zostać nadpisana w klasach dziedziczących po tej klasie.
public abstract class ElementDecorator extends Vehicle {
public abstract String getDescription();
}
A teraz stwórzmy dwie podklasy klasy abstrakcyjnej ElementDecorator. Niech to będzie klasa RadioDecorator oraz ElectricMirrorsDecorator. Należy zwrócić uwagę na to, że każda podklasa posiada dodatkową zmienną obiektową typu Vehicle. Reprezentuję ona “dekorowany” obiekt.
Klasa RadioDecorator…
public class RadioDecorator extends ElementDecorator {
Vehicle vehicle;
public RadioDecorator(Vehicle vehicle) {
this.vehicle = vehicle;
}
@Override
public String getDescription() {
return vehicle.getDescription() + ", radio";
}
@Override
public double getPrice() {
return vehicle.getPrice() + 500.0;
}
}
oraz ElectricMirrorsDecorator…
public class ElectricMirrorsDecorator extends ElementDecorator {
Vehicle vehicle;
public ElectricMirrorsDecorator(Vehicle vehicle) {
this.vehicle = vehicle;
}
@Override
public String getDescription() {
return vehicle.getDescription() + ", electric mirrors";
}
@Override
public double getPrice() {
return vehicle.getPrice() + 300.0;
}
}
A teraz stwórzmy podstawowy samochód Golf III z radiem i elektrycznymi lusterkami.
public class MainDecorator {
public static void main(String[] args) {
Vehicle golf3 = new GolfIII();
golf3 = new RadioDecorator(golf3);
golf3 = new ElectricMirrorsDecorator(golf3);
System.out.println("Vehicle : " + golf3.getDescription());
System.out.println("Price: " + golf3.getPrice() + " zł");
}
}
Na początku tworzymy obiekt podstawowy golf3. Następnie “dekorujemy” go elementem dodatkowym poprzez utworzenie obiektu typu RadioDecorator i przekazanie konstruktorowi jako argument obiekt podstawowy (golf3). W kolejnym kroku również dekorujemy obiekt podstawowy (już udekorowany radiem), tym razem jednak poprzez utworzenie drugiego obiektu dodatkowego. Na końcu wyświetlamy opis i cenę nowego samochodu. I w ten oto sposób powstaje nam obiekt golf3 z radiem i elektrycznymi lusterkami.
Teraz jeśli chcielibyśmy inną wersje samochodu, np. z podgrzewanymi fotelami to wystarczy zaimplementować klasę tylko dla tego dodatku i “udekorować” nim nasz podstawowy element. Warunkiem jest aby nowa klasa była rozszerzeniem klasy abstrakcyjnej ElementDecorator.
Jak widać taki kod jest dużo łatwiejszy w utrzymaniu i bardzo łatwy w rozbudowie. Unikamy również powielania kodu, ponieważ wykorzystujemy każdy dodatek jako osobną klasę i dodajemy jej zachowania do “dekorowanego” obiektu w sposób dynamiczny.
Zalety wzorca Decorator
- Wykorzystanie kompozycji przed dziedziczeniem, co pozwala na dynamiczną zmianę zachowania obiektu podczas działania programu,
- Możliwość dowolnego łączenia ze sobą kolejnych dodatkowych obiektów.
Wady wzorca Decorator
- Interfejs (lub klasa abstrakcyjna) dekoratora musi być dokładnie taki sam jak klasy dekorowanej,
- Wykorzystanie Decoratora wymusza zarządzanie dużą liczbą obiektów, co zwiększa prawdopodobieństwo popełnienia błędu w kodzie.
Kiedy stosować wzorzec
Wzorzec Decorator należy stosować w sytuacji kiedy trzeba dodawać nowe zadania do poszczególnych obiektów w sposób dynamiczny oraz gdy potrzebny jest mechanizm do obsługi zadań, które można cofnąć.