Знакомимся с аспектно-ориентированным программированием в PHP

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


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

Но все ли так удобно?


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

Почему практически все?


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

Как можно решить данную проблему?

   
Решение этой проблемы было изобретено давно — дополнить существующую модель ООП некоторым расширением, которое позволит описывать такие взаимодействия формально. Было проведено исследование группой инженеров Xerox PARC, в результате которого они предложили новую парадигму - аспектно-ориентированное программирование. Суть идеи проста — позволить программе взглянуть на себя «со стороны» с помощью механизма рефлексии и при надобности — провести изменение конечного кода. Имея возможность изменять конечный код, АОП получает неограниченный доступ к созданию хуков в любом месте кода и к расширению этого кода с помощью советов.
 
Для того, чтобы описать это поведение были предложены следующие термины:

1. Advice — совет. Это действие, которое нужно выполнить. Для утверждения «мыть руки перед едой» советом будет «мыть руки». Как видите, советы описывают вполне реальные действия из реального мира, а значит, могут быть представлены в виде методов, анонимных функций и замыканий. Каждый совет знает, к какому именно месту он относится, поэтому в нем доступна почти вся информация о действии.

2. Joinpoint — точка внедрения. Данный термин определяет конкретное место в программном коде, в которое может быть добавлен совет. В AspectJ, который является первоисточником АОП, доступно большое количество точек внедрения: обращение к методу, выполнение метода, инициализация класса, создание нового объекта, обращение к свойству объекта. Библиотека Go! может работать с вызовами публичных и защищенных методов, как динамических, так и статических, в том числе, и в финальных классах и трейтах; поддерживается перехват защищенных и публичных свойств у объекта. Для примера, точкой внедрения для «мыть руки перед едой» будет «начало еды», или, говоря терминами ООП — выполнение метода «кушать()» у класса «человек».

3. Pointcut — срез точек внедрения. Срез задает некоторое множество точек внедрения, в которых нужно применить совет. В мире АОП это аналог SELECT из SQL. Синтаксис для задания среза точек может быть различным, как и сама реализация, но как правило, это фильтры, которые получают на вход множество точек внедрения, из которых нужно отобрать только подходящие. В библиотеке Go! для задания срезов в основном используются аннотации, но в будущем будет поддержка xml, yaml и с помощью кода. Срез точек может выглядеть так: все публичные методы в классе, все статические методы, имеющие название *init(), все методы с определенной аннотацией и т.д.

Помимо самой сигнатуры, срез определяет еще и относительное место для внедрения совета: перед, после, вокруг точки внедрения. Для нашего случая с мытьем рук срез точек будет определять единственную точку внедрения: «перед выполнением метода человек->кушать()». Если же мы хотим добиться идеальной чистоплотности, то можем определить точку внедрения «перед выполнением методов человек->*()», что позволит нам указать наше желание всегда мыть руки перед любым действием. Однако внимательный читатель может сообразить, что метод «человек->мытьРуки()» тоже попадает под этот срез. Чем это грозит и как этого избежать — останется на самостоятельное изучение, чтобы подогреть интерес.

4. Aspect — основная единица в рамках АОП, которая позволяет собрать воедино срезы точек с теми советами, которые нужно применить. Так как наш случай относится к здоровому образу жизни, то можно назвать его HealthyLiveAspect и внести туда наш срез и несколько советов: мыть руки перед едой и чистить зубы после еды. Давайте составим список того, что мы имеем на текущий момент. Мы имеем класс «человека» с понятными методами, в них нет дополнительной логики, которая смешалась бы с основным кодом метода. Мы имеем отдельный класс аспекта с необходимыми советами. Осталось сделать самую малость — соединить их в одно целое и получить готовый код. Этот процесс называется вплетением.

