Wstrzyknięcie zależności; dobre praktyki ograniczania kodu płyty kotłowej

25

Mam proste pytanie i nawet nie jestem pewien, czy ma odpowiedź, ale spróbujmy. Piszę w C ++ i używam wstrzykiwania zależności, aby uniknąć stanu globalnego. Działa to całkiem nieźle i często nie spotykam się z nieoczekiwanymi / niezdefiniowanymi zachowaniami.

Jednak zdaję sobie sprawę, że w miarę rozwoju mojego projektu piszę dużo kodu, który uważam za bojler. Gorzej: fakt, że kodu jest więcej, niż kod rzeczywisty sprawia, że ​​czasami trudno go zrozumieć.

Nic nie jest dobrym przykładem, więc chodźmy:

Mam klasę o nazwie TimeFactory, która tworzy obiekty Time.

Aby uzyskać więcej informacji (nie jestem pewien, czy ma to znaczenie): Obiekty czasu są dość złożone, ponieważ czas może mieć różne formaty, a konwersja między nimi nie jest ani liniowa, ani prosta. Każdy „Czas” zawiera Synchronizator do obsługi konwersji i aby upewnić się, że mają ten sam, właściwie zainicjowany synchronizator, używam TimeFactory. TimeFactory ma tylko jedną instancję i ma szerokie zastosowanie, więc kwalifikuje się do singletonu, ale ponieważ jest zmienny, nie chcę, aby był singletonem

W mojej aplikacji wiele klas musi tworzyć obiekty Time. Czasami te klasy są głęboko zagnieżdżone.

Powiedzmy, że mam klasę A, która zawiera instancje klasy B i tak dalej do klasy D. Klasa D musi tworzyć obiekty czasu.

W mojej naiwnej implementacji przekazuję TimeFactory do konstruktora klasy A, który przekazuje go do konstruktora klasy B i tak dalej, aż do klasy D.

Teraz wyobraź sobie, że mam kilka klas, takich jak TimeFactory i kilka hierarchii klas, takich jak ta powyżej: Tracę całą elastyczność i czytelność, które, jak sądzę, mogę uzyskać za pomocą wstrzykiwania zależności.

Zaczynam się zastanawiać, czy w mojej aplikacji nie ma poważnej wady projektowej ... Czy jest to zło konieczne przy użyciu zastrzyku uzależnienia?

Co myślisz ?

Dinaiz
źródło
5
Zadałem to pytanie programistom zamiast stackoverflow, ponieważ, jak zrozumiałem, programiści bardziej zajmują się zagadnieniami związanymi z projektowaniem, a stackoverflow dotyczy pytań „kiedy jesteś przed kompilatorem”. Z góry przepraszam, jeśli się mylę.
Dinaiz,
2
programowanie zorientowane na aspekt, nadaje się również do tego zadania - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov
Czy jest fabryka klasy A dla klas B, C i D? Jeśli nie, to dlaczego tworzy ich wystąpienia? Jeśli tylko klasa D potrzebuje obiektów czasu, dlaczego miałbyś wprowadzać inną klasę za pomocą TimeFactory? Czy klasa D naprawdę potrzebuje TimeFactory, czy może wystarczy do niej wstrzyknąć instancję Time?
simoraman
D musi tworzyć obiekty Czasu, z informacjami, które powinna posiadać tylko D. Muszę przemyśleć resztę twojego komentarza. W moim kodzie może występować problem z „kto tworzy kogo”
Dinaiz,
„kto tworzy, kto” jest rozwiązany przez kontenery IoC, zobacz moją odpowiedź w szczegółach. „kto tworzy, kto” jest jednym z najlepszych pytań, jakie możesz zadać, korzystając z Dependency Injection.
GameDeveloper

Odpowiedzi:

23

W mojej aplikacji wiele klas musi tworzyć obiekty Time

Wydaje się, że twoja Timeklasa jest bardzo podstawowym typem danych, który powinien należeć do „ogólnej infrastruktury” twojej aplikacji. DI nie działa dobrze dla takich klas. Zastanów się, co to znaczy, gdyby klasa taka stringmusiała zostać wstrzyknięta do każdej części kodu, która używa łańcuchów, a trzeba by użyć stringFactoryjedynej możliwości tworzenia nowych łańcuchów - czytelność twojego programu zmniejszyłaby się o rząd wielkość.

Więc moja sugestia: nie używaj DI do ogólnych typów danych, takich jak Time. Napisz testy jednostkowe dla Timesamej klasy, a kiedy to zrobisz, używaj jej wszędzie w swoim programie, tak jak stringklasa, vectorklasa lub jakakolwiek inna klasa standardowej biblioteki lib. Użyj DI dla komponentów, które powinny być naprawdę oddzielone od siebie.

