Dlaczego powinienem używać zastrzyku zależności?

95

Trudno mi znaleźć zasoby na temat tego, dlaczego powinienem używać zastrzyku zależności . Większość zasobów, które widzę, wyjaśnia, że ​​po prostu przekazuje instancję obiektu do innej instancji obiektu, ale dlaczego? Czy to tylko dla czystszej architektury / kodu czy wpływa to na wydajność jako całość?

Dlaczego powinienem wykonać następujące czynności?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Zamiast tego?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}
Daniel
źródło
8
Wprowadzasz zakodowaną zależność w celu dezaktywacjiProfile () (co jest złe). W pierwszym jest więcej oddzielonego kodu, co ułatwia zmianę i testowanie.
Aulis Ronkainen
3
Dlaczego miałbyś zrobić pierwszy? Podajesz ustawienie, a następnie ignorujesz jego wartość.
Phil N DeBlanc
57
Nie zgadzam się z głosowaniem negatywnym. Chociaż przedmiot może być uznany przez ekspertów za trywialny, pytanie ma tę zaletę: jeśli należy zastosować odwrócenie zależności, to należy uzasadnić jego użycie.
Flater
13
@PhilNDeBlanc: Ten kod jest wyraźnie uproszczony i tak naprawdę nie wskazuje na logikę świata rzeczywistego. deactivateProfileSugeruje mi jednak, że ustawienie wartości isActivefalse bez dbania o jej poprzedni stan jest tutaj właściwym podejściem. Wywołanie metody z natury oznacza, że ​​masz zamiar ustawić ją jako nieaktywną, a nie uzyskać jej aktualny (nieaktywny) status.
Flater
2
Twój kod nie jest przykładem wstrzykiwania lub inwersji zależności. Jest to przykład parametryzacji (która często jest znacznie lepsza niż DI).
jpmc26,

Odpowiedzi:

104

Zaletą jest to, że bez wstrzykiwania zależności, twoja klasa Profile

  • musi wiedzieć, jak utworzyć obiekt ustawień (narusza zasadę pojedynczej odpowiedzialności)
  • Zawsze tworzy obiekt ustawień w ten sam sposób (tworzy ścisłe połączenie między nimi)

Ale z zastrzykiem zależności

  • Logika tworzenia obiektów ustawień znajduje się gdzie indziej
  • Łatwo jest używać różnego rodzaju obiektów ustawień

Może to wydawać się (lub nawet być) nieistotne w tym konkretnym przypadku, ale wyobraź sobie, że nie mówimy o obiekcie Settings, ale obiekcie DataStore, który może mieć różne implementacje, takim, który przechowuje dane w plikach, a innym, który przechowuje je w baza danych. A w przypadku testów automatycznych potrzebujesz także próbnej implementacji. Teraz naprawdę nie chcesz, aby klasa Profile zapisywała kod, którego używa - a co ważniejsze, naprawdę nie chcesz, aby klasa Profile wiedziała o ścieżkach systemu plików, połączeniach DB i hasłach, więc tworzenie obiektów DataStore musi się zdarzyć gdzie indziej.

Michael Borgwardt
źródło
23
This may seem (or even be) irrelevant in this particular caseMyślę, że w rzeczywistości jest to bardzo istotne. Jak uzyskasz ustawienia? Wiele systemów, które widziałem, będzie miało zakodowany domyślny zestaw ustawień i publiczną konfigurację, więc musisz załadować oba i zastąpić niektóre wartości ustawieniami publicznymi. Możesz nawet potrzebować wielu źródeł domyślnych. Być może dostajesz niektóre z dysku, inne z DB. Tak więc cała logika nawet uzyskiwania ustawień może i jest często nietrywialna - na pewno nie powinien lub nie powinien dbać o coś pochłaniającego kod.
VLAZ,
Moglibyśmy również wspomnieć, że inicjalizacja obiektu dla nietrywialnego komponentu, takiego jak usługa sieciowa, byłaby $setting = new Setting();strasznie nieefektywna. Tworzenie instancji wtrysku i obiektu odbywa się raz.
vikingsteve
9
Uważam, że używanie próbnych testów do testowania powinno mieć większy nacisk. Możliwe, że jeśli spojrzysz tylko na kod, zawsze będzie to obiekt Settings i nigdy się nie zmieni, więc przekazanie go wydaje się zmarnowanym wysiłkiem. Jednak raz pierwszy spróbować przetestować obiekt Profil sama bez również potrzebują Ustawienia obiektu (przy użyciu atrapa obiektu zamiast w postaci roztworu) potrzeba jest bardzo widoczne.
JPhi1618,
2
@ JPhi1618 Myślę, że problem z podkreśleniem „DI jest dla testów jednostkowych” polega na tym, że po prostu prowadzi do pytania „dlaczego potrzebuję testów jednostkowych”. Odpowiedź może wydawać się dla ciebie oczywista, a korzyści na pewno istnieją, ale dla kogoś, kto dopiero zaczyna, mówiąc: „musisz zrobić to skomplikowane brzmienie, aby zrobić inne skomplikowane brzmienie”, wydaje się to trochę wyłączenie. Warto więc wspomnieć o różnych zaletach, które mogą lepiej odnosić się do tego, co robią teraz.
IMSoP
1
@ user986730 - to bardzo dobra uwaga i podkreśla, że ​​prawdziwą zasadą jest tutaj odwrócenie zależności , którego wstrzyknięcie jest tylko jedną techniką. Na przykład w C nie można tak naprawdę wstrzykiwać zależności za pomocą biblioteki IOC, ale można odwracaj je w czasie kompilacji, dołączając różne pliki źródłowe dla twoich prób itp., co ma ten sam efekt.
Stephen Byrne
64

