【问题标题】:Constructor injection versus setter injection for Parent propertyParent 属性的构造函数注入与 setter 注入
【发布时间】:2015-08-11 05:40:32
【问题描述】:

我正在尝试找出对一些遗留代码使用依赖注入的最佳方法,这将需要很长时间才能重构并且必须逐步完成。大多数旧类使用“父”属性来确定各种事物,父属性通常通过构造函数参数传入,如下所示:

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

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

这是我们遗留代码库的典型特征。但是,当转向接口和构造函数注入时,Spring4D 框架在创建 Child 对象时不知道 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 你确实是对的,这就是我打算使用的,但想知道是否有替代方案?我现在将发布代码。
  • 我的问题是:为什么你觉得这比直接在 TParentObject 中调用 TChildObject 的构造函数更好?我想真正的代码有点复杂,但仍然:如果你有父/子关系,这些类可能无论如何都知道彼此。如果不是这种情况,我建议使用工厂模式。将发布一些代码。
  • 我正在尝试解耦许多旧的“意大利面条”代码单元,并使它们成为具有独立单元接口的小型、干净的类,以便我们可以为所有类创建单元测试。我们更喜欢尽可能多地移除紧密耦合的依赖,让框架通过注入来处理。它非常适合我们正在编写的新代码,但是迁移遗留代码更具挑战性:-)
  • 如果你想去掉这个父子耦合,你可以这样做吗?没有父母,孩子还能存在吗?还是父母需要比孩子长寿?

标签: delphi dependency-injection spring4d


【解决方案1】:

首先,您不应该使用服务定位器来替换 ctor 调用。这只会让事情变得更糟。我知道人们认为这样做很聪明,但实际上你是在用对某个全局状态的依赖替换对另一个类的一个简单依赖,再加上要求(消费类)控制之外的一些其他代码将依赖放入容器中。这不会导致代码更容易但更难维护。

加上所有other reasons 为什么你应该远离它。服务定位器在遗留应用程序中的用途可能有限,可以在应用程序中间引入组合根以从该点开始 DI,但不是以您显示的方式。

如果父母需要孩子,那么只需注入它。现在的问题是,如果你想创建一个父母,你首先需要孩子,但孩子需要父母。如何做到这一点?有两种解决方案。但是其中之一与pure 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&lt;...&gt;,因为此方法不需要RTTI(这就是为什么在另一种情况下需要IFactory&lt;...&gt;)。

第二个版本使用字段注入,因此与纯 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 的广泛帖子 - 这个父/子在 Spring4D 示例中非常有用。我们相当广泛地使用 DSharp.Mocks,因此需要尝试并决定哪种方法是最好的。我同意您关于使用 ServiceLocator 替换 ctor 的评论,但不知道还能做什么。在任何情况下,您认为使用 ServiceLocator 是“可以的”,还是永远都可以?我注意到您使用 GlobalContainer.Resolve 而不是 ServiceLocator - 您能描述一下区别吗?再次感谢您持续的社区工作是首屈一指的。
  • 我正在尝试构建最新的开发分支,但它一直失败:Spring.Container.CreationContext.pas(108):错误 E2033:实际和正式 var 参数的类型必须相同
  • 现在应该可以工作了。至于服务定位器。我通常直接从容器中解析,因为发生这种情况的位置并不深入应用程序(即组合根)。我建议不要使用 ServiceLocator - 在精心设计的架构中它没有任何位置。在遗留应用程序中它可能有一个临时位置,但正如我所说,它可能会使事情变得更糟而不是更好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多