5. Weaving — процесс вплетения кода советов в исходный код. Этот механизм разбирает исходный код с помощью рефлексии и применяет советы в точках внедрения. Библиотека Go! использует уникальную для PHP технологию Load-Time Weaving, которая позволяет отследить момент загрузки класса и изменить этот код до его анализа парсером PHP. Это дает возможность динамически изменять код класса, без изменений в исходном коде со стороны разработчиков. Работает это все следующим образом: в начале программы мы инициализируем ядро АОП, добавляя туда наши аспекты, после этого передаем управление основной программе. При создании объекта человека сработает автоматическая загрузка класса, которая определит нужное имя файла и попытается его загрузить. В этот момент вызов будет перехвачен ядром, далее будет выполнен статический анализ кода и проверка текущих аспектов. Ядро обнаружит, что в коде есть класс «человек» и что нужно внедрить советы в этот класс, поэтому трансформеры кода в ядре изменят оригинальное имя класса, создадут прокси-класс с оригинальным названием класса и переопределенным методом «кушать», и передадут список советов для данной точки. Дальше парсер PHP разбирает этот код и загружает его в память, при этом по имени исходного класса уже будет находиться класс-декоратор, поэтому обычный вызов метода «кушать» будет обернут в точку соединения с подключенными советами.

Лучше один раз увидеть, чем сто раз услышать.


Давайте опишем все то, что мы обсудили с помощью кода. Реализуем первым делом класс человека-разумного, который будет уметь кушать, спать, работать, а также мыть руки и чистить зубы. Метод __clone пока делать не будем :)

/**
 * Human class example
 */
class Human
{

    /**
     * Eat something
     */
    public function eat()
    {
        echo "Eating...", PHP_EOL;
    }

    /**
     * Clean the teeth
     */
    public function cleanTeeth()
    {
        echo "Cleaning teeth...", PHP_EOL;
    }

    /**
     * Washing up
     */
    public function washUp()
    {
        echo "Washing up...", PHP_EOL;
    }

    /**
     * Working
     */
    public function work()
    {
        echo "Working...", PHP_EOL;
    }

    /**
     * Go to sleep
     */
    public function sleep()
    {
        echo "Go to sleep...", PHP_EOL;
    }
}


На текущий момент все написано по фэн-шую: каждый метод занимается только своей работой, ничего лишнего в методах нет. Но у нас нет логики мытья рук перед едой, а также чистки зубов после еды и перед сном:


/**
 * Human class example
 */
class Human
{

    /**
     * Eat something
     */
    public function eat()
    {
        $this->washUp();
        echo "Eating...", PHP_EOL;
        $this->cleanTeeth();
    }

    // ....

    /**
     * Go to sleep
     */
    public function sleep()
    {
        $this->cleanTeeth();
        echo "Go to sleep...", PHP_EOL;
    }
}


Типичный программист без особых раздумий сделает так, как приведено выше: внесет вызов нужных методов в код методов «кушать» и «спать», нарушив принцип единственной ответственности каждого из этих методов. Благо, условия простые, можно разобраться с тем, почему это было сделано именно здесь. В реальной же жизни все куда печальнее: как часто вам попадается код, который делает разные вещи в одном месте и нет никакого намека на то, что этот кусок кода должен быть тут? Это и есть та самая знаменитая метрика: WTF/строчку кода. Думаю, у каждого есть такие примеры в коде ).

Следующая категория программистов ощущает подвох в том, что нужно изменять логику метода и они идут в другую крайность: делают новые методы в классе, которые объединяют в себе логику нескольких методов. А вам знакомы методы вида «мытьРукиAndКушать()»?

С одной стороны, основные методы перестанут содержать эту логику, но наш класс начнет раздуваться, появится возможность вызвать метод «кушать()» напрямую, интерфейс класса будет завален ненужными методами и количество ошибок в коде начнет расти. Опять не вариант.

Другие варианты я уже рассматривал в своей статье на хабре по рефакторингу с АОП, поэтому перейду сразу к АОП.

Аспектно-ориентированный подход


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

Основоположник аспектно-ориентированного программирования (АОП) Грегор Кикзалес в интервью поделился своим видением сущности АОП: "… АОП по существу – очередной этап в развитии механизмов структурирования. Сегодня понятно, что объекты не заменяют процедуры, а являются только способом создания механизма структурирования более высокого уровня. И аспекты тоже не заменяют объекты; они лишь предоставляют еще одну разновидность структурирования."

Давайте попробуем описать наш случай с позиции аспектно-ориентированной парадигмы. Для этого возьмем «чистый» класс «человека» с нашими «чистыми» методами и опишем советы для нужных методов с помощью класса аспекта: 

namespace Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;

/**
 * Healthy live aspect
 */