Wstrzykiwanie zależności ułatwia testowanie kodu.

Nauczyłem się tego z pierwszej ręki, kiedy miałem za zadanie naprawić trudny do złapania błąd w integracji Magento z PayPal.

Pojawiłby się problem, gdy PayPal informował Magento o nieudanej płatności: Magento nie zarejestrował prawidłowo nieprawidłowości.

Ręczne testowanie potencjalnej poprawki byłoby bardzo żmudne: trzeba by w jakiś sposób uruchomić powiadomienie PayPal „Nieudane”. Musisz przesłać e-czek, anulować go i poczekać, aż błąd się skończy. Oznacza to ponad 3 dni na przetestowanie jednoznakowej zmiany kodu!

Na szczęście wydaje się, że twórcy rdzenia Magento, którzy opracowali tę funkcję, mieli na myśli testy i zastosowali wzorzec wstrzykiwania zależności, aby uczynić ją trywialną. To pozwala nam zweryfikować naszą pracę za pomocą prostego przypadku testowego takiego jak ten:

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

Jestem pewien, że wzorzec DI ma wiele innych zalet, ale zwiększona testowalność jest jedną z największych korzyści w mojej głowie .

Jeśli jesteś ciekawy rozwiązania tego problemu, sprawdź repozytorium GitHub tutaj: https://github.com/bubbleupdev/BUCorefix_Paypalstatus

Eric Seastrand
źródło
4
Wstrzykiwanie zależności ułatwia testowanie kodu niż kod z zależnościami na stałe. Całkowite wyeliminowanie zależności z logiki biznesowej jest jeszcze lepsze.
Ant P
1
Jednym z głównych sposobów robienia tego, co sugeruje @AntP, jest parametryzacja . Logika przekształcania wyników wracających z bazy danych do obiektu używanego do wypełnienia szablonu strony (powszechnie znanego jako „model”) nie powinna nawet wiedzieć, że ma miejsce pobieranie; wystarczy pobrać te obiekty jako dane wejściowe.
jpmc26,
4
@ jpmc26 rzeczywiście - mam tendencję do omawiania funkcjonalnego rdzenia, imperatywnej powłoki , która jest tak naprawdę wymyślną nazwą do przesyłania danych do twojej domeny zamiast wstrzykiwania do niej zależności. Domena jest czysta, może być testowana jednostkowo bez prób, a następnie jest po prostu owinięta w powłokę, która dostosowuje takie rzeczy, jak wytrwałość, wysyłanie wiadomości itp.
Ant P
1
Myślę, że skupienie się wyłącznie na testowalności jest szkodliwe dla przyjęcia DI. To sprawia, że ​​jest to nieprzyjemne dla osób, które czują, że albo nie potrzebują dużo testów, albo myślą, że już je kontrolują. Twierdziłbym, że pisanie czystego kodu wielokrotnego użytku bez DI jest praktycznie niemożliwe. Testowanie jest bardzo daleko na liście korzyści i rozczarowanie, że ta odpowiedź zajmuje 1. lub 2. miejsce w każdym pytaniu na temat korzyści DI.
Carl Leth,
26

Dlaczego (o co w tym chodzi)?

Dlaczego powinienem używać zastrzyku zależności?

Najlepszym mnemonikiem, jaki znalazłem do tego, jest „ nowy klej ”: za każdym razem, gdy używasz newswojego kodu, kod ten jest powiązany z tą konkretną implementacją . Jeśli wielokrotnie użyjesz nowego w konstruktorach, utworzysz łańcuch konkretnych implementacji . A ponieważ nie można „mieć” instancji klasy bez jej zbudowania, nie można rozdzielić tego łańcucha.

