Несколько обработчиков одного события

Вопросы программирования и использования среды Lazarus.

Модератор: Модераторы

Несколько обработчиков одного события

Сообщение А.Н. » 16.05.2010 12:24:53

У меня есть объект, который по таймеру перечитывает БД ("Диспетчер обновлений").
Если таблицы обновились, он создаёт событие. Обновления некоторых таблиц должны отобразиться в окнах.
Окон может быть несколько. Из одного окна вызвано другое. И во всех надо сделать обновление.

Пример:
Обновились данные клиента, который сейчас отображён.
В окне менеджера договоров должны отобразиться изменённые данные.
Если открыто окно менеджера клиентов, там также должны отобразиться изменённые данные.

Вопрос:
Как сделать оповещения нескольких объектов?

Уточнения:
1.) Нежелательно, чтобы объект "Диспетчер обновлений", вызывал методы объектов менеджеров.
И, вообще, знал о их существовании. (Хотя бы потому, что, в таком случае, вероятны "циклические" включения модулей, поскольку объект может потребоваться в менеджерах (к примеру, для форсированного обновления после нажатия кнопки)).
2.) Я думал насчёт механизма сообщений в Lazarus... Ведь такой есть?
Рассылать "широковещательные" сообщения каким-либо образом, вроде, самая привлекательная и просто реализуемая идея.
3.) Делать "цепочку" обработчиков, кажется, не особо просто...
4.) Callback - тоже лишние проблемы. Хотя, не знаю.
5.) Список адресов обработчиков? Может... Не знаю. Что скажете?

Добавлено спустя 2 часа 47 минут 2 секунды:
Есть конечно, вариант - использовать один обработчик в главном объекте, из которого вызывать подчинённые.
Но, как-то, вроде, не труъ... :-(
А.Н.
постоялец
 
Сообщения: 230
Зарегистрирован: 13.03.2010 12:23:58

Re: Несколько обработчиков одного события

Сообщение Odyssey » 16.05.2010 19:31:20

Как вариант, можно сделать регистрацию менеджеров в диспетчере, типа "подписки" на уведомления об обновлении.

Если нежелательно, чтобы диспетчер знал о менеджерах, можно обойтись регистрацией методов (например TNotifyEvent), т.е. по сути callback'ов. Вроде лишних проблем навскидку не видно, кроме того, что хранить NotifyEvent'ы придётся в массиве или в TList с приведением типа.

Если иерархия наследования менеджеров не занята, то можно объявить их базовый тип в модуле диспетчера, что-то типа TSubscriber, о котором диспетчер знает. Тогда диспетчер не будет знать о реализациях менеджеров, а менеджеры будут наследоваться от TSubscriber и регистрироваться у диспетчера. Если менеджеры уже наследуются от чего-то определённого, можно использовать интерфейсы. По сравнению с callback'ами плюс в более наглядной регистрации - Dispatcher.RegisterSubscriber(Manager1) вместо Dispatcher.RegisterCallback(Manager1.UpdateNotifier). Ну и в перспективе, если потребуется расширить интерфейс взаимодействия, изменять класс/интерфейс T/ISubscriber будет проще, чем переключаться с TNotifyEvent на что-то ещё.

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

Что касается механизма сообщений -- тут мне сложно что-то сказать. Я видел не очень много кода, но почему-то нигде кроме GUI-библиотек (LCL, fpGUI) не использовались message. Судя по документации, RTL предоставляет только объявление таких методов, автоматически делая их виртуальными, и вызов через MyObject.Dispatch(Msg). Похоже что "из коробки" нет ни очереди сообщений, ни broadcast-рассылки, для этого используются возможности ОС (SendMessage/PostMessage под Win32). ЕМНИП, Грэм в fpGUI делал свою очередь сообщений. Т.е. заморочек с этим побольше чем с регистрацией.

Да и сама идея широковещательной рассылки в такой ситуации, имхо, не очень. Если использовать её где попало без особой необходимости, в конце концов объекты на каждый чих будут тревожить всю систему.
Odyssey
энтузиаст
 
Сообщения: 580
Зарегистрирован: 29.11.2007 17:32:24

Re: Несколько обработчиков одного события

Сообщение Timid » 16.05.2010 23:39:43

А.Н. писал(а):У меня есть объект, который по таймеру перечитывает БД ("Диспетчер обновлений").
Если таблицы обновились, он создаёт событие. Обновления некоторых таблиц должны отобразиться в окнах.
Окон может быть несколько. Из одного окна вызвано другое. И во всех надо сделать обновление.


Не заморачивайтесь лишний раз. Добавьте в объект (диспетчер обновлений) методы подписки/отписки на обновление.
Код: Выделить всё
SetMyUpdateDispatcherListener(TableNname:string; UpdateListener:TNotifyEvent):integer;
UnsetMyUpdateDispatcherListener(ListenerCode:integer);

В объекте введите тип
Код: Выделить всё
MyOneTableUpdateListener = record
  ListenerCode:integer;
  TableName:string;
  ListenerMethods:updatelistener;
end;

MyTableUpdateListeners = array of MyOneTableUpdateListener;


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

Метод работы:
При передаче "слушателя" заносите данные в динамический массив MyTableUpdateListeners, генерируя уникальный индекс для "отписывания" и возращаете его в результат.

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

Как сделать "отписку" - сами, наверное, догадались :)
Timid
постоялец
 