class HealthyLiveAspect implements Aspect
{
    /**
     * Pointcut for eat method
     *
     * @Pointcut("execution(public Human->eat(*))")
     */
    protected function humanEat() {}

    /**
     * Method that recommends to wash up before eating
     *
     * @param MethodInvocation $invocation Invocation
     * @Before(pointcut="humanEat()") // Short pointcut name
     */
    protected function washUpBeforeEat(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->washUp();
    }

    /**
     * Method that recommends to clean teeth after eating
     *
     * @param MethodInvocation $invocation Invocation
     * @After(pointcut="Aspect\HealthyLiveAspect->humanEat()") // Full-qualified pointcut name
     */
    protected function cleanTeethAfterEating(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->cleanTeeth();
    }

    /**
     * Method that recommends to clean teeth before going to sleep
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("execution(public Human->sleep())")
     */
    protected function cleanTeethBeforeSleep(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->cleanTeeth();
    }
}


Думаю, этот код достаточно понятный сам по себе, но лучше добавить некоторые комментарии.

Во-первых, мы определили срез точек — пустой метод humanEat(), помеченный с помощью специальной аннотации @Pointcut("execution(public Human->eat(*))"). В принципе, можно было не создавать отдельный срез, а указать его перед каждым советом отдельно, но так как у нас есть несколько советов для этого среза, то можно вынести его в отдельное определение. Сам метод и его код при определении среза не используются и служат лишь для указания и идентификации среза в самом аспекте.

Во-вторых, мы описали сами советы в виде методов аспекта, указав с помощью аннотации @Before и @After конкретное место внедрения для среза. Можно сразу задавать срезы в аннотации, как это сделано в методе cleanTeethBeforeSleep: @Before("execution(public Human->sleep())"). Каждый совет получает на вход нужную информацию о точке выполнения благодаря объекту MethodInvocation, содержащему в себе вызываемый объект (класс для статического метода), аргументы вызываемого метода, а также информацию о точке в программе (рефлексия метода). Аналогичная информация может быть получена и для обращения к свойствам объектов.

Теперь запустим наш код:
include isset($_GET['original']) ? './autoload.php' : './autoload_aspect.php';

// Test case with human
$man = new Human();
echo "Want to eat something, let's have a breakfast!", PHP_EOL;
$man->eat();
echo "I should work to earn some money", PHP_EOL;
$man->work();
echo "It was a nice day, go to bed", PHP_EOL;
$man->sleep();


Если мы запустим данный код в браузере, то можно будеть увидеть следующий вывод:

Want to eat something, let's have a breakfast!
Washing up...
Eating...
Cleaning teeth...
I should work to earn some money
Working...
It was a nice day, go to bed
Cleaning teeth...
Go to sleep...


Для интересующихся прочими файлами:
autoload.php
/**
 * Show all errors in code
 */
ini_set('display_errors', true);

/**
 * Register PSR-0 autoloader for our code, any components can be used here
 */
spl_autoload_register(function($originalClassName) {
    $className = ltrim($originalClassName, '\\');
    $fileName  = '';
    $namespace = '';
    if ($lastNsPos = strripos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';

    $resolvedFileName = stream_resolve_include_path($fileName);
    if ($resolvedFileName) {
        require_once $resolvedFileName;
    }
    return (bool) $resolvedFileName;
});

ob_start(function($content) {
    return str_replace(PHP_EOL, "<br>" . PHP_EOL, $content);
});

autoload_aspect.php
include '../src/Go/Core/AspectKernel.php';
include 'DemoAspectKernel.php';

// Initialize demo aspect container
DemoAspectKernel::getInstance()->init(array(
    // Configuration for autoload namespaces
    'autoload' => array(
        'Go'               => realpath(__DIR__ . '/../src'),
        'TokenReflection'  => realpath(__DIR__ . '/../vendor/andrewsville/php-token-reflection/'),
        'Doctrine\\Common' => realpath(__DIR__ . '/../vendor/doctrine/common/lib/')
    ),
    // Default application directory
    'appDir' => __DIR__ . '/../demos',
    // Cache directory for Go! generated classes
    'cacheDir' => __DIR__ . '/cache',
    // Include paths for aspect weaving
    'includePaths' => array(),
    'debug' => true
));


DemoAspectKernel.php
use Aspect\HealthyLiveAspect;

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

/**
 * Demo Aspect Kernel class
 */
class DemoAspectKernel extends AspectKernel
{

    /**
     * Returns the path to the application autoloader file, typical autoload.php
     *
     * @return string
     */
    protected function getApplicationLoaderPath()
    {
        return __DIR__ . '/autoload.php';
    }

    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     *
     * @return void
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new HealthyLiveAspect());
    }
}