Jako przykład wyobraź sobie, że piszesz grę wideo na samochód wyścigowy. Zacząłeś od klasy Game, która tworzy a RaceTrack, która tworzy 8 Cars, z których każda tworzy a Motor. Teraz, jeśli chcesz 4 dodatkowe Carsz innym przyspieszeniem, będziesz musiał zmienić każdą wymienioną klasę , z wyjątkiem może Game.

Kod czyszczący

Czy to tylko dla czystszej architektury / kodu

Tak .

Jednak w tej sytuacji może to wydawać się mniej jasne , ponieważ jest to bardziej przykład tego, jak to zrobić . Faktyczna przewaga pokazuje się tylko wtedy, gdy zaangażowanych jest kilka klas i trudniej jest ją zademonstrować, ale wyobraź sobie, że użyłbyś DI w poprzednim przykładzie. Kod tworzący te wszystkie rzeczy może wyglądać mniej więcej tak:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

Dodanie tych 4 różnych samochodów można teraz zrobić, dodając następujące wiersze:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • Nie było żadnych zmian w RaceTrack, Game, Car, lubMotor były konieczne - co oznacza, że możemy być w 100% pewien, że nie wprowadzi jakieś nowe błędy!
  • Zamiast przeskakiwać między kilkoma plikami, możesz zobaczyć pełną zmianę na ekranie w tym samym czasie. Wynika to z postrzegania tworzenia / konfiguracji / konfiguracji jako własnej odpowiedzialności - zbudowanie silnika nie jest zadaniem samochodu.

Uwagi dotyczące wydajności

czy to wpływa na wydajność jako całość?

Nie . Ale szczerze mówiąc, może tak być.

Jednak nawet w takim przypadku jest to tak absurdalnie mała ilość, że nie musisz się tym przejmować. Jeśli w pewnym momencie w przyszłości, trzeba napisać kod dla Tamagotchi z równowartości 5 MHz CPU i 2MB pamięci RAM, to może ty może trzeba dbać o to.

W 99,999% * przypadków będzie miało lepszą wydajność, ponieważ spędzasz mniej czasu na naprawianiu błędów i więcej czasu na ulepszaniu algorytmów obciążających zasoby.

* całkowicie skompletowany numer

Dodano informacje: „na stałe”

Nie popełnijcie błędu, jest to nadal bardzo „zakodowane na stałe” - liczby są zapisywane bezpośrednio w kodzie. Nie zakodowane na stałe oznaczałoby coś w rodzaju przechowywania tych wartości w pliku tekstowym - np. W formacie JSON - a następnie odczytania ich z tego pliku.

W tym celu należy dodać kod do odczytu pliku, a następnie parsowania JSON. Jeśli ponownie rozważysz przykład; w wersji innej niż DI, a Carlub a Motorteraz musi odczytać plik. To nie brzmi tak, jakby miało to zbyt duży sens.

W wersji DI dodajesz go do kodu konfigurującego grę.

R. Schmitz
źródło
3
Reklama zakodowana na stałe, tak naprawdę nie ma tak dużej różnicy między kodem a plikiem konfiguracyjnym. Plik dołączony do aplikacji jest źródłem, nawet jeśli czytasz dynamicznie. Przeciąganie wartości z kodu do plików danych w formacie json lub innym formacie „config” nic nie pomaga, chyba że wartości powinny zostać zastąpione przez użytkownika lub zależeć od środowiska.
Jan Hudec
3
Rzeczywiście zrobiłem Tamagotchi raz na Arduino (16MHz, 2KB)
Jungkook
@JHHecec True. Właściwie miałem tam dłuższe wyjaśnienie, ale postanowiłem je usunąć, aby było krótsze i skupiłem się na tym, jak odnosi się do DI. Jest więcej rzeczy, które nie są w 100% poprawne; ogólnie odpowiedź jest bardziej zoptymalizowana, aby „przesunąć” punkt „DI” bez wydłużania go. Innymi słowy, chciałbym to usłyszeć, kiedy zaczynałem od DI.
R. Schmitz
17

Zawsze byłem zaskoczony zastrzykiem zależności. Wydawało się, że istnieje tylko w sferach Jawy, ale te sfery mówiły o tym z wielką czcią. To był jeden z wielkich Wzorów, o których mówi się, że wprowadzają porządek w chaos. Ale przykłady zawsze były skomplikowane i sztuczne, ustanawiając problem, a następnie podejmując się jego rozwiązania, czyniąc kod bardziej skomplikowanym.

