Dependency Inject (DI) "friendly" library

Zastanawiam się nad projektem biblioteki C#, która będzie miała kilka różnych funkcji wysokiego poziomu. Oczywiście, te funkcje wysokiego poziomu będą zaimplementowane przy użyciu SOLID Zasady projektowania klas w miarę możliwości. W związku z tym prawdopodobnie powstaną klasy przeznaczone do regularnego korzystania bezpośrednio przez konsumentów oraz "klasy wsparcia", które są zależnościami tych bardziej powszechnych klas" użytkowników końcowych".

Pytanie brzmi, jak najlepiej zaprojektować bibliotekę tak, aby jest:

  • DI Agnostic - chociaż dodanie podstawowego "wsparcia" dla jednej lub dwóch popularnych bibliotek DI (StructureMap, Ninject, itp.) wydaje się rozsądne, chcę, aby konsumenci mogli korzystać z biblioteki z dowolnym frameworkiem DI.
  • non-di usable - jeśli konsument biblioteki nie używa DI, biblioteka powinna być tak łatwa w użyciu, jak to tylko możliwe, zmniejszając ilość pracy, jaką użytkownik musi wykonać, aby utworzyć wszystkie te "nieistotne" zależności tylko po to, aby dostać się do "prawdziwych" klas, które chcą użyj.

Moim obecnym myśleniem jest dostarczenie kilku "modułów rejestracji DI" dla wspólnych bibliotek DI (np. rejestru StructureMap, modułu Ninject) oraz klas set lub Factory, które nie są DI i zawierają sprzężenie z tymi kilkoma fabrykami.

Myśli?

Author: Pete, 2010-01-12

4 answers

Jest to właściwie proste do zrobienia, gdy zrozumiesz, że DI jest o wzorcach i Zasadach , a nie technologii.

Aby zaprojektować API w sposób niezależny od kontenera DI, postępuj zgodnie z tymi ogólnymi zasadami:

Program do interfejsu, a nie implementacji

Zasada ta jest w rzeczywistości cytatem (z pamięci) z wzorców projektowych , Ale zawsze powinna być twoimprawdziwym celem . DI jest tylko środkiem do osiągnięcia tego end .

Stosuj zasadę Hollywood

Zasada Hollywood w terminach DI mówi: Nie dzwoń do kontenera DI, to zadzwoni do ciebie .

Nigdy nie pytaj bezpośrednio o zależność, wywołując kontener z twojego kodu. Poproś o to pośrednio, używając Constructor Injection .

Use Constructor Injection

Gdy potrzebujesz zależności, poproś o nią statycznie przez konstruktor:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

Zawiadomienie jak klasa usług gwarantuje swoje niezmienniki. Po utworzeniu instancji zależność jest gwarantowana, że będzie dostępna dzięki kombinacji klauzuli Guard i słowa kluczowego readonly.

Użyj Abstract Factory, jeśli potrzebujesz krótkotrwałego obiektu

Zależności wstrzykiwane za pomocą Constructor Injection wydają się być długotrwałe, ale czasami potrzebny jest krótkotrwały obiekt lub skonstruowanie zależności na podstawie wartości znanej tylko w czasie wykonywania.

Zobacz to Aby uzyskać więcej informacji.

Komponuj tylko w ostatniej odpowiedzialnej chwili

Utrzymuj obiekty odsprzęgnięte do samego końca. Zwykle możesz poczekać i podłączyć wszystko do punktu wejścia aplikacji. Jest to tzw. korzeń składowy .

Więcej szczegółów tutaj:

Uprość korzystanie z fasady

Jeśli uważasz, że powstałe API staje się zbyt złożone dla początkujących użytkowników, zawsze możesz podać kilka klas Facade , które zawierają typowe kombinacje zależności.

Aby zapewnić elastyczną elewację o wysokim stopniu odkrywalności, można rozważyć zapewnienie płynnego budowniczych. Coś takiego:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}
Pozwala to użytkownikowi na utworzenie domyślnego Foo poprzez napisanie
var foo = new MyFacade().CreateFoo();

