- PVSM.RU - https://www.pvsm.ru -
Начиная с версии 5.4.0, в PHP появится новая конструкция языка — трейты (traits [1]), реализующая возможность использования примеси (mix in [2]). Механизм примесей является еще одним механизмом повторного использования кода и присутствует в том или ином виде в других языках, например, Ruby, Python, Common Lisp, etc.
Примеси позволяют использовать существующий код при реализации поведения классов, избегая при этом многих проблем множественного наследования. Взаимодействующие друг с другом примеси, объединенные в отдельные программные библиотеки, представляют собой очень мощный инструмент при реализации сложной логики.
Следует отметить, что реализации примесей в PHP существуют как минимум [3] с версии 4.0.1, и в настоящее время присутствуют, чаще всего под именем behavior, в ряде популярных фреймворков, например, в Yii, Symfony, Doctrine, CakePhp, Propel.
Цель статьи — продемонстрировать и сравнить несколько основных подходов к реализации примесей в PHP до версии 5.4.0, базирующихся только лишь на функциях самого языка и не использующих сторонние расширения, как-то, например, функцию runkit_method_copy [4] из PECL runkit.
При сравнении будут использованы следующие критерии:
Способ основан на идее использования магических методов __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 и систему уникальных имен.
Плюсы и минусы:
Этот способ основан на некоторой особенности переменной $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 | |
|---|---|---|
| имеет ли результат микширования тот же тип, что и сам объект | Да | Да |
| может ли одна примесь взаимодействовать с другой | Да | Да |
| можно ли проверить, что результат микширования имеет ту или иную примесь | Да | Да |
| можно ли добавить примесь к произвольному классу | Нет | Да |
| можно ли добавить примесь к уже созданному объекту “на лету” | Да | Нет |
| насколько проста реализация | Проста и очевидна | Связана с генерацией кода |
То нетрудно заметить, что недостатки первого способа решаются вторым, равно как и наоборот, а также сделать вывод относительно области применения того или иного способа.
Первый является частью дизайна проекта, и поэтому его область применения – это задачи конструирования сложных бизнес-объектов.
Второй позволяет выделить примеси в отдельные независимые библиотеки и применять их в любых проектах, поэтому его область применения – легковесные задачи специфичные для целого ряда ваших проектов.
Спасибо.
Автор: grelkin
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/38393
Ссылки в тексте:
[1] traits: http://php.net/manual/en/language.oop5.traits.php
[2] mix in: http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D1%81%D1%8C_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)
[3] как минимум: http://www.advogato.org/article/470.html
[4] runkit_method_copy: http://php.net/manual/en/function.runkit-method-copy.php
[5] Источник: http://habrahabr.ru/post/186196/
Нажмите здесь для печати.