Блог


Паттерны в PHP. Компоновщик (Composite).

Компоновщик (Composite), это паттерн, который помогает разрулить ситуацию, не предусмотренную штатными средствами. А именно множественное наследование. Впрочем он может быть полезен и при работе с одним классом, но все по порядку.


Один из четырех китов ООП - наследование последнее время всё чаще предается гонениям. Заменяющий его паттерн "Декоратор" и делегирование мы уже рассматривали. Теперь еще один, который наносит наследованию хук слева. Компоновщик. Уже из названия видно, что он должен что то скомпоновать, совместить, собрать в кучу.

Как обычно, рассмотрим пример из жизни.

Допустим мы хотим испить горячего кофе со сливками (как то странно звучит название водки...). Если у нас есть кофемашина, которая сразу выдаст желаемый результат, то славно. А если нет, а есть кофеварка и чайничек с молочным продуктом, или взбиватель сливок, то потребуется несколько лишних движений, чтобы такую машину собрать.

Кофемашина, по по сути, наследует возможности кофеварки и взбивателя сливок:

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
<?php 
// Кофеварка
class CoffeeMaker
{
    public function 
getCoffee()
    {
        return 
'кофе';
    }
}
// Взбиватель сливок
class СreamMaker
{
    public function 
getСream()
    {
        return 
'сливки';
    }
}
// Кофемашина. Пытаемся отнаследовать функционал первых двух классов
class CoffeMashin extends CoffeeMakerСreamMaker
{
    public function 
getCappuccino()
    {
        
$coffe $this->getCoffee();
        
$cream $this->getСream();
        return 
$coffe .' + '$cream;
    }
}

echo (new 
CoffeMashin)->getCappuccino();


 

Красиво, но работать к сожалению не будет, так как PHP не поддерживает множественное наследование. 

Что ж. Переделаем кофемашину так, чтобы она увидела оба объекта:

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
<?php 
// Теперь это уже композиция 
class CoffeMashin  

    protected 
$coffeeMaker$creamMaker

    public function 
__construct()  
    {    
        
$this->coffeeMaker = new CoffeeMaker;   
        
$this->creamMaker  = new СreamMaker
    }  

    protected function 
getCoffee() 
    { 
        return 
$this->coffeeMaker->getCoffee(); 
    } 

    protected function 
getСream() 
    { 
        return 
$this->creamMaker->getСream(); 
    } 
     
    public function 
getCappuccino()  
    {  
        
$coffe $this->getCoffee();  
        
$cream $this->getСream();  
        return 
$coffe .' + '$cream;  
    } 
}  

$mashin = new CoffeMashin

echo 
$mashin->getCappuccino(); 


 

Это уже эмуляция множественного наследования. Примитивная, но всё же. Что мы тут сделали. Первым делом поместили в свойства класса необходимые объекты. А потом сделали методы-обертки для этих объектов.

Но, хотя в таком виде компановщик работает, и это уже отвечает требованиям паттерна, он получился не ахти. Так как очень сильно связан, потому что построен по принципу композиции. Может работать только с двумя конкретными классам. А кофемашина должна уметь готовить всевозможные рецепты напитка, не только капучино. Допустим мне очень нравится айс-кофе. Со льдом. Так что переделаем кофемашину так, чтобы она могла делать всевозможные варианты, используя агрегацию (помещение объектов извне).

Но прежде нужно переделать компоненты, они должны иметь одинаковые методы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// Кофеварка 
class CoffeeMaker 

    public function 
get() 
    { 
        return 
'кофе'
    } 

// Взбиватель сливок 
class СreamMaker 

    public function 
get() 
    { 
        return 
'сливки'
    } 


 



Вообще, прожжеными адептами ООП здесь рекомендуется приляпать intrface. Я не вижу в них практической пользы, так что не стану. Можно еще оформить это абстракцией, но тогда пропадет чистота эксперимента, так как появится наследование. Так что примите как есть.

Продолжим. Теперь, когда компоненты приведены к единообразию, можно сделать универсальную кофемашину:

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
<?php   
// Кофемашина, теперь универсальный компановщик. 
class CoffeMashin 
{
    protected 
$units = [];

    public function 
add($component
    {   
        
$this->units[] = $component;
    }    
 
    public function 
getCoffe() 
    { 
        
$ingredients = [];
       
        foreach (
$this->units as $component) { 
            
$ingredients[] = $component->get();
        }
        
        return 
implode(' + '$ingredients); 
    } 



$mashin = new CoffeMashin;
// Задаем рецептуру, задействуя нужные компоненты 
$mashin->add(new CoffeeMaker);
$mashin->add(new СreamMaker);


echo 
$mashin->getCoffe(); 


 



Вот теперь можно смело приготовить и холодный кофе, нужно только добавить в кофемашину функционал льдогенератора:

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
<?php   
// Льдогенератор 
class IceMaker 

    public function 
__construct()
    {
        
$plumbing = new Plumbing;
        
$this->water $plumbing->get();
    }
 
    public function 
get() 
    { 
        return 
$this->freezer(); 
    } 
    
    protected function 
freezer() 
    { 
        return 
str_replace('вода''лед'$this->water); 
    } 


// Водопровод 
class Plumbing 

    public function 
get() 
    { 
        return 
'вода'
    } 


/////////////////////////////////////

$mashin = new CoffeMashin;

$mashin->add(new CoffeeMaker);
$mashin->add(new СreamMaker);
$mashin->add(new IceMaker);

echo 
$mashin->getCoffe();


 



В этой ситуации интересно вот что. Льдогенератор пользуется водой из водопровода. Но так как у него тоже есть общий метод get(), можно просто разбавить кофе водой, заменив объект класса IceMaker на Plumbing. Получается, что кофемашина на столько универсальна, что может вбирать в себя не только конкретные "производители" ингридиентов, но так же и их обработчики.

Это называется "древовидная структура объектов". Льдогенератор тут - составной элемент (так как пользуется следующим уровнем дерева), а водопровод, кофеварка и взбиватель сливок - листовые элементы. Они конечны и просто выдают результат.

Можно усложнить компановщик, допустим добавить метод удаления объектов из реестра, или заставить его работать "рекурсивно", то есть поместить в него его же собственный объект, что бы он проходил по всем узлам дерева. Но это отдельная песня, здесь показан основной принцип.

Плюсы паттерна очевидны, теперь, по традиции, пройдемся по минусам.

Первый и самый неприятный. При агрегации, в отличие от наследования, компановщик не знает, какие свойства и методы есть в агрегируемом объекте, а значит их вызов затруднен.

Второй, это вероятность изменения состояния объекта вклиниванием. На нашем примитивном примере это показать трудно, но при сложных реализациях значения свойств могут быть изменены между вызовами методов компановщика. При наследовании это невозможно.

Третий. Достаточно сложно разграничить типы объектов. Допустим в кофеварку можно легко закинуть объект "кошка", если у него есть метод get(), но что получится, лучше не представлять.

Так что не стоит кидаться в крайности, иногда наследование всё же уместнее. Всему своё место. 

Теги:

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


 
Наверх