Все верно подмечено, что вызов даже магического метода будет быстрее если он отдаст данные из кэша. Это же справедливо и для аспектов, только работает еще быстрее, потому что там нет вообще этих магических методов, только прямые вызовы.
На самом деле гораздо больше неприятностей доставляет тот факт, что использование прокси-классов не дает возможности использовать в методах указание ожидаемых интерфейсов и классов.
Да, помимо завязки на контейнер симфони и ее архитектуру, есть еще несколько отличий. Если попробовать перехватить статический метод — JMSAopBundle отдыхает, перехватить метод в финальном классе — опять отдыхает. Пишем явно (new Class())->methodName() — опять отдыхает. У меня будет еще Introduction и перехват финальных методов, чем JMSAopBundle еще не скоро обзаведется.
Однако создание декораторов и подмена их в классе весьма неплохое решение и для работы в Symfony2 с АОП я его рекомендую больше, чем Go! Но вот не все он умеет, далеко не все…
Уточнение: не в 10 раз, потому что если бы не было вынесенного кода в совете, то он бы был в самом методе, точнее, повсюду в методах в разных формах и вариациях. По-факту, при одинаковом функционале в коде совета и в коде метода получается пропорция 4:1-5:1, что весьма неплохо.
Тут огорчу любителей паттерна «Прокси» — у них этот же код работает еще медленней, чем код с аспектом за счет использования __call(), func_get_args() и call_user_func_array(). Но об этом никто не вспоминает при сравнении с АОП, а ведь надо сравнивать с альтернативой аналогичного кода.
Идем дальше, берем реальные цифры. Вызов одного метода без аспекта, но с такой же логикой — 10 мкс, с аспектом — 50 мкс. В случае с хайлоадом это очень и очень важно, а если вы работаете над бизнес-приложениями, как я, то для вас не будут новинкой методы, работающие и несколько минут, но для которых очень важно иметь читаемый и удобный код, который постоянно нужно править.
И последний довод — аспекты используются для тех методов, которые вызываются редко, но они очень важны, оверхед в пару десятков мкс на них даже не заметен. И понятно, что указывать все методы всех классов и логировать все, получая замедление в 5 раз — выстрел в себе в ногу.
Все нужно использовать с умом и правильно, как и любой другой инструмент.
В настоящий момент — еще нет, но само решение к продакшену готово.
Поэтому сразу предупреждение для всех — разворачивайте код с аспектами параллельно, а не вместо основного кода.
Фишка АОП в том, что вы не меняете код приложения, поэтому можно написать код и включать аспекты только при наличии волшебного параметра в запросе — это делается запросто. Нет параметра в запросе — не используем аспекты вообще, никаких изменений в логике приложения. Есть наш заветный ключик — переключаемся именно для нашего запроса на код с аспектами. После того, как проверите, что все ок — убираем наш ключик и переходим на аспекты.
Само вплетение аспектов проводится ровно один раз через load-time рефлексию, оверхед не измерял, потому что он не имеет особого значения в режиме нормальной работы приложения. Код с вплетенными аспектами кладется в кэш и дальше вызываются сразу нужные методы с нужными обработчиками. Никакой runtime-проверки на различные условия не выполняется. Все советы (Closure) сериализуются в самом коде класса и дальше могут быть закэшированы опкод-кэшером.
Выполнение методов с советами максимально оптимизировано и сопоставимо со скоростью выполнения метода при добавлении аналогичного совета для метода на уровне С, к примеру, с помощью экстеншена PHP-AOP.
Замерил скорость выполнения кода с советами в Go! и экстеншена PHP-AOP. Тест: 10000 итераций, один совет, в котором мы выводим на экран перед вызовом метода класса его имя, имя метода и аргументы. Результат:
Экстеншен: 428-604 мс
Моя либа: 502-710 мс
Без советов: 50-60мс
Можно склонировать библиотеку к себе, поставить зависимости и натравить веб-сервер на папку demos. В ней как раз лежит пример работы простого класса с вплетенными аспектами и кэшированием. Как говорится, лучше один раз увидеть вживую, чем сто раз услышать, а еще можно залезть туда с XDebug-ом.
Именно так будет в скором времени реализована поддержка Introduction, как раз с помощью трейтов. Можно будет указать: добавь пожалуйста указанный интерфейс к следующим классам, а также, добавь еще и реализацию интерфейса с помощью трейта такого-то.
Посмотрел на AspectPHP, странно, что он мне не попался раньше.
Да, идея аналогичная — делать load-time рефлексию кода, но проанализировав ее, я понял, что скорость работы конечного кода (с вплетенными аспектами) тут явно не учитывалась, тогда как Go! и создавался вокруг этой цели. Отсюда я могу сделать предположение, что попытка использовать в боевом режиме AspectPHP на большом количестве классов — будет обречена на провал из-за низкой скорости конечного кода.
Помимо этого, AspectPHP не учитывает необходимость корректной работы с опкод-кэшером, поэтому будут проблемы с этим в боевом режиме. Ну и еще Go! умеет перехватывать обращения к публичным и защищенным свойствам объектов (с ограничениями, правда), а также умеет работать с метаинформацией классов в load-time благодаря интеграции с Doctrine2. Скоро еще будет Introduction для PHP 5.4 с использованием трейтов.
Ожидал этого вопроса. Вопрос на самом деле очень сложный в технической реализации и объяснении.
Но попробую объяснить. Когда перехватываются динамические методы — там все просто, мы всегда знаем объект благодаря $this. Поэтому нам не нужно указывать scope.
Со статическими методами все намного сложнее — нужно переопределить статический метод, после этого вызвать оригинальный метод через Reflection. А в PHP сейчас есть сложность с тем, чтобы указать scope при вызове статического метода через рефлексию. Вот поэтому и ломается LSB. Только в 5.4 можно создать замыкание в дочернем классе, которое форвардит запрос к родительскому методу и сменить ему scope на корректный через метод bindTo()
Да, конечно, нужно просто при формировании ключа для кэширования добавить еще сами параметры метода, которые доступны через вызов $invocation->getArguments():
По-сути, реализация этой библиотеки и представляет собой портированную версию Spring в PHP. Изучение Spring и AspectJ дало мне много полезной информации и пищи к размышлению. Я сейчас работаю над стандартизацией интерфейсов для АОП в PHP, они будут совместимы с аналогичными в Java: aopalliance/php. Возможно, удастся добиться совместимости между разными фреймворками и библиотеками в PHP как в Java.
Сами аспекты описываются классами, как в Spring-е и используются аннотации для задания поинткатов, однако есть возможность создавать Advisor-ы сразу в коде. Каждый аспект разбивается на серию независимых советников, которые регистрируются отдельно.
static $memoryCache — так и задумывалось, потому что один аспект связывается со всеми классами и более того, аспект — это синглтон, он регистрироваться должен только один раз.
Нет, наоборот, не торопился, у меня были все праздники на написание статьи, поэтому писал потихоньку, но хочется оставить интригу для будущих статей с примерами и рефакторингом. Это только вводная часть. Обязательно учту ваше пожелание на будущее.
На самом деле гораздо больше неприятностей доставляет тот факт, что использование прокси-классов не дает возможности использовать в методах указание ожидаемых интерфейсов и классов.
(new Class())->methodName()
— опять отдыхает. У меня будет еще Introduction и перехват финальных методов, чем JMSAopBundle еще не скоро обзаведется.Однако создание декораторов и подмена их в классе весьма неплохое решение и для работы в Symfony2 с АОП я его рекомендую больше, чем Go! Но вот не все он умеет, далеко не все…
Тут огорчу любителей паттерна «Прокси» — у них этот же код работает еще медленней, чем код с аспектом за счет использования __call(), func_get_args() и call_user_func_array(). Но об этом никто не вспоминает при сравнении с АОП, а ведь надо сравнивать с альтернативой аналогичного кода.
Идем дальше, берем реальные цифры. Вызов одного метода без аспекта, но с такой же логикой — 10 мкс, с аспектом — 50 мкс. В случае с хайлоадом это очень и очень важно, а если вы работаете над бизнес-приложениями, как я, то для вас не будут новинкой методы, работающие и несколько минут, но для которых очень важно иметь читаемый и удобный код, который постоянно нужно править.
И последний довод — аспекты используются для тех методов, которые вызываются редко, но они очень важны, оверхед в пару десятков мкс на них даже не заметен. И понятно, что указывать все методы всех классов и логировать все, получая замедление в 5 раз — выстрел в себе в ногу.
Все нужно использовать с умом и правильно, как и любой другой инструмент.
Поэтому сразу предупреждение для всех — разворачивайте код с аспектами параллельно, а не вместо основного кода.
Фишка АОП в том, что вы не меняете код приложения, поэтому можно написать код и включать аспекты только при наличии волшебного параметра в запросе — это делается запросто. Нет параметра в запросе — не используем аспекты вообще, никаких изменений в логике приложения. Есть наш заветный ключик — переключаемся именно для нашего запроса на код с аспектами. После того, как проверите, что все ок — убираем наш ключик и переходим на аспекты.
Выполнение методов с советами максимально оптимизировано и сопоставимо со скоростью выполнения метода при добавлении аналогичного совета для метода на уровне С, к примеру, с помощью экстеншена PHP-AOP.
Замерил скорость выполнения кода с советами в Go! и экстеншена PHP-AOP. Тест: 10000 итераций, один совет, в котором мы выводим на экран перед вызовом метода класса его имя, имя метода и аргументы. Результат:
Экстеншен: 428-604 мс
Моя либа: 502-710 мс
Без советов: 50-60мс
Да, идея аналогичная — делать load-time рефлексию кода, но проанализировав ее, я понял, что скорость работы конечного кода (с вплетенными аспектами) тут явно не учитывалась, тогда как Go! и создавался вокруг этой цели. Отсюда я могу сделать предположение, что попытка использовать в боевом режиме AspectPHP на большом количестве классов — будет обречена на провал из-за низкой скорости конечного кода.
Помимо этого, AspectPHP не учитывает необходимость корректной работы с опкод-кэшером, поэтому будут проблемы с этим в боевом режиме. Ну и еще Go! умеет перехватывать обращения к публичным и защищенным свойствам объектов (с ограничениями, правда), а также умеет работать с метаинформацией классов в load-time благодаря интеграции с Doctrine2. Скоро еще будет Introduction для PHP 5.4 с использованием трейтов.
Но попробую объяснить. Когда перехватываются динамические методы — там все просто, мы всегда знаем объект благодаря
$this
. Поэтому нам не нужно указывать scope.Со статическими методами все намного сложнее — нужно переопределить статический метод, после этого вызвать оригинальный метод через Reflection. А в PHP сейчас есть сложность с тем, чтобы указать scope при вызове статического метода через рефлексию. Вот поэтому и ломается LSB. Только в 5.4 можно создать замыкание в дочернем классе, которое форвардит запрос к родительскому методу и сменить ему scope на корректный через метод
bindTo()
$invocation->getArguments()
:static $memoryCache — так и задумывалось, потому что один аспект связывается со всеми классами и более того, аспект — это синглтон, он регистрироваться должен только один раз.