PHP - статьи

Основы аспектно-ориентированного подхода


Аспектно-ориентированное программирование (АПО) позволяет выделить сквозную функциональность в отдельные декларации - аспекты. Можно определить функциональность для строго заданных точек выполнения программы JoinPoints (вызов метода, инициация класса, доступ к полю класса и т.д.). В языках, поддерживающих АОП, чаще используются назначения для множества точек - Pointcut. Функциональность в точках определяет программный код, который принято вежливо называть Advice (AspectJ). Таким образом, в пространстве аспектов описывается сквозная функциональность для определенного множества компонентов системы. В самих компонентах представлена лишь бизнес-логика, которую они призваны реализовать. В ходе компиляции программы компоненты связываются с аспектами (Weave).

Чтобы лучше понять базисы АОП, давайте вернемся к примеру с выделением аспекта мониторинга производительности (см. ). Нам требуется снять показания таймера на входе и на выходе всех методов классов Model, Document, Record, Dispatcher. Итак, мы имеем аспект Logging. Нам потребуется завести к нему Pointcut с перечислением всех требуемых функций. В большинстве языков программирования, поддерживающих АОП, для охвата сразу всех методов класса можно использовать специальную маску. Теперь можно описать для этого Pointcut. Создаем Advice на входе в методы из списка Pointcut (Before) и на выходе из них (After). Advice для событий Before, After, Around - наиболее популярны в языках, поддерживающих АОП, но порой доступны и другие события.

Итак, по результатам данного примера можно отметить для себя по поводу базисных деклараций АОП следующее:

Aspect - определение некоторого набора сквозной функциональности, нацеленной на конкретную задачу;

Poincut - код применимости аспекта. Отвечает на вопросы, где и когда может быть применена функциональность данного аспекта (см. )

Advice - код функциональности объекта. Непосредственно код функциональности для заданных событий. Другими словами, это то, что будет выполнено для объектов, указанных в Pointcut.

Все еще сложно вникнуть в этот подход? Полагаю, все встанет на свои места, когда мы привнесем немного предметности. Давайте начнем с самого простого примера. Я некогда написал эту маленькую библиотеку специально, чтобы проиллюстрировать как преимущества подхода АОП, так и его доступность. Кроме того, для того, чтобы воспользоваться данной библиотекой вам не потребуются глубокие знания PHP и специального программного обеспечения. Вам будет достаточно включить библиотеку aop.lib.php в ваши скрипты под управлением PHP 4 (или выше) в его стандартной комплектации. Мы можем определить некоторый аспект для сквозной функциональности (скажем, ведение журналов транзакций) посредством инициирования класса Aspect.


$aspect1 = new Aspect();

Далее мы можем создать Pointcut и сообщить, какие методы он затрагивает.

$pc1 = $aspect1->pointcut("call Sample::Sample or call Sample::Sample2");

Осталось лишь сообщить программный код для входной и выходной точек методов текущего среза.

$pc1->_before("print 'Aspect1 preprocessor<br />';"); $pc1->_after("print 'Aspect1 postprocessor<br />';");

Аналогичным образом мы можем описать дополнительный аспект, например:

$aspect2 = new Aspect(); $pc2 = $aspect2->pointcut("call Sample::Sample2"); $pc2->_before("print 'Aspect2 preprocessor<br />';"); $pc2->_after("print 'Aspect2 postprocessor<br />';");

Для того, чтобы задействовать один или несколько аспектов, достаточно воспользоваться функцией Aspect::apply()

Aspect::apply($aspect1); Aspect::apply($aspect2);

Как вам, возможно, известно, в PHP до 5-й версии достаточно проблематично обслуживать события методов и классов. Если для событий глобального характера, таких как ошибки PHP, можно написать собственный обработчик, то для обработки, скажем, событий на входе и выходе методов придется "вручную" расставить "оповещатели". В нашем случае потребуется расставлять специальные функции

Advice::_before(); и Advice::_after(); class Sample { function Sample() { Advice::_before(); print 'Class initilization<br />'; Advice::_after(); return $this; } function Sample2() { Advice::_before(); print 'Business logic of Sample2<br />'; Advice::_after(); return true; } }

Как видно из примера, до кода бизнес-логики метода и после него установлены "оповещатели" этих событий. Когда процессор PHP минует такой "оповещатель", он проверяет, нет ли активных аспектов. В случае наличия такового, PHP проверяет, указана ли текущая функция в диапазоне Pointcut. Если и это условие верно, вызывается назначенная нами функция для данного события (например для Advice::_before()). Видите - как я и обещал, все достаточно просто. Но дает ли этот подход реальную пользу?

Давайте представим, что мы расставили во всех методах классов наших скриптов "оповещатели" и подключили библиотеку aop.lib.php. И вот однажды нам потребовалось получить подробный отчет о распределении нагрузки по выполняемым функциям нашего проекта. Мы создаем аспект и назначаем ему Pointcut, охватывающий все функции проекта.



$Monitoring = new Aspect(); $pc3 = $Monitoring->pointcut("call *::*");

Как показано в примере, мы можем воспользоваться маской *::*. Далее в Advice мы можем воспользоваться традиционной функцией вычисления точного времени в миллисекундах

