Wzorzec projektowy dekorator

Dekorator jest jednym z najczęściej wykorzystywanych wzorców projektowych. Wspomaga ponowne użycie obiektów w różnych kontekstach. Ogranicza konieczność wielokrotnego powtarzania kodu, oraz ilość stosowanych klas.

Czy warto czytać dalej?

Zanim zagłębisz się w dalszą treść, małe ostrzeżenie. Ten artykuł przeznaczony jest dla czytelników mających pewną minimalną wiedzę na temat programowania zorientowanego na obiekty. Zadaj sobie więc następujące pytania:

  • Czy wiem co to jest interfejs?
  • Czy wiem czym jest klasa abstrakcyjna?
  • Czym wiem czym jest mechanizm polimorfizmu?

Jeśli odpowiedziałeś ‘nie’ na jedno z powyższych pytań, być może przed dalszą lekturą warto skupić się na znalezieniu odpowiedzi…

Czym jest dekorator

Czym jest dekorator i do czego służy?

W skrócie, dekorator umożliwia programowanie bez niepotrzebnego powtarzania kodu.
1. Ogranicza niepotrzebne powtarzanie fragmentów kodu.
2. W prosty sposób umożliwia sobie wzbogacanie obiektów o nowe cechy, funkcjonalności, bez ingerencji w istniejące już, wcześniej napisane i przetestowane obiekty.
3. Umożliwia ograniczenie ilości typów (klas) obiektów do minimum, które jest to rzeczywiście niezbędne.

Punkt pierwszy należy rozumieć jako trzymanie się metodyki DRY (ang. Don’t Repeat Yourself, pol. Nie powtarzaj się) – reguły stosowanej w programowaniu, zalecającej by separować często powtarzający się kod źródłowy i jedynie odwoływać się do niego. Oczywiście same użycie wzorca wiąże się z wprowadzeniem dodatkowej warstwy abstrakcji co pociąga za sobą dodatkowy kod. Tak więc nie należy sprawę dobrze przemyśleć, czy aby na pewno wielkość i złożoność pisanego programu usprawiedliwia wprowadzanie Dekoratora.
O ile pierwszy punkt, dotyczy większości wzorców projektowych to dwa kolejne pasują szczególnie do omawianego wzorca.

Przykłady (nie) z życia wzięte

Zaznaczam, że prezentowane przeze mnie przykłady są oderwane od rzeczywistości. Wręcz surrealistyczne. Jest to zamierzone. Zakładam, że w ten sposób łatwiej będzie Ci się skupić na sednie samego wzorca zamiast podświadomie analizować logikę biznesową.

Załóżmy, że w naszym programie mamy obiekt klasy Jajo posiadający pewną funkcjonalność. Co jest tą funkcjonalnością? Nasze jajo, posiada różne umiejętności, ponadto ma swoje imię. Dodajmy także że nasze jajo mówi. Konkretnie, potrafi się przedstawić.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Jajo{
  protected $jajo;
  protected $umiejetnosci = array(
    'plywac',
    'latac',
    'spiewac',
    'skakac',
    'recytowac poezje'
  );
  protected $imie = "Strusie Jajo";

  public function przedstawSie(){
    echo "Jestem ". $this->imie .", umiem: " .
      implode(   $this->umiejetnosci, ', ' ) ;
  }
}

$jajoDomowe = new Jajo();
$jajoDomowe->przedstawSie();
$jajoOgrodowe = new Jajo();
$jajoOgrodowe->przedstawSie();
$jajoDlaBabci = new Jajo();

$jajoDlaBabci->przedstawSie();
//Output:
Jestem Strusie Jajo, umiem:
plywac, latac, spiewac, skakac, recytowac poezje.
Jestem Strusie Jajo, umiem:
plywac, latac, spiewac, skakac, recytowac poezje.
Jestem Strusie Jajo, umiem:
plywac, latac, spiewac, skakac, recytowac poezje.

Jak widać utworzyliśmy 3 jaja. Jedno z nich będzie mieszkać z nami w domu a drugie zostawimy sobie w naszym ogrodzie a trzecie podarujemy naszej babci :) .

