Jaki jest „właściwy” sposób implementacji DI w .NET?

22

Szukam implementacji wstrzykiwania zależności w stosunkowo dużej aplikacji, ale nie mam w tym doświadczenia. Studiowałem koncepcję i kilka implementacji IoC oraz dostępnych wtryskiwaczy zależności, takich jak Unity i Ninject. Jednak jedna rzecz mnie umyka. Jak powinienem zorganizować tworzenie instancji w mojej aplikacji?

Myślę o tym, że mogę stworzyć kilka konkretnych fabryk, które będą zawierały logikę tworzenia obiektów dla kilku konkretnych typów klas. Zasadniczo klasa statyczna z metodą wywołującą metodę Ninject Get () instancji statycznego jądra w tej klasie.

Czy będzie to poprawne podejście do implementacji wstrzykiwania zależności w mojej aplikacji, czy też powinienem wdrożyć ją zgodnie z inną zasadą?

użytkownik3223738
źródło
5
Nie sądzę, że istnieje właściwy sposób, ale wiele prawy sposoby, w zależności od projektu. Stosuję się do innych i sugeruję wstrzyknięcie konstruktora, ponieważ będziesz w stanie upewnić się, że każda zależność jest wstrzykiwana w jednym punkcie. Dodatkowo, jeśli sygnatury konstruktora rosną zbyt długo, będziesz wiedział, że klasy robią za dużo.
Paul Kertscher
Trudno odpowiedzieć, nie wiedząc, jaki projekt .net budujesz. Na przykład dobra odpowiedź dla WPF może być złą odpowiedzią dla MVC.
JMK
Miło jest zorganizować wszystkie rejestracje zależności w module DI dla rozwiązania lub dla każdego projektu, a być może dla niektórych testów, w zależności od tego, jak głęboko chcesz przetestować. O tak, oczywiście, powinieneś użyć zastrzyku konstruktora, inne rzeczy są przeznaczone dla bardziej zaawansowanych / szalonych zastosowań.
Mark Rogers

Odpowiedzi:

30

Nie myśl jeszcze o narzędziu, którego będziesz używać. Możesz wykonać DI bez kontenera IoC.

Pierwszy punkt: Mark Seemann ma bardzo dobrą książkę o DI w .Net

Po drugie: skład główny. Upewnij się, że cała konfiguracja została wykonana w punkcie wejścia projektu. Reszta kodu powinna wiedzieć o zastrzykach, a nie o każdym używanym narzędziu.

Po trzecie: wstrzyknięcie konstruktora jest najbardziej prawdopodobną drogą (istnieją przypadki, w których nie chcesz tego, ale nie tak wiele).

Po czwarte: rozważ użycie fabryk lambda i innych podobnych funkcji, aby uniknąć tworzenia niepotrzebnych interfejsów / klas wyłącznie w celu wstrzyknięcia.

Miyamoto Akira
źródło
5
Wszystkie doskonałe porady; szczególnie pierwsza część: dowiedz się, jak wykonać czystą DI, a następnie zacznij szukać kontenerów IoC, które mogą zmniejszyć ilość kodu płyty kotłowej wymaganą przez to podejście.
David Arno
6
... lub pomiń pojemniki IoC razem - aby zachować wszystkie zalety weryfikacji statycznej.
Den
Podoba mi się ta rada, która jest dobrym punktem wyjścia, zanim przejdę do DI, a właściwie mam książkę Marka Seemanna.
Snoop
Mogę poprzeć tę odpowiedź. Z powodzeniem zastosowaliśmy poor-mans-DI (odręczny bootstrapper) w dość dużej aplikacji, rozdzierając elementy logiki bootstrappera w modułach tworzących aplikację.
wigy
1
Bądź ostrożny. Stosowanie zastrzyków lambda może być szybką drogą do szaleństwa, szczególnie w przypadku uszkodzeń projektowych wywołanych testem. Wiem. Zszedłem tą ścieżką.
jpmc26
13

Twoje pytanie składa się z dwóch części - jak prawidłowo wdrożyć DI i jak refaktoryzować dużą aplikację, aby móc korzystać z DI.