function getMicrotime() { list($usec, $sec) = explode(" ",microtime()); return ((float)$usec + (float)$sec); }

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

Чтобы у вас не сложилось мнение, что АОП помогает решать лишь какие-то частные задачи, давайте рассмотрим еще пример.

Как известно, PHP лояльно относится к типам переменных. С одной стороны, это не может не радовать нас, так нет необходимости постоянно заботиться о соответствие заданным типам и тратить время на их декларацию. С другой стороны - это причина множества возможных ошибок. При большом количестве функций в проекте едва ли возможно удержать в памяти заведенный нами же синтаксис для них. Но достаточно лишь поменять местами аргументы функции, и ее поведение может стать непредсказуемым. Может ли здесь нам помочь АОП? А почему бы и нет?! Давайте вспомним диаграмму, приведенную на . Как мы видим, классы Document и Record содержат однотипные методы add, update, delete, copy, get. Хорошо организованная программная архитектура подразумевает однотипный синтаксис для этих методов: add($id, $data), update($id, $data), delete($id), copy($id1,$id2), get($id). АОП нам может помочь организовать как программную архитектуру, так и самих себя. Мы можем завести аспект валидации входных параметров и определить диапазон Pointcut для методов классов Document и Record. Функция события входа для методов add, update, delete, copy, get может проверять тип первого аргумента. Если он не является целочисленным (integer), то можно смело сообщать об ошибке. Можно также завести второй Pointcut для методов add и update. В данном случае будет проверяться тип второго аргумента. Он, очевидно, должен соответствовать типу массив (array).

Таким же образом мы можем выносить за пределы бизнес-логики проекта протоколирование транзакций, мы можем в любой момент определить диапазон функций, требующих дополнительных проверок на предмет безопасности, и т.д.



Что особенно интересно, посредством АОП мы можем назначить специфичный вывод сообщения о системной ошибке для определенного набора функций. Допустим, ряд наших функций участвует в формировании кода разметки WML (WAP), WSDL (SOAP), RSS/ATOM или SVG. Разумеется, в данном случае недопустимо выводить на экран HTML-разметку с сообщением об ошибке. "Оповещатель" в обработчике ошибок PHP заставит систему отобразить сообщение либо в требуемой разметке XML, либо оповестить нас без отображения сообщения на экране (например, по Email).

Каждый, кому приходилось участвовать в разработке тиражируемых программных продуктов, знает, насколько непросто решить проблему обновления версий продукта. Конечно, все мы знаем о наличие специального программного обеспечения для контроля версий, например, о CVS (Concurrent Versions System). Однако проблема состоит в том, что для каждого нового продукта на базе нашего тиражируемого продукта требуется некоторая кастомизации, и часто достаточно сложно выяснить, не затронет ли обновление области, адаптированные под конкретный проект. Наверняка кто-нибудь из вас встречался с проблемой, когда после очередного обновления версии базового продукта приходилось восстанавливать весь проект из резервных копий. А представьте себе ситуацию, когда "пропажа" кастомизации в отдельных интерфейсах проекта выясняется спустя продолжительное время после обновления версии базового продукта! Вы скажите "А причем здесь АОП?!". Дело в том, что АОП как раз может помочь в решении данной проблемы. Мы ведь можем перенести весь код кастомизации проекта как сквозную функциональность за пределы основной бизнес-логики. Достаточно определить аспект наших интересов, указать область его применимости (Pointcut) и разработать соответствующий код функциональности. Имеет смысл взглянуть на то, как это может работать.

Вернемся к моему излюбленному примеру с ПО для управления содержанием сайта. Подобный продукт наверняка будет содержать функцию для отображения списков записей (recordsets) Record::getList() и подготовки кода для отображения данного списка View::applyList(). Record::getList() получает в качестве аргументов идентификатор recordset и параметры выборки в нем. Возвращает эта функция массив с данными по результатам выборки. Функция View::applyList() принимает на входе этот массив и формирует код оформления для него, например HTML-код таблицы. Допустим, в нашем продукте каталог товаров представлен в виде подобных списков записей. Это универсальное решение для тиражируемого продукта, но для конкретного проекта, базированного на нем, требуется отображать дополнительную колонку в списках. Например, в базовом продукте принято отображать таблицу с полями "артикул", "наименование товара". А требуется к ним добавить поле "Рейтинг покупателей". Для этого мы всего лишь пишем Advice для функции Record::getList(), по которому на ее выходе в возвращаемый массив будет введена дополнительная колонка. Если наша функция View::applyList() не способна автоматически адаптироваться под изменения во входящем массиве, то придется написать Advice и для нее. Допустим, спустя время заказчик потребовал от нас выделить все строки в списках, обозначающие товары, которые отсутствуют на складе. Мы дописываем Advice для View::applyList(), где сверяем значение атрибута записей "Наличие на складе" и соответствующим образом оформляем их. Обратите внимание, что мы можем завести отдельную папку Plugins для деклараций аспектов и скриптов их функциональности. Таким образом, вся кастомизация для любого из проектов будет сосредоточена в одно заданной папке. В дальнейшем у нас уже не будет проблем с обновлением версий. Мы сможем смело обновлять любые системные скрипты, за исключением тех, что представлены в папке Plugins.


Содержание раздела