构造函数注入与设置器注入对于父属性的区别

13

我试图找出最好的依赖注入方式,以用于一些需要长时间重构并且必须逐步完成的旧代码。大多数旧类使用“Parent”属性来确定各种事情,并且经常通过构造函数参数传递父属性,如下所示:

constructor TParentObject.Create;
begin
  FChildObject := TChildObject.Create(Self);
end;

constructor TChildObject.Create(AParent: TParentObject)
begin
  FParent := AParent;
end;

这相当典型于我们的遗留代码库。但是,当移动到接口和构造函数注入时,Spring4D框架在创建Child对象时不知道Parent。因此它将只获取一个新的parent而不是现有的那个。当然,我可以创建一个属性getter/setter,但这将表明类的一个“可选”属性,实际上这是一个必需的属性。请参见下面的代码以获得更多解释:

unit uInterfaces;

interface

uses
  Spring.Collections;

type

  IChildObject = interface;

  IParentObject = interface
  ['{8EA8F9A2-E627-4546-8008-0A77DA2B16F1}']
    function GetSomethingRequiredByChild: string;
    procedure SetSomethingRequiredByChild(const Value: string);
    property SomethingRequiredByChild: string read GetSomethingRequiredByChild write SetSomethingRequiredByChild;
    function GetChild: IChildObject;
    property Child: IChildObject read GetChild;
  end;

  // This introduces a property getter/setter
  // However it also implies that Parent can be NIL which it cannot
  IChildObject = interface
  ['{ECCA09A6-4A52-4BE4-A72E-2801160A9086}']
    function GetParent: IParentObject;
    procedure SetParent(const Value: IParentObject);
    property Parent: IParentObject read GetParent write SetParent;
  end;

  TParentObject = class(TInterfacedObject, IParentObject)
  private
    FChild: IChildObject;
    FSomethingRequiredByChild: string;
    function GetChild: IChildObject;
    function GetSomethingRequiredByChild: string;
    procedure SetSomethingRequiredByChild(const Value: string);
  public
    constructor Create;
  end;

  TChildObject = class(TInterfacedObject, IChildObject)
  private
    FParent: IParentObject;
    function GetParent: IParentObject;
    procedure SetParent(const Value: IParentObject);
  public
    // This requries a Parent object, but how does the Spring4D resolve the correct parent?
    constructor Create(const AParent: IParentObject);
  end;

implementation

uses
  Spring.Services;

{ TParentObject }

constructor TParentObject.Create;
begin
  // Here is the old way...
  FChild := TChildObject.Create(Self); // Old way of doing it

  // This is the Service Locator way...
  FChild := ServiceLocator.GetService<IChildObject>;
  // I would prefer that the Parent is assigned somehow by the Service Locator
  // IS THIS POSSIBLE - or am I dreaming?
  FChild.Parent := Self;
end;

function TParentObject.GetChild: IChildObject;
begin
  Result := FChild;
end;

function TParentObject.GetSomethingRequiredByChild: string;
begin
  Result := FSomethingRequiredByChild;
end;

procedure TParentObject.SetSomethingRequiredByChild(const Value: string);
begin
  FSomethingRequiredByChild := Value;
end;

{ TChildObject }

constructor TChildObject.Create(const AParent: IParentObject);
begin
  FParent := AParent;
end;

function TChildObject.GetParent: IParentObject;
begin
  Result := FParent;
end;

procedure TChildObject.SetParent(const Value: IParentObject);
begin
  FParent := Value;
end;

end.

也许有一些方法学,可以使用 DI 框架设置父对象,但我不知道该如何操作。

希望我的问题表述清楚了。如果需要,我可以提供更多的描述或代码示例。


