Паттерн 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

Вам также может понравиться

Об авторе amdy

12 комментариев

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

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

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

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

  5. Буду некропостером, но не беда 🙂

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

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

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

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

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

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

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

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

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