Что же у нас получилось? Во-первых, логика самих методов в классе «человека» не содержит никакого мусора. Уже отлично, потому что их будет легче поддерживать. Во-вторых, нет никаких методов, дублирующих основной функционал методов класса. В-третьих, логика представлена в виде понятных советов в аспекте, потому что советы имеют реальный смысл, а это означает, что мы только что сделали декомпозицию по функциональности и сделали наше приложение более структурированным! Более того, сам класс аспекта описывает вполне понятную цель — здоровый образ жизни. Теперь все на своих местах и мы можем легко изменять как дополнительную логику, так и работать с чистой логикой самих методов.

Надеюсь, этот нехитрый пример поможет вам лучше понять концепцию аспектной парадигмы и попробовать свои силы в описании аспектной составляющей процессов в реальном мире. Для интересующихся — этот пример доступен в исходном коде библиотеки в папке demos/life.php, его можно запустить и изучить )

Ссылки:
  1. Official site http://go.aopphp.com
  2. Source code https://github.com/lisachenko/go-aop-php

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

Интересна ли вам тема аспектно-ориентированного программирования?

Что вы думаете об АОП в PHP?

Поделиться публикацией

Комментарии 55

    0
    Без комментариев не будет работать?
      0
      Будет, но на текущем уровне этот вариант задания срезов точек более предпочтительный, так как движок будет разбирать скоро грамматику срезов, выполняя построение AST. Задавать срезы руками с помощью классов будет значительно сложнее и менее читабельно. Но планы есть и на ваш случай ) Спрос рождает предложение.
        0
        Вот ещё вопрос. Аспект же может переопределить значения аргументов вызываемой функции или её результат работы. Если так, то возникает проблема правильной очередности работы аспектов, если их несколько на метод. Или я заблуждаюсь?
          0
          Аспекты, как и события, следует использовать в тех случаях, когда очередность не важна.

          Например, если вызовы метода логируются, параметры проходят некоторую валидацию, плюс результат кешируется, то порядок выполнения данных действий не важен.

          А за аспект, который увеличивает параметр на единицу, надо убивать.
            +1
            Может быть аспект проверяющий доступ или кэш. Аргументы не трогает, но от очередности исполнения зависит в целом работа системы.
              0
              Отличный аргумент, однозначно в список собственных рекомендаций!
            0
            Аспект позволяет сейчас изменить только те значения, которые были переданы по ссылке. Но можно добавить и возможность менять все аргументы, это дело одного сеттера ) Но нужно ли это сейчас? Думаю, что пока рано. А вот результат вызова метода изменять можно, так работает самый мощный тип совета — Around, с помощью которого делается кэширование и многое другое. Если советов несколько, они будут применяться в определенном порядке.
            И да, советы имеют сортировку, которой можно управлять, в документации есть пример.
              0
              Лучше согласиться с комментатором выше. Для управления сортировкой нужно владеть полной информацией об аспектах или иметь заданный архитектурой порядок, например как у событий в DOM.
        +9
        Типичный программист без особых раздумий сделает так, как приведено выше: внесет вызов нужных методов в код методов «кушать» и «спать», нарушив принцип единственной ответственности каждого из этих методов. Благо, условия простые, можно разобраться с тем, почему это было сделано именно здесь.

        Вообще с помощью классов описываются не только состояния но и процессы.
        И в данном случае в классе Human не было бы метода eat. Был бы метод принимать пищу. А вот процесс кушать, был вынесен в отдельный класс:
        class Eat { 
           function __construct(Human $human) { 
               //...
           }
           public function eat(Food $food) {
                $this->human->washUp();
                echo "Eating...", PHP_EOL;
                $this->human->cleanTeeth();
           }
        }
        

        Или же с помощью контекста:
        class HumanContext { 
           public function eat(Human $human, Food $food) {
                $human->washUp();
                echo "Eating...", PHP_EOL;
                $human->cleanTeeth();
           }
        }
        


        В случае же с аспектами, вы словно затыкаете костылями не верную архитектуру проекта. И когда изучаешь код, ты получаешь очередной дополнительный уровень сложности в связи с аспектами. Рефакторинг начинает приносить ТОННЫ геморроя.
        Плюс ко всему к этому, IDE для поддержки аспектов нет.

        Выражаю сейчас своё большое ИМХО, которое вообще не претендует на объективность. По мне, так АОП в таком виде огромное зло, которое ничего хорошего не принесет.
          +4
          Насчет поддержки в IDE — наверное, вы не заметили inline-докблок в теле советов. Моя IDE прекрасно это понимает, давая мне возможность легко делать рефакторинг кода, находить места использования, использовать автодополнение и даже создавать новые методы в классе «человек», при этом находясь внутри класса аспекта.
          Но определить точки, где внедряются аспекты — это да, IDE не подскажет без плагина. Тут с вами соглашусь.
          Насчет всего остального — я пытаюсь поделиться с сообществом своими знаниями и решениями. Как и все новое, оно будет и должно вызывать недоверие. Поэтому я очень ценю любые конструктивные замечания и предложения, чтобы попытаться о них подумать. А вот приводить альтернативы в виде шаблонов OOP не нужно, их и так все знают, в сети полно материала на эту тему.
            +2
            Можно узнать, а что у вас за IDE?

            А вот приводить альтернативы в виде шаблонов OOP не нужно, их и так все знают, в сети полно материала на эту тему.

            Вы сами же себе противоречите
            Типичный программист без особых раздумий сделает так, как приведено выше: внесет вызов нужных методов в код методов «кушать» и «спать», нарушив принцип единственной ответственности каждого из этих методов.


            Ну ладно, соглашусь, что с шаблонами я не совсем к месту, но все же, в чем тогда прелесть АОП?
            Рефакторинг, когда IDE не подходит под парадигму задача не тривиальная.
              0
              IDE — старый добрый phpStorm, но строчные докблоки должны поддерживаться любой нормальной IDE, поэтому этот код вполне удобно рефакторить.

              Фраза насчет «типичного» программиста — это легкая гипербола, конечно. Понятно, что мы люди взрослые и делаем сложные вещи правильно. Но пытаться объяснить АОП по-другому никак не получается, поэтому пришлось делать акценты на проблемах кода с ООП.

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

                Хм, наврное мы друг друга не совсем поняли. И тут моя вина, но я говорил про такую конструкцию, которая не понимает IDE (в том числе и PHPStrom). Соответственно про сложность рефакторинга я говорил, смотря на данную конструкцию. Но об этом вы уже меня предупредили выше.
                /**
                 * @Before("execution(public Human->sleep())")
                 */
                
                  0
                  Ах, вы про это. Да, здесь помощи от IDE не получишь, как и в регулярных выражениях. А срез, по-сути, это и есть регулярное выражение для кода. Но внутри можно спокойно задавать срезы ручками с помощью кода, связывая с замыканиями в нужных точках. Это для advanced-пользователей. Позже будет конфиг аспектов в виде xml с валидной схемой, а также yml.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Беглый поиск в гугле выдает www.aspectc.org/. Конечно, это реализовано в виде препроцессора C++-кода.
            • НЛО прилетело и опубликовало эту надпись здесь
              +2
              Думаю, что АОП в C++ можно сделать еще следующим образом: берем таблицу методов класса, подменяем указатель на метод своим указателем, а потом просто вызываем оригинальный метод, передавая ему управление. Такие вещи можно делать даже в рантайме, будет работать очень быстро. Аналогичный подход используется для эксплуатации уязвимостей переполнения буфера в программе, так что однозначно — можно.
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Такой подход намного проще реализовать на ObjectiveC :) Там, по идее, достаточно вариации на тему NSProxy.
                +4
                Библиотека Go! использует уникальную для PHP технологию Load-Time Weaving, которая позволяет отследить момент загрузки класса и изменить этот код до его анализа парсером PHP.

                Уникальную в том смысле, что другие библиотеки не используют препроцессинг, или в том смысле, что в других средах (не-PHP) такой возможности нет?
                  +1
                  Вы меня подловили ) Данную фразу правильней трактовать так: «больше нет библиотек на PHP, которые делают Load-Time-Weaving». Т.е. относится только к PHP. В других языках препроцессинг конечно же есть и существует там очень давно.
                  +1
                  Производительность тестировали? Особенно интересует сравнительное тестирование с варианте с включенным кэшированием опкода. У меня серьезные подозрения, что все будет плохо.

                  Если не совсем ясно, с чем сравнивать — могу предложить сравнить с использованием вместо аспектов замыканий и ключевого слова yield (в php 5.5 alpha) или явной передачей Callable $yieldFn в php 5.4.
                    0
                    Для моего сферического примера в вакууме для этой статьи получились следующие цифры:

                    Вариант | время без опкод-кэшера/с опкод-кэшером (apc.stat=off)

                    Оригинальный код (без логики) 0.2-0.3мс/0.19-0.25мс
                    Хардкод логики в методах 0.22-0.34мс/0.20-0.25мс
                    Оригинальный код с вызовами callable замыканий 0.5-0.7мс/0.25-0.37мс
                    Оригинальный код c АОП (debug=false) 7-8мс/1,6-2.2мс

                    Это само время выполнения кода, сюда не входит автозагрузка классов, инициализация кэша и прочее.
                    Лучше, конечно, делать цикл вызовов одинаковых методов с помощью разных способов (callable, call_user_func, __call, AOP и т.д), чтобы получить более правильные цифры, они будут отличаться от единичных вызовов и разница уже будет не 6-8 раз, а меньше. АОП внутри использует кэширование invocation-ов, что позволяет запускать даже огромные проекты типа ZF2, полностью заменив ВЕСЬ код аспектным (см. примеры на гитхабе).
                    Более того, есть возможность сделать это еще быстрее (хотя уже сейчас скорость моей библиотеки сопоставима со скоростью работы экстеншена PHP-AOP, а иногда и превышает ее из-за технической реализации)
                      0
                      АОП внутри использует кэширование invocation-ов

                      А можете прогнать все 4 варианта (с опкод-кэшером) через ab -n3000 -c30 и опубликовать RPS?
                    +9
                    Интересно, одному мне не нравится идея программирования внутри комментария?
                      0
                      Ага, как-то это от лукавого… Комментарии нужны для комментариев, а если язык не поддерживает изначально парадигму, то не нужно лепить ее поверх с помощью библиотек. Такой код явно будет уступать по производительности нативному, усложнит общее восприятие, да и вообще не факт, что будет с этого профит. Имхо.
                        +4
                        Профит с этого есть. Мы в свое время использовали аспектный подход в Джаве и он показал себя удобным для того, чтобы не влезая в код бизнес логики, навесить мониторинг производительности, логгирование входящих параметров, кэширование и прочие сервисные вещи. Очень было удобно и как раз очень хорошо сегрегировался бизнес код от каких-то инструментальных вещей.
                        0
                        Если вы читаете php.internals, то обнаружите, что с вами солидарно очень много разработчиков. Но есть необходимость в метаданных, поэтому и возникают разные RFC: annotations, annotations-in-docblock.
                        Популярные фреймворки используют свои парсеры для анализа аннотаций в док-блоках и позволяют управлять проектом «in one place» — сам код и конфиг к нему находится в одном месте, что очень удобно в некоторых случаях. Посмотрите на Symfony2, Doctrine2, FLOW
                        0
                        Долго не мог понять, почему в статье упомянут PHP, а потом говорится про библиотеку Go! Сразу с языком Go про ассоциировалось. Я так понимаю, это какая-то библиотека, которую вы разрабатываете? Вот нашел, это не она — github.com/lisachenko/go-aop-php?
                          0
                          В конце статьи именно эта ссылка и указана. Исторически так сложилось, что в момент закладки фундамента этой библиотеки я еще не знал про язык Go, он еще не был популярен так, как сейчас.

                          А название было дано как внутренний мотиватор — «давай, делай!». Потому что написание этой библиотеки — сплошная борьба со словом «нельзя»: нельзя подменить класс в PHP проксей (сохранив старое название класса), нельзя перехватить метод в финальном классе (потому что нельзя сделать extends), нельзя перехватить статический метод (потому что в коде они вызываются напрямую, не используя возможные прокси), нельзя сохранить LSB и решить проблему scope при перехвате статического метода, нельзя перехватить обращение к публичному свойству объекта, нельзя сделать прокси для трейта, нельзя перехватить метод в трейте, нельзя изменить код класса так, чтобы xdebug по-прежнему работал с основным классом, нельзя динамически изменить код класса при этом сохранив возможность использовать опкод-кэшер, нельзя сериализовать замыкания и прочие технические проблемы.
                            0
                            Помнится была у меня мысль попробовать скрутить АОП и PHP, но даже малая часть того, что вы перечислили, привела к мысли, что без статической кодогенерации или расширений языка не обойтись и мысль умерла. Респект!
                              +1
                              Тут как говорится: «терпение и труд — все перетрут» ) Спасибо за оценку, это меня очень вдохновляет.
                              Как раз в текущую минуту рождается еще одна часть, которая очень нужна, но которую непросто сделать — лексер и грамматика для срезов. Это позволит разбирать любые правильные конструкции в деревья и проверять их, подсказывая пользователю ошибки в синтаксисе при разборе, а также позволит использовать логические объединения срезов с помощью AND, OR, NOT.
                              Еще нащупал почву, как делать runtimе-проверки (сейчас только статические на этапе вплетения аспектов). Это позволит определять замыкание, которое будет само принимать решение, выполнять совет или нет в зависимости от свойств объекта, или переданных ему параметров.
                          0
                          Ссылка для JS-программистов: joose.it.
                            0
                            Жесть!
                            В языках поддерживающих метаданные, такие как данные рефлекции всё куда понятнее выглядит.
                              0
                              Это и есть та самая знаменитая метрика: WTF/строчку кода.

                              Интересно, насколько больше единицы у нас?
                                0
                                Я такие штуки нахожу постоянно в любом коде, включая и свой. Хороший показатель — не больше пары WTF/10000 строк кода.
                                0
                                Вообще говоря, тема интересная, но ведь это страшно ухудшает читаемость кода. Частично согласен с комментарием выше в том, что это похоже на сложную систему костылей и подпорок. Разбирая код, с учётом DI, наследования и прочих штучек ООП, не всегда поймёшь, откуда что берётся, а тут ещё и такое: зовёшь конкретно один метод eat(), а выполняются два. И попробуй потом найти, почему так.
                                Короче говоря, писать в таком ключе — интересно, а читать и отлаживать код — закат солнца вручную =)
                                  0
                                  Не совсем согласен. Часто ли вы отлаживаете код кэширования внутри метода, или логирования? Как правило, отладка начинается с установки точки останова внутри метода, отсеивая вызовы кэширования, логирования и прочие как ненужные. Когда отлаживается код — отлаживается работа конкретного метода, а потом уже всей системы в целом. Читать код с аспектами значительно проще, потому что весь мусор из методов и классов вычищается в отдельный класс-аспект и применяется по всему коду. Достаточно знать, как вплетаются аспекты в конечный код, после этого наступает озарение и понимание сути всех вещей.
                                  Но без озарения для новичков это будет довольно весело и интересно ) Сумерки. Кодинг. Рассвет.
                                    0
                                    Может, есть у кого идеи, как это сделать понятно? Что мне нужно дать разработчику, чтобы он знал о содержимом? В принципе, при отладке доступны объекты точки доступа, в них видны сами советы.
                                      0
                                      Сильно похоже, что никак. Это из той же оперы, что и вызов функций/методов по имени (типа $func()): писать удобно и интересно, а разбираться потом — не очень.
                                    0
                                    Спасибо, весьма познавательно, вовремя и доходчиво. На данный момент AOP для меня одна из горячих тем. В проекте над которым сейчас работаем думаю он был бы весьма полезен, во всяком случаи разгрузил бы кучу вызовов методов в сложной логике проекта.
                                      0
                                      Очень интересно было бы получить фидбек, если вы будете это использовать. Пока у меня несколько положительных отзывов о реальном использовании: в одном случае добавлено кэширование в проекте с упрощением кода сервисов, в другом — сделано логирование всех действий в админке Symfony2 на базе SonataAdmin.
                                        0
                                        Конечно. Попробуем, отпишемся.
                                          +1
                                          Отличное завершение этой ветки ) Очень приятно видеть, что моя работа может пригодиться в реальных проектах. Если будут вопросы — связывайтесь со мной, с удовольствием помогу.
                                      +2
                                      А по-моему поддержка кода становится только сложнее:
                                      — если добавлять аннотации — их всё равно нужно добавлять, если не добавлять — всё становится ещё менее очевидно
                                      — на препроцессинг кода тратятся такты процессора
                                      — смотришь на метод «кушать», но можешь не догадываться, что там будет вызвано «мытьё рук» — теряется читабельность, усложняется отладка
                                      — код расползается на объекты/аспекты, что тоже усложняет поддержку
                                      — нужны дополнительные библиотеки

                                      Если нужен такой функционал — скройте методы, и добавьте __call(), в котором с помощью switch/case навешивайте дополнительные обработчики на вызовы нужных методов.

                                      Это мое личное мнение, могу быть не прав.
                                        +1
                                        Насчёт вашего метода реализации такого функциональности не согласен. Прелесть аспектов как раз в том, что они не требуют изменения исходного кода аспектируемого (термин мой :) класса. Ладно если код свой и между проектами не шарится. А если «ещё вчера» понадобилось «внезапно» логировать или кэшировать вызовы методов какой-то сторонней библиотеки? Форкать её, получая весь геморой с поддержкой форка? Писать враппер/прокси и перелопачивать весь код, заменяя вызовы, рискуя где-то пропустить?

                                        Тут можно спорить о реализации (аннотации/что-то другое, удобная/неудобная, сильно тормозит/не сильно), но сам принцип вроде хорош, если не пихать его везде где есть возможность, а четко понимать области его применения и последствия. Честно говоря, пример с человеком мне кажется не очень удачным, поскольку вызовы метода объекта вызывают его же методы — это хорошо, что методы тут чистые, не изменяют свойства объекта, что для ООП вообще не характерно. А так, да, действительно, отладка может в ад превратиться, особенно если аспекты ещё и параметры вызова меняют. Но вот для ортогональных логике аспектируемого класса вещей — то же логирование, кэширование, генерация или подсчёт событий, отладочный вывод, в общем какие-то сервисные вещи, АОП по-моему чуть ли не идеал (при хорошей реализации).
                                          0
                                          Пожалуй, соглашусь, область применения несколько уже, чем описана в статье, и применять нужно очень осторожно.
                                            0
                                            Вижу, вы уловили и понимаете суть вещей в АОП. Это хорошо! Потому что пример в статье действительно не является рекомендацией делать так, как там описано. Пример был выбран специально для упрощения понимания того, как и что происходит в аспектной парадигме, как это описывается и что получается. Уровень — начальный, ознакомительный.
                                            В реальной же жизни — все проще и банальнее, выносим в аспекты только то, что мусорит в коде — логирование, кэширование, авторизацию. Для этих вещей лучше ничего не придумаешь.
                                            Если пойти еще дальше, то на верхнем уровне будут такие вещи как Autowire, когда все классы сами укладываются при статической инициализации в контейнер и инжектятся в виде сервисов в другие сервисы (пример Google.Guice), реализация паттернов в АОП (те же события можно генерировать в заданной точке и обрабатывать в традиционном понимании с помощью диспетчеров) и много-много всего интересного.
                                              0
                                              Интересовался темой на примере Java, но, как писал выше, реализация в PHP казалась неоправданно трудной. Мысль перехватить autoload в голову не пришла.

                                              А пример можно было сделать в той же предметной области, но введя ещё один объект, например, свет в ванной комнате. Чтобы не мусорить включением/выключением в основном коде, когда руки моешь или зубы чистишь, а автоматический выключатель на любые действия в ванной повесить :) Хотя и так нормально получилось.
                                          0
                                          Но похожих результатов можно добиться с помощью событий?
                                            0
                                            Да, можно, заранее делая программу сложнее, потому что вам придется рефакторить ваш код, добавляя логику обработки события. Более того, события будут работать медленней АОП )
                                            В прошлой своей статье я как раз приводил сравнение с событиями, почитайте на хабре.
                                              0
                                              Да, почитаю. Спасибо.
                                                0
                                                Собственно, код в любых случаях придётся рефакторить. Так как логика исполнения переехала из метода — в комментарии к методу. А ведь самая вкусная часть в АОП — это как раз «срезы».
                                              0
                                              И эти люди запрещают мне ковырять в носу использовать global

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

                                              Самое читаемое