Dubrowsky
Хроники одного дупла
Блогово  →  WebDev  → 

Событийно-ориентированная модель (PHP)

20 Ноября 2007 года

Мне тут в процессе ваяния портальчега на PHP коллега предложил испробовать событийно-ориентированный подход. Я сначала открестился, мотивировав тем, что "хватит уже новых подходов на один проект". Однако, в своей цмске все-таки придумал небольшой кусок кода - и, надо сказать, мне понравилось ) Ниже вкратце опишу решение, его плюсы и минусы, и как его можно применить.

Решение получилось весьма простое, чем я и доволен. Создаем 2 класса - myEventManager и myEvent (префикс можно более осмысленный юзать). Вот что они умеют:

myEventManager

public addEventListener (str eventName, callback eventHandler); Метод добавляет объект/функцию eventHandler, "слушающую" событие с названием eventName. Если в качестве callback передать название класса + название метода, при событии метод-обработчик будет вызван статически. В моей реализации в качестве eventName можно передавать *, тогда обработчик будет реагировать на любые события. public fireEvent (myEvent eventObj) ; Принимает объект класса myEvent, смотрит на его тип (название), находит все обработчики, ожидающие срабатываня события такого типа, и вызывает их, передав событие в качестве параметра. Достаточно одного глобально доступного объекта на систему, так что тут можно применить паттерн Singleton, если не лень =)

myEvent

public __construct (str eventName, array eventParams = array()); Конструктор - принимает название события и массив параметров. public setParam (str paramName, mixed paramValue); Устанавливает отдельный параметр. public getName (); Через этот метод можно получить название события. Используется в eventManager'е для поиска обработчиков. Может использоваться и в обработчике, если он отвечает на несколько событий. public getParam (str paramName); Находит и возвращает параметр события по имени.

Плюсы и минусы

Два основных плюса такого подхода - сокращение числа объектов, которые нужно держать в глобальной области видимости, и легкость подключения/отключения дополнительного функционала. Особенно много пользы можно получить при хорошем уровне декомпозиции системы, когда "типовые" операции проходят через одну точку системы - в этой точке и нужно создавать события. Реализация и внедрение действительно не требуют больших усилий. Мне почему-то казалось, что это событийный подход - это отдельная религия, под которую придется перестраивать всю систему. На деле прикрутка вышеупомянутых классов к системе (вполне сформировавшейся системе, со своими принципами и своей религией) - заняла у меня всего один вечер, вместе с созданием событий во всех нужных местах. К минусам можно отнести необходимость хранения информации об устройстве параметров конкретного события. Для основных событий параметры придется документировать. Код инициализации события выглядит несколько неказисто. Нужно следить за последовательностью действий, чтоб обработчики назначались раньше, чем возникают события. Сейчас нет механизма удаления обработчиков и изменения порядка их вызовов, как реализовать это аккуратно и красиво - я пока не придумал. Кроме того, у меня, в большинстве случаев, появляется лишний метод-обработчик, который достает из события нужные параметры и вызывает с ними другой, уже настоящий метод. Ясно, что без специального метода-обработчика с классом пришлось бы общаться только через события, что не всегда удобно и необходимо. С другой стороны, если у класса много обязанностей, я приноровился в качестве обработчика использовать только один метод, содержащий switch($e->getName()), с вызовами своих методов в case'ах - тогда вся информация об устройстве параметров события сохраняется внутри одного метода, и если вдруг что-то поменяется, будет достаточно переписать только его. Я пока не решил, типизация событий - это хорошо или плохо. Т.е. нужно ли строить цепочку наследований типа baseEvent - systemEvent - structureMutationEvent - itemAddEvent. С одной стороны, это повысит строгость работы с параметрами, с другой - много писанины.

Варианты использования

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

Индексация сайта

Есть задача - для поиска по сайту строить поисковый индекс "на лету", т.е. сразу при изменении данных, а не по крону или как-то еще. В моей системе все операции со структурой сайта (создание, изменение, перемещение и удаление объектов-записей) осуществляются через вызовы методов объекта DataRecord. Я изменил эти методы, добавив туда создание соответствующих событий: ItemCreated, ItemModified, ItemMoved, ItemRemoved. В параметры событиям выставляется информация об изменяемом объекте: его поля (в случае изменения - старые и новые), тип данных, указатель на родителя (если объект перемещается или создается и если он "участвует" в структуре сайта). В самом начале загрузки системы (и админки, и front-end'а) на все эти события в качестве обработчиков вешается метод класса, отвечающего за индексацию. В классе-индексаторе содержится информация, какие типы данных нужно индексировать, как именно, и много других тонкостей. Важно, что через систему событий индексатор легко и непринужденно получает из "внешнего мира" всю нужную информацию и соответствующим образом обновляет индекс. Этот пример можно обобщить: при помощи событийной модели можно создать механизм для поддержания целостности и непротиворечивости данных в системе (в нашем случае поисковый индекс - это дублирующиеся данные, с которыми обычно бывает куча проблем).

Системный лог

Используя набор событий из предыдущего примера, я без труда наваял класс logWriter, сохраняющий подробную информацию о том, кто что когда и с какого айпишника натворил в системе. Событие ModuleProcessingStarted позволяет определить, какой модуль системы инициировал изменения, и найти ошибку либо в этом модуле, либо в ДНК пользователя :)

