Czy istnieje wzorzec inicjowania obiektów utworzonych za pośrednictwem kontenera DI

147

Próbuję zmusić Unity do zarządzania tworzeniem moich obiektów i chcę mieć pewne parametry inicjalizacji, które nie są znane do czasu wykonania:

W tej chwili jedynym sposobem, w jaki mogłem wymyślić, jak to zrobić, jest posiadanie metody Init w interfejsie.

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

Następnie, aby go użyć (w Unity), zrobiłbym to:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

W tym scenariuszu runTimeParamparametr jest określany w czasie wykonywania na podstawie danych wejściowych użytkownika. Ten trywialny przypadek po prostu zwraca wartość, runTimeParamale w rzeczywistości parametr będzie czymś w rodzaju nazwy pliku, a metoda initialize zrobi coś z plikiem.

Stwarza to szereg problemów, a mianowicie to, że Initializemetoda jest dostępna w interfejsie i można ją wywołać wiele razy. Ustawienie flagi w implementacji i wyrzucenie wyjątku przy powtarzającym się wywołaniu Initializewydaje się być niezgrabne.

W momencie, w którym rozwiązuję mój interfejs, nie chcę nic wiedzieć o implementacji IMyIntf. Chcę jednak wiedzieć, że ten interfejs wymaga pewnych jednorazowych parametrów inicjalizacyjnych. Czy istnieje sposób, aby w jakiś sposób dodać adnotacje (atrybuty?) Do interfejsu tymi informacjami i przekazać je do frameworka podczas tworzenia obiektu?

Edycja: nieco bardziej opisałem interfejs.

Igor Zevaka
źródło
9
Brakuje Ci sensu używania kontenera DI. Zależności powinny zostać rozwiązane za Ciebie.
Pierreten
Skąd bierzesz potrzebne parametry? (plik konfiguracyjny, db, ??)
Jaime
runTimeParamjest zależnością określaną w czasie wykonywania na podstawie danych wejściowych użytkownika. Czy alternatywą powinno być podzielenie go na dwa interfejsy - jeden do inicjalizacji, a drugi do przechowywania wartości?
Igor Zevaka
zależność w IoC zwykle odnosi się do zależności od innych klas typu ref lub obiektów, które można określić na etapie inicjalizacji IoC. Jeśli Twoja klasa potrzebuje tylko niektórych wartości do działania, wtedy przydaje się metoda Initialize () w Twojej klasie.
The Light
To znaczy wyobraź sobie, że w twojej aplikacji jest 100 klas, na których można zastosować to podejście; wtedy będziesz musiał stworzyć dodatkowe 100 klas fabrycznych + 100 interfejsów dla swoich klas i możesz uciec, gdybyś używał tylko metody Initialize ().
The Light

Odpowiedzi:

276

W każdym miejscu, w którym do skonstruowania określonej zależności potrzebna jest wartość czasu wykonywania, rozwiązaniem jest Abstract Factory .

Posiadanie metod inicjalizacji na interfejsach pachnie nieszczelną abstrakcją .

W twoim przypadku powiedziałbym, że powinieneś modelować IMyIntfinterfejs na podstawie tego, jak chcesz go używać, a nie jak zamierzasz tworzyć jego implementacje. To szczegół implementacji.

Dlatego interfejs powinien wyglądać po prostu:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Teraz zdefiniuj fabrykę abstrakcyjną:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

Możesz teraz utworzyć konkretną implementację, IMyIntfFactoryktóra tworzy konkretne wystąpienia, IMyIntftakie jak ta:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

Zwróć uwagę, jak to pozwala nam chronić niezmienniki klasy za pomocą readonlysłowa kluczowego. Żadne śmierdzące metody inicjalizacji nie są konieczne.

IMyIntfFactoryRealizacja może być tak proste, jak to:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

W przypadku wszystkich konsumentów, w których potrzebujesz IMyIntfinstancji, po prostu pobierasz zależność IMyIntfFactory, żądając jej za pośrednictwem Constructor Injection .

Każdy kontener DI Container wart swojej soli będzie mógł automatycznie połączyć IMyIntfFactoryinstancję, jeśli zarejestrujesz ją poprawnie.

Mark Seemann
źródło
13
Problem polega na tym, że metoda (taka jak Initialize) jest częścią twojego API, podczas gdy konstruktor nie. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann
13
Ponadto metoda Initialize wskazuje Temporal Coupling: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann
2
@Darlene Możesz użyć leniwie zainicjowanego dekoratora, jak opisano w sekcji 8.3.6 mojej książki . Podaję również przykład czegoś podobnego w mojej prezentacji Big Object Graphs Up Front .
Mark Seemann
2
@Mark Jeśli tworzenie MyIntfimplementacji przez fabrykę wymaga więcej niż runTimeParam(czytaj: inne usługi, które można by rozwiązać za pomocą IoC), nadal masz do czynienia z rozwiązaniem tych zależności w fabryce. Podoba mi się odpowiedź @PhilSandler polegająca na przekazaniu tych zależności do konstruktora fabryki w celu rozwiązania tego problemu - czy to również Ty podchodzisz do tego?
Jeff
2
Świetna rzecz, ale twoja odpowiedź na to drugie pytanie naprawdę dotarła do mnie.
Jeff,
15