Сообщения: 290
Зарегистрирован: 21.11.2007 21:33:15

Re: Несколько обработчиков одного события

Сообщение Climber » 17.05.2010 08:55:53

Буквально в прошлый четверг делал для себя такую штуку (шаблон Publisher - Subscriber) называется. Вот моя реализация.
Для подписки или отписки передаем ссылку на объект и название события, для уведомления подписчиков - вызов Notify. Уведомить можно любой объект, который реализует интерфейс ISubscriber.
Код: Выделить всё
unit Publisher;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Subscriber;

type

  { TPublisher }

  TPublisher = class
  private
    FList: TStringList;
  protected
  public
    constructor Create;
    destructor Destroy; override;
    function SubscriptionIndex(ASubscriber: TObject; AEvent: string): longint;
    procedure Subscribe(ASubscriber: TObject; AEvent: string); virtual;
    procedure SubscribeSomeEvents(ASubscriber: TObject; AEvents: array of string); virtual;
    procedure Unsubscribe(ASubscriber: TObject; AEvent: string); virtual;
    procedure Notify(AEvent: string); virtual;
  end;

implementation

{ TPublisher }

function TPublisher.SubscriptionIndex(ASubscriber: TObject; AEvent: string): longint;
{ Возвращает индекс подписки объекта ASubscriber на событие AEvent.
  -1, если не подписан }
var i: longint=0;
begin
  Result:=-1;
  while (i<FList.Count) do
    begin
      if ((FList.Strings[i] = AEvent) and (FList.Objects[i] = ASubscriber)) then
           Result:=i;
      i:=i+1;
    end;
end;

constructor TPublisher.Create;
begin
  FList:=TStringList.Create;
end;

destructor TPublisher.Destroy;
begin
  FreeAndNil(FList);
  inherited Destroy;
end;

procedure TPublisher.Subscribe(ASubscriber: TObject; AEvent: string);
{ Подписывает объект ASubscriber на событие AEvent }
begin
  if SubscriptionIndex(ASubscriber, AEvent) = -1 then
     FList.AddObject(AEvent, ASubscriber);
end;

procedure TPublisher.SubscribeSomeEvents(ASubscriber: TObject; AEvents: array of string);
var i: longint;
begin
  for i:=0 to high(AEvents) do
    Subscribe(ASubscriber, AEvents[i]);
end;

