Kolejnym wzorcem projektowym, który opiszę będzie tytułowy dekorator. Wzorzec ten łatwo można sobie wyobrazić jako implementację pracy dekoratora wnętrz, który dostając mieszkanie nieurządzone zajmuje się przyozdabianiem go w różne gadżety. W efekcie dostajemy to samo mieszkanie jednak jest ono już zmodyfikowane. Jak to wygląda w programowaniu? Zapraszam do lektury.
Spis Treści
Przedstawmy problem
Wzorzec dekorator zakłada istnienie dwóch rodzajów klas, które nazwiemy potocznie klasą dekorowana i dekorująca. Na przywołanym przykładzie dekoratora wnętrz klasą dekorowaną będzie mieszkanie, a klasą dekorującą ozdoba, bądź element dekoratorski, który przystraja to mieszkanie. Każda czynność wykonana przez dekoratora w mieszkaniu powoduje, że mieszkanie pozostaje ciągle mieszkaniem lecz zyskuje nowe funkcjonalności.
W tym momencie można by wprost pomyśleć o tym, że w takim razie dziedziczenie idealnie nadaje się do tego aby taki mechanizm odwzorować. Problem polega na tym, że każdy dekorator ma całą paletę utartych ścieżek i elementów które stosuje w swojej pracy. W końcu po co przy każdym projekcie wymyślać koło od nowa. Tworzenie finalnego projektu za każdym razem wymagałoby napisania przez dekoratora wielu klas, z których każda kolejna dodawałaby jeden efekt w mieszkaniu co przy wielu projektach przez niego wykonywanych namnożyłoby mnóstwo klas, które ciężko byłoby ponownie wykorzystać. Zobaczmy przykład poniżej:
mieszkanie 1
class Mieszkanie {
int iloscScian = 4;
}
class MieszkanieJasne
extends Mieszkanie{
String kolor = "biały";
}
class MieszkanieBialeDuze
extends MieszkanieBiale{
double powierzchniaM2 = 120;
}
class MieszkanieBialeDuzeZBalkonem
extends MieszkanieBialeDuze{
boolean balkon = true;
}
mieszkanie 2
class Mieszkanie {
int iloscScian = 4;
}
class MieszkanieCiemne
extends Mieszkanie{
String kolor = "czarny";
}
class MieszkanieCiemneMale
extends MieszkanieCiemne{
double powierzchniaM2 = 42;
}
class MieszkanieCiemneMaleZTarasem
extends MieszkanieCiemneMale{
boolean taras = true;
}
Zauważmy że gdybyśmy chcieli tylko utworzyć mieszkanie jasne małe z tarasem to możemy wykorzystać tylko klasę MieszkanieJasne, natomiast pozostałe 2 należałoby dopisać. To nie jest wygodne rozwiązanie. Dodatkowo pojawia się problem w momencie gdy chcemy porównać ze sobą dwa obiekty. Zauważmy że tak naprawdę obydwa są różnego typu więc wprost nie można ich do siebie przyrównać.
Rozwiążmy problem
Idealnym rozwiązaniem dla dekoratora wnętrz byłoby gdyby mógł każde mieszkanie komponować z gotowych utworzonych wcześniej komponentów, a następnie dla własnego rozeznania ocenić atrakcyjność rozwiązania w oparciu o jakiś parametr. W tym momencie triumfalnie wkracza wzorzec dekorator.
Na samym początku popatrzmy na końcowy fragment kodu, na którym pracuje dekorator wnętrz tworząc kolejne kompozycje.
Mieszkanie piekneMieszkanie = new MieszkanieNowe(); //tworzymy nasze piękne mieszkanie
piekneMieszkanie = new Jasne(piekneMieszkanie); //dekorujemy mieszkanie jasnymi ścianami
piekneMieszkanie = new ZTarasem(piekneMieszkanie); //dodajemy aranżację tarasu
Zwróć uwagę, że cały czas pracujemy na naszym pierwotnie utworzonym obiekcie i opakowujemy go kolejnymi nowymi obiektami. Nie zmieniamy typu obiektu ponieważ ciągle korzystamy z klasy Mieszkanie. Zarówno obiekt dekorowany piekneMieszkanie jak i obiekty dekorujące są tego samego typu co może wydawać się mało intuicyjne ale daje ogromne możliwości. Zapisane rozwiązanie można przedstawić graficznie w następujący sposób:
Aby zapewnić zgodność typów wszystkie klasy rozszerzają bazową klasę abstrakcyjną Mieszkanie:
package com.company;
public abstract class Mieszkanie {
String opis; //zawiera opis dodawanej funkcjonalnosci
public String getOpis() {
return opis;
}
public abstract int indeksWartosci(); //zwraca wartosc wspolczynnika atrakcyjnosci mieszkania
}
Na tej podstawie tworzymy naszą klasę dekorowaną MieszkanieNowe(później można tworzyć podobnie inne klasy jak np. DomJednorodzinny, Szeregowiec itp.):
package com.company;
public class MieszkanieNowe extends Mieszkanie { //rozszerzamy klase Mieszkanie
public MieszkanieNowe() {
opis = "Mieszkanie z rynku pierwotnego";
}
@Override
public int indeksWartosci() {
return 50;
}
}
Stworzyliśmy w ten sposób klasę dekorowaną która jest punktem wyjściowym całej kompozycji. Teraz przejdziemy do tworzenia dekoratorów a zaczniemy od tego, że jak wcześniej wspomniałem klasa dekorująca musi być tego samego typu co klasa dekorowana. Zatem stwórzmy klasę:
package com.company;
public abstract class Ozdoba extends Mieszkanie{
public abstract String getOpis();
}
Tę klasę wykorzystamy do tworzenia właściwych klas dekorujących jak np:
package com.company;
public class Jasne extends Ozdoba {
Mieszkanie mieszkanie;
public Jasne(Mieszkanie mieszkanie) {
this.mieszkanie = mieszkanie;
}
@Override
public int indeksWartosci() {
return mieszkanie.indeksWartosci() + 4;
}
@Override
public String getOpis() {
return mieszkanie.getOpis() + " + Sciany jasne";
}
}
package com.company;
public class ZTarasem extends Ozdoba {
Mieszkanie mieszkanie;
public ZTarasem(Mieszkanie mieszkanie) {
this.mieszkanie = mieszkanie;
}
@Override
public int indeksWartosci() {
return mieszkanie.indeksWartosci() + 10;
}
@Override
public String getOpis() {
return mieszkanie.getOpis() + " + Taras";
}
}
Zwróć uwagę, że w konstruktorze obydwu klas przekazuję dotychczas istniejący obiekt mieszkania już jakoś udekorowaanego i w metodach indeksWartosci() oraz getOpis() dopisuję do poprzedniego obiektu nowe dane.
Właśnie stworzyliśmy prostą aplikację implementującą wzorzec dekorator. Na sam koniec zobaczmy jak zadziała nasza aplikacja po poskładaniu wszystkich klas w mainie:
package com.company;
public class Main {
public static void main(String[] args) {
Mieszkanie piekneMieszkanie = new MieszkanieNowe(); //tworzymy nasze piękne mieszkanie
piekneMieszkanie = new Jasne(piekneMieszkanie); //dekorujemy mieszkanie jasnymi ścianami
piekneMieszkanie = new ZTarasem(piekneMieszkanie); //dodajemy aranżację tarasu
System.out.println("Indeks wartości: " + piekneMieszkanie.indeksWartosci() + ", zawiera: " + piekneMieszkanie.getOpis());
}
}
Wyjście programu wygląda następująco:
Indeks wartości: 64, zawiera: Mieszkanie z rynku pierwotnego + Sciany jasne + Taras
Process finished with exit code 0
Zauważ, że teraz bardzo łatwo dokładać można nowe klasy, zarówno dekorowane jak i dekorujące, ponieważ za każdym razem możemy korzystać bezpiecznie z wcześniej już napisanych klas bez obawy o ich wzajemną kompatybilność.
Na sam koniec przedstawię jeszcze definicję wzorca dekorator, która powinna być już dla Ciebie bardziej zrozumiała po przeczytaniu tego artykułu:
W powyższej definicji warto również zwrócić uwagę na słowo dynamiczne, ponieważ nic nie stoi na przeszkodzie aby naszemu dekoratorowi wnętrz w przyszłości zaimplementować rozwiązanie oparte na sztucznej inteligencji. Zbierało by ono informacje o właścicielu mieszkania i na tej podstawie w trakcie działania programu symulowałoby jak można by urządzić klientowi mieszkanie.
Mam nadzieję że mój artykuł przekonał Cię i dał zrozumienie co do wzorca dekorator, a jeśli masz uwagi to zapraszam do komentarza bądź do kontaktu poprzez formularz.