看到实际“创建”子对象的代码会很有趣。我闻到了服务定位器的味道。 - Stefan Glienke
哈哈,Stefan,你说得没错,这正是我计划使用的,但我想知道是否有其他选择?我现在会发布代码。 - Rick Wheeler
1
我的问题是:为什么您觉得这比直接在TParentObject中调用TChildObject的构造函数更好?我猜实际代码可能会更复杂,但是如果您有一个父/子关系,那么这些类可能已经互相知道了。如果不是这种情况,我建议使用工厂模式。稍后会发布一些代码。 - Stefan Glienke
我正在尝试解耦许多旧的“意大利面条”代码单元,并将它们变成小而干净的类,使用独立的接口单元,以便我们可以为所有类创建单元测试。我们希望尽可能地消除紧密耦合的依赖关系,并通过注入让框架处理。这对于我们正在编写的新代码非常完美,但是迁移遗留代码则更具挑战性 :-) - Rick Wheeler
1
如果您想要消除这种父子耦合,您能够直接这样做吗?孩子是否可以独立于其父母存在?或者说父母需要比孩子更长寿? - David Heffernan
@DavidHeffernan 目前子程序需要从父程序中获取信息以便正确运行。也许在经过更多的代码修订和大量重构后,这种情况可以改变,但目前必须保持这种关系。 - Rick Wheeler
1个回答

16
首先,你不应该使用服务定位器来替换构造函数调用。这只会使情况变得更糟。我知道人们认为这样做很聪明,但实际上,你是将一个简单的依赖关系替换为对某个全局状态的依赖,还需要一些其他代码(消费类之外的代码)将依赖项放入容器中。这不会导致编写更简单易维护的代码,而是变得更加困难。
此外还有其他原因让你远离它。服务定位器在遗留应用程序中可能具有有限的用途,可以在应用程序中间引入组合根来开始依赖注入过程,但不能像你展示的方式那样使用。
如果父对象需要子对象,那么就注入它。现在问题是,如果你想创建一个父对象,你首先需要子对象,但子对象也需要父对象。如何实现呢?有两种解决方案。但其中一种不符合纯DI的要求。
我先展示一种使用容器提供的工厂的方法(需要发布时间点的最新开发分支版本):
unit ParentChildRelationShip.Types;

interface

uses
  SysUtils,
  Spring,
  Spring.Container.Common;

type
  IChildObject = interface;

  IParentObject = interface
    ['{8EA8F9A2-E627-4546-8008-0A77DA2B16F1}']
    function GetChild: IChildObject;
    property Child: IChildObject read GetChild;
  end;

  IChildObject = interface
    ['{ECCA09A6-4A52-4BE4-A72E-2801160A9086}']
    function GetParent: IParentObject;
    property Parent: IParentObject read GetParent;
  end;

  TParentObject = class(TInterfacedObject, IParentObject)
  private
    FChild: IChildObject;
    function GetChild: IChildObject;
  public
    constructor Create(const childFactory: IFactory<IParentObject, IChildObject>);
  end;

  TChildObject = class(TInterfacedObject, IChildObject)
  private
    FParent: WeakReference<IParentObject>;
    function GetParent: IParentObject;
  public
    constructor Create(const AParent: IParentObject);
  end;

implementation

{ TParentObject }

constructor TParentObject.Create;
begin
  FChild := childFactory(Self);
end;

function TParentObject.GetChild: IChildObject;
begin
  Result := FChild;
end;

{ TChildObject }

constructor TChildObject.Create(const AParent: IParentObject);
begin
  FParent := AParent;
end;

function TChildObject.GetParent: IParentObject;
begin
  Result := FParent;
end;

end.

program ParentChildRelation;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Spring.Container,
  Spring.Container.Common,
  ParentChildRelationShip.Types in 'ParentChildRelationShip.Types.pas';

procedure Main;
var
  parent: IParentObject;
  child: IChildObject;
begin
  GlobalContainer.RegisterType<IParentObject,TParentObject>;
  GlobalContainer.RegisterType<IChildObject,TChildObject>;
  GlobalContainer.RegisterFactory<IFactory<IParentObject,IChildObject>>(TParamResolution.ByValue);
  GlobalContainer.Build;
  parent := GlobalContainer.Resolve<IParentObject>;
  child := parent.Child;
  Assert(parent = child.Parent);
end;

begin
  try
    Main;
  except
    on E: Exception do
      Writeln(E.Message);
  end;
  ReportMemoryLeaksOnShutdown := True;
end.