Na pierwszą część odpowiada dobrze @Miyamoto Akira (szczególnie zalecenie przeczytania książki Marka Seemanna o „wstrzykiwaniu zależności w .net”. Blog Marks jest również dobrym darmowym zasobem.

Druga część jest znacznie bardziej skomplikowana.

Dobrym pierwszym krokiem byłoby po prostu przeniesienie całej instancji do konstruktorów klas - nie wstrzykiwanie zależności, tylko upewnienie się, że wywołujesz tylko newkonstruktor.

Spowoduje to wyróżnienie wszystkich naruszeń SRP, które popełniłeś, dzięki czemu możesz zacząć dzielić klasę na mniejszych współpracowników.

Kolejnym zagadnieniem, które znajdziesz, będą klasy oparte na parametrach środowiska wykonawczego podczas budowy. Zwykle można to naprawić, tworząc proste fabryki, często Func<param,type>inicjując je w konstruktorze i wywołując je metodami.

Następnym krokiem byłoby stworzenie interfejsów dla twoich zależności i dodanie drugiego konstruktora do twoich klas, który oprócz tych interfejsów. Twój bezparametrowy konstruktor odświeżyłby konkretne instancje i przekazał je nowemu konstruktorowi. Jest to powszechnie nazywane „B * stard Injection” lub „Poor mans DI”.

To da ci możliwość przeprowadzenia niektórych testów jednostkowych, a jeśli to był główny cel refaktora, może to być miejsce, w którym się zatrzymasz. Nowy kod zostanie napisany z zastrzykiem konstruktora, ale stary kod może nadal działać tak, jak napisano, ale nadal może być testowany.

Możesz oczywiście pójść dalej. Jeśli zamierzasz użyć kontenera IOC, następnym krokiem może być zastąpienie wszystkich bezpośrednich wywołań neww bezparametrowych konstruktorach wywołaniami statycznymi do kontenera IOC, zasadniczo (ab) przy użyciu go jako lokalizatora usług.

Spowoduje to wyświetlenie większej liczby przypadków parametrów konstruktora środowiska wykonawczego, z którymi należy się uporać.

Po wykonaniu tej czynności możesz rozpocząć usuwanie konstruktorów bez parametrów i przefiltrować do czystego DI.

Ostatecznie będzie to dużo pracy, więc upewnij się, że zdecydujesz, dlaczego chcesz to zrobić, i nadaj priorytet tym częściom kodu, które najbardziej skorzystają na refaktorze

Steve
źródło
3
Dzięki za bardzo wyszukaną odpowiedź. Dałeś mi kilka pomysłów, jak podejść do problemu, przed którym stoję. Cała architektura aplikacji jest już zbudowana z myślą o IoC. Głównym powodem, dla którego chcę używać DI, nie jest nawet testowanie jednostkowe, jest to premia, ale raczej umiejętność zamiany różnych implementacji dla różnych interfejsów zdefiniowanych w rdzeniu mojej aplikacji, przy jak najmniejszym wysiłku. Ta aplikacja działa w ciągle zmieniającym się środowisku i często muszę zamieniać części aplikacji, aby korzystać z nowych implementacji zgodnie ze zmianami w środowisku.
user3223738
1
Cieszę się, że mogłem pomóc, i tak, zgadzam się, że największą zaletą DI jest luźne sprzęganie i możliwość łatwej rekonfiguracji, co przynosi, a testy jednostkowe są miłym efektem ubocznym.
Steve
1

Najpierw chcę wspomnieć, że znacznie utrudniasz sobie to, dokonując refaktoryzacji istniejącego projektu, a nie rozpoczynając nowy.

Powiedziałeś, że jest to duża aplikacja, więc na początek wybierz mały komponent. Najlepiej komponent „liść-węzeł”, który nie jest używany przez nic innego. Nie wiem, jaki jest stan testów automatycznych w tej aplikacji, ale przełamiesz wszystkie testy jednostkowe tego komponentu. Więc bądź na to przygotowany. Krok 0 polega na napisaniu testów integracji dla komponentu, który będziesz modyfikować, jeśli jeszcze nie istnieją. W ostateczności (brak infrastruktury testowej; brak wpisu do jej napisania), wymyśl szereg ręcznych testów, które możesz wykonać, aby sprawdzić, czy ten komponent działa.

Najprostszym sposobem określenia celu dla refaktora DI jest usunięcie wszystkich wystąpień „nowego” operatora z tego komponentu. Zasadniczo można je podzielić na dwie kategorie:

  1. Niezmienna zmienna składowa: są to zmienne ustawiane raz (zazwyczaj w konstruktorze) i nieprzypisywane na czas życia obiektu. W tym celu można wstrzyknąć instancję obiektu do konstruktora. Na ogół nie jesteś odpowiedzialny za pozbywanie się tych obiektów (nie chcę powiedzieć, że nigdy tutaj, ale tak naprawdę nie powinieneś ponosić tej odpowiedzialności).

  2. Wariant element członkowski zmienna / zmienna metody: są to zmienne, które będą zbierać śmieci w pewnym momencie podczas życia obiektu. W tym przypadku będziesz chciał wstrzyknąć fabrykę do swojej klasy, aby zapewnić te wystąpienia. Odpowiadasz za usuwanie obiektów utworzonych przez fabrykę.

Twój kontener IoC (wygląda na to, że tak wygląda) weźmie odpowiedzialność za tworzenie instancji tych obiektów i implementację interfejsów fabrycznych. Cokolwiek korzysta ze zmodyfikowanego komponentu, musi wiedzieć o kontenerze IoC, aby mógł odzyskać komponent.

Po wykonaniu powyższych czynności będziesz mógł czerpać korzyści, jakie masz nadzieję uzyskać z DI w wybranym komponencie. Teraz byłby dobry moment, aby dodać / naprawić te testy jednostkowe. Jeśli istniały testy jednostkowe, musisz podjąć decyzję, czy chcesz je połączyć razem, wstrzykując prawdziwe obiekty, czy pisać nowe testy jednostkowe przy użyciu próbnych testów.

Po prostu powtórz powyższe czynności dla każdego komponentu aplikacji, przesuwając odniesienie do kontenera IoC w górę, dopóki tylko główny nie będzie musiał o tym wiedzieć.

Jon
źródło
1
Dobra rada: zacznij od małego komponentu zamiast „
WIELKIEGO przepisywania
0

Prawidłowym podejściem jest użycie wstrzykiwania konstruktora, jeśli go używasz

Myślę o tym, że mogę stworzyć kilka konkretnych fabryk, które będą zawierały logikę tworzenia obiektów dla kilku konkretnych typów klas. Zasadniczo klasa statyczna z metodą wywołującą metodę Ninject Get () instancji statycznego jądra w tej klasie.

to kończy się na lokalizatorze usług, a nie na wstrzykiwaniu zależności.

Nisko latający pelikan
źródło
Pewnie. Wtrysk konstruktora. Powiedzmy, że mam klasę, która akceptuje implementację interfejsu jako jeden z argumentów. Ale nadal muszę utworzyć instancję implementacji interfejsu i przekazać ją gdzieś temu konstruktorowi. Najlepiej, aby był to scentralizowany fragment kodu.
user3223738
2
Nie. Podczas inicjowania kontenera DI należy określić implementację interfejsów, a kontener DI utworzy instancję i wstrzyknie do konstruktora.
Low Flying Pelican
Osobiście uważam, że zastrzyk konstruktora jest nadużywany. Zbyt często widziałem, że wstrzykuje się 10 różnych usług i tylko jedna jest rzeczywiście potrzebna do wywołania funkcji - dlaczego więc nie jest to część argumentu funkcji?
urbanhusky
2
Jeśli wstrzyknięto 10 różnych usług, dzieje się tak dlatego, że ktoś narusza SRP, które należy podzielić na mniejsze elementy.
Low Flying Pelican
1
@Fabio Pytanie brzmi: co by to kupiło. Jeszcze nie widziałem przykładu, w którym posiadanie gigantycznej klasy, która zajmuje się tuzinem zupełnie różnych rzeczy, jest dobrym projektem. Jedyne, co robi DI, to uczynić wszystkie te naruszenia bardziej oczywistymi.
Voo
0

Mówisz, że chcesz go użyć, ale nie mów dlaczego.

DI to nic innego jak zapewnienie mechanizmu generowania konkrecji z interfejsów.

To samo w sobie pochodzi z DIP . Jeśli Twój kod jest już napisany w tym stylu i masz jedno miejsce, w którym generowane są konkrecje, DI nie wnosi nic więcej na imprezę. Dodanie tutaj kodu frameworku DI po prostu rozproszy i zaciemni bazę kodu.

Zakładając, że chcesz go używać, zazwyczaj konfigurujesz fabrykę / konstruktora / kontenera (lub cokolwiek innego) na początku aplikacji, aby był wyraźnie widoczny.

Uwaga: bardzo łatwo jest rzucić własny, jeśli chcesz, zamiast zaangażować się w Ninject / StructureMap lub cokolwiek innego. Jeśli jednak masz rozsądną rotację personelu, może nasmarować koła, aby użyć uznanej ramy lub przynajmniej napisać ją w tym stylu, aby nie była zbytnio krzywą uczenia się.

Robbie Dee
źródło
0

Właściwie „właściwym” sposobem jest wcale NIE używanie fabryki, chyba że absolutnie nie ma innego wyboru (jak w testach jednostkowych i niektórych próbach - dla kodu produkcyjnego NIE używasz fabryki)! Jest to w rzeczywistości anty-wzór i należy go za wszelką cenę unikać. Istotą kontenera DI jest umożliwienie gadżetowi wykonania pracy za Ciebie.

Jak stwierdzono powyżej w poprzednim poście, chcesz, aby Twój gadżet IoC przejął odpowiedzialność za tworzenie różnych zależnych obiektów w Twojej aplikacji. Oznacza to, że gadżet DI może samodzielnie tworzyć różne instancje i zarządzać nimi. To jest cały punkt za DI - twoje obiekty NIGDY nie powinny wiedzieć, jak tworzyć i / lub zarządzać obiektami, na których polegają. W przeciwnym razie zrywa luźne połączenie.

Konwersja istniejącej aplikacji na wszystkie DI jest ogromnym krokiem, ale pomijając oczywiste trudności w zrobieniu tego, będziesz również chciał (tylko żeby twoje życie było trochę łatwiejsze) zbadać narzędzie DI, które automatycznie wykona większość twoich powiązań (rdzeń czegoś takiego jak Ninject to "kernel.Bind<someInterface>().To<someConcreteClass>()"wywołania, które wykonujesz w celu dopasowania deklaracji interfejsu do tych konkretnych klas, których chcesz użyć do implementacji tych interfejsów. To te wywołania „Bind”, które pozwalają Twojemu gadżetowi DI przechwytywać wywołania konstruktora i zapewniać niezbędne instancje obiektów zależnych. Typowym konstruktorem (pokazany tutaj pseudo kod) dla niektórych klas może być:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Zauważ, że nigdzie w tym kodzie nie było żadnego kodu, który utworzyłby / zarządzał / wydał ani wystąpienie SomeConcreteClassA lub SomeOtherConcreteClassB. W rzeczywistości żadna konkretna klasa nie została nawet przywołana. Więc ... gdzie stała się magia?

W części początkowej aplikacji miały miejsce następujące zdarzenia (znowu jest to pseudo kod, ale jest bardzo zbliżony do rzeczywistej (Ninject) rzeczy ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Ta odrobina kodu mówi gadżetowi Ninject, aby szukał konstruktorów, skanował je, szukał instancji interfejsów, które zostały skonfigurowane do obsługi (to są wywołania „Bind”), a następnie tworzył i zastępował instancję konkretnej klasy, gdziekolwiek instancja jest przywoływana.

Istnieje ładne narzędzie, które bardzo dobrze uzupełnia Ninject o nazwie Ninject.Extensions.Conventions (kolejny pakiet NuGet), które wykona większość tej pracy za Ciebie. Nie odrywając się od doskonałych doświadczeń edukacyjnych, przez które przechodzisz, gdy sam to budujesz, ale aby zacząć, może to być narzędzie do zbadania.

Jeśli pamięć służy, Unity (formalnie Microsoft jest teraz projektem Open Source) ma wywołanie metody lub dwie, które robią to samo, inne narzędzia mają podobne pomocniki.

Niezależnie od tego, którą ścieżkę wybierzesz, zdecydowanie przeczytaj książkę Marka Seemanna, aby uzyskać większą część szkolenia DI, jednak należy zauważyć, że nawet „wielcy” świata inżynierii oprogramowania (jak Mark) mogą popełniać rażące błędy - Mark zapomniał o wszystkim Ninject w swojej książce, więc oto kolejny zasób napisany tylko dla Ninject. Mam go i jest to dobra lektura: Mastering Ninject for Dependency Injection

Fred
źródło
0

Nie ma „właściwej drogi”, ale należy przestrzegać kilku prostych zasad:

  • Utwórz katalog główny kompozycji podczas uruchamiania aplikacji
  • Po utworzeniu katalogu głównego kompozycji wyrzuć odwołanie do kontenera / jądra DI (lub przynajmniej obuduj go, aby nie był bezpośrednio dostępny z Twojej aplikacji)
  • Nie twórz instancji za pomocą „nowego”
  • Przekaż konstruktorowi wszystkie wymagane zależności jako abstrakcję

To wszystko. Z pewnością są to zasady, a nie prawa, ale jeśli będziesz ich przestrzegać, możesz być pewien, że robisz DI (popraw mnie, jeśli się mylę).


Jak więc tworzyć obiekty w czasie wykonywania bez „nowego” i bez znajomości kontenera DI?

W przypadku NInject istnieje rozszerzenie fabryczne, które zapewnia tworzenie fabryk. Jasne, utworzone fabryki nadal mają wewnętrzną aktualizację jądra, ale nie jest dostępna z twojej aplikacji.

JanDotNet
źródło