апреля 11, 2010

Подписка и события (часть 1)

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

Зачем оно

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

Все, наверно, сталкивались с чем-то вроде

Button.OnClick := MyClickProc;


Это один из простейших способов отвязать код того, что должна делать кнопка, от кода самой кнопки (рисования, обработки мыши и т.д.).

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

Итак, задача: придумать удобную, изящную и безопасную реализацию на паскале системы событий, решающую задачи в предыдущем абзаце.

Архитектура

Событие - это набор данных, описывающих то, что произошло. Подписчик - тот, кто зарегистрировался на получение определенного события.

Есть два архитектурных направления:

1) Глобальное. Есть глобальный класс, в котором зарегистрированы все подписчики и события программы. При отправке сообщения он фильтрует подписчиков, выбирая тех, кому может быть интересно это событие. В операционных системах используется глобальная система сообщений.
2) Локальное. На каждое событие в программе создается отдельный экземпляр специального класса, хранящий всех подписчиков на это событие. При этом этот класс никак не зависим от других, и не обращается ни к чему глобальному.

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

Проектируем

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

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

До сих пор под событием понималась некоторая абстракция. Что же может быть событием в паскале?
1) Наследники специального базового класса TEvent. К примеру, можно написать TEventButtonClick и задать в нем поля Name, X, Y, Button, в которых передавать параметры. Тогда все методы-обработчики нажатия на мышь будут иметь вид

procedure OnMyClick(Ev: TEventButtonClick);

2) Набор параметров функции. Например, в выражении

procedure OnMyClick(const Name: String; X, Y, Button: Integer);

событие описывают параметры функции.

Разница этих двух методов лишь в том, как объявляется описание события. И первый способ мне не нравится: для описания класса потребуется описать private-поля, public-свойства (чтобы никто не изменил поля и ничего не испортил), и реализовать конструктор. Это гораздо больше, чем

type
  TButtonClickHandler = procedure (
                              const Name: String;
                              X, Y, Button: Integer
                            ) of object;

а самое неприятное - содержит в себе много дублирований.

Итак, после всего этого, мы приходим к следующему варианту (псевдокод):


type
  // так описываем тип нотифера
  TClickNotifier: specialize TNotifier<procedure (
                                                 const Name: String;
                                                 Button, X, Y: Integer
                                               ) of object>;
 
TButton = class
 private
  // его необходимо создать в конструкторе и удалить в деструкторе
  FClickNotifier: TClickNotifier;
 public
  ...
  property ClickNotifier: TClickNotifier read FClickNotifier;
end;

...

procedure TButton.OnClick(Button: Integer);
begin
  // так отправляем
  FClickNotifier.Send(Self.Name, Button, Self.X + 5, Self.Y + 5);
end;

...

procedure TMyObject.SomeAction(const Name: String; Button, X, Y: Integer);
begin
  // тут обрабатываем нажатие на кнопку
end;

...

// так происходит регистрация подписчиков
Button.ClickNotifier.Subscribe(A.SomeAction);
Button.ClickNotifier.Subscribe(B.SomeAction);


Ну, а о том, какие у этого есть проблемы, я расскажу в следующей статье цикла.


Комментариев нет:

Отправить комментарий

Постоянные читатели

Обо мне

Моя фотография
Мой e-mail: vitek_03(at)mail(dot)ru