Bardziej sensowne było, gdy inny twórca Pythona przekazał mi tę mądrość: po prostu przekazuje argumenty do funkcji. To prawie nie jest wzór; bardziej przypomina przypomnienie, że możesz poprosić o coś jako argument, nawet jeśli sam mógłbyś sobie wyobrazić rozsądną wartość.

Więc twoje pytanie jest mniej więcej równoważne z „dlaczego moja funkcja powinna brać argumenty?” i ma wiele takich samych odpowiedzi, a mianowicie: pozwolić dzwoniącemu na podejmowanie decyzji .

Oczywiście wiąże się to z pewnymi kosztami, ponieważ teraz zmuszasz osobę dzwoniącą do podjęcia pewnej decyzji (chyba że podasz argument jako opcjonalny), a interfejs jest nieco bardziej złożony. W zamian zyskujesz elastyczność.

Więc: Czy jest jakiś dobry powód, dla którego musisz konkretnie użyć tego konkretnego Settingtypu / wartości? Czy istnieje dobry powód, aby wywoływać kod, który może chcieć innego Settingtypu / wartości? (Pamiętaj, testy są kodem !)

Eevee
źródło
2
Tak. IoC w końcu kliknęło mnie, gdy zdałem sobie sprawę, że chodzi po prostu o „pozwolenie dzwoniącemu na podjęcie decyzji”. To IoC oznacza, że ​​sterowanie komponentem zostało przeniesione z autora komponentu na użytkownika komponentu. A ponieważ w tym momencie miałem już dość problemów z oprogramowaniem, które uważało się za mądrzejsze ode mnie, natychmiast sprzedano mi podejście DI.
Joker_vD
1
„po prostu przekazuje argumenty do funkcji. To prawie żaden wzorzec” To jest jedyne dobre lub nawet zrozumiałe wytłumaczenie DI, jakie słyszałem (cóż, większość ludzi nawet nie mówi o tym, co to jest , ale raczej o jego zaletach). Następnie używają skomplikowanego żargonu, takiego jak „odwrócenie kontroli” i „luźne sprzężenie”, które wymagają więcej pytań, aby zrozumieć, kiedy jest to naprawdę prosty pomysł.
addison
7

Podany przykład nie jest wstrzykiwaniem zależności w sensie klasycznym. Wstrzykiwanie zależności zwykle odnosi się do przekazywania obiektów w konstruktorze lub przy użyciu „wstrzykiwania ustawiacza” tuż po utworzeniu obiektu, aby ustawić wartość w polu w nowo utworzonym obiekcie.

Twój przykład przekazuje obiekt jako argument do metody instancji. Ta metoda instancji następnie modyfikuje pole tego obiektu. Zastrzyk zależności? Nie. Zerwanie enkapsulacji i ukrywanie danych? Absolutnie!

Teraz, jeśli kod był taki:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

Powiedziałbym, że używasz zastrzyku zależności. Istotną różnicą jest Settingsobiekt przekazywany do konstruktora lub aProfile obiektu.

Jest to przydatne, jeśli obiekt Settings jest drogi lub skomplikowany w budowie, lub Settings to interfejs lub klasa abstrakcyjna, w której istnieje wiele konkretnych implementacji w celu zmiany zachowania w czasie wykonywania.

Ponieważ bezpośrednio uzyskujesz dostęp do pola w obiekcie Ustawienia, a nie wywołujesz metodę, nie możesz skorzystać z polimorfizmu, który jest jedną z zalet wstrzykiwania zależności.

Wygląda na to, że ustawienia profilu są specyficzne dla tego profilu. W takim przypadku zrobiłbym jedną z następujących czynności:

  1. Utwórz instancję obiektu Settings wewnątrz konstruktora profilu

  2. Przekaż obiekt Ustawienia w konstruktorze i skopiuj poszczególne pola, które dotyczą profilu

Szczerze mówiąc, przekazanie obiektu Settings do, deactivateProfilea następnie zmodyfikowanie wewnętrznego pola obiektu Settings to zapach kodu. Obiekt Settings powinien być jedynym, który modyfikuje swoje pola wewnętrzne.

