Jak uniknąć szaleństwa konstruktora Dependency Injection?


300

Uważam, że moi konstruktorzy zaczynają wyglądać tak:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

z ciągle rosnącą listą parametrów. Ponieważ „Kontener” jest kontenerem do wstrzykiwania zależności, dlaczego nie mogę tego po prostu zrobić:

public MyClass(Container con)

dla każdej klasy? Jakie są wady? Jeśli to zrobię, wydaje mi się, że używam gloryfikowanego statycznego. Podziel się swoimi przemyśleniami na temat szaleństwa IoC i Dependency Injection.


64
dlaczego mijasz pojemnik? Myślę, że możesz źle zrozumieć MKOl
Paul Creasey

33
Jeśli twój konstruktor wymaga więcej lub więcej parametrów, być może robisz za dużo w tych klasach.
Austin Salonen,

38
Nie tak robisz wstrzykiwanie konstruktora. Obiekty w ogóle nie wiedzą o kontenerze IoC, podobnie jak nie powinny.
duffymo,

Możesz po prostu stworzyć pusty konstruktor, w którym wywołujesz DI bezpośrednio, prosząc o potrzebne rzeczy. To usunie konstruktora Madnesa, ale musisz upewnić się, że używasz interfejsu DI .. na wypadek zmiany systemu DI w połowie rozwoju. Szczerze mówiąc ... nikt już tego nie zrobi, nawet jeśli DI robi to, by wstrzyknąć do twojego konstruktora. doh
Piotr Kula,

Odpowiedzi:


409

Masz rację, że jeśli użyjesz kontenera jako Lokalizatora usług, będzie to mniej więcej gloryfikowana fabryka statyczna. Z wielu powodów uważam to za anty-wzór .

Jedną ze wspaniałych zalet wstrzykiwania konstruktora jest to, że łamanie zasady pojedynczej odpowiedzialności jest rażąco oczywiste.

Kiedy tak się stanie, nadszedł czas na refaktoryzację do usług fasadowych . Krótko mówiąc, utwórz nowy, bardziej gruboziarnisty interfejs, który ukrywa interakcję między niektórymi lub wszystkimi drobnoziarnistymi zależnościami, których obecnie potrzebujesz.


8
+1 za kwantyfikację nakładu refaktoryzacji w jedną koncepcję; niesamowite :)
Ryan Emerle

46
na serio? właśnie stworzyłeś pośrednią opcję przeniesienia tych parametrów do innej klasy, ale one nadal tam są! tylko bardziej skomplikowane, aby sobie z nimi poradzić.
niekwestionowany

23
@irreputable: W zdegenerowanym przypadku, w którym przenosimy wszystkie zależności do usługi agregacji, zgadzam się, że to tylko kolejny poziom pośredni, który nie przynosi żadnych korzyści, więc mój wybór słów był nieco opóźniony. Chodzi jednak o to, że przenosimy tylko niektóre drobnoziarniste zależności do usługi agregacji. Ogranicza to liczbę permutacji zależności zarówno w nowej usłudze agregującej, jak i dla pozostawionych zależności. To sprawia, że ​​oba są łatwiejsze w obsłudze.
Mark Seemann,

92
Najlepsza uwaga: „Jedną ze wspaniałych korzyści płynących z iniekcji konstruktora jest to, że łamanie zasady pojedynczej odpowiedzialności jest rażąco oczywiste”.
Igor Popov,

2
@DonBox W takim przypadku możesz napisać implementacje zerowego obiektu, aby zatrzymać rekursję. Nie to, czego potrzebujesz, ale chodzi o to, że Wtrysk Konstruktora nie zapobiega cyklom - tylko wyjaśnia, że ​​one tam są.
Mark Seemann

66

Nie sądzę, żeby twoi konstruktorzy klas mieli odniesienia do twojego okresu kontenera IOC. Jest to niepotrzebna zależność między klasą a kontenerem (typ zależności, jakiej IOC próbuje uniknąć!).


+1 W zależności od kontenera IoC trudno później zmienić ten kontener bez zmiany paczki kodu we wszystkich innych klasach
Tseng

1
Jak zaimplementowałbyś IOC bez parametrów interfejsu w konstruktorach? Czy źle czytam twój post?
J Hunt

@J Hunt Nie rozumiem twojego komentarza. Dla mnie parametry interfejsu oznaczają parametry, które są interfejsami dla zależności, tj. Jeśli zainicjuje się kontener wstrzykiwania zależności MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(parametry interfejsu). Nie ma to związku z postem @ pochodnym, który interpretuję jako powiedzenie, że pojemnik do wstrzykiwania zależności nie powinien wstrzykiwać się do swoich obiektów, tj.MyClass myClass = new MyClass(this)
John Doe,

25

Trudność przekazania parametrów nie stanowi problemu. Problem polega na tym, że twoja klasa robi za dużo i powinna być bardziej rozbita.