procedure TPublisher.Unsubscribe(ASubscriber: TObject; AEvent: string);
{ Подписывает объект ASubscriber на событие AEvent }
begin
  if SubscriptionIndex(ASubscriber, AEvent) > -1 then
     FList.Delete(SubscriptionIndex(ASubscriber, AEvent));
end;

procedure TPublisher.Notify(AEvent: string);
{ Уведомляет всех подписчиков на событие AEvent о том, что оно случилось. }
var i: longint;
begin
  for i:=0 to FList.Count-1 do
    if AEvent=FList.Strings[i] then
       (FList.Objects[i] as ISubscriber).Notify(AEvent);
end;

end.

Код: Выделить всё
unit Subscriber;

{$mode objfpc}{$H+}
{$OBJECTCHECKS ON}

interface

uses
  Classes, SysUtils, Forms;

type

  { ISubscriber }

  ISubscriber = interface
  ['{7B3C558B-00BE-424D-91AC-5F9D894CC51B}']
    procedure Notify(AEvent: string);
  end;

implementation

end.
В принципе, можно все в один модуль запихнуть, но не стоит.
Climber
постоялец
 
Сообщения: 415
Зарегистрирован: 03.06.2007 20:09:57
Откуда: Москва

Re: Несколько обработчиков одного события

Сообщение А.Н. » 17.05.2010 12:29:35

Спасибо всем за ответы.

Odyssey писал(а):Если иерархия наследования менеджеров не занята, то можно объявить их базовый
тип в модуле диспетчера, что-то типа TSubscriber, о котором диспетчер знает.
...
Если менеджеры уже наследуются от чего-то определённого, можно использовать интерфейсы.

У менеджеров есть базовый класс "Абстрактный менеджер". Возможно, конечно,
делать через иерархию. Но тут есть один минус.
Как таковых "менеджеров", наследуемых от данного класса у меня всего три.
Каждый менеджер работает с сущностью (например клиент).
Но есть ещё, например, окно, где могут изменяться справочники. И т.п.
Реально оно не является менеджером. Но, при изменении справочников другим пользователем,
если окно открыто, его тоже надо обновлять, я так думаю...

Что касается механизма сообщений -- тут мне сложно что-то сказать.
Я видел не очень много кода, но почему-то нигде кроме GUI-библиотек (LCL, fpGUI)
не использовались message.
...
Похоже что "из коробки" нет ни очереди сообщений, ни broadcast-рассылки,
для этого используются возможности ОС (SendMessage/PostMessage под Win32).

Просто я видел в коде какие-то сообщения не WM_*.
Меня это навело на мысль, что должен быть какой-то свой кроссплатформенный
механизм сообщений. Или "оболочка" для них над средствами конкретной ОС.

Если использовать её где попало без особой необходимости,
в конце концов объекты на каждый чих будут тревожить всю систему.

Да, об этом я не подумал. Получается, что широковещательная рассылка точно не подходит.

Timid писал(а):При подписке вы передаете в метод имя таблицы и ссылку на
обработчик, метод возвращает вам индекс. Его сохраняете в своей форме-окне.

Да, идея с занесением имени таблицы хорошая. Буду думать, как сделать.
Мне бы это позволило отделить "слой данных" от диспетчера.
И о назначении таблиц, тогда знали бы только менеджеры.

Сейчас я сделал достаточно тупо: есть объект, который, обрабатывая событие
"Диспетчера обновления таблиц", по имени таблицы выбирает действие:
Код: Выделить всё
// Тип события, создаваемого, при обновлении сущности.
TEntityUpdateEvent = procedure(const entity_id: TTableRecordID;
  const update_type: TUpdateType);
// Тип события, создаваемого, при обновлении справочника.
TInfoUpdateEvent = procedure(const table_name: string;
  const update_type: TUpdateType);

