delphi - Inyección de constructor versus inyección de colocador para propiedad de los padres
dependency-injection spring4d (1)
Estoy tratando de descubrir la mejor manera de usar la inyección de dependencia para algún código heredado que tardará en refactorizarse y debe hacerse gradualmente. La mayoría de las clases antiguas usan una propiedad "principal" para determinar varias cosas y la propiedad principal a menudo pasa a través de un argumento constructor de la siguiente manera:
constructor TParentObject.Create;
begin
FChildObject := TChildObject.Create(Self);
end;
constructor TChildObject.Create(AParent: TParentObject)
begin
FParent := AParent;
end;
Esto es bastante típico de nuestra base de código heredado. Sin embargo, cuando se mueve a interfaces e inyección de constructor, el marco Spring4D no conoce al elemento primario al crear el objeto Child. Por lo tanto, solo obtendrá un nuevo padre pero no el existente. Por supuesto, puedo crear un getter / setter de propiedad, pero esto indicaría una propiedad "opcional" para la clase, que en realidad es una propiedad obligatoria. Vea el código a continuación para más explicación:
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.
Tal vez hay alguna metodología que se puede utilizar y que no conozco para establecer el objeto padre usando el marco DI.
Espero que esta pregunta sea clara en lo que intento lograr. Me complace proporcionar más ejemplos de descripción / código cuando sea necesario.
En primer lugar, no debe usar el localizador de servicios para reemplazar las llamadas de ctor. Esto solo empeora las cosas. Sé que la gente piensa que son inteligentes haciendo eso, pero realmente estás reemplazando una dependencia simple en otra clase con una dependencia en algún estado global más el requisito de que algún otro código fuera del control (las clases consumidoras) coloque la dependencia en el contenedor. Eso no resulta en un código más fácil pero más difícil de mantener.
Además de todas las otras razones por las que debe mantenerse alejado de ella. El localizador de servicios puede tener un uso limitado en la aplicación heredada para introducir una raíz de composición en el medio de una aplicación para iniciar la DI a partir de ese momento, pero no de la manera que se muestra.
Si el padre necesita al niño, solo inyéctelo. Ahora el problema es si desea crear un padre, primero necesita al niño pero el niño necesita al padre. ¿Cómo lograr eso? Hay dos soluciones. Sin embargo, uno de ellos no es DI puro compatible.
Primero muestro el camino usando una fábrica provista por el contenedor (necesita la última versión de rama de desarrollo a partir del momento de la publicación):
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.
Si no desea utilizar una fábrica provista por un contenedor, explícitamente regístrese usted mismo. Luego, la llamada de RegisterFactory se reemplaza por esta:
GlobalContainer.RegisterInstance<TFunc<IParentObject,IChildObject>>(
function(parent: IParentObject): IChildObject
begin
Result := GlobalContainer.Resolve<IChildObject>([TValue.From(parent)]);
end);
Y el parámetro constructor se puede cambiar a TFunc<...>
ya que no necesita RTTI para este método (es por eso que necesitó IFactory<...>
en el otro caso).
La segunda versión usa inyección de campo y, por lo tanto, es DI pura incompatible. Tenga cuidado al escribir código como este ya que no funciona sin usar el contenedor o RTTI. Por ejemplo, si quiere probar estas clases puede ser difícil componerlas sin el contenedor. La parte importante aquí es PerResolve, que le dice al contenedor que reutilice la instancia una vez resuelta cada vez que se necesite otra dependencia que pueda satisfacer.
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.
Por cierto. Observe sus referencias entre padres e hijos cuando usa interfaces. Si se referencian entre sí obtendrán pérdidas de memoria. Puede resolver eso utilizando una referencia débil en un lado (generalmente la referencia principal en el niño).