是否可以为现有类(TInterfaced
或TInterfacedPersistent
的后代)添加和实现接口,以便将Model和View分成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;
使用基于GUID的RTTI(使用ClassGUID
),函数GetChildNodes
能够找到匹配的类并使用原始数据初始化它。 (每个数据集包含ClassGUID
和RawData
旁边的其他数据,如创建/更新的时间戳)
请注意,我的API(Database_Kernel.pas
)与供应商的节点类(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
到基节点类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不支持这样的东西。
除了将模型和视图混合在一个单元/类中并不好的事实之外,还会出现以下现实问题:如果供应商必须更改我的Database_Kernel.pas
API以扩展{{1使用TVMDNode
界面,只要我发布新版API IGraphicNode
,他就需要重新执行所有更改。
我该怎么办?我想很长时间用Delphi的OOP可能的解决方案。解决方法可能是将TVMDNode嵌套到容器类中,该容器类具有辅助RTTI,因此在找到Database_Kernel.pas
类之后,我可以搜索TVMDNode
类。但这听起来很扼杀,就像一个肮脏的黑客。
PS:此API是一个OpenSource / GPL项目。我试图保持与旧几代Delphi(例如6)的兼容,因为我想最大化可能的用户数量。但是,如果只能使用新一代Delphi语言解决上述问题,我可能会考虑删除Delphi 6对此API的支持。
答案 0 :(得分:2)
是的,这是可能的。
为了测试目的,我们实现了类似于获得全局/单例控制的东西。我们将单例更改为可以作为应用程序上的接口访问(不是TApplication
,我们自己的等价物)。然后我们添加了在运行时动态添加/删除接口的功能。现在我们的测试用例能够在需要时插入合适的模拟器。
我将描述一般方法,希望您能够将其应用于您的具体情况。
TInterfaceList
效果很好。function QueryInterface(const IID: TGUID; out Obj): HResult; virtual;
。您的实现将首先检查接口列表,如果未找到,将遵循基本实现。回答你的问题:
据我所知,该类现在可以告诉其他人它现在支持接口X,因此在运行时期间接口是ADDED。但我还需要从外部(另一个单元)实现界面的方法。这是怎么做到的?
添加界面时,您需要添加实现界面的对象实例。这与正常的属性 ... 实现 &lt; interface&gt; 技术非常相似,可以将接口的实现委托给另一个对象。关键的区别在于这是动态的。因此它将具有相同的限制:例如无法访问&#34;主机&#34;除非明确给出参考。
以下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.
答案 1 :(得分:1)
如果您应用factory design pattern,则可以保留保留数据并通过继承实现数据的功能,并仍然为表中存储的ClassGUID创建正确的实例。
对于每个节点类,都会有一个类工厂(或只是一个函数指针)负责创建正确的Delphi类。类工厂可以在内核单例对象的单元初始化部分(每个应用程序启动一次)注册自己。
然后内核单例将GUID映射到正确的工厂,而工厂又将调用正确的类实例构造函数(如http://delphipatterns.blog.com/2011/03/23/abstract-factory所示)
可以将包拆分为单独的DLL和以单独单元实现的类,仍然从一个基本TVMNode类继承。
现在使用RTTI的功能可以通过一些虚拟方法轻松地在后代类或工厂类中得到支持。
您可能还会考虑使用更简单的Data Transfer Objects来保存/加载TVMNodes,并且可能会从已经很好看的Object Relational Mapper或Object Persistence framework中获得一些灵感,因为您要解决的问题在我看来他们正在处理的问题(已经)
我不了解这个类的Delphi开源框架。但是,从其他语言,您可以查看Java Hibernate,Microsoft .NET Entity Framework或简约Google Protocol Buffers serializer