Kod naszego obiektu jest bardzo prosty, tym niemniej mogli byśmy przecież rozbudować go do momentu w którym stał by się skomplikowany. Gdybyśmy dla przykładu cechy takie jak śpiewanie, skakanie pływanie czy recytowanie poezji zaimplementowali jako osobne metody albo nawet pod obiekty zawarte w obiekcie naszego jaja, nasza klasa rozrosła by się do wielu linii kodu. Musieli byśmy poświęcić więc nieco czasu na testy i dokładne sprawdzenie kodu.
Na potrzeby tego artykułu nasze cechy pozostaną łańcuchami znaków zawartymi w tablicy. Cechy obiektu same w sobie nie są bowiem ważne.
Budując obiekt jaja, w pewnym momencie uznamy, że nasze jajo jest właśnie tym jajem o które od początku nam chodziło i nie chcemy już dalej ingerować w jego kod. Dochodzimy do wniosku, że mamy to co jest nam potrzebne, działa to dobrze i chcemy teraz odłożyć nasze jajo na bok i nic już w nim nie zmieniać dzięki czemu nie będziemy musieli analizować i testować go po raz kolejny.
Problem pojawia się, kiedy stwierdzamy, że śpiewające jajo nie było dobrym pomysłem. Śpiewające jajo drze się w dzień i w nocy nie pozwalając nam spać. Trzeba coś z tym zrobić.
Okazuje się, że nasz super przetestowany odłożony na bok kod będziemy musieli jednak przeorać. Przepisujemy więc go tak, że mamy Jajo2. które już nie śpiewa.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Jajo2{
  protected $jajo;
  //Już nie śpiewa
  protected $umiejetnosci = array(
    'plywac',
    'latac',
    'skakac',
    'recytowac poezje'
  );
  protected $imie = "Strusie Jajo";

  public function przedstawSie(){
    echo "Jestem ". $this->imie .", umiem: " .
      implode( $this->umiejetnosci, ', ' ) ;
  }
}

$jajoDomowe = new Jajo2();
$jajoDomowe->przedstawSie();
$jajoDlaBabci = new Jajo2();
$jajoDlaBabci->przedstawSie();

//Output:
Jestem Strusie Jajo, umiem:
plywac, latac, skakac, recytowac poezje.
Jestem Strusie Jajo, umiem:
plywac, latac, skakac, recytowac poezje.

Mamy teraz 2 obiekty jaj dwóch typów: Jajo i Jajo2. To ogrodowe (Jajo) śpiewa sobie dalej (ale na dworze, więc nam to nie przeszkadza). Jajo domowe (Jajo2) już śpiewać nie potrafi dzięki czemu możemy się wyspać. Wstajemy sobie rano, wychodzimy do ogródka a tu szok! Nasze Ogrodowe jajo dryfuje bezwładnie w basenie. Po analizie kodu związanego z pływaniem okazuje się, że nie przetestowaliśmy go jednak tak dobrze jak nam się zdawało. Poprawkę dotyczącą pływania będziemy musieli wprowadzić osobno zarówno dla jaja ogrodowego jak i jaja domowego. Więc poprawiamy. Trochę lipa, że nasz kod nie zachowuje DRY, przez co musimy poprawić dwa obiekty zamiast jednego, ale co tam, ważne że teraz będzie już dobrze… Ledwo skończyliśmy a tu przychodzi babcia, i pyta dlaczego jej jajo przestało śpiewać. I jeszcze prosi aby zrobić coś tak aby już więcej nie latało bo jej ucieka na żyrandol i jest z tym problem…
Wygląda na to, że potrzebny będzie trzeci typ Jaja, takie które śpiewa, ale nie lata. Ale co jeśli okaże się ze w naszym kodzie są wciąż jakieś błędy? Będziemy musieli poprawiać 3 obiekty! W tym momencie jasne staje się, że Dekorator jest tu niezbędny.

Wprowadzamy dekorator

Nasze jajo będzie teraz obiektem dekorowanym. Przerobimy kod, w taki sposób, aby Jajo nie potrafiło nic. Wszystkie jego umiejętności będą teraz przypisywane jaju w procesie dekoracji poprzez dekoratory.

  • Dekorator – Obiekt, którego celem jest dodawanie nowej cechy dekorowanemu przez siebie obiektowi.
  • Dekoracja – dodanie nowej cechy lub transformacja wewnętrznego stanu obiektu.
  • Obiekt dekorowany – Obiekt, który będzie wzbogacany o nową cechę. Ewentualnie obiekt, którego stan wewnętrzny będzie zmieniany w procesie dekoracji.

Będziemy więc mili gołe jajo, które nie ma żadnych cech. Lub też będzie posiadało jedynie te cechy, o których jesteśmy absolutnie pewni, że będą one potrzebne, w każdym obiekcie jaja. Oraz po jednym dekoratorze dla każdej cechy, w którą chcemy udekorować jajo.
Plusy tego rozwiązania:

  • Mamy tylko jeden obiekt jajo, wszystkie ewentualne błędy będą poprawiane w nim. Naprawa błędów w którymś z dekoratorów, także będzie ograniczała się jedynie do edycji jednego obiektu.
  • Możemy dodawać nowe cechy dla jaja nie poprzez edycję kodu jaja, ale poprzez pisanie nowego obiektu dekoratora.
  • Możemy dekorować jajo różnymi kombinacjami dekoratorów.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
//1. Interfejs, który muszą implementować obiekty
//   dekoratorów oraz obiekty dekorowane.
interface Dekorowalne{
  public function dekoruj( $umiejetnosci = array() );
}

//2. Część wspólna dla obiektów Dekoratorów
//   i obiektów dekorowanych.
abstract class JajoBazowe{
  protected $jajo;
  protected $umiejetnosci = array();
  public function __construct( $jajo = null ){
  $this->jajo = $jajo;
}

