Jakie są zalety wstrzykiwania zależności w przypadkach, gdy prawie wszyscy potrzebują dostępu do wspólnej struktury danych?

20

Istnieje wiele powodów, dla których globale są złe w OOP.

Jeśli liczba lub rozmiar obiektów wymagających współdzielenia jest zbyt duża, aby można je było efektywnie przekazać w parametrach funkcji, zwykle wszyscy zalecają Wstrzykiwanie zależności zamiast obiektu globalnego.

Jednak w przypadku, gdy prawie wszyscy muszą wiedzieć o pewnej strukturze danych, dlaczego wstrzykiwanie zależności jest lepsze niż obiekt globalny?

Przykład (uproszczony, aby ogólnie pokazać punkt, bez sięgania zbyt głęboko w konkretną aplikację)

Istnieje wiele wirtualnych pojazdów, które mają ogromną liczbę właściwości i stanów, od typu, nazwy, koloru, prędkości, położenia itp. Wielu użytkowników może nimi sterować zdalnie i ogromną liczbę zdarzeń (zarówno inicjowane i automatyczne) mogą zmieniać wiele swoich stanów lub właściwości.

Naiwnym rozwiązaniem byłoby po prostu zrobienie z nich globalnego pojemnika

vector<Vehicle> vehicles;

do którego można uzyskać dostęp z dowolnego miejsca.

Bardziej przyjaznym dla OOP rozwiązaniem byłoby posiadanie kontenera należącego do klasy, która obsługuje główną pętlę zdarzeń i tworzenie instancji w jej konstruktorze. Każda klasa, która tego potrzebuje i jest członkiem głównego wątku, otrzyma dostęp do kontenera za pomocą wskaźnika w swoim konstruktorze. Na przykład, jeśli wiadomość zewnętrzna przychodzi przez połączenie sieciowe, klasa (jedna dla każdego połączenia) obsługująca parsowanie przejmie kontrolę, a parser będzie miał dostęp do kontenera za pomocą wskaźnika lub odwołania. Teraz, jeśli przeanalizowany komunikat powoduje albo zmianę elementu kontenera, albo wymaga pewnych danych z niego, aby wykonać akcję, można go obsłużyć bez potrzeby rzucania około tysięcy zmiennych przez sygnały i szczeliny (lub, co gorsza, przechowywanie ich w parserze do późniejszego pobrania przez osobę, która wywołała parser). Oczywiście wszystkie klasy, które uzyskują dostęp do kontenera poprzez wstrzyknięcie zależności, są częścią tego samego wątku. Różne wątki nie będą miały do ​​niego bezpośredniego dostępu, ale wykonają swoją pracę, a następnie wyślą sygnały do ​​głównego wątku, a gniazda w głównym wątku zaktualizują kontener.

Jeśli jednak większość klas uzyska dostęp do kontenera, co tak naprawdę różni się od globalnego? Jeśli tak wiele klas potrzebuje danych w kontenerze, to czy „sposób wstrzykiwania zależności” nie jest tylko ukrytym globalnym?

Jedną odpowiedzią byłoby bezpieczeństwo wątków: chociaż staram się nie nadużywać globalnego kontenera, być może inny programista w przyszłości, pod presją bliskiego terminu, mimo wszystko użyje globalnego kontenera w innym wątku, nie dbając o wszystko przypadki kolizji. Jednak nawet w przypadku wstrzykiwania zależności można wskazać wskaźnik komuś, kto działa w innym wątku, co prowadzi do tych samych problemów.

vsz
źródło
6
Czekaj, mówisz o niemodyfikowalnych globalach i używasz linku do „dlaczego stan globalny jest zły”, aby to uzasadnić? WTF?
Telastyn
1
W ogóle nie usprawiedliwiam globalizacji. Myślę, że jasne jest, że zgadzam się z tym, że globały nie są dobrym rozwiązaniem. Mówię tylko o sytuacji, w której nawet stosowanie wstrzyknięcia zależności może być niewiele lepsze (lub może nawet prawie nie do odróżnienia od) globali. Więc odpowiedź może wskazać inne ukryte korzyści z wciąż przy iniekcji zależność w tej sytuacji, albo że inne podejście byłoby lepsze niż dwu
VSZ
9
Ale istnieje zdecydowana różnica między globals tylko do odczytu a stanem globalnym.
Telastyn
1
@vsz niezupełnie - pytanie dotyczy fabryk lokalizatorów usług jako sposobu obejścia problemu wielu różnych obiektów; ale jego odpowiedzi również odpowiadają na pytanie o umożliwienie klasom dostępu do danych globalnych zamiast do danych globalnych przekazywanych do klas.
gbjbaanb