Greg Burghardt
źródło
5
The example you give is not dependency injection in the classical sense.- To nie ma znaczenia. Ludzie z OO mają przedmioty w mózgu, ale nadal uzależniasz się od czegoś.
Robert Harvey
1
Kiedy mówisz o „ w sensie klasycznym ”, jak mówi @RobertHarvey, mówisz wyłącznie w kategoriach OO. Na przykład w programowaniu funkcjonalnym wstrzykiwanie jednej funkcji w drugą (funkcje wyższego rzędu) jest klasycznym przykładem tego wstrzykiwania zależności.
David Arno,
3
@RobertHarvey: Wydaje mi się, że brałem zbyt wiele swobód z „zastrzykiem zależności”. Miejsce, w którym ludzie najczęściej myślą o tym znaczeniu i używają tego terminu, odnosi się do pól na „wstrzykiwanym” obiekcie w czasie budowy obiektu lub bezpośrednio po nim w przypadku wstrzykiwania setera.
Greg Burghardt
@DavidArno: Tak, masz rację. Wydaje się, że OP ma w pytaniu fragment kodu PHP, więc odpowiadałem tylko w tym względzie i nie zajmowałem się programowaniem funkcjonalnym --- wszystko to jednak w przypadku PHP można zadać to samo pytanie z punktu widzenia programowania funkcjonalnego.
Greg Burghardt
7

Wiem, że spóźniam się na to przyjęcie, ale wydaje mi się, że brakuje ważnego punktu.

Dlaczego powinienem to zrobić:

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Nie powinieneś Ale nie dlatego, że wstrzykiwanie zależności jest złym pomysłem. To dlatego, że robi to źle.

Spójrzmy na te rzeczy za pomocą kodu. Zrobimy to:

$profile = new Profile();
$profile->deactivateProfile($setting);

kiedy wyciągniemy z tego to samo:

$setting->isActive = false; // Deactivate profile

Wydaje się to oczywiście stratą czasu. To kiedy robisz to w ten sposób. To nie jest najlepsze zastosowanie wstrzykiwania zależności. To nawet nie najlepsze wykorzystanie klasy.

Co jeśli zamiast tego mielibyśmy to:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

A teraz applicationmożna go aktywować i dezaktywować profilebez konieczności posiadania szczególnej wiedzy na temat tego setting, co się zmienia. Czemu to jest dobre? W razie potrzeby zmiany ustawienia. TheapplicationJest murem off z tych zmian tak, jesteś wolny, aby przejść orzechy w bezpiecznej przestrzeni zawartej bez przerwy oglądać wszystko jak najszybciej dotknąć czegoś.

Wynika to z odrębnej konstrukcji od zachowania zasady . Wzór DI jest tutaj prosty. Zbuduj wszystko, czego potrzebujesz, na jak najniższym poziomie, połącz je ze sobą, a następnie rozpocznij od tykania wszystkich zachowań.

W rezultacie masz oddzielne miejsce do decydowania o tym, co łączy się z czym, oraz inne miejsce do zarządzania tym, co mówi co do czego.

Spróbuj tego na czymś, co musisz utrzymywać w czasie i sprawdź, czy to nie pomaga.

candied_orange
źródło
6

Czy jako klient, kiedy zatrudniasz mechanika, który zrobi coś z twoim samochodem, oczekujesz, że ten mechanik zbuduje samochód od zera, aby potem z nim pracować? Nie, dajesz mechanikowi samochód, nad którym chcesz, aby pracował .

Jako właściciel garażu, kiedy instruujesz mechanika, aby zrobił coś z samochodem, czy spodziewasz się, że mechanik stworzy własny śrubokręt / klucz / części samochodowe? Nie, dostarczasz mechanikowi części / narzędzia, których potrzebuje

Dlaczego to robimy? Cóż Pomyśl o tym. Jesteś właścicielem garażu, który chce zatrudnić kogoś, kto zostanie twoim mechanikiem. Nauczysz ich mechaniki (= napiszesz kod).

Co będzie łatwiejsze:

  • Naucz mechanika, jak przymocować spoiler do samochodu za pomocą śrubokręta.
  • Naucz mechanika tworzenia samochodu, spoilera, śrubokręta, a następnie podłącz nowo utworzony spoiler do nowo utworzonego samochodu za pomocą nowo utworzonego śrubokręta.

Ogromne korzyści sprawiają, że mechanik nie tworzy wszystkiego od zera:

  • Oczywiście szkolenie (= rozwój) jest znacznie skrócone, jeśli tylko dostarczysz mechanikowi istniejące narzędzia i części.
  • Jeśli ten sam mechanik musi wykonać tę samą pracę wiele razy, możesz upewnić się, że ponownie użyje śrubokręta, zamiast zawsze wyrzucać stary i tworzyć nowy.
  • Dodatkowo mechanik, który nauczył się tworzyć wszystko, będzie musiał być bardziej ekspertem, a zatem będzie oczekiwać wyższej płacy. Analogia kodowania tutaj jest taka, że ​​klasa z wieloma obowiązkami jest znacznie trudniejsza do utrzymania niż klasa z jedną ściśle określoną odpowiedzialnością.
  • Dodatkowo, kiedy na rynek trafiają nowe wynalazki, a spoilery są teraz wytwarzane z węgla zamiast z tworzywa sztucznego; będziesz musiał przekwalifikować się (= przebudować) swojego eksperta. Ale twoja „prosta” mechanika nie będzie musiała być ponownie przeszkolona, ​​dopóki spoiler nadal będzie można zamocować w ten sam sposób.
  • Posiadanie mechanika, który nie polega na samochodzie, który sam zbudował, oznacza, że ​​masz mechanika, który jest w stanie obsłużyć każdy samochód, który może otrzymać. W tym samochody, które jeszcze nie istniały w momencie szkolenia mechanika. Jednak Twój mechanik nie będzie w stanie budować nowszych samochodów, które powstały po ich szkoleniu.

