在类之后添加接口

12
能否在已有的一个继承自 TInterfacedTInterfacedPersistent 的类中添加和实现一个接口,以此将模型和视图分开为两个单元?我需要这样做的一个简要解释是:我正在开发一个树形结构的开放式模型,其结构如下(非常简化和不完整,只是为了说明问题的概要):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;

厂商特定的东西.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;

使用基于 GUID 的运行时类型信息(RTTI)(使用 ClassGUID),函数GetChildNodes能够找到匹配的类并使用原始数据进行初始化。(每个数据集都包含ClassGUIDRawData,以及其他数据,如创建/更新时间戳)。

重要的是要注意,我的API(Database_Kernel.pas)严格与供应商的节点类(Vendor_Specific_Stuff.pas)分开。


供应商特定程序的GUI希望可视化节点,例如为它们提供用户友好的名称、图标等。

以下思路可行:

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

Vendor_Specific_Stuff.pasTVMDNode 的特定子类将实现 IGraphicNode 接口。

但是厂商还需要更改 Database_Kernel.pas,将 IGraphicNode 实现到基础节点类 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应该严格分离。
我的愿望是将接口IGraphicNode添加和实现到现有的TVMDNode类中(它已经是TInterfacedPersistent的子类,以允许接口),并放入一个单独的单元中。据我所知,Delphi不支持这样的方法。
除了在一个单一的单元/类中混合Model和View之外,还存在以下真实问题:如果供应商必须更改我的Database_Kernel.pas API以扩展TVMDNodeIGraphicNode接口,则他需要重新进行所有更改,一旦我发布了新版本的API Database_Kernel.pas
我该怎么办?我考虑了很久使用Delphi OOP可能的解决方案。一种解决方法可能是将TVMDNode嵌套到一个容器类中,该容器类具有次要的RTTI,因此在找到TVMDNode类之后,我可以搜索TVMDNodeGUIContainer类。但这听起来非常奇怪,像个肮脏的hack。
PS:这个API是一个OpenSource/GPL项目。我正在尝试保持与旧一代Delphi(例如6)的兼容性,因为我想最大化可能的用户数量。然而,如果解决上述问题的方法只能通过新一代的Delphi语言实现,我可能会考虑放弃对此API的Delphi 6支持。

3
我不知道为什么有人给你负评,但我觉得你的问题很合理。感谢你发布了一些有趣的内容,我给你点赞。 - apxcode
@FunctionR 我也曾经想过这个问题,因为通常只有糟糕的研究才会被投下反对票。也许我的问题太长了(然而,标题用一句话描述了它)。我倾向于解释为什么我需要某些东西,以避免出现“你为什么需要它?没有人需要那样做。”这样的评论。 - Daniel Marschall
1
供应商无法从TVMDNode派生出新类吗? - Sertac Akyuz
1
@rinn - 我不明白将 TGraphicNode = class(TVMDNode) 放在 'vendor_specific_stuff.pas' 中和将 IGraphicNode = interface(IInterface)TVendorVMDNode = class(TVMDNode, IGraphicNode) 放在 'vendor_specific_stuff.pas' 中的区别。可能是我弄丢了,你不必在意。 - Sertac Akyuz
@SertacAkyuz,感谢您抽出时间来查看这个(有点复杂)的问题。您提供的两种可能性都是可行的。可以使用接口IGraphicNode或使用共享派生类TGraphicVMDNode。这两种方法都适用于所有供应商的扩展。唯一的问题是当TVMDNode.GetChildNodes中的RTTI算法无法找到确切的类时,它会使用TVMDNode.Create作为最后的手段来实例化一个对象。这个对象不是供应商的TGraphicVMDNode的派生类,因此无法在程序的GUI中显示。 - Daniel Marschall
显示剩余2条评论
2个回答

3
是的,这是可能的。
我们实现了类似的功能以控制全局/单例用于测试目的。我们将我们的单例更改为在应用程序上作为接口可访问(不是TApplication,而是我们自己的等效物)。然后,我们添加了在运行时动态添加/删除接口的能力。现在,我们的测试用例能够在需要时插入适当的模拟。
我将描述一般方法,希望您能将其应用于特定情况。
  1. 添加一个字段来保存动态添加接口的列表。TInterfaceList很好用。
  2. 添加添加/删除动态接口的方法。
  3. 覆盖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.