Odpowiedzi:

37

w przypadku, gdy prawie wszyscy muszą wiedzieć o określonej strukturze danych, dlaczego wstrzykiwanie zależności jest lepsze od obiektu globalnego?

Zastrzyk zależności jest najlepszą rzeczą od krojonego chleba , podczas gdy globalne obiekty znane są od dziesięcioleci jako źródło wszelkiego zła , więc jest to dość interesujące pytanie.

Celem zastrzyku zależności nie jest po prostu zapewnienie, że każdy aktor, który potrzebuje jakiegoś zasobu, może go mieć, ponieważ oczywiście, jeśli sprawisz, że wszystkie zasoby będą globalne, to każdy aktor będzie miał dostęp do każdego zasobu rozwiązanego problemu, prawda?

Punkt wstrzyknięcia zależności to:

  1. Aby umożliwić podmiotom dostęp do zasobów w zależności od potrzeb , oraz
  2. Aby mieć kontrolę nad tym, która instancja zasobu jest dostępna dla danego aktora.

Fakt, że w konkretnej konfiguracji wszyscy aktorzy potrzebują dostępu do tej samej instancji zasobu, nie ma znaczenia. Zaufaj mi, pewnego dnia będziesz musiał zmienić konfigurację, aby aktorzy mieli dostęp do różnych instancji zasobu, a potem zdasz sobie sprawę, że zamalowałeś się w kącie. Niektóre odpowiedzi wskazywały już na taką konfigurację: testowanie .

Kolejny przykład: załóżmy, że podzieliłeś aplikację na klient-serwer. Wszyscy aktorzy na kliencie używają tego samego zestawu zasobów centralnych na kliencie, a wszyscy aktorzy na serwerze używają tego samego zestawu zasobów centralnych na serwerze. Załóżmy, że pewnego dnia zdecydujesz się utworzyć „autonomiczną” wersję aplikacji klient-serwer, w której zarówno klient, jak i serwer są spakowane w jednym pliku wykonywalnym i uruchomione na tej samej maszynie wirtualnej. (Lub środowisko wykonawcze, w zależności od wybranego języka).

Jeśli używasz wstrzykiwania zależności, możesz łatwo upewnić się, że wszyscy aktorzy klienta otrzymują instancje zasobów klienta do pracy, podczas gdy wszyscy aktorzy serwera otrzymują instancje zasobów serwera.

Jeśli nie używasz wstrzykiwania zależności, nie masz szczęścia, ponieważ tylko jedna globalna instancja każdego zasobu może istnieć na jednej maszynie wirtualnej.

Następnie należy rozważyć: czy wszyscy aktorzy naprawdę potrzebują dostępu do tego zasobu? naprawdę?

Możliwe, że popełniłeś błąd, przekształcając ten zasób w obiekt bogów (więc oczywiście każdy potrzebuje do niego dostępu), a może rażąco przeceniasz liczbę aktorów w twoim projekcie, którzy faktycznie potrzebują dostępu do tego zasobu .

Dzięki globals każdy wiersz kodu źródłowego w całej aplikacji ma dostęp do każdego zasobu globalnego. Dzięki wstrzykiwaniu zależności każda instancja zasobu jest widoczna tylko dla aktorów, którzy jej faktycznie potrzebują. Jeśli oba są takie same (aktorzy, którzy potrzebują określonego zasobu, stanowią 100% wierszy kodu źródłowego w twoim projekcie), to musiałeś popełnić błąd w swoim projekcie. Więc też

  • Przekształć ten wielki wielki bóg w mniejsze pod-zasoby, aby różni aktorzy potrzebowali dostępu do różnych jego fragmentów, ale rzadko aktor potrzebuje wszystkich swoich fragmentów lub

  • Przeanalizuj aktorów, aby z kolei zaakceptowali jako parametry tylko podzbiory problemu, nad którymi muszą pracować, aby nie musieli przez cały czas konsultować się z jakimś wielkim, wielkim, centralnym zasobem.