Jeśli zatrudnisz i wyszkolisz mechanika-eksperta, skończysz z pracownikiem, który kosztuje więcej, poświęca więcej czasu na wykonanie czegoś, co powinno być prostą pracą, i zawsze będzie musiał być przekwalifikowany za każdym razem, gdy jeden z wielu obowiązków tego wymaga do zaktualizowania.

Analogia programistyczna jest taka, że ​​jeśli użyjesz klas z zakodowanymi zależnościami, to skończysz z trudnymi do utrzymania klasami, które będą wymagały ciągłej przebudowy / zmian za każdym razem, gdy Settingstworzona jest nowa wersja obiektu ( w twoim przypadku), a ty Będę musiał rozwinąć wewnętrzną logikę, aby klasa mogła tworzyć różne typy Settingsobiektów.

Ponadto, kto spożywa twoją klasę, będzie musiał teraz poprosić klasę o utworzenie poprawnego Settingsobiektu, w przeciwieństwie do zwykłego przekazania klasie dowolnego Settingsobiektu, który chce przekazać. Oznacza to dodatkowy rozwój dla konsumenta, aby dowiedzieć się, jak poprosić klasę o stworzenie odpowiedniego narzędzia.


Tak, inwersja zależności wymaga nieco więcej wysiłku, niż zapisanie zależności na stałe. Tak, denerwujące jest pisanie więcej.

Jest to jednak ten sam argument, co wybór literału wartości kodu, ponieważ „deklarowanie zmiennych wymaga więcej wysiłku”. Technicznie poprawne, ale pro przewyższają wady o kilka rzędów wielkości .

Korzyści wynikające z odwrócenia zależności nie są odczuwalne podczas tworzenia pierwszej wersji aplikacji. Korzyści z inwersji zależności są odczuwalne, gdy trzeba zmienić lub rozszerzyć tę początkową wersję. I nie oszukuj się, myśląc, że za pierwszym razem dobrze to zrobisz i nie będziesz musiał przedłużać / zmieniać kodu. Będziesz musiał coś zmienić.


czy wpływa to na wydajność jako całość?

Nie wpływa to na wydajność środowiska wykonawczego aplikacji. Ale ma to ogromny wpływ na czas programowania (a tym samym wydajność) programisty .

Flater
źródło
2
Jeśli usunąłeś swój komentarz: „ Jako drobny komentarz, twoje pytanie skupia się na inwersji zależności, a nie na wstrzykiwaniu zależności. Wtrysk to jeden ze sposobów wykonania inwersji, ale to nie jedyny sposób. ”, To byłaby doskonała odpowiedź. Odwrócenie zależności można osiągnąć za pomocą iniekcji lub lokalizatora / globalizacji. Przykłady pytania dotyczą wstrzyknięcia. Pytanie dotyczy więc wstrzykiwania zależności (a także inwersji zależności).
David Arno
12
Wydaje mi się, że cała sprawa z samochodem jest nieco uciążliwa i myląca
Ewan
4
@ Później część problemu polega na tym, że nikt tak naprawdę nie zgadza się co do różnicy między wstrzykiwaniem zależności, odwróceniem zależności i odwróceniem kontroli. Jedno jest pewne, jednak „pojemnik” z pewnością nie jest potrzebny do wstrzyknięcia zależności do metody lub konstruktora. Czysta (lub biedna) DI w szczególności opisuje ręczne wstrzykiwanie zależności. To jedyny zastrzyk uzależnienia, którego osobiście używam, ponieważ nie lubię „magii” związanej z pojemnikami.
David Arno,
1
@ Flater Problem z przykładem polega na tym, że prawie na pewno nie modelowałbyś żadnego problemu w kodzie w ten sposób. Jest więc nienaturalny, wymuszony i dodaje więcej pytań niż odpowiedzi. To sprawia, że ​​bardziej prawdopodobne jest wprowadzanie w błąd i dezorientacja niż demonstracja.
jpmc26,
2
@gbjbaanb: W żadnym momencie moja odpowiedź nawet nie zagłębia się w polimorfizm. Nie ma dziedziczenia, implementacji interfejsu ani czegokolwiek przypominającego up / downcasting typów. Całkowicie błędnie interpretujesz odpowiedź.
Flater
0

