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

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

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

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

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

    Спасибо.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 7

      +2
      Добавьте, пожалуйста, в статью реализацию с использованием PHP 5.4 и трейтов.
        +8
        Хм, а когда trait в PHP 5.4 стали динамическими примесями?
          +1
          Пожалуй стало бы лучше, если б автор развёл понятия для уменьшения путаницы:
          1) Примеси — для языков со множественным наследованием. В некоторых языках могут задаваться как во время исполнения, так и во время проектирования;
          2) Черты — ответ PHP на просьбы о множественном наследовании. На текущий момент задаются только во время проектирования. Авторами PHP противопоставляется примесям из п.1;
          3) Поведения — по сути, синтаксическо-магический сахар над композицией. Часто — попытка замены наследования композицией.
          +1
          Если интересно посмотреть на динамические примеси, то это технология называется Introduction в мире AOP:

          Introduction: declaring additional methods or fields on behalf of a type. AOP allows you to introduce new interfaces (and a corresponding implementation) to any advised object. For example, you could use an introduction to make a class implement an IsModified interface, to simplify caching. (An introduction is known as an inter-type declaration in the AspectJ community.)


          Эта фишка уже поддерживается в моем АОП-фреймворке для PHP. Если вкратце — она дает возможность к любому классу добавить «на лету» интерфейсы и трейты без изменения исходного кода класса. Если интересно — небольшой материал есть тут: Aspect-Oriented Pointcuts and Advices (последний параграф)
            +3
            Начиная с версии 5.4.0, в PHP появится новая конструкция языка — трейты

            Welcome to the World of Tomorrow!

            image
              0
              Хорошая вещь для тех кто просто из принципа любит использовать каждую фичу языка, и практически бесполезная для всех остальных.
                0
                Если traits и goto появляются, значит это кому-то нужно. А вот кому, для меня тоже загадка.

              Only users with full accounts can post comments. Log in, please.