![](http://habrastorage.org/getpro/habr/post_images/f3d/d8f/d80/f3dd8fd80c60f6e7dbd37dfccd49cdae.jpg)
Буквально сегодня обсуждал с коллегой по работе интерфейсы. Он мне рассказал о своем интересном приеме, я ему о своем, но только по дороге домой я осознал всю мощь этих приемов, в особенности если объединить их вместе.
Любители удобной автоматики и 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/
Добавить комментарий