2015-08-11 14 views
12

Próbuję znaleźć najlepszy sposób użycia wtrysku zależności dla jakiegoś starszego kodu, który zajmie dużo czasu refaktorowi i musi być wykonywany stopniowo. Większość starych klas użyć „dominująca” właściwość do określania różnych rzeczy i własnością rodzic często mijają się za pomocą argumentu konstruktora następująco:Wstrzyknięcie konstruktora kontra wstrzykiwanie ustawiające dla właściwości nadrzędnej

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

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

Jest to dość typowe dla naszej bazy kodu starszego typu. Jednak podczas przechodzenia do interfejsów i iniekcji konstruktora obiekt macierzysty nie jest znany w ramach Spring4D podczas tworzenia obiektu Child. Więc po prostu dostanie nowego rodzica, ale nie istniejącego. Oczywiście mogę utworzyć właściwość getter/setter, ale to wskazywałoby na "opcjonalną" właściwość klasy, która jest naprawdę obowiązkową własnością. Zobacz poniższy kod, aby uzyskać więcej wyjaśnień:

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. 

Może jest jakaś metoda, która może być używana, że ​​nie jestem świadomy, aby ustawić obiektu nadrzędnego za pomocą ramy DI?

Mam nadzieję, że to pytanie jest jasne, co staram się osiągnąć. W razie potrzeby chętnie podam więcej opisu/kodu.

+0

Byłoby interesujące zobaczyć kod, który faktycznie "tworzy" obiekty podrzędne. Czuję lokator usług. –

+0

Haha Stefan, rzeczywiście masz rację, tego właśnie zamierzałem użyć, ale zastanawiałem się, czy istnieje alternatywa? Teraz opublikuję kod. –

+1

Moje pytanie brzmi: dlaczego uważasz, że jest to lepsze niż bezpośrednie wywoływanie konstruktora obiektu TChildObject w obiekcie TParentObject? Domyślam się, że prawdziwy kod jest nieco bardziej złożony, ale nadal: jeśli masz relację rodzic/dziecko, klasy mogą się nawzajem znać. Jeśli tak nie jest, sugerowałbym użycie wzorca fabrycznego. Zostanie opublikowany kod. –

Odpowiedz

14

Przede wszystkim nie należy używać lokalizatora usług do zastępowania wywołań ctor. To tylko pogarsza sprawę. Wiem, że ludzie myślą, że są mądrzy, ale tak naprawdę zastępują jedną prostą zależność na innej klasie zależnością od jakiegoś globalnego stanu oraz wymóg, że jakiś inny kod z (kontroli klas) umieszcza zależność w kontenerze. To nie powoduje łatwiejszego, ale trudniejszego do utrzymania kodu.

Plus wszystkie powody, dla których powinieneś trzymać się z dala od tego. Lokalizator usług może mieć ograniczone zastosowanie w starszej aplikacji do wprowadzenia głównego komponentu w środku aplikacji, aby uruchomić DI z tego punktu, ale nie w taki sposób, jaki jest wyświetlany.

Jeśli rodzic potrzebuje dziecka, po prostu go wstrzyknij. Problem polega na tym, że jeśli chcesz stworzyć rodzica, najpierw potrzebujesz dziecka, ale dziecko potrzebuje rodzica. Jak to osiągnąć? Istnieją dwa rozwiązania. Jednak jeden z nich nie jest kompatybilny z pure DI.

raz pierwszy pokazać drogę przy użyciu dostarczonych przez fabrykę pojemnika (wymaga najnowszej wersji rozwijać gałąź jak od chwili nadania):

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. 

Jeśli nie chcesz korzystać z pojemnika zaopatrzonego fabrykę ty jawnie zarejestruj się sam. Następnie RegisterFactory wezwanie otrzymuje z tego:

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

a parametr konstruktora można zmienić TFunc<...> gdyż nie potrzebują RTTI dla tej metody (dlatego potrzebne IFactory<...> w drugim przypadku).

Druga wersja wykorzystuje wtrysk polowy i dlatego jest niekompatybilna z DI - bądź ostrożny, pisząc kod w ten sposób, ponieważ nie działa bez użycia kontenera lub RTTI - na przykład, jeśli chcesz przetestować te klasy, może być trudno je skomponować bez pojemnika. Ważną częścią jest tutaj PerResolve, który nakazuje kontenerowi ponowne użycie raz rozstrzygniętej instancji, gdy potrzebna jest inna zależność, którą może zaspokoić.

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. 

Przy okazji. Obserwuj swoje referencje między rodzicem a dzieckiem podczas korzystania z interfejsów. Jeśli będą się nawzajem odnosić, dostaniesz wycieki pamięci.Możesz rozwiązać ten problem, używając słabej referencji z jednej strony (zazwyczaj odniesienia dla rodziców w dziecku).

+0

Dzięki za obszerny post Stefan - ten rodzic/dziecko byłby bardzo przydatny w przykładach Spring4D. Używamy DSharp.Mocks dość szeroko, więc będziemy musieli się rozejrzeć i zdecydować, które podejście jest najlepsze. Zgadzam się z waszym komentarzem na temat korzystania z ServiceLocator w celu zastąpienia ctor, ale nie wiem, co jeszcze zrobić. Czy są jakieś okoliczności, w których uważasz, że korzystanie z ServiceLocator jest "ok", czy też nigdy nie jest w porządku? Zauważyłem, że używasz GlobalContainer.Resolve zamiast ServiceLocator - czy mógłbyś opisać różnicę? Jeszcze raz dziękuję, że dalsza praca na rzecz społeczności nie ma sobie równych. –

+0

Próbuję zbudować najnowszą gałąź rozwijania, ale nadal nie działa: Spring.Container.CreationContext.pas (108): błąd E2033: Typy rzeczywistych i formalnych parametrów var muszą być identyczne –

+1

Powinien działać teraz. Jeśli chodzi o lokalizator usług. Zazwyczaj z kontenera usuwam bezpośrednio, ponieważ lokalizacja, w której to nastąpi, nie jest głęboka w aplikacji (to znaczy w katalogu głównym kompozycji). Polecam przeciwko korzystaniu z ServiceLocator - w dobrze zaprojektowanej architekturze nie ma żadnego miejsca. W starszej aplikacji może mieć tymczasowe miejsce, ale jak powiedziałem, może to pogorszyć sytuację, a nie poprawić. –

Powiązane problemy