Wstrzykiwanie zależności może działać jako wczesne ostrzeżenie dla klas, które stają się zbyt duże, szczególnie ze względu na rosnący ból przechodzenia przez wszystkie zależności.


43
Popraw mnie, jeśli się mylę, ale w pewnym momencie musisz „skleić wszystko razem”, a zatem musisz uzyskać więcej niż kilka zależności od tego. Na przykład w warstwie Widok, budując szablony i dane dla nich, musisz pobrać wszystkie dane z różnych zależności (np. „Usług”), a następnie umieścić wszystkie te dane w szablonie i na ekranie. Jeśli moja strona internetowa ma 10 różnych „bloków” informacji, potrzebuję 10 różnych klas, aby dostarczyć mi te dane. Więc potrzebuję 10 zależności w mojej klasie Widok / Szablon?
Andrew,

4

Natknąłem się na podobne pytanie dotyczące wstrzykiwania zależności konstruktorskich i stopnia złożoności wszystkich zależności.

Jednym z podejść, które stosowałem w przeszłości, jest użycie wzoru fasady aplikacji przy użyciu warstwy usługi. To miałoby zgrubne API. Jeśli ta usługa zależy od repozytoriów, użyłaby zastrzyku ustawiającego własności prywatnych. Wymaga to utworzenia abstrakcyjnej fabryki i przeniesienia logiki tworzenia repozytoriów do fabryki.

Szczegółowy kod z wyjaśnieniem można znaleźć tutaj

Najlepsze praktyki dotyczące IoC w złożonej warstwie usług


3

Przeczytałem cały ten wątek dwa razy i myślę, że ludzie odpowiadają tym, co wiedzą, a nie pytaniem.

Oryginalne pytanie JP wygląda na to, że konstruuje obiekty, wysyłając resolver, a następnie kilka klas, ale zakładamy, że te klasy / obiekty same są usługami, gotowe do wstrzyknięcia. Co jeśli nie są?

JP, jeśli chcesz wykorzystać DI i pragnąć chwały mieszania zastrzyku z danymi kontekstowymi, żaden z tych wzorów (lub rzekomych „anty-wzorów”) nie rozwiązuje tego specjalnie. W rzeczywistości sprowadza się to do użycia pakietu, który wesprze Cię w takim przedsięwzięciu.

Container.GetSevice<MyClass>(someObject1, someObject2)

... ten format jest rzadko obsługiwany. Wierzę, że trudność programowania takiego wsparcia, dodana do nędznej wydajności, która byłaby związana z implementacją, czyni go nieatrakcyjnym dla programistów open source.

Ale należy to zrobić, ponieważ powinienem być w stanie stworzyć i zarejestrować fabrykę dla MyClass, a ta fabryka powinna być w stanie odbierać dane / dane wejściowe, które nie są popychane jako „usługa” tylko ze względu na przekazywanie dane. Jeśli „anty-wzorzec” ma negatywne konsekwencje, wówczas wymuszenie istnienia sztucznych typów usług do przekazywania danych / modeli jest z pewnością negatywne (na równi z przeczuciem, że pakujesz swoje klasy w kontener. Obowiązuje ten sam instynkt).

Istnieją ramy, które mogą pomóc, nawet jeśli wyglądają nieco brzydko. Na przykład Ninject:

Tworzenie instancji za pomocą Ninject z dodatkowymi parametrami w konstruktorze

Dotyczy to .NET, jest popularny i wciąż nie jest tak czysty, jak powinien, ale jestem pewien, że jest coś w jakimkolwiek języku, który wybierzesz.


3

Wstrzyknięcie pojemnika to skrót, którego ostatecznie będziesz żałować.

Nadmierne wstrzyknięcie nie stanowi problemu, jest zwykle objawem innych wad strukturalnych, w szczególności oddzielenia problemów. To nie jest jeden problem, ale może mieć wiele źródeł, a to, co czyni go tak trudnym do naprawienia, polega na tym, że będziesz musiał poradzić sobie ze wszystkimi, czasami w tym samym czasie (pomyśl o rozplątywaniu spaghetti).

Oto niepełna lista rzeczy, na które należy zwrócić uwagę

Słaby projekt domeny (agregacja katalogu głównego…. Itp.)

Niewłaściwy podział problemów (skład usługi, polecenia, zapytania) Patrz CQRS i pozyskiwanie zdarzeń.

LUB Mapujący (bądź ostrożny, te rzeczy mogą doprowadzić cię do kłopotów)

Zobacz modele i inne DTO (nigdy nie używaj ponownie jednego z nich i staraj się ograniczyć je do minimum !!!!)


2

Problem:

1) Konstruktor z ciągle rosnącą listą parametrów.

2) Jeśli klasa jest dziedziczona (np .:), RepositoryBasewówczas zmiana sygnatury konstruktora powoduje zmianę klas pochodnych.

Rozwiązanie 1

Przekaż IoC Containerdo konstruktora

Dlaczego

  • Nigdy więcej rosnącej listy parametrów
  • Podpis Konstruktora staje się prosty