// Диспетчер обновлений.
TDBUpdateDispatcher = class(TObject)
private
  FTRDispatcher: TTablesRefreshDispatcher;
protected
public
  procedure Refresh();
  property Active: boolean read GetActive write SetActive;
public
  property OnClientUpdate: TEntityUpdateEvent read FOnClientUpdate
    write FOnClientUpdate;
  property OnCarUpdate: TEntityUpdateEvent read FOnCarUpdate
    write FOnCarUpdate;
  property OnContractUpdate: TEntityUpdateEvent read FOnContractUpdate
    write FOnContractUpdate;
  property OnUserUpdate: TEntityUpdateEvent read FOnUserUpdate
    write FOnUserUpdate;
  property OnInfosUpdate: TInfoUpdateEvent read FOnInfosUpdate
    write FOnInfosUpdate;
end;


В принципе, гораздо лучше сделать, чтобы менеджеры сами регистрировали обработчик события.

Сама схемка оповещения достаточно проста:
1.) При обновлении таблицы, триггер на сервере пишет имя таблицы, ключ, время и
тип обновления (вставка/изменение/удаление) в таблицу change_log.
2.) Объект "Диспетчер обновления таблиц" читает эту таблицу раз в n секунд.
3.) Производит обработку, в результате которой получается список обновлённых таблиц.
Т.е. выделяется последняя по времени запись с данным ID для полученной
таблицы и её время сравнивается с сохранённым.
4.) Во время второго прохода, создаются события для каждой обновлённой записи.
5.) Диспетчер обновления создаёт события по типам таблиц.
Для справочников записи не учитываются, а событие создаётся для всей таблицы.
6.) Менеджеры обрабатывают события, подходящие для них.
7.) Менеджеры оповещают остальные окна, которые требуют обновления.

Код: Выделить всё
// Тип события, создаваемого, при обновлении таблицы.
TTableUpdateEvent = procedure(const table_name: string;
  const update_type: TUpdateType; const record_id: TTableRecordID);

// Диспетчер обновления таблиц.
TTablesRefreshDispatcher = class(TObject)
public
  procedure ReadSettings();
  procedure Refresh();
public
  property Active: boolean read GetActive write SetActive;
  property OnUpdate: TTableUpdateEvent read FOnUpdate write FOnUpdate;
end;


Climber писал(а):Буквально в прошлый четверг делал для себя такую штуку (шаблон Publisher - Subscriber) называется.

О том я и думал, вначале.
Но сейчас решил немного по-другому, поскольку:
1.) У меня иерархия вызовов менеджеров.
2.) Главное окно - одновременно менеджер.

Главное окно обрабатывает события.
При необходимости, оповещает другие менеджеры/окна.
Если менеджер вызывает подчинённый менеджер, то главный оповещает подчинённого.

Добавлено спустя 1 минуту 55 секунд:
Конечно, может, с этим тоже проблемы будут...
А.Н.
постоялец
 
Сообщения: 230
Зарегистрирован: 13.03.2010 12:23:58

Re: Несколько обработчиков одного события

Сообщение Climber » 17.05.2010 13:05:58

А.Н. писал(а):2.) Главное окно - одновременно менеджер.