如果您不想使用容器提供的工厂,您可以显式地注册它。然后,将RegisterFactory调用替换为以下内容:

  GlobalContainer.RegisterInstance<TFunc<IParentObject,IChildObject>>(
    function(parent: IParentObject): IChildObject
    begin
      Result := GlobalContainer.Resolve<IChildObject>([TValue.From(parent)]);
    end);

构造函数参数可以更改为TFunc<...>,因为此方法不需要RTTI(运行时类型信息),这就是为什么在另一种情况下需要IFactory<...>的原因。

第二个版本使用字段注入,因此与纯DI不兼容-编写此类代码时要小心,因为如果不使用容器或RTTI,则无法正常工作-例如,如果您想测试这些类,则可能难以在没有容器的情况下组合它们。 这里重要的部分是PerResolve,它告诉容器在需要满足另一个依赖项时重用已解析的实例。

unit ParentChildRelationShip.Types;

interface

uses
  SysUtils,
  Spring;

type
  IChildObject = interface;

  IParentObject = interface
    ['{8EA8F9A2-E627-4546-8008-0A77DA2B16F1}']
    function GetChild: IChildObject;
    property Child: IChildObject read GetChild;
  end;

  IChildObject = interface
    ['{ECCA09A6-4A52-4BE4-A72E-2801160A9086}']
    function GetParent: IParentObject;
    property Parent: IParentObject read GetParent;
  end;

  TParentObject = class(TInterfacedObject, IParentObject)
  private
    [Inject]
    FChild: IChildObject;
    function GetChild: IChildObject;
  end;

  TChildObject = class(TInterfacedObject, IChildObject)
  private
    FParent: WeakReference<IParentObject>;
    function GetParent: IParentObject;
  public
    constructor Create(const AParent: IParentObject);
  end;

implementation

function TParentObject.GetChild: IChildObject;
begin
  Result := FChild;
end;

{ TChildObject }

constructor TChildObject.Create(const AParent: IParentObject);
begin
  FParent := AParent;
end;

function TChildObject.GetParent: IParentObject;
begin
  Result := FParent;
end;

end.

program ParentChildRelation;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Spring.Container,
  Spring.Container.Common,
  ParentChildRelationShip.Types in 'ParentChildRelationShip.Types.pas';

procedure Main;
var
  parent: IParentObject;
  child: IChildObject;
begin
  GlobalContainer.RegisterType<IParentObject,TParentObject>.PerResolve;
  GlobalContainer.RegisterType<IChildObject,TChildObject>;
  GlobalContainer.Build;
  parent := GlobalContainer.Resolve<IParentObject>;
  child := parent.Child;
  Assert(parent = child.Parent);
end;

begin
  try
    Main;
  except
    on E: Exception do
      Writeln(E.Message);
  end;
  ReportMemoryLeaksOnShutdown := True;
end.

顺便提一下,当使用接口时,请注意父对象和子对象之间的引用关系。如果它们相互引用,则会导致内存泄漏。您可以通过在其中一个对象上使用弱引用(通常是子对象中的父引用)来解决这个问题。


感谢您的详细帖子,Stefan - 这个Parent/Child在Spring4D示例中将非常有用。我们正在广泛使用DSharp.Mocks,因此需要进行测试并决定哪种方法最好。我同意您关于使用ServiceLocator替换ctor的评论,但不知道还能做什么。您认为在什么情况下使用ServiceLocator是“可以”的,或者永远不可以使用?我注意到您使用GlobalContainer.Resolve而不是ServiceLocator - 您能描述一下它们之间的区别吗?再次感谢您持续的社区工作是无与伦比的。 - Rick Wheeler
我正在尝试构建最新的开发分支,但它一直失败:Spring.Container.CreationContext.pas(108): error E2033: 实际和形式变量参数的类型必须相同。 - Rick Wheeler
1
现在应该可以工作了。至于服务定位器,我通常直接从容器中解析,因为解析的位置不深入应用程序(即组合根)。我建议不要使用ServiceLocator - 在良好设计的架构中,它没有任何位置。在传统应用程序中,它可能有一个临时位置,但是正如我所说,这可能会使事情变得更糟而不是更好。 - Stefan Glienke

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