Dlaczego nie

  • Sprawia, że ​​klasa jest ściśle sprzężona z pojemnikiem IoC. (To powoduje problemy, gdy 1. chcesz użyć tej klasy w innych projektach, w których używasz innego kontenera IoC. 2. decydujesz się zmienić kontener IoC)
  • Sprawia, że ​​twoja klasa jest mniej opisowa. (Naprawdę nie możesz spojrzeć na konstruktora klasy i powiedzieć, czego potrzebuje do działania.)
  • Klasa może uzyskać dostęp do potencjalnie wszystkich usług.

Rozwiązanie 2

Utwórz klasę grupującą wszystkie usługi i przekaż ją konstruktorowi

 public abstract class EFRepositoryBase 
 {
    public class Dependency
    {
        public DbContext DbContext { get; }
        public IAuditFactory AuditFactory { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
        }
    }

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    {
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    }
  }

Klasy pochodnej

  public class ApplicationEfRepository : EFRepositoryBase      
  {
     public new class Dependency : EFRepositoryBase.Dependency
     {
         public IConcreteDependency ConcreteDependency { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        }
     }

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
      { 
        _concreteDependency = dependency.ConcreteDependency;
      }
   }

Dlaczego

  • Dodanie nowej zależności do klasy nie wpływa na klasy pochodne
  • Klasa jest agnostyczna dla kontenera IoC
  • Klasa ma charakter opisowy (w aspekcie zależności). Zgodnie z konwencją, jeśli chcesz wiedzieć, od której klasy Azależy, informacje te są gromadzoneA.Dependency
  • Podpis konstruktora staje się prosty

Dlaczego nie

  • trzeba utworzyć dodatkową klasę
  • rejestracja usługi staje się złożona (musisz zarejestrować się X.Dependencyosobno)
  • Koncepcyjnie to samo co przekazywanie IoC Container
  • ..

Rozwiązanie 2 jest jednak surowe, jeśli istnieje przeciwko temu solidny argument, doceniony zostanie komentarz opisowy


1

To podejście, którego używam

public class Hero
{

    [Inject]
    private IInventory Inventory { get; set; }

    [Inject]
    private IArmour Armour { get; set; }

    [Inject]
    protected IWeapon Weapon { get; set; }

    [Inject]
    private IAction Jump { get; set; }

    [Inject]
    private IInstanceProvider InstanceProvider { get; set; }


}

Oto przybliżone podejście do wykonywania wstrzyknięć i uruchamiania konstruktora po wstrzyknięciu wartości. To jest w pełni funkcjonalny program.

public class InjectAttribute : Attribute
{

}


public class TestClass
{
    [Inject]
    private SomeDependency sd { get; set; }

    public TestClass()
    {
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    }
}

public class SomeDependency
{

}


class Program
{
    static void Main(string[] args)
    {
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    }
}

Obecnie pracuję nad projektem hobby, który działa tak: https://github.com/Jokine/ToolProject/tree/Core


-8

Jakiego systemu wstrzykiwania zależności używasz? Czy zamiast tego próbowałeś zastosować zastrzyk na bazie setera?

Zaletą wstrzykiwania opartego na konstruktorach jest to, że wygląda naturalnie dla programistów Java, którzy nie używają frameworków DI. Potrzebujesz 5 rzeczy do zainicjowania klasy, a następnie masz 5 argumentów dla swojego konstruktora. Minusem jest to, co zauważyłeś, staje się niewygodne, gdy masz wiele zależności.

Za pomocą Springa możesz przekazać wymagane wartości ustawieniom i możesz użyć @required adnotacji, aby wymusić ich wstrzyknięcie. Minusem jest to, że musisz przenieść kod inicjujący z konstruktora na inną metodę i mieć wywołanie Spring, które po wstrzyknięciu wszystkich zależności poprzez oznaczenie go @PostConstruct. Nie jestem pewien co do innych frameworków, ale zakładam, że robią coś podobnego.

Oba sposoby działają, to kwestia preferencji.


21
Powodem wstrzyknięcia konstruktora jest uwidocznienie zależności, nie dlatego, że wygląda to bardziej naturalnie dla programistów Java.
L-Four

8
Późny komentarz, ale ta odpowiedź mnie rozśmieszyła :)
Frederik Prijck

1
+1 za zastrzyki na bazie setera. Jeśli mam usługi i repozytoria zdefiniowane w mojej klasie, to cholernie oczywiste, że są to zależności. Nie muszę pisać masywnych konstruktorów wyglądających na VB6 i głupio przypisywać kod do konstruktora. To oczywiste, jakie są zależności w wymaganych polach.
Piotr Kula,

Zgodnie z 2018 r. Spring oficjalnie nie zaleca stosowania wstrzykiwania setera, z wyjątkiem zależności o rozsądnej wartości domyślnej. Podobnie jak w przypadku, jeśli zależność jest obowiązkowa dla klasy, zalecane jest wstrzyknięcie konstruktora. Zobacz dyskusję na temat setera kontra ctor DI
John Doe,
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.