Подтвердить что ты не робот

Инъекция конструктора и инъекция установщика для родительской собственности

Я пытаюсь найти лучший способ использовать инъекцию зависимостей для некоторого устаревшего кода, который займет много времени в рефакторе и должен быть сделан постепенно. Большинство старых классов используют свойство "Родитель" для определения различных вещей, и родительское свойство часто проходит через аргумент конструктора следующим образом:

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

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

Это довольно типично для нашей старой базы кода. Однако при переходе к интерфейсам и инсталляции конструктора родитель не известен платформой Spring4D при создании объекта Child. Поэтому он просто получит нового родителя, но не существующего. Конечно, я могу создать свойство 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?

Надеюсь, этот вопрос ясен, чего я пытаюсь достичь. Я рад предоставить дополнительное описание/пример кода, если это необходимо.

4b9b3361

Ответ 1

Прежде всего, вы не должны использовать локатор сервисов для замены вызовов ctor. Это только ухудшает ситуацию. Я знаю, что люди думают, что они умны, но на самом деле вы заменяете одну простую зависимость от другого класса с зависимостью от какого-то глобального состояния, а также за то, что какой-то другой код (контроль потребления классов) ставит зависимость в контейнер. Это не приводит к упрощению, но сложнее поддерживать код.

Плюс все другие причины, почему вы должны держаться подальше от него. Локатор службы может иметь ограниченное использование в устаревшем приложении, чтобы ввести корневой состав в середине приложения, чтобы запустить DI с этой точки, но не так, как вы показываете.

Если родитель нуждается в потомке, просто введите его. Теперь проблема в том, что если вы хотите создать родителя, вам сначала нужен ребенок, но ребенку нужен родитель. Как это сделать? Существует два решения. Однако один из них не совместим с чистым DI.

Сначала я показываю способ использования factory, предоставляемого контейнером (требуется последняя версия ветки на момент публикации):

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.

Если вы не хотите использовать контейнер factory, вы явно зарегистрируете его самостоятельно. Затем вызов 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.

Кстати. Следите за ссылками между родителями и дочерними элементами при использовании интерфейсов. Если они ссылаются друг на друга, вы получите утечку памяти. Вы можете решить эту проблему, используя слабую ссылку с одной стороны (обычно родительскую ссылку в дочернем элементе).