Jak wszystkie wzory, bardzo ważne jest pytanie „dlaczego”, aby uniknąć rozdętych wzorów.

W przypadku wstrzykiwania zależności można to łatwo dostrzec, myśląc o dwóch, prawdopodobnie najważniejszych aspektach projektu OOP ...

Niskie sprzęgło

Sprzężenie w programowaniu komputerowym :

W inżynierii oprogramowania sprzężenie jest stopniem współzależności między modułami oprogramowania; miara tego, jak blisko powiązane są dwie procedury lub moduły; siła relacji między modułami.

Chcesz osiągnąć niskie sprzężenie. Dwie rzeczy silnie powiązane oznaczają, że jeśli zmienisz jedną, najprawdopodobniej będziesz musiał zmienić drugą. Błędy lub ograniczenia w jednym prawdopodobnie spowodują błędy / ograniczenia w drugim; i tak dalej.

Tworzenie obiektów jednej klasy przez inne jest bardzo silnym sprzężeniem, ponieważ jeden musi wiedzieć o istnieniu drugiego; musi wiedzieć, jak go utworzyć (jakich argumentów potrzebuje konstruktor), a te argumenty muszą być dostępne podczas wywoływania konstruktora. Ponadto, w zależności od tego, czy język wymaga wyraźnej dekonstrukcji (C ++), wprowadzi to dalsze komplikacje. Jeśli wprowadzisz nowe klasy (tj. A NextSettingslub cokolwiek), musisz wrócić do oryginalnej klasy i dodać więcej wywołań do ich konstruktorów.

Wysoka spójność

Spójność :

W programowaniu komputerowym spójność odnosi się do stopnia, w jakim elementy wewnątrz modułu należą do siebie.

To jest druga strona medalu. Jeśli spojrzysz na jedną jednostkę kodu (jedną metodę, jedną klasę, jedną paczkę itp.), Chcesz mieć cały kod wewnątrz tej jednostki, aby mieć jak najmniej obowiązków.

Podstawowym przykładem może być wzorzec MVC: wyraźnie oddzielasz model domeny od widoku (GUI) i warstwę kontrolną, która skleja je ze sobą.

Pozwala to uniknąć nadmiaru kodu w przypadku dużych fragmentów, które wykonują wiele różnych rzeczy; jeśli chcesz zmienić jakąś jego część, musisz również śledzić wszystkie inne funkcje, starając się unikać błędów itp .; i szybko zaprogramujesz się w dziurze, w której bardzo trudno się wydostać.

Dzięki wstrzykiwaniu zależności delegujesz tworzenie lub śledzenie zależności, cóż, do dowolnych klas (lub plików konfiguracyjnych), które implementują twoją DI. Inne klasy nie będą się przejmować tym, co się właściwie dzieje - będą pracować z niektórymi rodzajowymi interfejsami i nie mają pojęcia, co to jest rzeczywista implementacja, co oznacza, że ​​nie są odpowiedzialne za inne rzeczy.

AnoE
źródło
-1

Powinieneś użyć technik, aby rozwiązać problemy, które są dobre w rozwiązywaniu problemów. Odwrócenie zależności i wtrysk nie różnią się.

Inwersja lub wstrzykiwanie zależności jest techniką, która pozwala Twojemu kodowi decydować, która implementacja metody zostanie wywołana w czasie wykonywania. Maksymalizuje to zalety późnego wiązania. Technika jest konieczna, gdy język nie obsługuje zastępowania w czasie wykonywania funkcji innych niż instancja. Na przykład Java nie ma mechanizmu zastępującego wywołania metody statycznej wywołaniami innej implementacji; w przeciwieństwie do Pythona, gdzie wszystko, co jest konieczne do zastąpienia wywołania funkcji, to powiązanie nazwy z inną funkcją (ponowne przypisanie zmiennej przechowującej funkcję).

Dlaczego chcielibyśmy różnicować implementację funkcji? Istnieją dwa główne powody:

  • Chcemy wykorzystywać podróbki do celów testowych. To pozwala nam przetestować klasę, która zależy od pobierania bazy danych bez faktycznego łączenia się z bazą danych.
  • Musimy wspierać wiele wdrożeń. Na przykład może być konieczne skonfigurowanie systemu obsługującego bazy danych MySQL i PostgreSQL.