Doktor Brown
źródło
Zupełnie masz rację. „czytelność twojego programu zmniejszyłaby się o rząd wielkości”. : dokładnie tak się stało tak. Pod warunkiem, że nadal chcę korzystać z fabryki, to nie jest takie złe uczynienie go singletonem?
Dinaiz,
3
@Dinaiz: chodzi o zaakceptowanie ścisłego powiązania między Timeinnymi częściami programu. Więc możesz zaakceptować również ścisłe połączenie z TimeFactory. Jednak unikałbym posiadania pojedynczego TimeFactoryobiektu globalnego ze stanem (na przykład informacje o lokalizacji lub coś w tym rodzaju) - co może powodować nieprzyjemne skutki uboczne dla twojego programu i bardzo utrudniać ogólne testowanie i ponowne użycie. Uczyń go bezpaństwowcem lub nie używaj go jako singletona.
Doc Brown
Właściwie to musi mieć stan, więc kiedy mówisz „Nie używaj go jako singletonu”, masz na myśli „kontynuuj stosowanie wstrzykiwania zależności, tj. Przekazując instancję do każdego obiektu, który tego potrzebuje”, prawda?
Dinaiz,
1
@Dinaiz: szczerze mówiąc, to zależy - od rodzaju stanu, rodzaju i liczby używanych części programu Timeoraz TimeFactoryod stopnia „ewolucji”, którego potrzebujesz do przyszłych rozszerzeń związanych z TimeFactoryitd.
Doc Brown
OK, postaram się utrzymać wstrzykiwanie zależności, ale mam bardziej sprytną konstrukcję, która nie wymaga tak dużo płyty kotłowej! wielkie dzięki doc ”
Dinaiz,
4

Co rozumiesz przez „tracę całą elastyczność i czytelność, jak sądzę, dzięki zastosowaniu wstrzykiwania zależności” - DI nie dotyczy czytelności. Chodzi o oddzielenie zależności między obiektami.

Wygląda na to, że masz klasę A tworzącą klasę B, klasę B tworzącą klasę C i klasę C tworzącą klasę D.

To, co powinieneś mieć, to wstrzyknięcie klasy B do klasy A. Wstrzyknięcie klasy C do klasy B. Wstrzyknięcie klasy D do klasy C.

C0deAttack
źródło
DI może dotyczyć czytelności. Jeśli masz zmienną globalną „magicznie” pojawiającą się w środku metody, to nie pomaga zrozumieć kodu (z punktu widzenia konserwacji). Skąd ta zmienna? Kto to jest właścicielem? Kto to inicjuje? I tak dalej ... Jeśli chodzi o twój drugi punkt, prawdopodobnie masz rację, ale nie sądzę, żeby miało to zastosowanie w moim przypadku. Dziękujemy za
poświęcenie
DI zwiększa czytelność głównie dlatego, że „nowy / usuń” zostanie magicznie usunięty z twojego kodu. Jeśli nie musisz się martwić o dynamiczny kod alokacji, łatwiej go odczytać.
GameDeveloper
Zapomnij również powiedzieć, że dzięki temu kod jest nieco silniejszy, ponieważ nie może dłużej przyjmować założeń dotyczących „sposobu” tworzenia zależności, „po prostu jej potrzebuje”. A to ułatwia utrzymanie klasy. Zobacz moją odpowiedź
GameDeveloper
3

Nie jestem pewien, dlaczego nie chcesz, aby fabryka czasu stała się singlem. Jeśli jest tylko jedna instancja w całej aplikacji, jest to de facto singleton.

Biorąc to pod uwagę, dzielenie się zmiennym przedmiotem jest bardzo niebezpieczne, z wyjątkiem sytuacji, gdy jest on odpowiednio chroniony przez synchronizowane bloki, w którym to przypadku nie ma powodu, aby nie był singletonem.

Jeśli chcesz wykonać wstrzyknięcie zależności, możesz przyjrzeć się ramkom wstrzykiwania sprężynowego lub innym wstrzykiwaniu zależności, które umożliwiłyby automatyczne przypisywanie parametrów za pomocą adnotacji

molyss
źródło
Wiosna jest dla java i używam C ++. Ale dlaczego 2 osoby cię głosowały? W twoim poście są punkty, z którymi się nie zgadzam, ale wciąż ...
Dinaiz
Zakładam, że głosowanie nie było związane z odpowiedzią na pytanie samo w sobie, być może równie dobrze jak wzmianką o Wiośnie. Jednak sugestia użycia Singletona była słuszna w mojej opinii i może nie odpowiadać na pytanie - sugeruje jednak ważną alternatywę i wywołuje interesujący problem projektowy. Z tego powodu mam +1.
Fergus In London
1

Ma to być uzupełniającą odpowiedzią na doktora Browna, a także odpowiedzieć na komentarze Dinaiz bez odpowiedzi, które są nadal związane z pytaniem.

Prawdopodobnie potrzebujesz ramy do wykonywania DI. Posiadanie złożonych hierarchii niekoniecznie oznacza zły projekt, ale jeśli musisz wstrzyknąć oddolne TimeFactory (od A do D) zamiast wstrzykiwać bezpośrednio do D, prawdopodobnie jest coś złego w sposobie wykonywania Dependency Injection.

