Search code examples
delphidependency-injectionspring4d

Constructor injection versus setter injection for Parent property


I'm trying to figure out the best way to use dependency injection for some legacy code which will take a long to refactor and has to be done gradually. Most of the old classes use a "Parent" property for determining various things and the parent property was often pass in via a constructor argument as follows:

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

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

This is fairly typical of our legacy code base. However when moving to interfaces and constructor injection, the Parent is not known by the Spring4D framework when creating the Child object. So it will just get a new parent but not the existing one. Of course I can create a property getter/setter but this would indicate an "optional" property for the class which is really a mandatory property. See the code below for more explanation:

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.

Maybe there is some methodology which can be used that I'm not aware of to set the parent object using the DI framework?

I hope this question is clear what I'm trying to achieve. I'm happy to provide more description/code example where necessary.


Solution

  • First of all you should not use the service locator to replace ctor calls. This is just making things worse. I know people think they are smart by doing that but really you are replacing one simple dependency on another class with a dependency on some global state plus the requirement that some other code out of (the consuming classes) control puts the dependency into the container. That does not result in easier but harder to maintain code.

    Plus all the other reasons why you should stay away from it. The service locator might have a limited use in legacy application to introduce a composition root in the middle of an application to start DI from that point on but not in a way you show.

    If parent needs the child then just inject it. Now the problem is if you want to create a parent, you first need the child but the child needs the parent. How to achieve that? There are two solutions. However one of them is not pure DI compatible.

    I first show the way using a factory provided by the container (needs latest develop branch version as of the time of posting):

    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.
    

    If you don't want to use a container provided factory you explicitly register it yourself. Then the RegisterFactory call is replaced with this one:

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

    And the constructor parameter can be changed to TFunc<...> as it does not need RTTI for this method (that is why you needed IFactory<...> in the other case).

    The second version uses field injection and thus is pure DI incompatible - be careful writing code like that as it does not work without using the container or RTTI - like if you want to test these classes it might become hard to compose them without the container. The important part here is the PerResolve which tells the container to reuse the once resolved instance whenever another dependency is needed that it can satisfy.

    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.
    

    By the way. Watch your references between parent and child when using interfaces. If they reference each other you will get memory leaks. You can solve that by using a weak reference on one side (usually the parent reference in the child).