Примеси позволяют использовать существующий код при реализации поведения классов, избегая при этом многих проблем множественного наследования. Взаимодействующие друг с другом примеси, объединенные в отдельные программные библиотеки, представляют собой очень мощный инструмент при реализации сложной логики.
Следует отметить, что реализации примесей в PHP существуют как минимум с версии 4.0.1, и в настоящее время присутствуют, чаще всего под именем behavior, в ряде популярных фреймворков, например, в Yii, Symfony, Doctrine, CakePhp, Propel.
Цель статьи — продемонстрировать и сравнить несколько основных подходов к реализации примесей в PHP до версии 5.4.0, базирующихся только лишь на функциях самого языка и не использующих сторонние расширения, как-то, например, функцию runkit_method_copy из PECL runkit.
При сравнении будут использованы следующие критерии:
- имеет ли результат микширования тот же тип, что и сам объект
- может ли одна примесь взаимодействовать с другой
- можно ли проверить, что результат микширования имеет ту или иную примесь
- можно ли добавить примесь к произвольному классу
- можно ли добавить примесь к уже созданному объекту “на лету”
- насколько проста реализация
Способ первый: Magic methods
Способ основан на идее использования магических методов __call, __get, и других: в результат микширования добавляется коллекция примесей, и реализация магических методов выбирает нужную примесь. Каждую примесь можно параметризовать ссылкой на результат, поддерживая таким образом взаимодействие примесей друг с другом.
Пример реализации:
abstract class Mixin { protected $mixedObject = null; public function setObject( MixedObject $object ) { $this->mixedObject = $object; } abstract public function getName(); } class MixedObject { private $mixins = array(); public function addMixin( Mixin $mixin ) { $mixin->setObject( $this ); $this->mixins[$mixin->getName()] = $mixin; } public function hasMixin( $mixinName ) { return array_key_exists( $mixinName, $this->mixins ); } public function __call( $name, $arguments ) { foreach ($this->mixins as $mixin) { if (is_callable( array( $mixin, $name ) )) { return call_user_func_array( array( $mixin, $name ), $arguments ); } } throw new \Exception('Unknown method call.'); } }
Пример использования:
class Foo extends MixedObject { public function objectFunc() { return 'FooName'; } } class Debuggable extends Mixin { public function getName() { return 'Debug'; } public function getDebug() { return sprintf( "%s", $this->mixedObject->objectFunc() ); } } class Loggable extends Mixin { public function getName() { return 'Log'; } public function getLog( $level ) { return $this->mixedObject->hasMixin( 'Debug' ) ? sprintf( "%s %s", $level, $this->mixedObject->getDebug() ) : sprintf( "%s", $level ); } } $foo = new Foo(); $foo->addMixin( new Debuggable() ); $foo->addMixin( new Loggable() ); print $foo->getDebug(); print $foo->getLog( 'info' );
Очевидно, что результат имеет тот же тип, что и сам объект. Также данный подход оставляет возможность примесям общаться как с самим объектом, так и друг с другом, используя ссылку $this->mixedObject и систему уникальных имен.
Плюсы и минусы:
- [+] решение прозрачное и понятное
- [+] можно добавить примесь к уже созданному объекту, можно даже с использованным ранее именем
- [-] результат должен быть унаследован от класса MixedObject, таким образом, для использования микширования необходимо выделение иерархии типов
- [-] условие уникальности имен примесей требует постоянного внимания и здесь, возможно, будет нелишним введение каких-либо конвенций
Способ второй: Object context
Этот способ основан на некоторой особенности переменной $this. А именно:
$this is a reference to the calling object (usually the object to which the method belongs, but possibly another object, if the method is called statically from the context of a secondary object).
Выделенные слова дают возможность такой реализации:
class Foo { public function objectFunc() { return 'FooName'; } } class Debuggable { public function getDebug() { return sprintf( "%s", $this->objectFunc() ); } } class Loggable { public function getLog( $level ) { return is_callable( array( $this, 'getDebug' ) ) ? sprintf( "%s %s", $level, $this->getDebug() ) : sprintf( "%s", $level ); } }
…и использования:
class MixedFoo extends Foo { public function getDebug() { return Debuggable::getDebug(); } public function getLog() { return Loggable::getLog( func_get_arg( 0 ) ); } } $foo = new MixedFoo(); $foo->getDebug(); $foo->getLog( 'info' );
Далее нетрудно автоматизировать генерацию кода класса MixedFoo, последующий eval, создание объекта сгенеренного класса и его возврат, получая в итоге примерно следующее:
$foo = Mixer::Construct( 'Foo', array( 'Debuggable', 'Loggable' ) ); $foo->getDebug(); $foo->getLog( 'info' );
Также можно для каждой примеси сделать отдельный интерфейс и добавить в список implements для генерируемого класса.
interface IMixinDebuggable { public function getDebug(); } ... $foo = Mixer::Construct( 'Foo', array( 'IMixinDebuggable' => 'Debuggable', 'Loggable' ) );
Это возможно, так как результат микширования будет реализовывать эти интерфейсы, и проверка на существование примеси тогда сведется к нативному вызову instanceof:
class Loggable { public function getLog( $level ) { return $this instanceof IMixinDebuggable ? sprintf( "%s %s", $level, $this->getDebug() ) : sprintf( "%s", $level ); } }
Плюсы и минусы:
- [+] нет необходимости наследовать расширяемый объект, таким образом, можно примешивать любые примеси к любым классам
- [+] в отличие от первого способа, “примешанные” методы получаются реализованными непосредственно в результате, поэтому не нужно тратить время на итерирование по коллекции, и код будет работать несколько быстрее
- [-] нет возможности расширить уже созданный объект произвольного класса
- [-] кодогенерация – это же отстой
Заключение
Оба способа позволяют динамически уточнять поведение классов, дополняя их существующей реализацией, и имеют право на применение.
Если вынести результаты в отдельную таблицу:
Magic methods | Object context | |
---|---|---|
имеет ли результат микширования тот же тип, что и сам объект | Да | Да |
может ли одна примесь взаимодействовать с другой | Да | Да |
можно ли проверить, что результат микширования имеет ту или иную примесь | Да | Да |
можно ли добавить примесь к произвольному классу | Нет | Да |
можно ли добавить примесь к уже созданному объекту “на лету” | Да | Нет |
насколько проста реализация | Проста и очевидна | Связана с генерацией кода |
То нетрудно заметить, что недостатки первого способа решаются вторым, равно как и наоборот, а также сделать вывод относительно области применения того или иного способа.
Первый является частью дизайна проекта, и поэтому его область применения – это задачи конструирования сложных бизнес-объектов.
Второй позволяет выделить примеси в отдельные независимые библиотеки и применять их в любых проектах, поэтому его область применения – легковесные задачи специфичные для целого ряда ваших проектов.
Спасибо.
ссылка на оригинал статьи http://habrahabr.ru/company/alawar/blog/186196/
Добавить комментарий