谢谢你的回答。我需要稍后尝试这个。我理解现在这个类可以告诉其他人它现在支持接口X,所以接口是在运行时添加的。但我还需要从外部(另一个单元)实现接口的方法。这该怎么做? - Daniel Marschall
@rinntech 我已经更新了示例代码。希望这能澄清你对实际_实现_工作方式的疑虑。 - Disillusioned
对不起,我不懂。请问您需要我修改什么内容吗?(1) 如果没有 TGraphicNode 实例,这个程序怎么能运行? TGraphicNode 是我需要定义的实现,否则 TVMDNode 只会知道它实现了 IID 标识符,但没有任何代码。 (2) 我应该如何创建一个参考 ANode 的实现者 TGraphicNode,就像您说的那样? ANodeTVMDNode 类型(TGraphicNode 的祖先类)。 如何将 TVMDNode 用作 TGraphicNode - Daniel Marschall
1
@rinntech,你的问题是如何在运行时(而不是编译时)向类添加接口。我已经演示了如何实现这一点。在我的答案中,我指出这种技术类似于编译时选项,即使用属性实现将接口的实现委托给独立对象:代理不会自动访问“主机”。您想要代理访问的任何额外信息/方法,都需要明确提供。现在您正在询问如何做到这一点 - 有许多技术可以实现。 - Disillusioned
1
@rinntech 在你的原始帖子中,你从未提到过"protected"一词——甚至在你的示例代码中也没有意外地提到。然而,类定义的唯一"afterwards"是"not compile-time",换句话说就是"run-time"。Delphi根本不支持重新定义类。在最近的Delphi版本中,最接近的可用功能是类助手,它根本不改变类,但可以调用新方法,就像它们是在该类上声明的一样。 - Disillusioned
显示剩余7条评论

1

如果应用工厂设计模式,你可以通过继承保留持久数据的能力,并为表格中存储的ClassGUIDs创建正确的实例。

对于每个节点类,都会有一个类工厂(或者只是一个函数指针),负责创建正确的Delphi类。类工厂可以在单元初始化部分(每次应用程序启动时一次)注册到内核单例对象中。

然后,内核单例将GUID映射到正确的工厂,工厂将调用正确的类实例构造函数(如http://delphipatterns.blog.com/2011/03/23/abstract-factory所示)。

包可以拆分为独立的DLL,并在独立的单元中实现类,仍从一个基础TVMNode类继承。

您现在使用RTTI的功能可以通过一些虚拟方法轻松地在派生类或工厂类中支持。

你可能还可以考虑使用更简单的数据传输对象来保存/加载 TVMNodes,并且可以从已经被认为很好的对象关系映射器对象持久性框架中获得一些灵感,因为我认为你试图解决的问题正是它们正在处理的问题(已经)

我不知道这个类别中好的 Delphi 开源框架。但是从其他语言中,你可以看一下Java HibernateMicrosoft .NET Entity Framework或者极简主义的Google Protocol Buffers serializer

enter image description here


非常感谢您的帮助。这个想法可行。 :-) 从技术上讲,该类没有通过接口进行扩展,而是告诉内核返回GUID“X”的扩展类,因此所有新对象都是扩展类型而不是原始类型。顺便问一下,您用什么工具创建了这个UML图? - Daniel Marschall
@rinntech 在你的问题中,进行一些 UML 建模(在编码之前)似乎是正确的做法。此外,一些序列图或活动图可能有助于澄清事情。对于绘图,我使用了Sparx Systems Enterprise Architect - 它不是免费的,除了反向工程和代码生成之外,它“知道”正确的 UML 图表看起来是什么样子,并且自己完成所有的绘图。我只需要说我想把什么放入模型中,图片的外观就会自动生成为符合 UML 的标准。据我所知,这是排名第一的 UML 工具(提供试用版)。 - xmojmr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接