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?
Odpowiedzi:
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:
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
readonly
sł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:
Pozwoliłoby to użytkownikowi na utworzenie domyślnego Foo, pisząc
Byłoby jednak bardzo odkrywalne, że można podać niestandardową zależność i można pisać
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 .
źródło
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:
Piszesz to w ten sposób:
Oznacza to, że podczas pisania kodu robisz dwie rzeczy:
Opieraj się na interfejsach zamiast klasach, gdy uważasz, że implementacja może wymagać zmiany;
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żaIList<T>
. Jeśli zaprojektujesz swoją klasę do użyciaIList<T>
(lubIEnumerable<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 takiBindingList<T>
, który jest używany w kilku funkcjach wiązania danych.Linq do SQL i EF są zbudowane całkowicie wokół
IDbConnection
powią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
INameCreationService
lubIExtenderProviderService
. Nawet nie bardzo wiem, co to, co konkretne zajęcia są . .NET faktycznie ma swój własny kontener IoCIContainer
, który się do tego przyzwyczaja, aComponent
klasa maGetService
metodę, która jest rzeczywistym lokalizatorem usług. Oczywiście nic nie stoi na przeszkodzie, abyś używał jednego lub wszystkich tych interfejsów bezIContainer
tego 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,
IServiceBehavior
któ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.
IContainer
to doskonały wybór, ponieważ jest częścią samego .NET Framework.źródło
IContainer
tak naprawdę nie jest kontenerem w jednym akapicie. ;)private readonly
pola? 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 readonly
pole 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.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
new
podniesienia 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ą.
źródło
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.)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.
źródło