После этого добавьте интерфейс в класс

Можно ли добавить и реализовать интерфейс к уже существующему классу (который является потомком TInterfaced или TInterfacedPersistent), чтобы выполнить разделение модели и представления на 2 единицы?

Небольшое объяснение, почему мне нужно что-то вроде этого:

Я разрабатываю древовидную структуру открытого типа, которая имеет следующую структуру (ОЧЕНЬ упрощенная и неполная, просто для иллюстрации контура проблемы):

Database_Kernel.pas

TVMDNode = class(TInterfacedPersistent);
public
  class function ClassGUID: TGUID; virtual; abstract; // constant. used for RTTI

  property RawData: TBytes {...};
  constructor Create(ARawData: TBytes);

  function GetParent: TVMDNode;
  function GetChildNodes: TList<TVMDNode>;
end;

Vendor_Specific_Stuff.pas

TImageNode = class(TVMDNode)
public
  class function ClassGUID: TGUID; override; // constant. used for RTTI

  // Will be interpreted out of the raw binary data of the inherited class
  property Image: TImage {...};
end;

TUTF8Node = class(TVMDNode)
public
  class function ClassGUID: TGUID; override; // constant. used for RTTI

  // Will be interpreted out of the raw binary data of the inherited class
  property StringContent: WideString {...};
end;

TContactNode = class(TVMDNode)
public
  class function ClassGUID: TGUID; override; // constant. used for RTTI

  // Will be interpreted out of the raw binary data of the inherited class
  property PreName: WideString {...};
  property FamilyName: WideString {...};
  property Address: WideString {...};
  property Birthday: TDate {...};
end;

Используя RTTI с использованием GUID (который использует ClassGUID), функция GetChildNodes может найти соответствующий класс и инициализировать его необработанными данными. (Каждый набор данных содержит ClassGUID и RawData рядом с другими данными, такими как созданные/обновленные временные метки)

Важно заметить, что мой API (Database_Kernel.pas) строго отделен от классов node поставщика (Vendor_Specific_Stuff.pas).


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

Следующие идеи работают:

IGraphicNode = interface(IInterface)
  function Visible: boolean;
  function Icon: TIcon;
  function UserFriendlyName: string;
end;

Относящиеся к поставщику потомки TVMDNode в Vendor_Specific_Stuff.pas будут реализовывать интерфейс IGraphicNode.

Но поставщику также необходимо изменить Database_Kernel.pas для реализации IGraphicNode в базовый класс node TVMDNode (который используется для "неизвестных" узлов, где RTTI не удалось найти соответствующий класс набора данных, поэтому, по крайней мере, двоичные исходные данные могут быть прочитаны с помощью TVMDNode.RawData).

Поэтому он изменит мой класс следующим образом:

TVMDNode = class(TInterfacedPersistent, IGraphicNode);
public
  property RawData: TBytes {...};
  class function ClassGUID: TGUID; virtual; abstract; // constant. used for RTTI
  constructor Create(ARawData: TBytes);
  function GetParent: TVMDNode;
  function GetChildNodes: TList<TVMDNode>;

  // --- IGraphicNode
  function Visible: boolean; virtual; // default behavior for unknown nodes: False
  function Icon: TIcon; virtual; // default behavior for unknown nodes: "?" icon
  function UserfriendlyName: string; virtual; // default behavior for unknown nodes: "Unknown"
end;

Проблема заключается в том, что IGraphicNode зависит от поставщика/программы и не должен находиться в API Database_Kernel.pas, так как GUI и Model/API должны быть строго разделены.

Мое желание состояло в том, что interace IGraphicNode можно было бы добавить и реализовать в существующий класс TVMDNode (который уже является потомком TInterfacedPersistent для разрешения интерфейсов) в отдельном модуле. Насколько мне известно, Delphi не поддерживает что-то вроде этого.

Помимо того, что смешивать модель и представление в одном отдельном модуле/классе нехорошо, будет существовать следующая реальная проблема: если поставщик должен изменить мой API Database_Kernel.pas для расширения TVMDNode с помощью IGraphicNode, ему необходимо повторно выполнить все его изменения, как только я выпущу новую версию своего API Database_Kernel.pas.

Что мне делать? Я очень долго думал о возможных решениях с Delphi OOP. Обходной путь может вставлять TVMDNode в класс контейнера, который имеет вторичный RTTI, поэтому после того, как я нашел класс TVMDNode, я мог бы искать класс TVMDNodeGUIContainer. Но это звучит очень душно и как грязный хак.

PS: Этот API представляет собой проект OpenSource/GPL. Я стараюсь оставаться совместимым со старыми поколениями Delphi (например, 6), так как я хочу максимизировать число возможных пользователей. Однако, если решение проблемы выше возможно только с новым поколением языков Delphi, я мог бы рассмотреть возможность отмены поддержки Delphi 6 для этого API.

Ответ 1

Вы можете сохранить возможность сохранения данных и реализовать их через наследование и по-прежнему создавать правильные экземпляры для ClassGUID, хранящихся в таблицах, если вы примените factory шаблон дизайна.

Для каждого класса node должен быть один класс factory (или просто указатель на функцию), ответственный за создание правильного класса Delphi. Заводы класса могут регистрироваться в секции инициализации устройства (один раз при запуске приложения) на объекте singleton ядра.