Singleton? Nie, dziękuję. Jeśli potrzebujesz tylko jednej istoty, udostępnij ją w kontekście aplikacji (użycie kontenera IoC dla DI, takiego jak Infector ++, wymaga tylko powiązania TimeFactory jako pojedynczej instancji), oto przykład (C ++ 11 przy okazji, ale tak C ++. Może przenieś do C ++ 11? Otrzymujesz aplikację Leak-Free za darmo):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

Teraz zaletą kontenera IoC jest to, że nie musisz przekazywać fabryki czasu do D. Jeśli twoja klasa „D” potrzebuje fabryki czasu, po prostu ustaw fabrykę czasu jako parametr konstruktora dla klasy D.

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

jak widzisz, wstrzykujesz TimeFactory tylko raz. Jak używać „A”? Bardzo proste, każda klasa jest wstrzykiwana, budowana w głównej postaci lub zakładana fabrycznie.

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

za każdym razem, gdy utworzysz klasę A, zostanie ona automatycznie wstrzyknięta (leniwa istnienie) ze wszystkimi zależnościami do D, a D zostanie wstrzyknięta TimeFactory, więc wywołując tylko 1 metodę masz pełną hierarchię (a nawet skomplikowane hierarchie są rozwiązywane w ten sposób usuwając DUŻO kodu płyty kotłowej): Nie musisz dzwonić „nowy / usuń”, a to bardzo ważne, ponieważ możesz oddzielić logikę aplikacji od kodu kleju.

D może tworzyć obiekty Czasu z informacjami, które może posiadać tylko D.

To proste, twój TimeFactory ma metodę „utwórz”, a następnie użyj innej sygnatury „utwórz (parametry)” i gotowe. Parametry bez zależności są często rozwiązywane w ten sposób. Eliminuje to również obowiązek wstrzykiwania rzeczy takich jak „ciągi” lub „liczby całkowite”, ponieważ to po prostu dodaje dodatkową płytę kotła.

Kto tworzy kogo? Kontener IoC tworzy istoty i fabryki, fabryki tworzą resztę (fabryki mogą tworzyć różne obiekty o dowolnych parametrach, więc tak naprawdę nie potrzebujesz stanu dla fabryk). Nadal możesz używać fabryk jako opakowań dla kontenera IoC: ogólnie mówiąc, wstrzykiwanie kontenera IoC jest bardzo złe i jest identyczne z używaniem lokalizatora usług. Niektóre osoby rozwiązały problem, pakując kontener IoC w fabrykę (nie jest to absolutnie konieczne, ale ma tę zaletę, że hierarchia jest rozwiązana przez kontener, a wszystkie fabryki stają się jeszcze łatwiejsze do utrzymania).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Nie nadużywaj również wstrzykiwania zależności, proste typy mogą być po prostu elementami klasy lub zmiennymi o zasięgu lokalnym. Wydaje się to oczywiste, ale widziałem ludzi, którzy wstrzykują „std :: vector” tylko dlatego, że istniała struktura DI, która na to pozwoliła. Zawsze pamiętaj o prawie Demetera: „Wstrzykuj tylko to, czego naprawdę potrzebujesz do wstrzyknięcia”

Twórca gier
źródło
Wow, nie widziałem twojej odpowiedzi, przepraszam! Bardzo pouczający panie! Upvoted
Dinaiz
0

Czy twoje klasy A, B i C również muszą tworzyć instancje czasu, czy tylko klasy D? Jeśli jest to tylko klasa D, A i B nie powinny wiedzieć nic o TimeFactory. Utwórz instancję TimeFactory w klasie C i przekaż ją do klasy D. Zauważ, że „tworzenie instancji” niekoniecznie oznacza, że ​​klasa C musi być odpowiedzialna za tworzenie instancji TimeFactory. Może odbierać DClassFactory z klasy B, a DClassFactory wie, jak utworzyć instancję Time.

Techniką, której często używam, gdy nie mam żadnego środowiska DI, jest dostarczenie dwóch konstruktorów, jednego akceptującego fabrykę i drugiego, który tworzy fabrykę domyślną. Drugi ma zwykle dostęp chroniony / pakiet i służy głównie do testów jednostkowych.

rmaruszewski
źródło
-1

Zaimplementowałem jeszcze inny framework wstrzykiwania zależności C ++, który niedawno został zaproponowany do ulepszenia - https://github.com/krzysztof-jusiak/di - biblioteka jest pozbawiona makr (darmowa), tylko nagłówek, C ++ 03 / C ++ 11 / C ++ 14 biblioteka zapewniająca bezpieczny typ, czas kompilacji, wstrzykiwanie zależności od konstruktora bez makr.

krzychoo
źródło
3
Odpowiadanie Stare pytania na temat P.SE to zły sposób na reklamowanie twojego projektu. Weź pod uwagę, że istnieją dziesiątki tysięcy projektów, które mogą mieć pewne znaczenie dla niektórych pytań. Gdyby wszyscy ich autorzy zamieścili takie informacje tutaj, jakość odpowiedzi witryny spadłaby.