Zwykle, gdy napotkasz taką sytuację, musisz ponownie przyjrzeć się projektowi i określić, czy mieszasz obiekty stanowe / dane z czystymi usługami. W większości (nie we wszystkich) przypadkach będziesz chciał zachować te dwa typy obiektów oddzielnie.

Jeśli potrzebujesz parametru specyficznego dla kontekstu przekazanego w konstruktorze, jedną z opcji jest utworzenie fabryki, która rozwiązuje zależności usług za pośrednictwem konstruktora i przyjmuje parametr czasu wykonywania jako parametr metody Create () (lub Generate ( ), Build () lub jakkolwiek nazwiesz metody fabryczne).

Posiadanie seterów lub metody Initialize () jest ogólnie uważane za zły projekt, ponieważ musisz "pamiętać", aby je wywołać i upewnić się, że nie otwierają zbyt wiele stanu twojej implementacji (tj. Co ma powstrzymać kogoś przed ponownym -calling initialize czy setter?).

Phil Sandler
źródło
5

Z taką sytuacją spotkałem się również kilka razy w środowiskach, w których dynamicznie tworzę obiekty ViewModel w oparciu o obiekty Model (bardzo dobrze opisane przez ten inny poście Stackoverflow ).

Podobało mi się jak rozszerzenie Ninject pozwalające na dynamiczne tworzenie fabryk w oparciu o interfejsy:

Bind<IMyFactory>().ToFactory();

Nie mogłem znaleźć podobnej funkcjonalności bezpośrednio w Unity ; więc napisałem własne rozszerzenie do IUnityContainer, które pozwala rejestrować fabryki, które będą tworzyć nowe obiekty na podstawie danych z istniejących obiektów zasadniczo mapujących z jednej hierarchii typów do innej hierarchii typów: UnityMappingFactory @ GitHub

Mając na celu prostotę i czytelność, otrzymałem rozszerzenie, które umożliwia bezpośrednie określanie mapowań bez deklarowania poszczególnych klas fabrycznych lub interfejsów (oszczędność czasu rzeczywistego). Po prostu dodajesz mapowania dokładnie tam, gdzie rejestrujesz klasy podczas normalnego procesu ładowania ...

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

Następnie wystarczy zadeklarować interfejs fabryki mapowania w konstruktorze dla CI i użyć jego metody Create () ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

Jako dodatkowy bonus, wszelkie dodatkowe zależności w konstruktorze mapowanych klas również zostaną rozwiązane podczas tworzenia obiektu.

Oczywiście to nie rozwiąże każdego problemu, ale jak dotąd bardzo mi się to służy, więc pomyślałem, że powinienem się nim podzielić. Więcej dokumentacji znajduje się na stronie projektu w GitHub.

jigamiller
źródło
1

Nie mogę odpowiedzieć konkretną terminologią Unity, ale wygląda na to, że dopiero uczysz się o wstrzykiwaniu zależności. Jeśli tak, zachęcam do przeczytania krótkiej, jasnej i pełnej informacji instrukcji obsługi Ninject .

To przeprowadzi Cię przez różne opcje, które masz podczas korzystania z DI, i jak uwzględnić konkretne problemy, które napotkasz po drodze. W twoim przypadku najprawdopodobniej będziesz chciał użyć kontenera DI do utworzenia wystąpienia obiektów i sprawić, by ten obiekt uzyskał odwołanie do każdej z jego zależności za pośrednictwem konstruktora.

Przewodnik zawiera również szczegółowe informacje na temat opisywania metod, właściwości, a nawet parametrów przy użyciu atrybutów do rozróżniania ich w czasie wykonywania.

Nawet jeśli nie używasz Ninject, przewodnik zawiera koncepcje i terminologię funkcjonalności, która pasuje do twojego celu, i powinieneś być w stanie zmapować tę wiedzę na Unity lub inne frameworki DI (lub przekonać cię, aby spróbować Ninject) .

antoni
źródło
Dziękuję za to. Obecnie oceniam frameworki DI i NInject miał być moim następnym.
Igor Zevaka
@johann: dostawcy? github.com/ninject/ninject/wiki/ ...
anthony
1

Myślę, że rozwiązałem to i wydaje się raczej zdrowe, więc musi być w połowie poprawne :))

Podzieliłem się IMyIntfna interfejsy „getter” i „setter”. Więc:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

Następnie realizacja:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()nadal można go wywołać wiele razy, ale używając fragmentów paradygmatu lokalizatora usług możemy to całkiem ładnie opakować, tak że IMyIntfSetterjest to prawie wewnętrzny interfejs, który różni się od IMyIntf.

Igor Zevaka
źródło
13
Nie jest to szczególnie dobre rozwiązanie, ponieważ opiera się na metodzie Initialize, która jest nieszczelną abstrakcją. Przy okazji, to nie wygląda jak Service Locator, ale bardziej jak Interface Injection. W każdym razie zobacz moją odpowiedź, aby uzyskać lepsze rozwiązanie.
Mark Seemann