Wprowadzenie
Wzorzec dekorator to wzorzec projektowy z kategorii strukturalnych, który pozwala dodawać nowe funkcje do istniejących obiektów bez zmiany ich struktury. Wzorzec ten opiera się na zasadzie kompozycji, gdzie obiekty są dekorowane, czyli opakowywane, przez inne obiekty, które dodają im dodatkowe funkcjonalności.
Komponenty
Wzorzec dekoratora składa się z następujących elementów:
- Komponent – podstawowy interfejs lub klasa abstrakcyjna, którą dekoratory rozszerzają/implemenują. Reprezentuje obiekt, do którego chcemy dodać funkcjonalności.
- Konkretny komponent – konkretne implementacje interfejsu komponentu. To obiekty, które chcemy dekorować.
- Dekorator – abstrakcyjna klasa lub interfejs, który definiuje interfejs dla dekoratorów konkretnej klasy komponentu.
- Konkretny dekorator – to konkretne implementacje dekoratora, które dodają lub modyfikują funkcjonalności komponentu bazowego.
Kiedy stosować wzorzec dekorator?
Wzorzec dekoratora jest szeroko stosowany w przypadkach, gdy istnieje potrzeba dodawania funkcjonalności do obiektów w sposób dynamiczny i elastyczny, zachowując jednocześnie ich jednolity interfejs.
Załóżmy, że tworzymy klasę Pizza (odpowiednik podstawowej pizzy – składa się z ciasta, sera, sosu pomidorowego). Jednak możemy mieć róże rodzaje kawy np. kawa z mlekiem, cappuccino, ale wszystkie składają się z tych samych składników co klasa Kawa plus dodatkowe składniki np. mleko, cukier, spienione mleko, bita śmietana. Pierwsze co przychodzi na myśl to stworzenie struktury klas, z jedną klasą nadrzędną, a następnie z klasami dziedziczącymi. Mogłoby to doprowadzić do powstania dużej ilości klas dziedziczących.
Stosując wzorzec dekorator zastępujemy mechanizm dziedziczenia poprzez kompozycję i delegację.
Kompozycja
Wzorzec dekorator wykorzystuje kompozycję poprzez opakowywanie oryginalnego obiektu (komponentu) w inne obiekty (dekoratory). Kompozycja polega tutaj na tworzeniu hierarchii obiektów, gdzie dekoratory są „składnikami” komponentu.
Na przykład, w naszym przykładzie kawy, mamy obiekt SimpleCoffee (komponent) i dekoratory MilkCoffee, SugarCoffee, itd., które są komponowane wokół tego komponentu.
Delegacja
Wzorzec dekoratora wykorzystuje delegację, aby przekazywać żądania do odpowiednich obiektów w hierarchii. Gdy klient korzysta z obiektu dekoratora, ten przekazuje żądanie dalej do wewnętrznego obiektu (komponentu) lub innego dekoratora w łańcuchu.
Na przykład, gdy klient wywołuje metodę getDescription() na obiekcie SugarCoffee, dekorator SugarCoffee przekazuje to żądanie do obiektu MilkCoffee, który z kolei przekazuje je do obiektu SimpleCoffee, aż dojdzie do samego komponentu.
Coffee (interfejs)
- interfejs Coffee reprezentuje napój kawowy
- posiada definicje dwóch metod cost() oraz getDescription()
interface Coffee {
public function cost();
public function getDescription();
}
Klasa SimpleCoffee implementująca interfejs Coffee
- implementujemy metody cost() oraz getDescription()
class SimpleCoffee implements Coffee {
public function cost() {
return 10; // Cena prostej kawy
}
public function getDescription() {
return "Prosta kawa";
}
}
Klasa MilkCoffee implementuje interfejs Coffee
- tworzymy zmienną $coffee
- w konstruktorze przekazujemy obiekt typu Coffee
- ustawiamy cenę oraz opis dla kawy z mlekiem
class MilkCoffee implements Coffee {
protected $coffee;
public function __construct(Coffee $coffee) {
$this->coffee = $coffee;
}
public function cost() {
return $this->coffee->cost() + 5; // Cena za kawę z mlekiem
}
public function getDescription() {
return $this->coffee->getDescription() . ", Mleko";
}
}
Klasa SugarCoffe implementuje interfejs Coffee
- tworzymy zmienną $coffee
- w konstruktorze przekazujemy obiekt typu Coffee
- ustawiamy cenę oraz opis dla kawy z cukrem
class SugarCoffee implements Coffee {
protected $coffee;
public function __construct(Coffee $coffee) {
$this->coffee = $coffee;
}
public function cost() {
return $this->coffee->cost() + 2; // Cena za kawę z cukrem
}
public function getDescription() {
return $this->coffee->getDescription() . ", Cukier";
}
}
Klasa WhippedCreamCoffee implementuje interfejs Coffee
- tworzymy zmienną $coffee
- w konstruktorze przekazujemy obiekt typu Coffee
- ustawiamy cenę oraz opis dla kawy z bitą śmietaną
class WhippedCreamCoffee implements Coffee {
protected $coffee;
public function __construct(Coffee $coffee) {
$this->coffee = $coffee;
}
public function cost() {
return $this->coffee->cost() + 3; // Cena za kawę z bitą śmietaną
}
public function getDescription() {
return $this->coffee->getDescription() . ", Bita śmietana";
}
}
Użycie dekoratorów
$simpleCoffee = new SimpleCoffee();
echo $simpleCoffee->getDescription() . " - " . $simpleCoffee->cost() . " zł\n";
$milkCoffee = new MilkCoffee($simpleCoffee);
echo $milkCoffee->getDescription() . " - " . $milkCoffee->cost() . " zł\n";
$sugarMilkCoffee = new SugarCoffee($milkCoffee);
echo $sugarMilkCoffee->getDescription() . " - " . $sugarMilkCoffee->cost() . " zł\n";
$sugarWhippedCreamCoffee = new WhippedCreamCoffee($sugarMilkCoffee);
echo $sugarWhippedCreamCoffee->getDescription() . " - " . $sugarWhippedCreamCoffee->cost() . " zł\n";
Wynik
Prosta kawa – 10 zł
Prosta kawa, Mleko – 15 zł
Prosta kawa, Mleko, Cukier – 17 zł
Prosta kawa, Mleko, Cukier, Bita śmietana – 20 zł
Podsumowanie
W tym przykładzie MilkCoffeee, SugarCoffee oraz WhippedCreamCoffee działają jako dekoratory.
- Dekorator MilkCoffee opakowuje obiekt SimpleCoffee i dodaje funkcjonalność (w tym przypadku dodaje mleko do kawy) bez zmiany jego pierwotnego interfejsu ani struktury.
- Dekorator SugarCoffee opakowuje obiekt MilkCoffee i dodaje funkcjonalność (w tym przypadku dodaje cukier do kawy) bez zmiany jego pierwotnego interfejsu ani struktury.
- Dekorator WhippedCreamCoffee opakowuje obiekt SugarCoffee i dodaje funkcjonalność (w tym przypadku dodaje bitą śmietanę do kawy).
Dzięki temu możemy elastycznie dodawać różne składniki do naszej kawy, a także łączyć je w dowolnej kolejności, co daje nam dużą elastyczność i możliwość łatwego rozszerzania funkcjonalności naszego kodu.
0 komentarzy