Mike Nakis
źródło
Nie jestem pewien, czy to rozumiem - z jednej strony mówisz, że DI pozwala na użycie wielu instancji zasobu, a globale tego nie robią, co jest po prostu nonsensem, chyba że łączysz pojedynczą zmienną statyczną z obiektami o wielu instancjach - nie ma powodu że „globalny” musi być albo pojedynczą instancją, albo pojedynczą klasą.
gbjbaanb
3
@gbjbaanb Załóżmy, że zasób to Zork. OP mówi, że nie tylko każdy obiekt w jego systemie potrzebuje Zork do pracy, ale także, że kiedykolwiek będzie istniał tylko jeden Zork, i że wszystkie jego obiekty będą potrzebowały dostępu tylko do tej jednej instancji Zork. Mówię, że żadne z tych dwóch założeń nie jest rozsądne: prawdopodobnie nie jest prawdą, że wszystkie jego obiekty potrzebują Zorków, i będą chwile, gdy niektóre z tych obiektów będą potrzebowały określonej instancji Zork, podczas gdy inne obiekty będą potrzebowały inna instancja Zork.
Mike Nakis,
2
@gbjbaanb Nie sądzę, że prosisz mnie o wyjaśnienie, dlaczego głupotą byłoby mieć dwie globalne instancje Zork, nazwać je ZorkA i ZorkB, a także zakodować na stałe w obiektach, które będą używać ZorkA, a które ZorkB, dobrze?
Mike Nakis,
1
Nie powiedziałem wszystkich, powiedziałem prawie wszystkich. Chodziło o pytanie, czy DI ma inne zalety oprócz odmowy dostępu przez tych kilku aktorów, którzy go nie potrzebują. Wiem, że „boskie przedmioty” są anty-wzorem, ale jednym z nich może być ślepe podążanie za najlepszymi praktykami. Może się zdarzyć, że głównym celem programu jest wykonywanie różnych czynności za pomocą określonego zasobu, w takim przypadku prawie każdy potrzebuje dostępu do tego zasobu. Dobrym przykładem może być program, który działa na obrazie: prawie wszystko w nim ma coś wspólnego z danymi obrazu.
vsz
1
@gbjbaanb Uważam, że pomysł polega na tym, aby jedna klasa klienta mogła pracować wyłącznie na ZorkA lub ZorkB, jak wskazano, aby nie decydowała, którą klasę klienta wybrać.
Eugene Ryabtsev,
39

Istnieje wiele powodów, dla których niezmienne globale są złe w OOP.

To wątpliwe twierdzenie. Link, którego użyjesz jako dowodu, odnosi się do globalnych zmiennych - stanowych . Są zdecydowanie źli. Globale tylko do odczytu są tylko stałymi. Stałe są względnie zdrowe. To znaczy, nie weźmiesz wartości pi do wszystkich swoich klas, prawda?

Jeśli liczba lub rozmiar obiektów wymagających współdzielenia jest zbyt duża, aby można je było efektywnie przekazać w parametrach funkcji, zwykle wszyscy zalecają Wstrzykiwanie zależności zamiast obiektu globalnego.

Nie, nie robią tego.

Jeśli twoje funkcje / klasy mają zbyt wiele zależności, ludzie zalecają zatrzymanie się i sprawdzenie, dlaczego twój projekt jest taki zły. Mnóstwo zależności jest znakiem, że twoja klasa / funkcja prawdopodobnie robi zbyt wiele rzeczy i / lub nie masz wystarczającej abstrakcji.

Istnieje wiele wirtualnych pojazdów, które mają ogromną liczbę właściwości i stanów, od typu, nazwy, koloru, prędkości, położenia itp. Wielu użytkowników może nimi sterować zdalnie i ogromną liczbę zdarzeń (zarówno inicjowane i automatyczne) mogą zmieniać wiele swoich stanów lub właściwości.

To koszmar projektowania. Masz wiele rzeczy, których można używać / nadużywać bez żadnego sposobu sprawdzania poprawności, bez ograniczeń współbieżności - to po prostu połączenie.

Jeśli jednak większość klas uzyska dostęp do kontenera, co tak naprawdę różni się od globalnego? Jeśli tak wiele klas potrzebuje danych w kontenerze, to czy „sposób wstrzykiwania zależności” nie jest tylko ukrytym globalnym?

Tak, łączenie się ze wspólnymi danymi wszędzie nie jest zbyt różne od globalnego i jako takie jest nadal złe.