It byłoby jednak bardzo możliwe, że można podać niestandardową zależność i można napisać

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

Jeśli wyobrażasz sobie, że Klasa MyFacade zawiera wiele różnych zależności, mam nadzieję, że jest jasne, w jaki sposób zapewniłaby odpowiednie wartości domyślne, jednocześnie czyniąc rozszerzalność możliwą do wykrycia.


FWIW, długo po napisaniu tej odpowiedzi, rozszerzyłem się na koncepcje tutaj i napisał dłuższy post na blogu o di-Friendly Libraries , i post towarzyszący o frameworkach przyjaznych DI .

 335
Author: Mark Seemann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:09:57

Termin "dependency injection" nie ma w ogóle nic wspólnego z kontenerem IoC, mimo że często widuje się je razem wymienione. Oznacza to po prostu, że zamiast pisać swój kod w ten sposób:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Piszesz to tak:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Oznacza to, że pisząc swój kod robisz dwie rzeczy:

  1. Polegaj na interfejsach zamiast na klasach, gdy uważasz, że implementacja może wymagać zmieniony;

  2. Zamiast tworzyć instancje tych interfejsów wewnątrz klasy, przekazuj je jako argumenty konstruktora (alternatywnie mogą być przypisane do właściwości publicznych; pierwszy to constructor injection, drugi to property injection ).

Nic z tego nie zakłada istnienia żadnej biblioteki DI, a to nie sprawia, że kod jest trudniejszy do napisania bez niej.

Jeśli szukasz przykładu z tego, nie patrzeć dalej niż sam. NET Framework:

  • List<T> implementuje IList<T>. Jeśli zaprojektujesz swoją klasę tak, aby używała IList<T> (lub IEnumerable<T>), możesz skorzystać z pojęć takich jak lazy-loading, jak Linq do SQL, Linq do encji i NHibernate, które robią za kulisami, zwykle poprzez wtrysk właściwości. Niektóre klasy frameworku faktycznie akceptują IList<T> jako argument konstruktora, na przykład BindingList<T>, który jest używany dla kilku funkcji wiązania danych.

  • Linq do SQL i EF są zbudowane w całości wokół IDbConnection i powiązanych interfejsów, które mogą być przekazywane przez publiczne konstruktory. Nie musisz ich jednak używać; domyślne konstruktory działają dobrze z łańcuchem połączeń znajdującym się gdzieś w pliku konfiguracyjnym.

  • Jeśli kiedykolwiek pracujesz nad komponentami WinForms, masz do czynienia z "usługami", takimi jak INameCreationService lub IExtenderProviderService. Nawet nie wiesz, co to są konkretne klasy . . NET faktycznie ma swój własny kontener IoC, IContainer, który jest używany do tego celu, a klasa Component ma metodę GetService, która jest faktycznym lokalizatorem usług. Oczywiście nic nie stoi na przeszkodzie, aby używać dowolnego lub wszystkich tych interfejsów bez IContainer lub tego konkretnego lokalizatora. Same usługi są tylko luźno połączone z kontenerem.

  • Kontrakty w WCF są zbudowane w całości wokół interfejsów. Rzeczywista konkretna klasa usług jest zwykle określana nazwą w pliku konfiguracyjnym, który jest zasadniczo DI. Wiele osób nie zdaje sobie z tego sprawy, ale jest całkowicie możliwe Zamiana tego systemu konfiguracyjnego na inny kontener IoC. Co ciekawsze, zachowania serwisowe są przykładami IServiceBehavior, które można dodać później. Ponownie, możesz łatwo podłączyć to do kontenera IoC I poprosić go o wybranie odpowiednich zachowań, ale funkcja jest całkowicie użyteczna bez niego.

I tak dalej i tak dalej. DI znajdziesz wszędzie w. NET, chodzi o to, że normalnie robi się to tak bezproblemowo, że nawet nie myślisz o tym jak o DI.

