Этот паттерн лучше объяснять снизу вверх. То есть по мере поступления проблем. Приступим.
Если кто читал предыдущую статью, то мог обратить внимание, что я имел наглость сравнить ООП с фаст-фудом. Буду последовательным, и возьму пример оттуда. Тем более он, на мой взгляд, весьма подходящий.
Представим себе точку быстрого питания, где решили делать гамбургеры. Нет ничего проще - берем два кусочка хлеба и суем между ними котлетку:
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
<?php
// Рецепт гамбургера
class Hamburger
{
public $bread = 'Хлеб';
public $cutlet = 'Котлетка';
public $hamburger;
// Собираем
public function compile()
{ // Сверху хлеб
$this->hamburger[] = $this->bread;
// Потом котлетка
$this->hamburger[] = $this->cutlet;
// и снизу хлеб
$this->hamburger[] = $this->bread;
}
// Получаем
public function create()
{
return implode('<br>', $this->hamburger);
}
}
// Делаем гамбургер
$hamburger = new Hamburger();
$hamburger->compile();
echo $hamburger->create();
Однако бизнесс не прет - не все клиенты довольны. Слишком уж примитивен гамбургер. Где помидорка? После долгого мозгового штурма, советом директоров принимается историческое решение: добавить томат. Однако по законам SOLID нельзя трогать основной рецепт. Да и не всем нравятся помидоры.
Не беда, можно сделать наследника, и в нем добавить вожделенный овощ:
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
<?php
// Рецепт гамбургера
class Hamburger
{
public $bread = 'Хлеб';
public $cutlet = 'Котлетка';
public $hamburger;
public function compile()
{
$this->hamburger[] = $this->bread;
$this->hamburger[] = $this->cutlet;
$this->hamburger[] = $this->bread;
}
public function create()
{
return implode('<br>', $this->hamburger);
}
}
// Расширим ассортимент, теперь с помидоркой!
class HamburgerWithTomato extends Hamburger
{
protected $tomato = 'Помидорка';
public function compile()
{
$this->hamburger[] = $this->bread;
// Добавим томат
$this->hamburger[] = $this->tomato;
$this->hamburger[] = $this->cutlet;
$this->hamburger[] = $this->bread;
}
}
// И делаем из них гамбургер по новому рецепту
$hamburger = new HamburgerWithTomato();
$hamburger->compile();
echo $hamburger->create();
Пока всё гладко. Но тут приходит новый клиент, и заявляет: хочу не с помидоркой, а с огурцом. Опять селекторное совещание, долгие дебаты, и новое историческое решение. Сделать еще одного наследника, расширив овощную базу. Не успели нарезать огурцы, а тут еще один брюзга: хочу на листике салата. И с майонезиком. Да что ты будешь делать... Еще наследника?
А вдруг с луком или сыром захотят? Генеральный бъет кулаком по столу. Глобально изменить структуру общепита! Чтобы можно было в основной рецепт гамбургера в любую минуту добавить любой ингридиент, и это малой кровью. Консилиум технологов приходит к такому решению - оставить основной рецепт без изменения, но сделать отдельный цех по сборке гамбургеров, где добавлять в них любые ингридиенты. Революционное решение!
Теперь можно легко добавить любые продукты. Хоть помидорку, хоть салат, хоть авокадо. Нужно только добавить еще один промежуточный класс - "Декоратор". Он и будет расширять ассортимент ингридиентов. Теперь основной цех занимается сборкой простых гамбургеров, а цех пряностей (декаратор) добавляет в них всякую бяку. Получилось то, чего и хотел шеф. Основной рецепт не тронут, но ассортимент расширен до невообразимых высот.
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
64
65
66
67
68
69
70
71
72
73
<?php
// Основной рецепт гамбургера
class Hamburger
{
public $bread = 'Хлеб';
public $cutlet = 'Котлетка';
public $hamburger;
public function compile()
{
$this->hamburger[] = $this->bread;
$this->hamburger[] = $this->cutlet;
$this->hamburger[] = $this->bread;
}
public function create()
{
return implode('<br>', $this->hamburger);
}
}
// А это и есть декоратор.
// Может добавить к основному набору продуктов что угодно,
// то есть задекорировать первоначальный функционал.
class HamburgersDecorator
{
protected $sandwich;
protected $ingredients = array();
// Принимаем простой гамбургер (объект)
public function __construct($sandwich)
{
$this->sandwich = $sandwich;
}
// Добавим новый метод, который принимает другие продукты.
// Его нет в классе гамбургера.
public function addIngridients($ingredients = array())
{
$this->ingredients = $ingredients;
}
// Новый цех сборки
public function compile()
{ // Сверху хлеб
$this->sandwich->hamburger[] = $this->sandwich->bread;
// Добавляем по вкусу что угодно. Это и есть декорация.
foreach ($this->ingredients as $product) {
$this->sandwich->hamburger[] = $product;
}
// Потом котлетка и снизу хлеб
$this->sandwich->hamburger[] = $this->sandwich->cutlet;
$this->sandwich->hamburger[] = $this->sandwich->bread;
}
// Вызываем остальные методы из исходного объекта с помощью магии
public function __call($method, $args = '')
{
return $this->sandwich->$method($args);
}
}
// Оборачиваем объект простого гамбургера декоратором
$hamburger = new HamburgersDecorator( new Hamburger() );
// Теперь можно использовать новый метод, добавляя ингридиенты
$hamburger->addIngridients(array('Помидорка', 'Майонез'));
// Остальное все как прежде
$hamburger->compile();
// Теперь с маянезиком!
echo $hamburger->create();
Теперь посетители довольны и громко чавкают, облизывая пальцы.
Смысл конструкции довольно прост. Если вызываемый метод есть в декораторе, то используется он. В нашем случае
compile(). По сути эмуляция перегрузки метода. А если нет - вызывается магический
__call(), и он делегирует (проксирует) вызов в исходный объект. У нас это соответственно
create().
По такой схеме не нужно в каждом наследнике реализовывать основной функционал - складывать пирамдку из хлеба и котлет. И не нужно трогать основной класс - рецепт гамбургера. Получилось своего рода наследование, но без наследования. Иногда бывает довольно полезно, когда нельзя трогать исходные классы, а наследование не подходит. И еще это сокращает объем кода, когда требуется много наследников.
Это довольно простая реализация декоратора. Бывают разные, простые, с магией, с вызовом
callback методов, декорация декораторов, декорация с проксированием, рекурсивная декорация, и так далее. Но сам принцип я постарался растолковать.
Николай aka twin
Роман