Трюки с интерфейсами в Delphi

от автора

Приветствую.
Буквально сегодня обсуждал с коллегой по работе интерфейсы. Он мне рассказал о своем интересном приеме, я ему о своем, но только по дороге домой я осознал всю мощь этих приемов, в особенности если объединить их вместе.
Любители удобной автоматики и MVC паттернов — прошу под кат.

Трюк 1. Умные Weak ссылки

Для тех кто не в курсе — Weak (слабые) ссылки — ссылки, не увеличивающие счетчик. Допустим у нас есть дерево:

INode = interface   function GetParent: INode;   function ChildCount: Integer;   function GetChild(Index: Integer): INode; end; 

Если бы внутри класса, реализующего интерфейс INode родитель и потомки хранились бы так:

TNode = class(TInterfacedObject, INode) private   FParent: INode;   FChild: array of INode; end; 

то дерево бы никогда не уничтожилось. Родитель держит ссылки на детей (и тем самым увеличивает им счетчик), а дети на родителя. Это классическая проблема циклических ссылок, и в этом случае прибегают к weak ссылкам. В новых XE делфях можно написать так:

TNode = class(TInterfacedObject, INode) private   [weak] FParent: INode;   FChild: array of INode; end; 

а в старых — хранят Pointer:

TNode = class(TInterfacedObject, INode) private   FParent: Pointer;   FChild: array of INode; end; 

Это позволяет обойти автоинкремент счетчиков, и теперь если мы потеряем указатель на родителя — все дерево прибьется, что и требовалось получить.

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

Но есть очень элегантное решение. И вот как это работает. Мы пишем интерфейс weak ссылки и класс, реализующий его:

  IWeakRef = interface     function IsAlive: Boolean;     function Get: IUnknown;   end;    TWeakRef = class(TInterfacedObject, IWeakRef)   private     FOwner: Pointer;   public     procedure _Clean;     function IsAlive: Boolean;     function Get: IUnknown;   end;  procedure TWeakRef._Clean; begin   FOwner := nil; end;  function TWeakRef.Get: IUnknown; begin   Result := IUnknown(FOwner); end;  function TWeakRef.IsAlive: Boolean; begin   Result := Assigned(FOwner); end; 

Тут обычный typecast до Pointer-а. Именно та weak ссылка, о которой я рассказывал выше. Но ключевой метод — IsAlive, который возвращает True — если объект на который ссылается weak ссылка — еще существует. Осталось только понять как красиво почистить FOwner.
Пишем интерфейс:

  IWeakly = interface   ['{F1DFE67A-B796-4B95-ADE1-8AA030A7546D}']     function WeakRef: IWeakRef;   end; 

который возвращает weak ссылку и пишем класс, реализующий этот интерфейс:

  TWeaklyInterfacedObject = class(TInterfacedObject, IWeakly)   private     FWeakRef: IWeakRef;   public     function WeakRef: IWeakRef;     destructor Destroy; override;   end;  destructor TWeaklyInterfacedObject.Destroy; begin   inherited;   FWeakRef._Clean; end;  function TWeaklyInterfacedObject.WeakRef: IWeakRef; var obj: TWeakRef; begin   if FWeakRef = nil then    begin     obj := TWeakRef.Create;     obj.FOwner := Self;     FWeakRef := obj;   end;   Result := FWeakRef; end; 

Мы просто добавили метод, раздающий всем одну weak ссылку. А поскольку сам объект всегда знает о своей weak ссылке — он просто чистит её в своем деструкторе. Осталось теперь только наследоваться от TWeaklyInterfacedObject вместо TInterfacedObject, и все. Никаких больше unsafe приведений типов, выстрелов в ногу, и нецензурной брани.

Трюк 2. Механизм подписчиков

Если вы еще не велосипедили систему плагинов в делфи и не использовали MVC паттернов — то вы счастливчик. В делфи все события — это просто один или два указателя на функцию(и инстанс). Поэтому если вы создали класс, сделали ему OnBlaBla свойство — то только кто-то один может узнать, что этот самый BlaBla наконец то произошел. Посему все начинают пилить свой механизм подписок, и часто тонут в отладке этих самых подписок.
События основанные на интерфейсах обычно реализуют так. Делают отдельный евент интерфейс, к примеру:

IMouseEvents = interface   procedure OnMouseMove(...);   procedure OnMouseDown(...);   procedure OnMouseUp(...); end; 

и передают его, вместо классического procedure of object; например в пару Subscribe/Unsubscribe методов:

IForm = interface   procedure SubscribeMouse(const subscriber: IMouseEvents);   procedure UnsubscribeMouse(const subscriber: IMouseEvents); end; 

Когда код разрастается, а интерфейс IMouseEvents чуть-чуть меняется (например добавили метод) — начинает сильно напрягать рефакторинг. Например один и тот же IMouseEvents используется в IForm, IButton, IImage и прочей нечисти. Везде надо правильно поправить подписку, добавить обход по подписчикам и т.п.
Я использую следующий трюк. Пишем интерфейс:

  IPublisher = interface   ['{CDE9EE5C-021F-4942-A92A-39FC74395B4B}']     procedure Subscribe  (const ASubscriber: IUnknown);     procedure Unsubscribe(const ASubscriber: IUnknown);   end; 

