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

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 на слабых ссылках + перегрузка через структуру TAutoPublisherDatas.pas — список нарисованных обектов + евент интерфейс при изменении этого спискаDrawForm.pas — класс реализующий форму на которой можно рисовать. Там же происходит подписка на евенты.HiddenForm.pas — скрытая главная форма (нужна лишь для того чтобы Application крутил оконный цикл)ну и файл проекта чуть-чуть изменен (там создаются формы на которых можно рисовать)

© Habrahabr.ru