Приветствую.
Буквально сегодня обсуждал с коллегой по работе интерфейсы. Он мне рассказал о своем интересном приеме, я ему о своем, но только по дороге домой я осознал всю мощь этих приемов, в особенности если объединить их вместе.
Любители удобной автоматики и 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 крутил оконный цикл)
ну и файл проекта чуть-чуть изменен (там создаются формы на которых можно рисовать)
Автор: MrShoor