У меня тоже так было раньше. Я думал: "у меня совсем простенькая программа, зачем мне MVC и прочие страшные слова". Теперь я все переделываю в очередной раз на "как правильно" (этот раз правильнее предыдущего, и наверняка будет продолжение 8) ).Теперь все "менеджеры" - это мои классы (потомки Publisher'a), а главный "менеджер" - TDataModule (и то только потому, что на нем удобно размещать компоненты доступа к БД).
Реально избавился от такой кучи геморроя, что уже не представляю, как у меня раньше все работало :roll:
Кстати, если логика в отдельном объекте, его удобно тестировать с помощью unit-тестов.

Основные идеи моего кода:
TStringList как хранитель таблицы подписок - потому что в этом классе реализовано уже все, что надо (можно хранить и искать строки, объекты, добавлять, удалять и много чего еще).
Подписка с помощью интерфейсов, а не объектов - как следствие, подписчиком может стать кто угодно, хоть любой класс LCL, хоть свой объект.
Climber
постоялец
 
Сообщения: 415
Зарегистрирован: 03.06.2007 20:09:57
Откуда: Москва

Re: Несколько обработчиков одного события

Сообщение А.Н. » 19.05.2010 22:32:25

У меня тоже так было раньше.

Да я специально перенёс "менеджер договоров" в главное окно.

Я думал: "у меня совсем простенькая программа, зачем мне MVC и прочие страшные слова".

Ну, про MVC я думал... Но поздно. :-(

Теперь я все переделываю в очередной раз на "как правильно" (этот раз правильнее предыдущего, и наверняка будет продолжение 8) ).

Хм... В принципе, а что я парюсь? Если не пойдёт, то и так сойдёт, как есть. А, если пойдёт, тогда и буду обновлять и менять. :-\

Теперь все "менеджеры" - это мои классы (потомки Publisher'a), а главный "менеджер" - TDataModule (и то только потому, что на нем удобно размещать компоненты доступа к БД).

Интересно что за Publisher..?
Мой "менеджер" - интерфейсный модуль, для работы с сущностью. Т.е., чисто интерфейсная "единица".
Потомок от TAbstractManager, который потомок TForm.

TStringList как хранитель таблицы подписок - потому что в этом классе реализовано уже все, что надо (можно хранить и искать строки, объекты, добавлять, удалять и много чего еще).
Подписка с помощью интерфейсов, а не объектов - как следствие, подписчиком может стать кто угодно, хоть любой класс LCL, хоть свой объект.

Вначале мне казалось, что в TStringList, всё-таки много лишнего для этой задачи.
Но, в общем-то, TStringList здесь как-раз к месту.
А.Н.
постоялец
 
Сообщения: 230
Зарегистрирован: 13.03.2010 12:23:58

Re: Несколько обработчиков одного события

Сообщение Timid » 20.05.2010 08:33:18

Не пользуйтесь объектами не предназначенными для этих целей! StringList предназначен для работы со списками строк и его очень важно правильно высвобождать при использовании дополнительных объектов. Будут утечки памяти.
Лучше использовать динамические массивы с записями собственной структуры - их корректно отрабатывает менеджер памяти.
Timid
постоялец
 
Сообщения: 290
Зарегистрирован: 21.11.2007 21:33:15

Re: Несколько обработчиков одного события

Сообщение Climber » 20.05.2010 09:11:15

Timid писал(а):Не пользуйтесь объектами не предназначенными для этих целей! StringList предназначен для работы со списками строк и его очень важно правильно высвобождать при использовании дополнительных объектов. Будут утечки памяти.
"Ты просто не умеешь их готовить!" (с)
Утечек не будет. Объект-подписчик создается и уничтожается совсем в другом месте, а StringList только хранит ссылку на него. И таки для хранения ссылок он вполне предназначен.
А.Н. писал(а):Интересно что за Publisher..?

Что такое MVC знаешь? А это "такой же, только зеленый".
Шаблон проектирования Publisher. Тут я правда не очень хорошо разбираюсь в терминологии. Иногда о Publisher'e и Subscriber'e говорят как будто бы о двух шаблонах, но по сути это две стороны одной медали. Один подписчик, другой издатель.Один вещает, второй слушает. Моя собственная реализация Publisher'a приведена в моем первом посте в этом топике.
Я создаю датамодуль и главную форму автоматически с помощью Application.CreateForm , а далее делаю так:
Создаю объект (потомок Publisher'a), который будет реализовывать логику. Например, в моей программе нужно реализовать подключение к базе, на которое дается три попытки. А еще мне надо иметь возможность принудительно отключать пользователей от базы. Я это делаю централизованно, через запись в таблицу определенной команды. Раз в минуту таблица должна проверяться.
Так вот, в своем объекте, который управляет подключением, я сделал три процедуры, которые принимают на вход 1 параметр типа Tobject.
В датамодуле создаю этот объект и пишу потом:
Код: Выделить всё
MainForm.OKButton.OnClick:=@MyObject.Procedure1;
MainForm.CancelButton.OnClick:=@MyObject.Procedure2;
DataModule.Timer.OnTimer:=@MyObject.Procedure3;

Потом еще подписываю главную форму на сообщения от этого объекта.
Таким образом главное окно вообще никак не видит этот объект, однако при клике на кнопке OK этот объект посылает на сервер логин и пароль и подключается. При нажатии на "отмену" программа закрывается и т. д. При этом главное окно имеет всего 4 процедуры, которые уведомляют пользователя:
1) о том, что он подключился
2) что пароль неправильный
3) что на сервере началась профилактика
4) что профилактика закончилась и можно продолжать работу.
И что самое главное, с помощью fpcunit можно написать тест для этого объекта и тестировать его отдельно от формы и датамодуля. При этом, если он пройдет тест (и тест будет правильно написан 8) ), то и в боевых условиях он будет работать.
Climber
постоялец
 
