Biblioteka „przyjazna” dla Dependency Inject (DI)


230

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ą wdrażane przy użyciu zasad projektowania klasy SOLID w jak największym stopniu. W związku z tym prawdopodobnie będą istnieć 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: jaki jest najlepszy sposób zaprojektowania biblioteki:

  • DI Agnostic - Chociaż dodanie podstawowej „obsługi” jednej lub dwóch wspólnych bibliotek DI (StructureMap, Ninject itp.) Wydaje się rozsądne, chcę, aby konsumenci mogli korzystać z biblioteki w dowolnym środowisku DI.
  • Nieużywalne DI - jeśli użytkownik biblioteki nie korzysta z DI, biblioteka powinna być tak łatwa w użyciu, jak to możliwe, zmniejszając ilość pracy, jaką musi wykonać użytkownik, aby stworzyć wszystkie te „nieistotne” zależności, aby się do niego dostać „prawdziwe” klasy, których chcą używać.

Obecnie myślę o dostarczeniu kilku „modułów rejestracji DI” dla wspólnych bibliotek DI (np. Rejestru StructureMap, modułu Ninject) oraz zestawu lub klas Factory, które nie są DI i zawierają sprzężenie z tymi kilkoma fabrykami.

Myśli?


Nowe odniesienie do niedziałającego linku artykuł (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Odpowiedzi:


360

Jest to właściwie proste, gdy zrozumiesz, że DI dotyczy wzorów i zasad , a nie technologii.

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

Zaprogramuj interfejs, a nie implementację

Ta zasada jest w rzeczywistości cytatem (z pamięci) z Wzorów projektowych , ale zawsze powinna być Twoim prawdziwym celem . DI jest tylko środkiem do osiągnięcia tego celu .

Zastosuj zasadę Hollywood

Zasada Hollywood w kategoriach DI mówi: nie dzwoń do DI Container, zadzwoni do ciebie .

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

Użyj wtrysku konstruktora

Gdy potrzebujesz zależności, poproś o nią statycznie za pomocą konstruktora:

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; }
    }
}

Zwróć uwagę, w jaki sposób klasa usługi gwarantuje niezmienniki. Po utworzeniu instancji zależność jest z pewnością dostępna dzięki kombinacji klauzuli ochronnej i readonlysłowa kluczowego.

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

Zależności wstrzykiwane za pomocą Constructor Injection bywają długotrwałe, ale czasem potrzebny jest obiekt krótkotrwały lub konstruowanie 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

Trzymaj obiekty odłączone do samego końca. Zwykle możesz poczekać i połączyć wszystko w punkcie wejścia aplikacji. Nazywa się to rootem kompozycji .

Więcej informacji tutaj:

Uprość za pomocą fasady

Jeśli uważasz, że wynikowy interfejs API staje się zbyt skomplikowany dla początkujących użytkowników, zawsze możesz podać kilka klas Fasady , które zawierają typowe kombinacje zależności.

Aby zapewnić elastyczną fasadę o wysokim stopniu wykrywalności, możesz rozważyć wprowadzenie Fluent Builders. 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);
    }
}

Pozwoliłoby to użytkownikowi na utworzenie domyślnego Foo, pisząc

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

Byłoby jednak bardzo odkrywalne, że można podać niestandardową zależność i można pisać

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 jasne jest, w jaki sposób zapewniłby prawidłowe wartości domyślne, jednocześnie umożliwiając wykrycie rozszerzalności.


FWIW, długo po napisaniu tej odpowiedzi, rozwinąłem zawarte w niej pojęcia i napisałem dłuższy post na blogu o bibliotekach przyjaznych DI oraz post towarzyszący o frameworkach przyjaznych DI .


21
Choć brzmi to świetnie na papierze, z mojego doświadczenia wynika, że ​​kiedy wiele wewnętrznych komponentów wchodzi w interakcje w skomplikowany sposób, kończy się to mnóstwem fabryk do zarządzania, co utrudnia utrzymanie. Ponadto fabryki będą musiały zarządzać stylem życia utworzonych komponentów. Po zainstalowaniu biblioteki w prawdziwym kontenerze będzie to kolidować z własnym stylem życia kontenera. Fabryki i fasady przeszkadzają prawdziwym kontenerom.
Mauricio Scheffer,