Plusem jest to, że oddzielasz konsumentów od samej zmiennej globalnej. Zapewnia to pewną elastyczność w tworzeniu instancji. To również nie zmusza cię do posiadania tylko jednej instancji. Możesz stworzyć różne, które zostaną przekazane różnym konsumentom. W razie potrzeby możesz także utworzyć różne implementacje interfejsu dla każdego konsumenta.

A ze względu na zmiany, które nieuchronnie wiążą się ze zmieniającymi się wymaganiami oprogramowania, taki podstawowy poziom elastyczności jest niezbędny .

Telastyn
źródło
przepraszam za pomyłkę z „niemodyfikowalną”, chciałem powiedzieć mutable i jakoś nie rozpoznałem mojego błędu.
vsz
„To koszmar projektowania” - wiem, że tak. Ale jeśli wymagania są takie, że wszystkie te zdarzenia muszą być w stanie dokonać zmian w danych, wówczas zdarzenia zostaną połączone z danymi, nawet jeśli podzielę je i ukryję pod warstwą abstrakcji. Cały program dotyczy tych danych. Na przykład, jeśli program musi wykonać wiele różnych operacji na obrazie, wszystkie lub prawie wszystkie jego klasy zostaną w jakiś sposób sprzężone z danymi obrazu. Samo powiedzenie „napiszmy program, który robi coś innego” jest niedopuszczalne.
vsz
2
Aplikacje, które mają wiele obiektów, które rutynowo uzyskują dostęp do niektórych baz danych, nie są zupełnie niespotykane.
Robert Harvey
@RobertHarvey - jasne, ale czy interfejs, na którym polegają, „może uzyskać dostęp do bazy danych” lub „jakiś trwały magazyn danych dla foos”?
Telastyn
1
Przypomina mi Dilberta, w którym PHB prosi o 500 funkcji, a Dilbert mówi „nie”. Więc PHB prosi o 1 funkcję, a Dilbert mówi pewnie. Następnego dnia Dilbert otrzymuje 500 żądań dotyczących 1 funkcji. Jeśli coś się nie skaluje, po prostu się nie skaluje, bez względu na to, jak je ubierzesz.
corsiKa
16

Są trzy główne powody, które powinieneś rozważyć.

  1. Czytelność. Jeśli każda jednostka kodu ma wszystko, co musi działać, albo jest wprowadzona, albo przekazana jako parametr, łatwo jest zajrzeć do kodu i natychmiast zobaczyć, co robi. Daje to lokalizację funkcji, która pozwala również lepiej rozdzielić obawy i zmusza do myślenia ...
  2. Modułowość. Dlaczego parser powinien wiedzieć o całej liście pojazdów i wszystkich ich właściwościach? Prawdopodobnie nie. Być może musi zapytać, czy identyfikator pojazdu istnieje lub czy pojazd X ma właściwość Y. Świetnie, oznacza to, że możesz napisać usługę, która to robi, i wstrzyknąć tylko tę usługę do twojego parsera. Nagle pojawia się projekt, który ma znacznie większy sens, a każdy fragment kodu obsługuje tylko dane, które są z nim związane. Co prowadzi nas do ...
  3. Testowalność W przypadku typowego testu jednostkowego chcesz skonfigurować środowisko dla wielu różnych scenariuszy, a wstrzyknięcie bardzo ułatwia jego wdrożenie. Ponownie, wracając do przykładu parsera, czy naprawdę chcesz zawsze ręcznie tworzyć całą listę pełnowartościowych pojazdów dla każdego przypadku testowego, który piszesz dla swojego parsera? A może po prostu stworzysz próbną implementację wspomnianego obiektu usługi, zwracając różną liczbę identyfikatorów? Wiem, który wybrałbym.

Podsumowując: DI nie jest celem, jest tylko narzędziem, które umożliwia osiągnięcie celu. Jeśli użyjesz go niewłaściwie (tak jak w twoim przykładzie), nie zbliżysz się do celu, nie ma magicznej właściwości DI, która uczyniłaby zły projekt dobrym.

