Блог


Паттерны в PHP (Decorator)

Этот паттерн лучше объяснять снизу вверх. То есть по мере поступления проблем. Приступим.

Если кто читал предыдущую статью, то мог обратить внимание, что я имел наглость сравнить ООП с фаст-фудом. Буду последовательным, и возьму пример оттуда. Тем более он, на мой взгляд, весьма подходящий.

Представим себе точку быстрого питания, где решили делать гамбургеры. Нет ничего проще - берем два кусочка хлеба и суем между ними котлетку:
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

Теги: Паттерны | PHP

Комментарии (5)

Semen
05-02-2016
Как просто оказалось
Иван
29-10-2017
Почему мы не можем сделать наследника, вместо декоратора, который будет заниматься расширенной версией гамбургера? ... то, что мы избавились от кучи наследников - это понятно - почему не сделать продвинутого наследника? В чем профит?
twin
30-10-2017
В свете последних тенденций наследование е в чести. Дело в том, что стало очень модным применять "внедрение зависимостей". Другими словами у нас нет доступа к классу, мы получаем уже готовый объект из контейнера. Причем чсато отконфигурированный, а так же собранный из нескольких объектов. Наследование тут не годится, нельзя наследоваться от объекта. А декорирование - что доктор прописал.
Екатерина
03-07-2018
Отличное объяснение!)
Роман
17-08-2021
странный декоратор очень отличается от декоратора паттерна

 
Наверх