4
Muszę jeszcze znaleźć projekt, który to wszystko robi .
Mauricio Scheffer,

31
W ten sposób opracowujemy oprogramowanie w Safewhere, więc nie dzielimy się wrażeniami ...
Mark Seemann

19
Myślę, że fasady powinny być kodowane ręcznie, ponieważ reprezentują znane (i przypuszczalnie powszechne) kombinacje elementów. Kontener DI nie powinien być konieczny, ponieważ wszystko można podłączyć ręcznie (pomyśl DI biednego człowieka). Przypomnij sobie, że fasada jest tylko opcjonalną klasą wygody dla użytkowników Twojego interfejsu API. Zaawansowani użytkownicy mogą chcieć ominąć fasadę i połączyć elementy według własnych upodobań. Mogą chcieć do tego użyć własnego DI Contaier, więc myślę, że byłoby nieuprzejmie narzucać im konkretny Kontener DI, jeśli nie będą go używać. Możliwe, ale niewskazane
Mark Seemann,

8
To może być najlepsza odpowiedź, jaką kiedykolwiek widziałem na SO.
Nick Hodges

40

Termin „wstrzykiwanie zależności” nie ma w ogóle nic wspólnego z kontenerem IoC, nawet jeśli często pojawiają się razem. 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 w ten sposób:

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 podczas pisania kodu robisz dwie rzeczy:

  1. Opieraj się na interfejsach zamiast klasach, gdy uważasz, że implementacja może wymagać zmiany;

  2. Zamiast tworzyć instancje tych interfejsów w klasie, przekaż je jako argumenty konstruktora (alternatywnie można je przypisać do właściwości publicznych; pierwszy to wstrzyknięcie konstruktora , drugi to wstrzyknięcie własności ).

Żadna z tych rzeczy nie zakłada istnienia żadnej biblioteki DI i tak naprawdę nie utrudnia pisania kodu bez niej.

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

  • List<T>wdraża IList<T>. Jeśli zaprojektujesz swoją klasę do użycia IList<T>(lub IEnumerable<T>), możesz skorzystać z pojęć takich jak leniwe ładowanie, ponieważ Linq do SQL, Linq do Entities i NHibernate robią wszystko za kulisami, zwykle poprzez zastrzyk własności. Niektóre klasy frameworku faktycznie akceptują IList<T>argument konstruktora, na przykład taki BindingList<T>, który jest używany w kilku funkcjach wiązania danych.

  • Linq do SQL i EF są zbudowane całkowicie wokół IDbConnectionpowiązanych interfejsów, które mogą być przekazywane za pośrednictwem publicznych konstruktorów. Nie musisz ich jednak używać; domyślne konstruktory działają dobrze, gdy łańcuch połączenia znajduje się gdzieś w pliku konfiguracyjnym.

  • Jeśli kiedykolwiek pracujesz nad komponentami WinForms, zajmujesz się „usługami”, takimi jak INameCreationServicelub IExtenderProviderService. Nawet nie bardzo wiem, co to, co konkretne zajęcia . .NET faktycznie ma swój własny kontener IoC IContainer, który się do tego przyzwyczaja, a Componentklasa ma GetServicemetodę, która jest rzeczywistym lokalizatorem usług. Oczywiście nic nie stoi na przeszkodzie, abyś używał jednego lub wszystkich tych interfejsów bez IContainertego konkretnego lokalizatora. Same usługi są tylko luźno połączone z kontenerem.

  • Kontrakty w WCF są zbudowane całkowicie wokół interfejsów. Rzeczywista konkretna klasa usługi jest zwykle przywoływana przez nazwę w pliku konfiguracyjnym, który jest zasadniczo DI. Wiele osób nie zdaje sobie z tego sprawy, ale można całkowicie wymienić ten system konfiguracji na inny kontener IoC. Być może bardziej interesujące jest to, że zachowania serwisowe to wszystkie przypadki, IServiceBehaviorktóre można dodać później. Ponownie, możesz łatwo podłączyć to do kontenera IoC i pozwolić mu wybrać odpowiednie zachowania, ale funkcja ta jest całkowicie użyteczna bez niego.

I tak dalej i tak dalej. DI można znaleźć wszędzie w .NET, po prostu normalnie robi się to tak płynnie, że nawet nie myślisz o nim jako o DI.