  public function przedstawSie(){
    echo "Jestem ". $this->imie .", umiem: " .
      implode( $this->umiejetnosci, ', ' ) . ".";
  }
}

//3. Obiekt dekorowany Jajo
class Jajo extends JajoBazowe
  implements Dekorowalne{
  protected $imie = "Strusie Jajo";
  public function dekoruj( $umiejetnosci = array() ){
  $this->umiejetnosci = $umiejetnosci;
  return $this;
  }
}

//4. Dekorator dodający cechę pływanie
class DekoratorPlywanie extends JajoBazowe
  implements Dekorowalne{
  public function dekoruj( $umiejetnosci = array() )
  {
    $umiejetnosci[] = 'plywac';
    return $this->jajo->dekoruj( $umiejetnosci );
  }
}
//5. Dekorator dodający cechę latanie
class DekoratorLatanie extends JajoBazowe
  implements Dekorowalne{
  public function dekoruj( $umiejetnosci = array() )
  {
    $umiejetnosci[] = 'latac';
    return $this->jajo->dekoruj( $umiejetnosci );
  }
}

//6. Dekoratory opakowują inne dekoratory
//   a na dnie leży obiekt dekorowany.
$strusieJajo = new DekoratorPlywanie(
  new DekoratorLatanie( new Jajo( ) )
);
//7. Dekorowanie
$udekorowaneStrusieJajo = $strusieJajo->dekoruj();
//8. Efekt dekoracji
$udekorowaneStrusieJajo->przedstawSie();

//Output:
Jestem Strusie Jajo, umiem: plywac, latac.

1. Dzięki użyciu interfejsu, możliwym będzie traktowanie dekoratorów oraz obiektów dekorowanych tak jak by były obiektami tego samego typu. Obiekt dekorowany Jajo oraz dekoratory takie jak DekoratorPlywanie dziedziczą bowiem ten sam interfejs.
2. W klasie abstrakcyjnej implementujemy te fragmenty obiektu, które będą wspólne dla wszystkich jaj. Co pozwoli na niepowtarzanie tego samego kodu w podobnych do siebie obiektach. Możemy dla przykładu dodać nowy obiekt dekorowany KurzeJajo bez potrzeby pisania dla niego metod __construct() czy przedstawSie().
3. Obiekt dekorowany Jajo implementuje metodę dekoruj() z interfejsu Dekorowalne oraz dziedziczy po klasie JajoBazowe.
Metoda dekoruj() jest centrum całego wzorca. Każdy dekorator i każdy obiekt dekorowany będzie posiadał metodę o tej nazwie, choć sposób jej implementacji będzie odmienny. Obiekt dekorowany w metodzie dekoruj przyjmuje tablicę z umiejętnościami, którą następnie przypisuje do jednego ze swych pól. Na koniec zwraca sam siebie.
4. i 5. Dekorator DekoratorPlywanie także przyjmuje tablice z umiejętnościami, dodaje do niej nową cechę ‘pływanie’ po czym wywołuje na przechowywanym przez siebie obiekcie typu Jajo metodę dekoruj() z tablicą umiejętności. Metoda dekoruj w przypadku dekoratora zwraca obiekt z pola jajo dekoratora.
Kluczowe jest zrozumienie, że dekorator w swoim polu jajo może przechowywać zarówno obiekt dekorowany, jak i kolejny dekorator. Zawartość pola jajo zostanie aktywowana dopiero w fazie uruchomienia programu. Nasz kod musimy więc napisać tak aby prawidłowo obsłużył oba przypadki.
Oczywiście nazwa ‘dekoruj’ nie jest wymagana. Metodę możemy nazwać inaczej, tak aby najlepiej oddawała to co robi.
6. Dekoratory można przyrównać do coraz mniejszych pudełek zamkniętych jedno w drugim, a obiekt dekorowany jest zamknięty w ostatnim z nich.
7. Uruchomienie procesu dekoracji. W tym momencie zawarte w sobie dekoratory uruchamiają kolejne poziomy obiektów, w efekcie nasz dekorowany obiekt „wypływa” na wierzch.
8. Jajo przedstawia swoje umiejętności.

A może proste dziedziczenie?

Warto dodać, że nasz problem, mogli byśmy rozwiązać poprzez proste dziedziczenie. Należało by w takim wypadku z obiektu JajoBazowe dziedziczyć nowe obiekty takie jak: JajoLatajacePlywajace, JajoSpiewajacePlywajaceLatajace, itd. Minusem tego rozwiązania jest to, że pewne cechy mogą wykluczać się wzajemnie, musieli byśmy więc utworzyć tyle obiektów dziedziczących po obiekcie JajoBazowe ile było by kombinacji cech. W wypadku dekoratora tworzymy tyle obiektów ile jest cech, kombinacje cech uzyskujemy poprzez dekorowanie obiektów wybranymi dekoratorami, jest to więc spora oszczędność kodu.

Tagi: ,

Dodaj odpowiedź