Możesz także wziąć pod uwagę odwrócenie kontenerów kontrolnych. Jest to technika, która ma na celu pomóc ci uniknąć dużych, splątanych drzew konstrukcyjnych, które wyglądają jak ten pseudokod:

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

Pozwala zarejestrować zajęcia, a następnie wykonuje dla Ciebie konstrukcję:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

Zauważ, że najłatwiej jest, jeśli zarejestrowane klasy mogą być bezpaństwowymi singletonami .

Słowo ostrzeżenia

Zauważ, że inwersja zależności nie powinna być twoją podstawową odpowiedzią na logikę oddzielania. Poszukaj możliwości użycia zamiast tego parametryzacji . Rozważ tę metodę pseudokodu, na przykład:

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

Możemy użyć inwersji zależności dla niektórych części tej metody:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

Ale nie powinniśmy, przynajmniej nie do końca. Zauważ, że stworzyliśmy klasę stanowąQuerier . Teraz zawiera odwołanie do jakiegoś zasadniczo globalnego obiektu połączenia. Powoduje to takie problemy, jak trudności w zrozumieniu ogólnego stanu programu i sposobu koordynacji między poszczególnymi klasami. Zwróć też uwagę, że jesteśmy zmuszeni sfałszować kwerendę lub połączenie, jeśli chcemy przetestować logikę uśredniania. Dalej Lepszym podejściem byłoby zwiększenie parametryzacji :

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

Połączenie byłoby zarządzane na jeszcze wyższym poziomie, który jest odpowiedzialny za całą operację i wie, co zrobić z tym wyjściem.

Teraz możemy przetestować logikę uśredniania całkowicie niezależnie od zapytań, a ponadto możemy jej użyć w wielu różnych sytuacjach. Możemy zapytać, czy w ogóle potrzebujemy obiektów MyQuerieri Averager, a może odpowiedź jest taka, że ​​nie zrobimy tego, jeśli nie zamierzamy przeprowadzać testów jednostkowych StuffDoer, a nie testowanie jednostkowe StuffDoerbyłoby całkowicie rozsądne, ponieważ jest tak ściśle powiązane z bazą danych. Bardziej sensowne byłoby po prostu pozwolić, aby testy integracji obejmowały to. W takim przypadku możemy być w porządku fetchAboveMini averageDatastosować metody statyczne.

jpmc26
źródło
2
Wstrzykiwanie zależności jest techniką, która ma na celu pomóc ci uniknąć dużych, splątanych drzew budowlanych… ”. Twój pierwszy przykład po tym twierdzeniu jest przykładem zastrzyku uzależnienia czystego lub biednego człowieka. Drugi to przykład użycia kontenera IoC do „zautomatyzowania” wstrzyknięcia tych zależności. Oba są przykładami zastrzyku zależności w działaniu.
David Arno,
@DavidArno Tak, masz rację. Poprawiłem terminologię.
jpmc26
Istnieje trzeci główny powód do różnicowania implementacji lub przynajmniej zaprojektowania kodu przy założeniu, że implementacja może się różnić: zachęca programistę do luźnego łączenia i unikania pisania kodu, który będzie trudny do zmiany, jeśli nowa implementacja zostanie wdrożona w pewnym momencie w przyszłość. I chociaż może to nie być priorytetem w niektórych projektach (np. Wiedząc, że aplikacja nigdy nie będzie ponownie odwiedzana po jej pierwszym wydaniu), będzie w innych (np. Tam, gdzie model biznesowy firmy stara się oferować rozszerzone wsparcie / rozszerzenie swoich aplikacji ).
Flater
Wtrysk Flater Dependency nadal powoduje silne sprzężenie. Łączy logikę z określonym interfejsem i wymaga, aby dany kod wiedział o wszystkim, co robi ten interfejs. Rozważ kod, który przekształca wyniki pobrane z bazy danych. Jeśli użyję DI do ich rozdzielenia, kod nadal musi wiedzieć, że następuje pobieranie DB i wywołać go. Lepszym rozwiązaniem jest, jeśli kod transformacji nawet nie wie, że ma miejsce pobieranie . Jedynym sposobem, aby to zrobić, jest przekazanie wyników pobierania przez osobę dzwoniącą zamiast wstrzykiwania programu pobierającego.
jpmc26,
1
@ jpmc26 Ale w twoim (komentarz) przykładzie baza danych nie musiała nawet być zależnością na początek. Oczywiście unikanie zależności jest lepsze w przypadku luźnego łączenia, ale DI koncentruje się na implementowaniu potrzebnych zależności.
Flater