Паттерн Decorator

В больших проектах, особенно где ядро должно оставаться стабильным зачастую требуется изменения поведения объекта рантайм без дописывания кода в сам класс. Есть несколько вариантов, в этот раз я продемонстрирую паттерн Decorator (декоратор, wrapper, обёртка). Нам довелось активно использовать сей паттерн при разработке проета для IBM на базе Sugar CRM. В шуге заложено много гибкости, но некоторые части написаны очень очень давно и наружу торчит только Bean, который мы и декорировали чтобы добавить функционал или подменить стандартную реализацию. К сожалению, это не стало серебряной пулей и нам довелось хлебать кислые щи дырявой оловянной ложкой.Дырявая ложкаСуть решения проста, создаём класс обёртку, засовываем в него наш исходный класс и дальше подменяем исходный класс на эту обёртку. Обёртка дёргает либо свои определённые атрибуты и методы, либо проксирует их на оригинальный класс. Реализация сего паттерна на PHP очень проста и прекрасна, демонстрирует сильные стороны языка. В помощь мы берём волшебные методы, __get __set __call вызывается, когда атрибута или класса не существует.

Код декоратора:

<?php
namespace Blog;

class Decorator
{
    protected $class;

    public function __construct($class)
    {
        $this->class = $class;
    }

    public function __get($name)
    {
        return $this->class->{$name};
    }

    public function __set($name, $value)
    {
        $this->class->{$name} = $value;
    }

    public function __call($method, $arguments = [])
    {
        return call_user_func_array([$this->class, $method], $arguments);

    }
    public function render()
    {
        return 'decorator ' . $this->class->render();
    }

    public function decoratorOnlyMethod()
    {
        return true;
    }
}

В конструкторе мы принимаем исходный объект и присваиваем его закрытом атрибуту class, затем в волшебных методах проксируем вызовы на этот объект. В случае существования метода в декораторе с названием метода исходного объекта будет дёргаться метод декоратора, в моём примере добавляется строка «decorator » и конкатенируется с результатом вызова оригинального метода. А вот метода decoratorOnlyMethod в исходном объекте не существует и он добавляет новый функционал.

Код класса инстанс которого мы будем декорировать

<?php
namespace Blog;

class Object
{
    protected $id;

    public function getId()
    {
        return $this->id;
    }

    public function setId($value)
    {
        $this->id = $value;
    }

    public function render()
    {
        return "id = " . $this->getId();
    }
}

Пояснять не буду, там закрыты атрибут id и методы для доступа к нему, изменения и метод где используется значение этого атрибута.

Ну и собственно код теста который демонстрирует функционал и помогает понять как это работает в результате.

<?php
require_once 'Object.php';
require_once 'Decorator.php';
use Blog\Decorator;
use Blog\Object;

class DecoratorTest extends PHPUnit_Framework_TestCase
{
    public function testOriginalRender()
    {
        $origin = new Object();
        $origin->setId(666);
        $this->assertEquals('id = 666', $origin->render());
    }

    public function testProxySetGet()
    {
        $origin = new Object();
        $decorator = new Decorator($origin);
        $this->assertNotEquals(777, $decorator->getId());
        $decorator->setId(777);
        $this->assertEquals(777, $decorator->getId());

    }

    public function testOverridingRender()
    {
        $origin = new Object();
        $decorator = new Decorator($origin);
        $origin->setId(666);
        $this->assertEquals('decorator id = 666', $decorator->render());

        $decorator->setId(777);
        $this->assertEquals('decorator id = 777', $decorator->render());
    }

    public function testDecoratorOnlyMethod()
    {
        $origin = new Object();
        $decorator = new Decorator($origin);
        $this->assertTrue($decorator->decoratorOnlyMethod());
    }
}

Не хочется комментировать каждую строчку, пишу пост в спешке, так как наболел и давно не писал в блог. Буду в дальнейшем стараться писать хотя бы раз в месяц. Если есть вопросы, задавайте, получилось как-то куцо и возможно не совсем понятно, но на это есть комментарии, да и приятно чувствовать вовлечённность.

Код примеров заливаю на гитхаб, смотрите, пуллреквестируйте, болейте за Спартак. https://github.com/AmdY/Blog/tree/master/decorator

Паттерн Decorator: 13 комментариев

  1. Зураб

    К сожалению так не получится декорировать методы (тут getId), которые используются (вызываются) в декорируемом классе.

  2. fluid

    Давно от Тебя постов не было… А написал понятно, да, молодец:) Даешь «Приемы объектно-ориентированного проектировани.я Паттерны проектирования.», это ты какой уже паттерн описал? Где-то 3й, Money — не в счет, он получился не очень, не учитывает плюшки работы с float )

  3. IAD

    Можно сделать ещё один шажок. Сделать реализацию getId() в декораторе, без обращения в декодируемый объект. Получим возможность передавать lazy объект в другие, которым нужен только id для операций. Фактически сэкономим на куче запросов в субд.
    И как бонус, можно сделать второй шажок и получим возможность инвалидировать объекты в IdentityMap и такой хитрый декторатор сможет восстановить объект при фактическом обращении. Но тут придётся внедрить в него DI на модель декорируемого объекта.

  4. NickSun

    Одна небольшая деталь:
    public function __construct(Object $class)
    {
    $this->class = $class;
    }
    Так будет понятно что мы собираемся декорировать + проверка

  5. artoodetoo

    Буду некропостером, но не беда :)

    В принципе подход годный, удручает только одно: т.к. декоратор ни коим образом не наследует от декорируемого класса, то чисто формально он не получает его интерфейсы! Поэтому проверка вроде

    if ($decoratedObject instanceof ContainerAware) {
    // ... set container or whatever
    }

    провалится, хотя de facto декорированный объект продолжает выполнять контракт. Такова цена «магии».

    1. AmdY Автор записи

      Ну, никто не мешает декоратор отнаследовать от декорируемого класса, это частая практика.
      class Decorator extends Object { …. }, тогда проверки проходить будут. Главное не забывать, что в декорируемом классе тоже могут быть магические методы.

      1. Вадим

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

        1. AmdY Автор записи

          Ключевым моментом является то, что нам приходит объект, порождённый где-то в системе на которую мы влиять не можем, соответственно, мы его менять не можем, потому декорируем.
          Магия с extends нужна для дружбы с тайпхинтингом и обхода зоны видимости, это скорее костыль.
          Ну и сам паттерн во многом костыль, который применяется из-за отсутствие грамотного DI.

  6. Oleg

    А почему бы не использовать наследование? и получается что декоратор должен сожержать реализацию всех методов передаваемого объекта?

    1. AmdY Автор записи

      Я выше писал об этом
      >>Ключевым моментом является то, что нам приходит объект, порождённый где-то в системе на которую мы влиять не можем.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *