Jak powinno wyglądać DI, gdy mamy miliony klas?

0

Sytuacją dość nagminną w korpoprojektach jest ich absolutnie gigantyczny rozmiar. Ilość klas pojedynczej aplikacji idzie w miliony. I to tylko licząc "nasze" klasy, z wyłączeniem wszelkich 3rd party.

(Nieco osobną sprawą jest, czy istotnie ilość klas powinna / musi być tak wielka. Dość, że bardzo często jest, dlatego w tym wątku chciałbym potraktować taką sytuację jako z góry narzuconą, bez dygresji, czy i jak można ją zmniejszyć.)

Załóżmy teraz, że zgodnie z promowanymi przez niektórych "najlepszymi praktykami" znaczna część tych klas będzie wstrzykiwana przez DI. Załóżmy na przykład, że na pojedynczą aplikację składa się 5 milionów klas, cykl życia jednej piątej z nich jest zarządzany przez jakiś framework DI, zatem framework DI musi obsłużyć milion klas.

Jak to powinno wyglądać?

Znam tylko dwa podejścia do DI:

Tam rejestrowanie serwisów i uruchamianie aplikacji wygląda mniej więcej tak:

using DependencyInjection.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();

using IHost host = builder.Build();

host.Run();

(przykł. zaczerpnięty z ww. strony dokumentacji)

Czyli co, będziemy mieć w jednym pliku i w jednej metodzie (main) milion linijek builder.Services.blablabla?

  • @jarekr000000 , o ile pamiętam, twierdził kilkakrotnie, by nie używać żadnych frameworków DI, tylko ręcznie stworzyć cały graf zależności w main.

Zatem, jak rozumiem, main będzie wyglądał jakoś tak:

public void Main()
{
    var dep1 = new Dep1();
    var dep2 = new Dep2();
    var dep3 = new Dep3(dep1);
    var dep4 = new Dep4(dep2, dep3);
    var program = new Program(dep1, dep3, dep4);
    program.Run();
}

Kręcimy się w kółko: znowu mamy metodę main na milion linijek.

Prócz powyższych, zdaje się, są jakieś frameworki DI, w których rejestracja serwisów odbywa się deklaratywnie, w pliku konfiguracyjnym xml. Więc będziemy mieć plik XML na milion tagów?

Czy rejestarcja zależności dla celów DI jest wyjątkiem od zasady, by nie tworzyć plików / metod na miliony linijek?
Czy też jest jakiś sposób, by tego uniknąć, tyle że ja go nie widzę?

4
YetAnohterone napisał(a):
  • @jarekr000000 , o ile pamiętam, twierdził kilkakrotnie, by nie używać żadnych frameworków DI, tylko ręcznie stworzyć cały graf zależności w main.

Zatem, jak rozumiem, main będzie wyglądał jakoś tak:

public void Main()
{
    var dep1 = new Dep1();
    var dep2 = new Dep2();
    var dep3 = new Dep3(dep1);
    var dep4 = new Dep4(dep2, dep3);
    var program = new Program(dep1, dep3, dep4);
    program.Run();
}

Kręcimy się w kółko: znowu mamy metodę main na milion linijek.

pro tip: metody można dzielić na mniejsze. co więcej, można je nawet dzielić na klasy.

YetAnohterone napisał(a):

Załóżmy teraz, że zgodnie z promowanymi przez niektórych "najlepszymi praktykami" znaczna część tych klas będzie wstrzykiwana przez DI.

wstrzykiwanie zależności to nie tylko 'najlepsze praktyki', ale przede wszystkim porządna testowalność.

brak porządnych testów prowadzi do destruktywnych refaktorów, a to prowadzi do unikania refaktorów w ogóle i narastania szajsowatego kodu.

p.s.
jest dużo frameworków czy tam kontenerów do wstrzykiwania (nie chce mi się nawet sprawdzać co jak się nazywa, bo tego nie zamierzam używać) z obsługą magicznych adnotacji. adnotujesz klasy, kontener skanuje classpatha (czy tam jak to się nazywa w .nocie) i automagicznie buduje graf zależności (potrzebne instancje klas). zaletą magicznych adnotacji jest n.p. to, że jest sexy magia i zmniejszenie rozmiaru kodu produkcyjnego o ~1%, a wadą jest to, że nie wiadomo co się skąd bierze, zwłaszcza w wyizolowanych testach i produkuje się wstrzykujące frankensteiny na potrzeby testów.

dla przejrzystości i zrozumiałości kodu, stosuję ręczne wstrzykiwanie.

1

Kręcimy się w kółko: znowu mamy metodę main na milion linijek.

To ty tak powiedziałeś :) a wcale tak nie musi być. Jeśli zakładasz, że w main będziesz łączył wszystkie klasy w całej aplikacji - to tak, będzie to gigantyczne i totalnie nie do utrzymania, zgadzam się.

Nie musisz budować grafu wszystkich zależności w jednej funkcji. Raczej podzielisz ten graf na kilka (kilkanaście (kilkadziesiąt)) mniejszych, stworzysz je w innych miejscach (np. w osobnych pakietach, w osobnych modułach mavena, albo po prostu w osobnych funkcjach EDIT sory założyłem że to Java xD ale pewnie w .net jest analogicznie), a następnie je połączysz do kupy.

3

No dobra, ale przecież możesz sobie w ASP.NET Core robić extension method na IServiceCollection i elo. Grupowalbym je jakoś mądrze w zależności od tego co realizują.

Ostatnio pracuje nad modularnym monolitem i cały moduł rejestruje przy pomocy jednego wywołania, a moduł ma swoje kontrolery, konfigurację i serwisy...

Jakbym to chciał trzymać na Program.cs aplikacji wjazdowej to też zaraz by było pierdylion linii.

1

Zarejestrować wszystko z assembly, pójść na kawę i fajrant.

0

Nie polecam korzystać z framework'ów do di najlepiej, chyba że chcemy wstrzyknąć jakiś jeden wejściowy obiekt.

1

Złożoność kodu budującego drzewo zależności możesz wyabstrachować tak jak każdy inny kod np. wprowadzająć metody ukrywające szczegóły, przerzucając kod do innych modułów

To co piszesz to problem IMO bardzo teoretyczny. Złożoność kodu przy 10**6 klas będzie tak duża, że albo ogarniesz dobrą modułowość albo utoniesz w kodzie, którego nie da się pojąć.

0
YetAnohterone napisał(a):

Sytuacją dość nagminną w korpoprojektach jest ich absolutnie gigantyczny rozmiar. Ilość klas pojedynczej aplikacji idzie w miliony.

Serio?
Ile takich projektów spotkałeś?

Prócz powyższych, zdaje się, są jakieś frameworki DI, w których rejestracja serwisów odbywa się deklaratywnie, w pliku konfiguracyjnym xml. Więc będziemy mieć plik XML na milion tagów?

Taka konfiguracja przez XML raczej wymaga kilku linijek per rejestracja byłoby ich

Czy rejestarcja zależności dla celów DI jest wyjątkiem od zasady, by nie tworzyć plików / metod na miliony linijek?
Czy też jest jakiś sposób, by tego uniknąć, tyle że ja go nie widzę?

Tak, można np użyć kontenera, który wspiera skanowanie dllelek i rejestrację przez konwencję, np. Autofaca i takim kodem zarejestrować dowolną liczbę klas:

builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
       .Where(t => t.Name.EndsWith("Service"))
       .AsImplementedInterfaces();
1

Nie spotkałem się z czymś takim. Większe projekty są dzielone na biblioteki i moduły, każdy moduł zajmuje się rejestracją swoich zależności.
Nawet jeśli masz projekt z milionami klas (w co raczej wątpię, nie wierzę w aż takie patologie projektowe i w to że ktoś jest w stanie to utrzymywać) to i tak nie będziesz w stanie go przerobić nagle na wykorzystywanie DI i ilość linijek w Main to będzie najmniejszy problem.

2
YetAnohterone napisał(a):

Czyli co, będziemy mieć w jednym pliku i w jednej metodzie (main) milion linijek builder.Services.blablabla?

  • @jarekr000000 , o ile pamiętam, twierdził kilkakrotnie, by nie używać żadnych frameworków DI, tylko ręcznie stworzyć cały graf zależności w main.

Zatem, jak rozumiem, main będzie wyglądał jakoś tak:

public void Main()
{
    var dep1 = new Dep1();
    var dep2 = new Dep2();
    var dep3 = new Dep3(dep1);
    var dep4 = new Dep4(dep2, dep3);
    var program = new Program(dep1, dep3, dep4);
    program.Run();
}

Kręcimy się w kółko: znowu mamy metodę main na milion linijek.

Chyba Ty.

Czy też jest jakiś sposób, by tego uniknąć, tyle że ja go nie widzę?

Tak, wymyśliłeś sobie problem, który nie istnieje. Tzn. za długo myślałes o frameworkach DI i zapomniałeś o tym, że kod można być normalnie modularny i deklarować zależności w róznych modułach osobno. Każdy taki modulik ma potem kilka, kilkanaście klas - i co więcej nadal tworzenie zależności w danym module nie musi odbywać się w jednej metodzie.

Załóżmy teraz, że zgodnie z promowanymi przez niektórych "najlepszymi praktykami" znaczna część tych klas będzie wstrzykiwana przez DI

Nie wiem skąd takie praktyki - i nie wiem ile to jest znaczna część.
Użymam DI ("ręcznego), jeśli faktycznie jest potrzeba, tzn. jakaś klasa faktycznie ma potencjalnie różne zależności. To wcale nie tak często.

Mam teraz przed oczami projekt gdzie jest ponad 10k klas. Cała klasa z main ma 200 linijek. Nie mam sposobu, żeby sprawdzić ile z tych klas korzysta jakoś z DI, bo nie mam żadnego frameworka...

0
  1. Dzielisz aplikacje na moduły
  2. Każdy moduł ma "własne DI" którym rozwiązuje wewnętrzne zależności - żebyś mnie dobrze zrozumiał nie chodzi o to że masz mieć N kontekstów tylko o logiczny podział. Na przykład wystarczy żeby interface X był prywatny dla modułu A czyli nie eksportowany na zewnątrz i już w ten sposób będzie niedostępny dla innych modułów. Bardziej zaawansowane liby mają lepsze lub gorsze wsparcie dla tej funkcjonalnosci https://medium.com/tompee/dagger-2-scopes-and-subcomponents-d54d58511781
  3. Każdy moduł wystawia "publiczne API" z którego mogą korzystać inne moduły

Generalnie każdy moduł powinien mieć też swoją prywatną klasę z zależnościami i potem w aplikacji należy te klasy rejestrować:

di.registerModule(ModuleA.class);
di.registerModule(ModuleB.class);

...

class ModuleA {
  void register(Container di) {
    di.register(Foo.class, FooImpl.class);
    ....
  }
}

Generalnie problem nie jest nowy, wystarczy googleć pod hasłami "modulit DI".

2
YetAnohterone napisał(a):

Sytuacją dość nagminną w korpoprojektach jest ich absolutnie gigantyczny rozmiar. Ilość klas pojedynczej aplikacji idzie w miliony. I to tylko licząc "nasze" klasy, z wyłączeniem wszelkich 3rd party.

DI to tutaj najmniejszy problem. Są/były środowiska (np. w mobilkach) gdzie runtime zakładał że liczba instancji metaklasy jest ograniczona. Tj. liczba unikatowych klas (łącznie z ich hierarchią) mających aktualnie instancje nie mogła przekroczyć pewnej liczby bo inaczej runtime się wywalał - dot. to głównie implementacji Obj-C runtime dla iOS gdzie każda klasa jest de-facto singletonem ogólniejszej meta-klasy.

Nie potrafię znaleźć teraz informacji ale swego czasu FB natknął się na taki problem przy swojej aplikacji na iOSa. Prawdopodobnie ograniczenie to przestało mieć znaczenie z migracją na 64 bity.

3

Błędnie myślisz, że wszystkie klasy muszą być wstrzykiwane przez DI. Nie wszystkie i nawet nie większość.

Jeśli klasa A potrzebuje do działania instancji klasy B, to może sobie instancję klasy B stworzyć sama - bo można potraktować B jako po prostu jej wewnętrzny szczegół implementacyjny. Testowanie jest nadal możliwe - przetestujesz osobno B, a potem A razem z B. Nie ma problemu.

0
Krolik napisał(a):

Jeśli klasa A potrzebuje do działania instancji klasy B, to może sobie instancję klasy B stworzyć sama - bo można potraktować B jako po prostu jej wewnętrzny szczegół implementacyjny. Testowanie jest nadal możliwe - przetestujesz osobno B, a potem A razem z B. Nie ma problemu.

Dlatego lubię dynamiczność Obj-C. Jeśli potraktować metody obiektu jako jej szczegół implementacyjny to można swizzlując podmieniać ich wskaźniki na własne implementacje i testować zachowanie klasy w hybrydowych scenariuszach. Co prawda w powyższym występuje pewien problem zachowania tożsamości no ale w końcu przecież po to wymyślono duck-typing.

1 użytkowników online, w tym zalogowanych: 0, gości: 1