В Delphi, как и в большинстве других современных объектно-ориентированных языков существует поддержка
делегирования.
Она основана на определении свойств в классах, тип которых — метод объекта с предопределённым интерфейсом. Рассылка события — это обращение к такому свойству как к методу с передачей в него параметров.
В такой реализации присутствуют ограничения, которые препятствуют применению делегирования для
динамического (runtime) расширения функциональности объектов средствами делегирования:
Поддержка ED реализована в TSBaseClass, расположенном в модуле Sonar.Core.BaseClass. В этом сообщении рассматривается часть функциональности, обеспечиваемой этим модулем.
Как следует из изложенного выше, события расширенного делегирования всегда рассылаются в контексте объекта. Т.е. для рассылки события нужен получатель. Как быть в случае, если событием должно быть разослано глобально, в приложение?
Sonar.Core.BaseClass содержит для этой цели специальный синглтон AppEventList, распределяемый в приложении в случае необходимости обработки глобальных событий или событий уровня приложения. Для того, чтобы разослать такое событие достаточно в SendEvent указать AppEventList в качестве получателя.
См. также репозиторий Mercurial с исходными текстами.
Она основана на определении свойств в классах, тип которых — метод объекта с предопределённым интерфейсом. Рассылка события — это обращение к такому свойству как к методу с передачей в него параметров.
Пример
ПоказатьСкрыть
- В контексте объекта могут происходить только события, описанные в классе. В runtime нет возможности добавить в класс дополнительные свойства, соответственно, нельзя обеспечить рассылку событий, о которых класс «не знает».
- Архитектурно заложено, что метод-обработчик события может быть только один. В некоторых случаях
этого оказывается недостаточно, поэтому приходится прибегать к опасным манипуляциям со
ссылками.
ПримерПоказатьСкрыть
Поддержка ED реализована в TSBaseClass, расположенном в модуле Sonar.Core.BaseClass. В этом сообщении рассматривается часть функциональности, обеспечиваемой этим модулем.
Примечание: Ссылка на репозиторий с исходными текстами приводится в конце этого сообщения.ED вполне можно рассматривать как реализацию шаблона проектирования Наблюдатель (Observer) со следующими дополнительными соглашениями:
- Терминология:
-
Событие (предмет рассылки) представляет собой запись предопределённой структуры,
содержащую помимо прочего ссылку на параметры события, которые могут быть использованы
в обработчиках.
Определение TEventПоказатьСкрыть
- Рассылка события: процесс передачи управления обработчикам (метод, процедура или замыкание с предопределённым интерфейсом) события, происходящего в контексте какого-либо объекта-потомка TSBaseClass, называемого получателем (Receiver)
-
Идентификатор события: строка, значение которой уникально, желательно для всего
приложения. Идентификатор события используется при рассылке для определения списка
установленных на событие обработчиков.
Рекомендуется использовать специальное представление GUID, которое можно получить с помощью специальной программы EventId, выводящей представление GUID, предназначенное для вставки в исходный код на Delphi. В коде идентификаторы событий выглядит, например так:
Примеры идентификаторов событийПоказатьСкрыть - Установка и снятие обработчиков: соответственно, процедуры добавления
обработчика в соответствующий список, ассоциированый с идентификатором события в
получателе, и исключение обработчика из этого списка.
Для того, чтобы обработчик получал управление при рассылке события, он должен быть предварительно установлен. Соответственно, после снятия обработчика с события он перестанет получать управление в процессе рассылки события. - Класс-обработчик (Handler class): класс, содержащий определение методов-обработчиков событий.
-
Событие (предмет рассылки) представляет собой запись предопределённой структуры,
содержащую помимо прочего ссылку на параметры события, которые могут быть использованы
в обработчиках.
- В качестве обработчика события могут быть использованы метод, процедура или замыкание
(анонимный метод) с предопределённым интерфейсом
Разновидности обработчиков событийПоказатьСкрытьУстановка и снятие обработчиков событийПоказатьСкрыть
- При рассылке события указывается его получатель, идентификатор события (или
непосредственная ссылка на список обработчиков — см. далее), ссылка на запись параметров и,
опционально, обработчик по-умолчанию.
Способы рассылки событийПоказатьСкрыть
Пример рассылки события и использования обработчиков различных видовПоказатьСкрыть- Обработчик по-умолчанию соответствует виртуальному методу базового класса
- Любой установленный обработчик будет соответствовать его перекрытию в классе-потомке
- Вызов PassEvent в обработчике соответствует вызову метода предка (inherited) в перекрытой версии этого метода в потомке
- Если класс-потомок TSBaseClass, содержащий обработчики событий расширенного делегирования
освобождается, его методы-обработчики будут автоматически сняты с соответствующих событий объектов
— никаких специальных действий выполнять не требуется.
Тем не менее, допустимо размещать обработчики событий ED в классах, не являющихся потомками TSBaseClass. Но в этом случае, при освобождении экземпляров таких классов, например в деструкторе, потребуется снять все установленные обработчики с соответствующих событий. - При освобождении объекта-потомка TSBaseClass в его контексте последовательно происходят два
события - ev_Terminate и ev_Finalize.
- ev_Terminate рассылается до вызова деструктора - точнее, в перекрытом
BeforeDestruction, по этой причине в потомках TSBaseClass перекрытие BeforeDestruction не
допускается, этот метод объявлен как final. При необходимости выполнить действия до вызова
деструктора, следует перекрывать специально объявленный в TSBaseClass виртуальный метод
DoBeforeDestruction.
В момент рассылки ev_Terminate объект ещё сохраняет работоспособность. Обработка этого события обычно связана с завершающими действиями с объектом - например, это событие можно связать с методом CloseSelf, также объявленном в TSBaseClass, выполняющим освобождение объекта-обработчика (см. предыдущий пример - соответствующие действия выполняются в TSampleHandlerClass.Create). - ev_Finalize происходит уже в деструкторе TSBaseClass. Обработка этого события может применяться аналогично ev_Terminate (закрытие объектов-обработчиков, для которых объект, в контексте которого рассылаются обрабатываемые ими события, является в сущности владельцем). Главное отличие состоит в том, что если объект-обработчик освобождается на ev_Terminate владельца, он не сможет обрабатывать события, которые могут происходить в его деструкторе. Если отложить закрытие объекта-обработчика на момент ev_Terminate, такие события могут быть им обработаны.
Демонстрация передачи управления при освобождении объектов-потомков TSBaseClassПоказатьСкрыть - ev_Terminate рассылается до вызова деструктора - точнее, в перекрытом
BeforeDestruction, по этой причине в потомках TSBaseClass перекрытие BeforeDestruction не
допускается, этот метод объявлен как final. При необходимости выполнить действия до вызова
деструктора, следует перекрывать специально объявленный в TSBaseClass виртуальный метод
DoBeforeDestruction.
- В некоторых случаях событие происходит очень часто и возникает потребность в минимизации
накладных расходов ED на рассылку. В таких ситуациях можно воспользоваться вторым способом рассылки
событий, указав в качестве параметра не пару (Получатель, Идентификатор события), а непосредственно
список обработчиков.
Список обработчиков может быть получен посредством функции GetEventId, в которую передаётся Получатель и идентификатор события. После этого можно воспользоваться одним из overload-вариантов SendEvent, предназначенных для рассылки события списку обработчиков. Такой способ рассылки избавлен от накладных расходов на поиск списка обработчиков по идентификатору события в контексте получателя.
При использовании этого метода рассылки следует учитывать, что список обработчиков в контексте получателя события появится только после того, как будет установлен хотя бы один обработчик. Далее, если с события снимается единственный обработчик, соответствующий список, в котором он содержался, будет автоматически освобождён. Разумеется, если список обработчиков был получен посредством GetEventInfo в ситуации, когда этот список существовал, после его автоматического освобождения попытка разослать событие используя полученный список приведёт к Access Violation.
В ED предусмотрен механизм, обеспечивающий актуальность ссылки на список обработчиков события. При появлении списка обработчиков события (установка первого обработчика на событие) и при освобождении этого списка (снятие единственного обработчика) в потомке TSBaseClass происходит рассылка события ev_EventListChange, в обработчике по-умолчанию которой производится вызов виртуального метода EventListChange. Это даёт возможность при необходимости держать всегда актуальный список обработчиков как на уровне получателя (нужно перекрыть метод EventListChange), так и в любом другом объекте (следует обработать событие ev_EventListChange, происходящее в получателе).Примечание: Это стандартная техника, в таком случае говорят, что действие (в данном случае вызов EventListChange) обёрнуто событием (в данном случае ev_EventListChange).
Демонстрация использования TSBaseClass.EventListChange и ev_EventListChangeПоказатьСкрыть
Как следует из изложенного выше, события расширенного делегирования всегда рассылаются в контексте объекта. Т.е. для рассылки события нужен получатель. Как быть в случае, если событием должно быть разослано глобально, в приложение?
Sonar.Core.BaseClass содержит для этой цели специальный синглтон AppEventList, распределяемый в приложении в случае необходимости обработки глобальных событий или событий уровня приложения. Для того, чтобы разослать такое событие достаточно в SendEvent указать AppEventList в качестве получателя.
См. также репозиторий Mercurial с исходными текстами.
Монументальненько...
ОтветитьУдалитьХорошо описано, но, боюсь, одной статьи для понимания может быть мало. Поздравляю с почином в OpenSource :) Вижу, начали анонимные функции использовать, фолдинг настроили? Жаль нет винды под рукой чтобы поиграться.
P.S. А FastMM из репа я всё-таки бы удалил - это сторонний продукт.
Виктор, спасибо на добром слове :-)
Удалить«боюсь, одной статьи для понимания может быть мало.»
-- Да, я понимаю. И готовлю продолжение... :-)
«начали анонимные функции использовать, фолдинг настроили?»
-- Ну, если я использую инструмент мэйнстрима (IDE) я должен стараться делать это так, как принято в мэйнстриме :-)
«А FastMM из репа я всё-таки бы удалил - это сторонний продукт.»
-- Ну, я же не претендую на его авторство... Там есть два обстоятельства:
1. FastMM позволяет контролировать утечки памяти, а это - существенная часть модульного теста, который я сделал.
2. Я там изменил inc-файл FastMM - включил опцию FullDebugMode при появлении отвечающего за эту функцию DLL рядом с исполняемым модулем.
Но наверное, Вы правы - любой при желании может его (FastMM) скачать, да и работает всё и без него...
Только сейчас дошло - теперь есть возможность мой диплом запустить :) Знаю, правда, что он никому не нужен...
ОтветитьУдалить