biziclop
źródło
+1 za zwięzłą odpowiedź dotyczącą problemów związanych z konserwacją. Więcej odpowiedzi P: SE powinno być tak.
dodgethesteamroller
1
Twoje podsumowanie jest bardzo dobre. Ooooo dobrze. Jeśli myślisz, że [wzór miesiąca] lub [modne słowo miesiąca] w magiczny sposób rozwiąże twoje problemy, będziesz miał zły czas.
corsiKa,
@ corsiKa Byłem niechętny DI (a dokładniej Wiosna), ponieważ nie widziałem sensu tego. Wydawało się to po prostu okropnym kłopotem bez namacalnych korzyści (było to już za dni konfiguracji XML). Chciałbym znaleźć dokumentację na temat tego, co DI kupuje wtedy. Ale nie zrobiłem tego, więc sam musiałem to zrozumieć. To zajęło dużo czasu. :)
biziclop
3

Tak naprawdę chodzi o testowalność - zwykle obiekt globalny jest bardzo łatwy w utrzymaniu i łatwy do odczytania / zrozumienia (oczywiście gdy już się zorientujesz), ale jest to stałe narzędzie, a kiedy przychodzisz podzielić swoje klasy na pojedyncze testy, okazuje się, że twój globalny obiekt utknął mówiąc „co ze mną”, a zatem twoje izolowane testy wymagają włączenia globalnego obiektu w nie. Nie pomaga to, że większość platform testowych nie obsługuje łatwo tego scenariusza.

Więc tak, wciąż jest globalny. Podstawowy powód, dla którego nie został zmieniony, tylko sposób, w jaki jest dostępny.

Jeśli chodzi o inicjalizację, jest to nadal problem - wciąż musisz ustawić konfigurację, aby testy mogły zostać uruchomione, aby nic tam nie zyskać. Przypuszczam, że możesz podzielić duży obiekt zależny na wiele mniejszych i przekazać odpowiedni do klas, które ich potrzebują, ale prawdopodobnie masz jeszcze większą złożoność, aby przeważyć wszelkie korzyści.

Niezależnie od tego, jest to przykład „machania ogonem psa”, potrzeba przetestowania decyduje o tym, jak zbudowana jest baza kodu (podobne problemy pojawiają się w przypadku elementów takich jak klasy statyczne, np. Bieżąca data / godzina).

Osobiście wolę umieścić wszystkie moje dane globalne w obiekcie globalnym (lub kilku), ale zapewniam akcesoria, aby uzyskać bity, których potrzebują moje klasy. Następnie mogę wyśmiewać te, aby zwrócić dane, które chcę, jeśli chodzi o testowanie bez dodatkowej złożoności DI.

gbjbaanb
źródło
„zwykle obiekt globalny jest bardzo łatwy w utrzymaniu” - nie, jeśli można go modyfikować. Z mojego doświadczenia wynika, że ​​posiadanie tak wielu zmiennych globalnych stanów może być koszmarem (chociaż istnieją sposoby, aby uczynić to znośnym).
sleske,
3

Oprócz odpowiedzi, które już masz,

Istnieje wiele wirtualnych pojazdów, które mają ogromną liczbę właściwości i stanów, od typu, nazwy, koloru, prędkości, położenia itp. Wielu użytkowników może nimi sterować zdalnie i ogromną liczbę zdarzeń (zarówno inicjowane i automatyczne) mogą zmieniać wiele swoich stanów lub właściwości.

Jedną z cech charakterystycznych programowania OOP jest zachowanie właściwości, których naprawdę potrzebujesz dla określonego obiektu,

TERAZ JEŻELI Twój obiekt ma zbyt wiele właściwości - powinieneś podzielić go dalej na podobiekty.

Edytować

Wspomniałem już, dlaczego obiekty globalne są złe, dodałbym do tego jeden przykład.

Musisz przeprowadzić testowanie jednostkowe kodu GUI formularza systemu Windows, jeśli używasz globalnego, musisz utworzyć wszystkie przyciski, a na przykład zdarzenia, aby utworzyć odpowiedni przypadek testowy ...

Matematyka
źródło
To prawda, ale na przykład analizator składni będzie musiał wiedzieć wszystko, ponieważ może zostać odebrane dowolne polecenie, które może wprowadzić zmiany we wszystkim.
vsz
1
„Zanim przejdę do twojego rzeczywistego pytania” ... czy odpowiesz na jego pytanie?
gbjbaanb
Pytanie @gbjbaanb zawiera dużo tekstu, proszę dać mi trochę czasu na jego prawidłowe przeczytanie
Matematyka