Класс который будет реализовывать этот интерфейс (пусть это будет TBasePublisher) умеет только добавлять и удалять из списка какие-то интерфейсы. В дальнейшем мы пишем классы, которые я называю броадкастеры. Вот у нас есть евент интерфейс:

  IGraphEvents = interface   ['{2C7EF06A-2D63-4F25-80BC-7BA747463DB6}']     procedure OnAddItem(const ASender: IGraphList; const AItem: TGraphItem);     procedure OnClear(const ASender: IGraphList);   end; Мы наследуемся от TBasePublisher и реализуем вот такой броадкастер:   TGraphEventsBroadcaster = class(TBasePublisher, IGraphEvents)   private     procedure OnAddItem(const ASender: IGraphList; const AItem: TGraphItem);     procedure OnClear(const ASender: IGraphList);   end;  procedure TGraphEventsBroadcaster.OnAddItem(const ASender: IGraphList; const AItem: TGraphItem); var arr: TInterfacesArray;     i: Integer;     ev: IGraphEvents; begin   arr := GetItems;   for i := 0 to Length(arr) - 1 do       if Supports(arr[i], IGraphEvents, ev) then ev.OnAddItem(ASender, AItem); end;  procedure TGraphEventsBroadcaster.OnClear(const ASender: IGraphList); var arr: TInterfacesArray;     i: Integer;     ev: IGraphEvents; begin   arr := GetItems;   for i := 0 to Length(arr) - 1 do       if Supports(arr[i], IGraphEvents, ev) then ev.OnClear(ASender); end; 

то есть сам броадкастер у нас реализует евент интерфейс, и в реализации просто рассылает всем подписчикам тот же евент. Преимущество — все реализовано в одном месте, оно не скомпилируется если вы хоть немного поменяете IGraphEvents. Теперь зоопарк IForm, IButton, IImage просто создают внутри себя TGraphEventsBroadcaster и вызывают его методы, как будто у IForm всего один подписчик.

Трюк 3. Умные Weak ссылки + механизм подписчиков

Но все что я описал выше про подписчиков — плохо. Дело в том, что тут сплошь и рядом будут циклические ссылки, вы замахаетесь разбираться с порядком финализации и отписыванием. Вы добавите слабые ссылки, но погрязнете в отладке мусорных ссылок. Вот тут то и пригодятся умные слабые ссылки, описанные в самом начале. Мы просто пишем вот такой интерфейс издателя (который принимает IWeakly из начала статьи):

  IPublisher = interface   ['{CDE9EE5C-021F-4942-A92A-39FC74395B4B}']     procedure Subscribe  (const ASubscriber: IWeakly);     procedure Unsubscribe(const ASubscriber: IWeakly);   end; 

Внутри себя издатель TBasePublisher хранит массив слабых ссылок TWeakRefArr = array of IWeakRef;

  TBasePublisher = class(TInterfacedObject, IPublisher)   private     FItems: TWeakRefArr;   protected     function GetItems: TWeakRefArr;   public     procedure Subscribe  (const ASubscriber: IWeakly);     procedure Unsubscribe(const ASubscriber: IWeakly);   end; 

А броадкастер теперь только проверяет слабую ссылку на жизнеспособность, получает нормальную, и направляет евент в неё. Броадкастер поменялся вот так:

procedure TGraphEventsBroadcaster.OnAddItem(const ASender: IGraphList; const AItem: TGraphItem); var arr: TWeakRefArr;     i: Integer;     ev: IGraphEvents; begin   arr := GetItems;   for i := 0 to Length(arr) - 1 do     if IsAlive(arr[i]) then       if Supports(arr[i].Get, IGraphEvents, ev) then ev.OnAddItem(ASender, AItem); end;  procedure TGraphEventsBroadcaster.OnClear(const ASender: IGraphList); var arr: TWeakRefArr;     i: Integer;     ev: IGraphEvents; begin   arr := GetItems;   for i := 0 to Length(arr) - 1 do     if IsAlive(arr[i]) then       if Supports(arr[i].Get, IGraphEvents, ev) then ev.OnClear(ASender); end; 

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

Трюк 4. Перегрузка в помощь

Последний штрих:

  TAutoPublisher = packed record     Publisher: IPublisher;     class operator Add(const APublisher: TAutoPublisher; const ASubscriber: IWeakly): Boolean;     class operator Subtract(const APublisher: TAutoPublisher; const ASubscriber: IWeakly): Boolean;   end;  class operator TAutoPublisher.Add(const APublisher: TAutoPublisher;   const ASubscriber: IWeakly): Boolean; begin   APublisher.Publisher.Subscribe(ASubscriber);   Result := True; end;  class operator TAutoPublisher.Subtract(const APublisher: TAutoPublisher;   const ASubscriber: IWeakly): Boolean; begin   APublisher.Publisher.Unsubscribe(ASubscriber);   Result := True; end; 

Я думаю он понятен без слов. Мы просто делаем MyForm.MyEvents + MySubscriber; — мы подписались. Вычли: MyForm.MyEvents — MySubscriber; — отписались.

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

IntfEx.pas — реализация умных слабых ссылок, базового класса издателя TBasePublisher на слабых ссылках + перегрузка через структуру TAutoPublisher
Datas.pas — список нарисованных обектов + евент интерфейс при изменении этого списка
DrawForm.pas — класс реализующий форму на которой можно рисовать. Там же происходит подписка на евенты.
HiddenForm.pas — скрытая главная форма (нужна лишь для того чтобы Application крутил оконный цикл)
ну и файл проекта чуть-чуть изменен (там создаются формы на которых можно рисовать)

ссылка на оригинал статьи http://habrahabr.ru/post/219685/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *