Autor Wątek: [C#] Kontener DI a rozmywanie struktury aplikacji  (Przeczytany 2649 razy)

Offline ShadowDancer

  • Redaktor

# Styczeń 09, 2016, 21:32:16
Mam wrażenie, że coś robię nie teges i poprzez intensywne użycie kontenera DI (konkretnie Autofac) struktura mojej aplikacji zaciemnia się.


Chodzi mi oto, że mamy np IA implementowane przez A, gdzie A potrzebuje IB, B potrzebuje IC - w sumie mamy 3 klasy związane z realizacją czegoś, jednak patrząc tylko na IA nie jesteśmy w stanie powiedzieć co się z tym dalej dzieje (z perspektywy osoby, która np. wchodzi w projekt).


Zastanawiam się, czy sprawy nie rozwiązywałoby np. tworzenie małych drzewek typu return
new A(new B(new C))) - tylko jak wtedy kontrolować to? Np. chciałbym zamienić B na B`. Gdy wiele klas ma jako zależność IB to rozwiązanie nie wygląda już tak różowo (i tracimy możliwość dekorowania przez kontener).


Druga sprawa - dużo klas w systemie dzieli jakąś funkcjonalność (dajmy na to logger i konfigurację), w związku z czym powstała abstrakcyjna klasa bazowa (BaseClass), która przyjmuje te dwie zależności, które są opakowne w klasę BaseClassParams.

Czy to jest dobry pomysł? Z jednej strony jest to fajne, bo resharper automatycznie generuje te konstruktory i całość jest w miarę zwięzła (Imo lepsze niż przekazywanie 2 parametrów do  konstruktora klasy bazowej). Z drugiej strony wszędzie mamy narąbane tego base(BaseClassParams), pomimo że część klas np. nie korzysta z konfiguracji lub loggera. W praktyce ten konstruktor ma z 5 różnych serwisów, a niektóre klasy nie korzystają ze wszystkich zależności).

Czy nie lepszym rozwiązaniem byłoby (::evil::)property injection? Zalety:
#1 brak syfu w konstruktorze (widzimy tam to co jest ważne)
#2 brak niepotrzebnych zależności

Wady:
Wisi magiczne property używane np. przez refleksję, code analysis podkreśla na szaro i czasami takie property wylatuje (no bo nikt z tego nie korzysta, cnie?), a potem runtime exception.

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline deadeye

  • Użytkownik

# Styczeń 10, 2016, 01:08:24
Good patterns gone bad :D

DI jest ok, ale service locator w wielu przypadkach to złe rozwiązanie - nawet na stronie Autofaca jest link do artykułu http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/

Drzewka zależności są lepsze. Jak chcesz mieć możliwość łatwej zmiany B na B', to użyj wzorca Factory http://stackoverflow.com/a/16910876

Wzorzec Factory to po prostu manualna implementacja tego, co robi Autodac - tyle że w tym wypadku jest to wyraźnie widoczne w kodzie jak są tworzone obiekty, i dzięki temu kompilator oraz programista mogą nad tym zapanować.

Co do klas typu logger i konfig, to te klasy wrzucane przez service locatora to już duże przegięcie. Do takich klas nadaje się singleton, bo powinny być dostępne wszędzie i nie zaśmiecać klas które je używają. "Klasyczny" singleton jest zły do testów, ale tylko wtedy jeśli ma zmienny stan. Jeśli klasy typu logger czy konfig nie trzymają stanu, to mogą być klasycznym singletonem (np jeśli konfig jest readonly).
Jeśli jednak trzymają stan, to zrób zwyczajnie coś takiego - zrób klasę statyczną, która przyjmuje instancję klas logger i konfig, i daje do nich statyczny dostęp. Ustawienie tych instancji zrób w entry poincie aplikacji.
Coś takiego:
class Environment
{
          static  public       Logger Logger { get; private set; }
          static  public       Config Config{ get; private set; }

         void Initialize(Logger logger, Config config) { ...}
}
Wywołujesz raz Initialize na początku aplikacji, i wstawiasz tam dowolne instancje, i w całej aplikacji masz je dostępne. W dowolnym momencie pracy nad projektem możesz zmienić, jakie instancje są tam podstawiane.

Testy są wykonywane jeden po drugim, a nie naraz -  więc w przypadku testów można wywoływać Initialize w fazie Setup każdego testu. To nadpisuje stare instancje klas i tworzy nowe, więc nie ma problemu z przenoszeniem efektów jednego testu do drugiego.

Oczywiscie to jest tight coupling, ale w przypadku rzeczy które i tak są wszędzie potrzebne, łatwiej jest robić tak niż robić DI, które i tak sprawia że te klasy są wymagane.
« Ostatnia zmiana: Styczeń 10, 2016, 01:18:05 wysłana przez deadeye »

Offline ShadowDancer

  • Redaktor

# Styczeń 10, 2016, 20:03:13
Nigdzie nie napisałem, że używam service locatora, O.o


Chodzi mi o to, że manualnie konstruując graf obiektów (nie używając kontenera) w composition root jest tam pewna informacja i drugi programista przeglądając ten graf może wyciągnąć wnioski, natomiast w przypadku kontenera cała magia dzieje się w jego konfiguracji, która to ze swojej natury jest liniowa i ciężko coś z tego wynieść.
Fabryka nie rozwiązuje żadnych problemów, ponieważ informacje o tym jak coś działa zamiast w konfiguracji kontenera pochowane są w klasach fabrykach.


Natomiast w drugim przypadku logger i config to były tylko przykłady, chodziło mi o pewne zależności współdzielone przez jakąś część systemu, jednak nie bardzo nadają się one do wydzielenia do klasy statycznej.
« Ostatnia zmiana: Styczeń 10, 2016, 20:10:41 wysłana przez ShadowDancer »

Offline deadeye

  • Użytkownik

# Styczeń 11, 2016, 20:41:07
Nigdzie nie napisałem, że używam service locatora, O.o
Kontener DI to właśnie wzorzec service locator.


Cytuj
Chodzi mi o to, że manualnie konstruując graf obiektów (nie używając kontenera) w composition root jest tam pewna informacja i drugi programista przeglądając ten graf może wyciągnąć wnioski, natomiast w przypadku kontenera cała magia dzieje się w jego konfiguracji, która to ze swojej natury jest liniowa i ciężko coś z tego wynieść.
Twój problem jest dokładnie opisany w artykule który podlinkowałem w poprzednim poście.
Cytuj
Fabryka nie rozwiązuje żadnych problemów, ponieważ informacje o tym jak coś działa zamiast w konfiguracji kontenera pochowane są w klasach fabrykach.
Fabryka rozwiązuje dużo problemów, ponieważ fabryka nie wywołuje fabryk obiektów zależnych, tylko składa obiekt od podstaw.
Czyli nie masz:
new C( BFactory.Create() )
tylko jak to nazywasz drzewko:
new C (new B(new A()))

dzięki temu składniki C masz w jednym miejscu, i widać je od razu, w przeciwieństwie do konfiga DI gdzie widzisz tylko pierwszą zależność. Ale jak chcesz zmienić klasę B na B', to zmieniasz ją tylko w fabryce w jednym miejscu.
Poza tym fabryka w przeciwieństwie do service locatorów ma zależności w kodzie, więc błędy wykrywane są compile-time, i po wejściu do fabryki od razu widać całą listę zależności danego obiektu. Poza tym możesz przeklikać drzewko wykonywania obiektów w IDE, przechodząc przez kolejne konstruktory, a w service locatorach jest to niemożliwe w łatwy sposób.

Offline ShadowDancer

  • Redaktor

# Styczeń 12, 2016, 02:07:15
Kontener DI to właśnie wzorzec service locator.
Widzę, że nie bardzo się rozumiemy. Kontener DI może być używany jako service locator, ale nie musi. Mój kod ma tylko w kilku plikach (1/assembly) referencje do kontenera i poza tym nie jest świadomy jego istnienia.

Twój problem jest dokładnie opisany w artykule który podlinkowałem w poprzednim poście.Fabryka rozwiązuje dużo problemów, ponieważ fabryka nie wywołuje fabryk obiektów zależnych, tylko składa obiekt od podstaw.
Nie mam żadego z dwóch problemów opisanych w tym artykule. Mój probelm jest blisko związany, jednak dotyczy czego innego niż te omawiane w artykule. Kod nie ma referencji do kontenera w żadnej postaci (statycznej, przez interfejs czy jakiejkolwiek).
Co do fabryk, to właśnie ja nie potrzebuję takiej fabryki. W sensie nie chcę tracić elastyczności kontenera. System jest ogromny i nie wyobrażam sobie klepania kilkudziesięciu wywołań, powtarzając się w wielu miejscach (chyba, że nie zrozumieliśmy się co do "fabryka nie wywołuje fabryk obiektów zależnych"). Taki kod ( https://gist.github.com/yegor256/c76c06baee1f74e3100e#file-agents-java ) wydaje mi się masakrą.

Poza tym fabryka w przeciwieństwie do service locatorów ma zależności w kodzie, więc błędy wykrywane są compile-time, i po wejściu do fabryki od razu widać całą listę zależności danego obiektu.
Analogicznie widzę teraz w konstruktorze obiektu.

Offline matheavyk

  • Użytkownik
    • rabagames.com

# Styczeń 12, 2016, 07:40:36
Nie siedzę w temacie na co dzień, a dawne nauki zapomniałem, więc przepraszam, że nie wiem, o czym mówicie, a się odezwę. Może ten link pomoże: http://www.wiktorzychla.com/2012/12/di-factories-and-composition-root.html . Pamiętam, że kiedy się tym interesowałem, to rozwiązania przedstawione w tym poście wydawały mi się słuszne... w razie, gdybym mówił nie na temat, to mnie upomnijcie i usunę tę odpowiedź :P

Offline ArekBal

  • Użytkownik

# Styczeń 22, 2016, 08:59:31
Żadnego statycznego Environment.
Ani wspólnej klasy bazowej dla Obiektu w systemie. Taka zbyt ogólna klasa bazowa to koszmar. Potem naciskasz kropkę i nie wiesz co do czego.

Jak masz takie wspólne "usługi" i nie chcesz wstrzykiwać osobno 8 interfaceów to je pogrupuj logicznie razem w interfacey grupujące i te wstrzykuj.
Uwaga: Ale nie upychaj wszystkiego w jednego commona... bo przecież nie oto w tym chodzi.

Property Injection rozwiązuje tylko jeden problem... kosmetyczny. Daruj je sobie. ;)

Te ręcznie pisane drzewko ma kilka wad.
Jak zaczniesz dopisywać kolejne usługi w konstruktorze to musisz też w drzewku uaktualnić.
Drzewko nie będzie też tak różowe, jak będziesz chciał współdzielić instancje. Albo gdy potem bedziesz chciał zamienić z single instance na per call.

Offline ShadowDancer

  • Redaktor

# Styczeń 24, 2016, 20:40:57
Dzięki za odpowiedzi, po przemyśleniu tego doszedłem do wniosku, że kontener DI to w projekcie mojego kalibru "mniejsze zło".