Jeśli chcesz zaprojektować bibliotekę z obsługą DI dla maksymalnej użyteczności, najlepszą sugestią jest prawdopodobnie dostarczenie własnej domyślnej implementacji IoC przy użyciu lekkiego kontenera. IContainerto doskonały wybór, ponieważ jest częścią samego .NET Framework.


2
Prawdziwą abstrakcją dla kontenera jest IServiceProvider, a nie IContainer.
Mauricio Scheffer,

2
@Mauricio: Oczywiście masz rację, ale spróbuj wyjaśnić komuś, kto nigdy nie korzystał z systemu LWC, dlaczego IContainertak naprawdę nie jest kontenerem w jednym akapicie. ;)
Aaronaught

Aaron, ciekawi mnie, dlaczego używasz prywatnego zestawu; zamiast tylko określać pola jako tylko do odczytu?
jr3

@Jreeter: Masz na myśli, dlaczego nie są to private readonlypola? To świetnie, jeśli są używane tylko przez klasę deklarującą, ale OP określił, że jest to kod na poziomie frameworku / biblioteki, co oznacza podklasę. W takich przypadkach zazwyczaj chcesz mieć istotne zależności narażone na podklasy. Mógłbym napisać private readonlypole i właściwość z jawnymi obiektami pobierającymi / ustawiającymi, ale ... zmarnowałem miejsce w przykładzie i więcej kodu do utrzymania w praktyce, bez żadnej realnej korzyści.
Aaronaught

Dziękuję za wyjaśnienie! Zakładałem, że dziecko będzie miało dostęp tylko do pól.
jr3

5

EDYCJA 2015 : czas mijał, teraz zdaję sobie sprawę, że to wszystko było wielkim błędem. Pojemniki IoC są okropne, a DI to bardzo zły sposób radzenia sobie z efektami ubocznymi. W efekcie należy unikać wszystkich odpowiedzi tutaj (i samego pytania). Wystarczy pamiętać o skutkach ubocznych, oddzielić je od czystego kodu, a wszystko inne albo się ułoży, albo jest nieistotne i niepotrzebnie skomplikowane.

Oryginalna odpowiedź brzmi:


Musiałem zmierzyć się z tą samą decyzją podczas tworzenia SolrNet . Zaczynałem od celu, aby być przyjaznym DI i niezależnym od kontenerów, ale kiedy dodawałem coraz więcej wewnętrznych komponentów, wewnętrzne fabryki szybko stały się niemożliwe do zarządzania, a powstała biblioteka była nieelastyczna.

Skończyło się na tym, że napisałem własny, bardzo prosty, wbudowany kontener IoC, jednocześnie oferując funkcję Windsor i moduł Ninject . Integracja biblioteki z innymi kontenerami to tylko kwestia prawidłowego okablowania komponentów, więc mogłem łatwo zintegrować ją z Autofac, Unity, StructureMap, czymkolwiek.

Minusem tego jest to, że straciłem zdolność do newpodniesienia poziomu usługi. Wziąłem również zależność od CommonServiceLocator, której mogłem uniknąć (mógłbym to zreformować w przyszłości), aby ułatwić wdrożenie wbudowanego kontenera.

Więcej szczegółów w tym poście na blogu .

MassTransit wydaje się polegać na czymś podobnym. Ma interfejs IObjectBuilder , który tak naprawdę jest IServiceLocator CommonServiceLocator z kilkoma innymi metodami, a następnie implementuje to dla każdego kontenera, tj. NinjectObjectBuilder i zwykłego modułu / obiektu, tj . MassTransitModule . Następnie polega na IObjectBuilder, aby utworzyć instancję tego, czego potrzebuje. Jest to oczywiście poprawne podejście, ale osobiście nie podoba mi się to zbytnio, ponieważ tak naprawdę przesuwa się wokół kontenera, wykorzystując go jako lokalizator usług.

MonoRail implementuje również swój własny kontener , który implementuje stary, dobry IServiceProvider . Ten kontener jest używany w całym tym frameworku poprzez interfejs, który udostępnia dobrze znane usługi . Aby uzyskać konkretny pojemnik, ma on wbudowany lokalizator usługodawców . Obiekt Windsor wskazuje tego lokalizatora usługodawcy na Windsor, co czyni go wybranym usługodawcą.

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