Отладка "на горячую"

Создаем класс debugWriter, следящий за событиями debugMessage и errorMessage, и, в зависимости от прав пользователя и настроек, выводящий отладочные сообщения и сообщения об ошибках на экран, в журнал, или не выводящий вовсе.

Нагрузочное тестирование

Пишем таймер, считающий время между началом загрузки отдельных модулей по событию ModuleProcessingStarted, и сохраняющий куда-то эти данные. Грузим сайт, анализируем, что получилось, находим проблемный модуль. Расставляем в его коде события timerEvent, считаем таймером, на что уходит больше времени, переписываем проблемное место. Справедливости ради скажу, что такую штуку я накодил, потестировал, но реальной нужды в ней пока не было.

Общение между модулями системы

Есть система, состоящая из множества различных модулей. Есть общие элементы страницы (например, заголовок документа или "путь"-breadcrumbs), данные для которых берутся из разных модулей. Можно таскать в глобальной видимости объект, умеющий создавать такие элементы страницы. Однако, если мы захотим переименовать его или один из его методов, или разделить на несколько объектов - нам придется переписывать клиентский код во всех модулях. Вместо этого я в модулях вызываю события setPageHeader, addPathItem и т.д., а при загрузке системы назначаю один из модулей в качестве их обработчика.

Что можно улучшить

Мне кажется, должно получиться неплохо, если попытаться добавить обратную связь. Т.е. дать обработчикам событий возможность не только самим что-то делать, но и влиять на поведение того места системы, где было создано событие. Объект-событие передается в обработчик по ссылке, обработчик меняет параметры, переданные в событии, далее инициализатор события зачитывает измененные параметры и работает уже с ними. Например, так можно создавать всевозможные препроцессоры - "типограф", проверка орфографии, расстановка внутренних ссылок на кивурдах в статьях, и т.д. Главное - не потонуть в интерфейсах :) Еще будет полезно классифицировать события, чтоб вешать обработчик на целый класс событий. Это можно сделать либо через типизацию + цепочку наследований (таким образом во многих языках обрабатываются исключения-exceptions). Либо тупо передавать в конструктор myEvent массив с именами классов события. Буду благодарен всем, кто захочет поделиться опытом, что-то дополнить, уточнить или покритиковать =)

Камменты

Kolyaj21.11.2007, 15:10#
> Мне кажется, должно получиться неплохо, если попытаться добавить обратную связь.

Как же без этого. Т.е. надо чтобы метод fireEvent "говорил" не только "Я тут кой-чего сделал, никому не надо?", а еще и "Я тут кой-чего собираюсь сделать, как думаете надо ли?". Для этого логичнее всего у myEvent сделать метод stop, после вызова которого обработка события останавливается (в том числе и самим инициатором события).
Николай Дубровскийон самый21.11.2007, 22:37#
Насчет "собираюсь кой-чего сделать" - согласен. Собственно, на изменения я повесил по 2 события - onBefore и onAfter. С добавлением метода stop() я бы не торопился - далеко не для всех событий он имеет смысл. Ну если только действительно типизировать события, дробя на классы. Но мне лень, так что скорее я для начала буду юзать что-то вроде $myEvent->setParam('cancel', true) в обработчике =)
дубровский-mobile29.12.2007, 06:57#
Пишу с мобильнега :)

Типо тупой
проверочный каммент....

cool!
Николай Дубровскийон самый09.01.2008, 07:31#
Нашел занятный набор буков: Аспектно-ориентированная веб-разработка и PHP - интересно почитать, особливо в связи с "beforeSmth" и "afterSmth". Если доведется заюзать - отпишу об ощущениях :)
Сергей09.03.2008, 23:36#
Идейка и вправдо инетесная. Посмотрим че получится..
Данила09.11.2008, 14:50#
Спасибо за статью. Как раз собираюсь переписать свою CMS с нуля, пригодится.

Написать коммент: памятка постеру

 

Крутые посты wtf??? →

02.10.2012 · 94 каммента · рейтинг 13.21
15.02.2013 · 24 каммента · рейтинг 6.79
23.01.2013 · 21 каммент · рейтинг 6.34
06.03.2008 · 29 камментов · рейтинг 6.14
30.01.2013 · 13 камментов · рейтинг 4.99

Последне камменты

Статсы