Jeśli chcesz zaprojektować bibliotekę z obsługą DI dla maksymalnej użyteczności, najlepszą propozycją jest prawdopodobnie dostarczenie własnej domyślnej implementacji IoC przy użyciu lekkiego kontenera. IContainer jest świetnym wyborem, ponieważ jest częścią samego. NET Framework.

 37
Author: Aaronaught,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-12 02:30:26

EDIT 2015: Czas minął, teraz zdaję sobie sprawę, że to wszystko było wielkim błędem. Pojemniki IoC są okropne, a DI jest bardzo słabym sposobem radzenia sobie ze skutkami ubocznymi. Skutecznie, wszystkie odpowiedzi tutaj (i samo pytanie) należy unikać. Po prostu bądź świadomy efektów ubocznych, oddziel je od czystego kodu, a Wszystko inne albo wpadnie na swoje miejsce, albo będzie nieistotne i zbędne.

Oryginalna odpowiedź:


I had to face this same decyzja przy opracowywaniu SolrNet . Zacząłem od tego, by być przyjaznym dla DI i nie wymagającym kontenerów, ale gdy dodałem coraz więcej wewnętrznych komponentów, wewnętrzne fabryki szybko stały się nie do zarządzania, a powstała Biblioteka była nieelastyczna.

Zakończyłem pisanie własnego bardzo prostego wbudowanego kontenera IoC, zapewniając jednocześnie Windsor facilityi Ninject module. Integracja biblioteki z innymi kontenerami to tylko kwestia prawidłowo okablowane komponenty, żebym mógł łatwo zintegrować je z Autofac, Unity, StructureMap, cokolwiek.

Minusem tego jest to, że straciłem możliwość tylko new podnieść serwis. Wziąłem również zależność od CommonServiceLocator, którego mogłem uniknąć (mógłbym go refaktorować w przyszłości), aby ułatwić implementację osadzonego kontenera.

Więcej szczegółów w tym blogu .

MassTransit wydaje się polegać na czymś podobne. Posiada interfejs iobjectbuilder , który jest tak naprawdę Iservicelocator CommonServiceLocator z kilkoma innymi metodami, a następnie implementuje to dla każdego kontenera, tj. NinjectObjectBuilder i zwykły moduł/obiekt, tj. MassTransitModule. Następnie opiera się na IObjectBuilder, aby utworzyć instancję tego, czego potrzebuje. Jest to oczywiście poprawne podejście, ale osobiście nie podoba mi się to bardzo, ponieważ w rzeczywistości również przechodzi wokół kontenera much, używając go jako lokalizatora usług.

MonoRail implementuje swój własny kontener , który implementuje stary, dobry IServiceProvider . Ten kontener jest używany w tym frameworku za pośrednictwem interfejsu, który udostępnia dobrze znane Usługi . Aby uzyskać betonowy pojemnik, ma wbudowany lokalizator dostawcy usług . W związku z tym, że nie jest to możliwe, nie jest to konieczne, ponieważ nie jest to konieczne. dostawca.

Podsumowując: nie ma idealnego rozwiązania. Podobnie jak w przypadku każdej decyzji projektowej, kwestia ta wymaga równowagi między elastycznością, konserwacją i wygodą.

 4
Author: Mauricio Scheffer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-22 11:29:14

To, co chciałbym zrobić, to zaprojektować moją bibliotekę w sposób di container agnostyczny, aby ograniczyć zależność od kontenera w jak największym stopniu. Pozwala to na zamianę kontenera DI na inny, jeśli zajdzie taka potrzeba.

Następnie udostępnij warstwę nad logiką DI użytkownikom biblioteki, aby mogli korzystać z dowolnego frameworka wybranego przez interfejs. W ten sposób mogą nadal korzystać z funkcjonalności DI, które zostały ujawnione i mogą swobodnie korzystać z dowolnego innego frameworka dla swoich własnych cele.

Pozwolenie użytkownikom biblioteki na podłączenie własnego frameworka DI wydaje mi się nieco złe, ponieważ znacznie zwiększa ilość konserwacji. To również staje się bardziej środowiskiem wtyczek niż prostym DI.

 1
Author: Igor Zevaka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-12 00:39:58