Динамические примеси в PHP

в 10:29, , рубрики: php, Блог компании «Alawar Entertainment», проектирование взаимодействия, Проектирование и рефакторинг, метки: ,

Начиная с версии 5.4.0, в PHP появится новая конструкция языка — трейты (traits), реализующая возможность использования примеси (mix in). Механизм примесей является еще одним механизмом повторного использования кода и присутствует в том или ином виде в других языках, например, Ruby, Python, Common Lisp, etc.

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

Следует отметить, что реализации примесей в 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
имеет ли результат микширования тот же тип, что и сам объект Да Да
может ли одна примесь взаимодействовать с другой Да Да
можно ли проверить, что результат микширования имеет ту или иную примесь Да Да
можно ли добавить примесь к произвольному классу Нет Да
можно ли добавить примесь к уже созданному объекту “на лету” Да Нет
насколько проста реализация Проста и очевидна Связана с генерацией кода

То нетрудно заметить, что недостатки первого способа решаются вторым, равно как и наоборот, а также сделать вывод относительно области применения того или иного способа.

Первый является частью дизайна проекта, и поэтому его область применения – это задачи конструирования сложных бизнес-объектов.

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

Спасибо.

Автор: grelkin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js