Ядро singleton затем сопоставило бы GUID, чтобы исправить factory, который в свою очередь вызовет правильный конструктор экземпляра класса (как показано на http://delphipatterns.blog.com/2011/03/23/abstract-factory)

Пакеты могут быть разделены на отдельные библиотеки DLL и классы, реализованные в отдельных единицах, все еще наследуемых от одного базового класса TVMNode.

Функции, которые вы используете RTTI для, могут быть легко доступны в классах потомков или в классах factory с помощью некоторых виртуальных методов.

Вы также можете использовать более простой Объекты передачи данных для сохранения/загрузки TVMNodes и, возможно, вдохнуть вдохновение в уже хорошо воспринимаемый Объект Relational Mapper или Объектно-ориентированная структура как проблема, которую вы пытаются решить, как мне кажется, именно те проблемы, с которыми они справляются (уже)

Я не знаю о хороших фреймах с открытым исходным кодом Delphi этого класса. Но с других языков вы можете посмотреть Java Hibernate, Microsoft.NET Entity Framework или минималистичный Сериализатор буферов протоколов Google

enter image description here

Ответ 2

Да, это возможно.

Мы реализовали нечто подобное, чтобы получить контроль над глобальными/синглтонами для целей тестирования. Мы изменили наши синглтоны на доступность в качестве интерфейсов приложения (не TApplication, наш собственный эквивалент). Затем мы добавили возможность динамически добавлять/удалять интерфейсы во время выполнения. Теперь наши тестовые примеры могут подключать подходящие макеты по мере необходимости.

Я опишу общий подход, надеюсь, вы сможете применить его к особенностям вашей ситуации.

  • Добавить поле для хранения списка динамически добавленного интерфейса. TInterfaceList работает красиво.
  • Добавить методы добавления/удаления динамических интерфейсов.
  • Переопределить function QueryInterface(const IID: TGUID; out Obj): HResult; virtual;. Ваша реализация сначала проверит список интерфейсов, и если не найден, будет отложена базовая реализация.

Изменить: Пример кода

Чтобы ответить на ваш вопрос:

Я понимаю, что теперь класс может сказать другим, что он теперь поддерживает интерфейс X, поэтому интерфейс был добавлен во время выполнения. Но мне также нужно ОСУЩЕСТВИТЬ методы интерфейса извне (другое устройство). Как это делается?

При добавлении интерфейса вы добавляете экземпляр объекта, который реализует интерфейс. Это очень похоже на обычное свойство ... реализует <interface> чтобы делегировать реализацию интерфейса другому объекту. Главное различие заключается в динамическом. Таким образом, он будет иметь такие же ограничения: например. нет доступа к "хозяину", если явно не указана ссылка.

Следующий тестовый пример DUnit демонстрирует упрощенную версию метода в действии.

unit tdDynamicInterfaces;

interface

uses
  SysUtils,
  Classes,
  TestFramework;

type
  TTestDynamicInterfaces = class(TTestCase)
  published
    procedure TestUseDynamicInterface;
  end;

type
  ISayHello = interface
    ['{6F6DDDE3-F9A5-407E-B5A4-CDF91791A05B}']
    function SayHello: string;
  end;

implementation

{ ImpGlobal }

type
  TDynamicInterfaces = class(TInterfacedObject, IInterface)
  { We must explicitly state that we are implementing IInterface so that
    our implementation of QueryInterface is used. }
  private
    FDynamicInterfaces: TInterfaceList;
  protected
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AddInterface(AImplementedInterface: IInterface);
  end;

type
  TImplementor = class (TInterfacedObject, ISayHello)
  { NOTE: This could easily have been implemented in a separate unit. }
  protected
    {ISayHello}
    function SayHello: string;
  end;

{ TDynamicInterfaces }

procedure TDynamicInterfaces.AddInterface(AImplementedInterface: IInterface);
begin
  { The simplest, but least flexible approach (see also QueryInterface).
    Other options entail tagging specific GUIDs to be associated with given
    implementation instance. Then it becomes feasible to check for duplicates
    and also dynamically remove specific interfaces. }
  FDynamicInterfaces.Add(AImplementedInterface);
end;

constructor TDynamicInterfaces.Create;
begin
  inherited Create;
  FDynamicInterfaces := TInterfaceList.Create;
end;

destructor TDynamicInterfaces.Destroy;
begin
  FDynamicInterfaces.Free;
  inherited Destroy;
end;

function TDynamicInterfaces.QueryInterface(const IID: TGUID; out Obj): HResult;
var
  LIntf: IInterface;
begin
  { This implementation basically means the first implementor added will be 
    returned in cases where multiple implementors support the same interface. }
  for LIntf in FDynamicInterfaces do
  begin
    if Supports(LIntf, IID, Obj) then
    begin
      Result := S_OK;
      Exit;
    end;
  end;

  Result := inherited QueryInterface(IID, Obj);
end;

{ TImplementor }

function TImplementor.SayHello: string;
begin
  Result := 'Hello. My name is, ' + ClassName;
end;

{ TTestDynamicInterfaces }

procedure TTestDynamicInterfaces.TestUseDynamicInterface;
var
  LDynamicInterfaceObject: TDynamicInterfaces;
  LInterfaceRef: IUnknown;
  LFriend: ISayHello;
  LActualResult: string;
begin
  LActualResult := '';

  { Use ObjRef for convenience to not declare interface with "AddInterface" }
  LDynamicInterfaceObject := TDynamicInterfaces.Create;
  { But lifetime is still managed by the InterfaceRef. }
  LInterfaceRef := LDynamicInterfaceObject;

  { Comment out the next line to see what happens when support for 
    interface is not dynamically added. }
  LDynamicInterfaceObject.AddInterface(TImplementor.Create);

  if Supports(LInterfaceRef, ISayHello, LFriend) then
  begin
    LFriend := LInterfaceRef as ISayHello;
    LActualResult := LFriend.SayHello;
  end;

  CheckEqualsString('Hello. My name is, TImplementor', LActualResult);
end;

end.