12
Choć szanuję twoją opinię, uważam, że twoje zbyt ogólne „wszystkie inne odpowiedzi tutaj są nieaktualne i należy ich unikać” niezwykle naiwne. Jeśli od tego czasu się czegoś nauczyliśmy, zastrzyk zależności jest odpowiedzią na ograniczenia językowe. Wydaje się, że bez ewolucji lub porzucania naszych obecnych języków będziemy potrzebować DI, aby spłaszczyć nasze architektury i osiągnąć modułowość. IoC jako ogólna koncepcja jest tylko konsekwencją tych celów i zdecydowanie pożądanym. Kontenery IoC nie są absolutnie konieczne ani dla IoC, ani dla DI, a ich przydatność zależy od tworzonych przez nas kompozycji modułów.
tne

@tne Twoja wzmianka o tym, że jestem „wyjątkowo naiwna”, jest niemile widzianym ad-hominemem. Napisałem złożone aplikacje bez DI lub IoC w C #, VB.NET, Java (języki, które według Ciebie „trzeba” IoC najwyraźniej sądzić po twoim opisie), zdecydowanie nie chodzi o ograniczenia językowe. Chodzi o ograniczenia pojęciowe. Zapraszam do zapoznania się z programowaniem funkcjonalnym zamiast uciekania się do ataków typu ad-hominem i złych argumentów.
Mauricio Scheffer

18
Wspomniałem, że uważam twoje oświadczenie za naiwne; to nic nie mówi o tobie osobiście i chodzi tylko o moją opinię. Nie czuj się obrażony przez przypadkowego nieznajomego. Sugerowanie, że nie znam wszystkich odpowiednich podejść do programowania funkcjonalnego, również nie jest pomocne; nie wiesz tego i pomylisz się. Wiedziałem , że masz tam wiedzę (szybko spojrzałeś na swojego bloga), dlatego denerwowało mnie to, że pozornie kompetentny facet powiedziałby coś w stylu „nie potrzebujemy IoC”. IoC dzieje się dosłownie wszędzie w programowania funkcyjnego (..)
tne

4
oraz ogólnie w oprogramowaniu wysokiego poziomu. W rzeczywistości języki funkcjonalne (i wiele innych stylów) faktycznie dowodzą, że rozwiązują IoC bez DI, myślę, że oboje się z tym zgadzamy i to wszystko, co chciałem powiedzieć. Jeśli poradziłeś sobie bez DI we wspomnianych językach, jak to zrobiłeś? Zakładam, że nie przy użyciu ServiceLocator. Jeśli zastosujesz techniki funkcjonalne, otrzymasz odpowiednik zamknięć, które są klasami wstrzykiwanymi w zależności (gdzie zależności są zmiennymi zamkniętymi). Jak inaczej? (Jestem naprawdę ciekawy, bo nie podoba DI albo.)
tne

1
Kontenery IoC to globalne zmienne słowniki, które zabierają parametryczność jako narzędzie do wnioskowania, dlatego należy tego unikać. IoC (koncepcja), jak w „nie dzwoń do nas, zadzwonimy” ma zastosowanie techniczne za każdym razem, gdy przekazujesz funkcję jako wartość. Zamiast DI zamodeluj domenę za pomocą narzędzi ADT, a następnie napisz interpretatory (por. Bezpłatnie).
Mauricio Scheffer

1

Chciałbym zaprojektować bibliotekę w sposób agnostyczny dla kontenera DI, aby jak najbardziej ograniczyć zależność od kontenera. Pozwala to w razie potrzeby wymienić pojemnik DI na inny.

Następnie ujawnij warstwę powyżej logiki DI użytkownikom biblioteki, aby mogli używać dowolnej struktury wybranej przez interfejs. W ten sposób mogą nadal korzystać z udostępnionej przez Ciebie funkcji DI i mogą swobodnie korzystać z innych ram do własnych celów.

Zezwolenie użytkownikom biblioteki na podłączenie własnego frameworka DI wydaje mi się nieco błędne, ponieważ znacznie zwiększa nakłady serwisowe. To również staje się bardziej środowiskiem wtyczek niż proste DI.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.