Pull to refresh

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

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

Идея weak ссылок была придумана Дмитрием Ильиных из Maxidix s.r.o. и доработана мной.

upd. Начиная с вресии Delphi 10.1 ввели полноценные [weak] ссылки. Под капотом они реализованы по аналогии с вышеописанными weak ссылками, и автоматически превращаются в nil если интерфейс сливается. Подробнее тут: habr.com/ru/post/282035
Tags:
Hubs:
+19
Comments 26
Comments Comments 26

Articles