Сообщения: 415
Зарегистрирован: 03.06.2007 20:09:57
Откуда: Москва

Re: Несколько обработчиков одного события

Сообщение А.Н. » 20.05.2010 09:58:59

Timid писал(а):Не пользуйтесь объектами не предназначенными для этих целей!
StringList предназначен для работы со списками строк и его очень важно
правильно высвобождать при использовании дополнительных объектов.
Будут утечки памяти.
Лучше использовать динамические массивы с записями собственной структуры -
их корректно отрабатывает менеджер памяти.

1. С динамическими массивами сложнее работать.
Каждый раз нужно проверять размер.
А, если, например, произошло удаление (отписка объекта) из середины...
Имеется несколько вариантов реализации. Короче, сложности лишние.
Список как-раз подходит для такой задачи.
2. Наверное, вариант со списком требует меньше памяти
(хотя это и не принципиально), поскольку нужно хранить только строки и адреса
объектов. В случае массивов пришлось бы хранить структуры, состоящие из строк,
адресов объектов и т.д. Либо вариант с массивом будет намного сложнее.
Но не знаю точно.
3. Здесь нужен список, который не управляет объектами, а только хранит адреса.
Как раз, TStringList. Причём, идентификация производится по строкам.

Минусы, конечно, тоже есть:
1. Names/Values. Тут это явно лишнее.
2. Есть некоторая избыточность, поскольку, названия событий могут
дублироваться (в реализации Climber).
3. Общий минус - это то, что объект, который подписался, должен сам отписаться,
при уничтожении. Если, например, он динамический, - это имеет значение.
Или нужно использовать какие-то хитрые механизмы для проверки существования.
Ну, если подпиской не управляет какой-то другой объект.

climber писал(а):Что такое MVC знаешь? А это "такой же, только зеленый".
Шаблон проектирования Publisher. Тут я правда не очень хорошо разбираюсь в
терминологии. Иногда о Publisher'e и Subscriber'e говорят как будто бы о двух
шаблонах, но по сути это две стороны одной медали. Один подписчик, другой
издатель.Один вещает, второй слушает. Моя собственная реализация Publisher'a
приведена в моем первом посте в этом топике.

Я извиняюсь, тут я притормозил. У вас TPublisher и называется. %-)
Короче, шаблон Publisher/Subscriber.
Теперь понятно.
А.Н.
постоялец
 
Сообщения: 230
Зарегистрирован: 13.03.2010 12:23:58


Вернуться в Lazarus

Кто сейчас на конференции

Сейчас этот форум просматривают: Google [Bot] и гости: 45

Рейтинг@Mail.ru