В больших проектах, особенно где ядро должно оставаться стабильным зачастую требуется изменения поведения объекта рантайм без дописывания кода в сам класс. Есть несколько вариантов, в этот раз я продемонстрирую паттерн 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
К сожалению так не получится декорировать методы (тут getId), которые используются (вызываются) в декорируемом классе.
Точнее, декорировать-то получится, но получится какая-то хрень.
Покажи пример кода, там должно быть аналогично Decorator::render
http://pastebin.com/c4NPjcQj
PS. Тут в комментах у тебя код подсвечивается? Нада бы сделать предварительный просмотр….
Давно от Тебя постов не было… А написал понятно, да, молодец:) Даешь «Приемы объектно-ориентированного проектировани.я Паттерны проектирования.», это ты какой уже паттерн описал? Где-то 3й, Money — не в счет, он получился не очень, не учитывает плюшки работы с float )
Можно сделать ещё один шажок. Сделать реализацию getId() в декораторе, без обращения в декодируемый объект. Получим возможность передавать lazy объект в другие, которым нужен только id для операций. Фактически сэкономим на куче запросов в субд.
И как бонус, можно сделать второй шажок и получим возможность инвалидировать объекты в IdentityMap и такой хитрый декторатор сможет восстановить объект при фактическом обращении. Но тут придётся внедрить в него DI на модель декорируемого объекта.
Одна небольшая деталь:
public function __construct(Object $class)
{
$this->class = $class;
}
Так будет понятно что мы собираемся декорировать + проверка
Буду некропостером, но не беда 🙂
В принципе подход годный, удручает только одно: т.к. декоратор ни коим образом не наследует от декорируемого класса, то чисто формально он не получает его интерфейсы! Поэтому проверка вроде
if ($decoratedObject instanceof ContainerAware) {
// ... set container or whatever
}
провалится, хотя de facto декорированный объект продолжает выполнять контракт. Такова цена «магии».
Ну, никто не мешает декоратор отнаследовать от декорируемого класса, это частая практика.
class Decorator extends Object { …. }, тогда проверки проходить будут. Главное не забывать, что в декорируемом классе тоже могут быть магические методы.
зачем тогда вообще нужен декоратор, если мы и так получим все свойства и методы родителя? мы так же может расширять и переопределять класс как хотим. Вообщем не понял я вообще этой магии…
Ключевым моментом является то, что нам приходит объект, порождённый где-то в системе на которую мы влиять не можем, соответственно, мы его менять не можем, потому декорируем.
Магия с extends нужна для дружбы с тайпхинтингом и обхода зоны видимости, это скорее костыль.
Ну и сам паттерн во многом костыль, который применяется из-за отсутствие грамотного DI.
А почему бы не использовать наследование? и получается что декоратор должен сожержать реализацию всех методов передаваемого объекта?