Jaka jest zasada inwersji zależności i dlaczego jest ważna?

178

Jaka jest zasada inwersji zależności i dlaczego jest ważna?

Phillip Wells
źródło
3
Śmieszna ilość odpowiedzi na ten temat przy użyciu terminów „wysokiego poziomu” i „niskiego poziomu” z Wikipedii. Te terminy są niedostępne i prowadzą wielu czytelników do tej strony. Jeśli zamierzasz powrócić do Wikipedii, zdefiniuj te terminy w swoich odpowiedziach, aby nadać kontekst!
8bitjunkie

Odpowiedzi:

107

Sprawdź ten dokument: Zasada odwrócenia zależności .

Zasadniczo mówi:

  • Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obie powinny zależeć od abstrakcji.
  • Abstrakcje nigdy nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.

Krótko mówiąc, dlaczego jest to ważne: zmiany są ryzykowne, a polegając na koncepcji zamiast na wdrożeniu, zmniejszasz potrzebę zmian w miejscach połączeń telefonicznych.

W efekcie DIP zmniejsza sprzężenie między różnymi fragmentami kodu. Chodzi o to, że chociaż istnieje wiele sposobów implementacji, powiedzmy, narzędzia do rejestrowania, sposób, w jaki z niego korzystasz, powinien być stosunkowo stabilny w czasie. Jeśli możesz wyodrębnić interfejs, który reprezentuje koncepcję rejestrowania, ten interfejs powinien być znacznie bardziej stabilny w czasie niż jego implementacja, a strony wywołujące powinny być znacznie mniej dotknięte zmianami, które możesz wprowadzić podczas utrzymywania lub rozszerzania tego mechanizmu rejestrowania.

Zależność implementacji od interfejsu daje możliwość wyboru w czasie wykonywania, która implementacja jest lepiej dopasowana do konkretnego środowiska. W zależności od przypadków może to być również interesujące.

Carl Seleborg
źródło
31
Ta odpowiedź nie mówi, dlaczego DIP jest ważny, ani nawet czym jest DIP. Przeczytałem oficjalny dokument DIP i uważam, że jest to naprawdę słaba i nieuzasadniona „zasada”, ponieważ opiera się na błędnym założeniu: moduły wysokiego poziomu są wielokrotnego użytku.
Rogério
3
Rozważmy wykres zależności dla niektórych obiektów. Zastosuj DIP do obiektów. Teraz każdy obiekt będzie niezależny od implementacji innych obiektów. Testowanie jednostkowe jest teraz proste. Możliwa jest późniejsza refaktoryzacja w celu ponownego wykorzystania. Zmiany projektu mają bardzo ograniczony zakres zmian. Problemy projektowe nie są kaskadowe. Zobacz także wzorzec AI „Blackboard” dotyczący inwersji danych w zależności od danych. Razem, bardzo potężne narzędzia do uczynienia oprogramowania zrozumiałym, łatwym w utrzymaniu i niezawodnym. Zignoruj ​​iniekcję zależności w tym kontekście. To nie ma związku.
Tim Williscroft
6
Klasa A używająca klasy B nie oznacza, że ​​A „zależy” od implementacji B; zależy to tylko od publicznego interfejsu B. Dodanie oddzielnej abstrakcji, od której teraz zależą zarówno A, jak i B, oznacza tylko, że A nie będzie już zależało od czasu kompilacji od publicznego interfejsu B. Izolację między jednostkami można łatwo osiągnąć bez tych dodatkowych abstrakcji; w Javie i .NET istnieją specyficzne narzędzia do mockowania, które radzą sobie we wszystkich sytuacjach (metody statyczne, konstruktory itp.). Stosowanie DIP sprawia, że ​​oprogramowanie jest bardziej złożone i trudniejsze w utrzymaniu, a ponadto nie jest bardziej testowalne.
Rogério
1
Więc czym jest odwrócenie kontroli? sposób na odwrócenie zależności?
user20358
2
@Rogerio, zobacz odpowiedź Dereka Greera poniżej, wyjaśnia to. AFAIK, DIP mówi, że to A określa, czego A potrzebuje, a nie B, który mówi, czego A potrzebuje. Tak więc interfejs, którego potrzebuje A, nie powinien być podawany przez B, ale dla A.
ejaenv,
145

Książki Agile Software Development, Principles, Patterns and Practices oraz Agile Principles, Patterns i Practices in C # są najlepszymi zasobami do pełnego zrozumienia pierwotnych celów i motywacji stojących za zasadą odwrócenia zależności. Artykuł „The Dependency Inversion Principle” jest również dobrym źródłem, ale ze względu na fakt, że jest to skondensowana wersja szkicu, który ostatecznie trafił do wcześniej wspomnianych książek, pomija ważną dyskusję na temat koncepcji posiadanie pakietu i interfejsu, które są kluczem do odróżnienia tej zasady od bardziej ogólnych zaleceń „programować do interfejsu, a nie implementacji”, które można znaleźć w książce Design Patterns (Gamma, et. al).

Podsumowując, zasada odwrócenia zależności polega przede wszystkim na odwróceniu konwencjonalnego kierunku zależności z komponentów „wyższego poziomu” na komponenty „niższego poziomu”, tak że komponenty „niższego poziomu” są zależne od interfejsów należących do komponentów „wyższego poziomu” . (Uwaga: komponent „wyższego poziomu” odnosi się tutaj do komponentu wymagającego zewnętrznych zależności / usług, niekoniecznie jego pozycji koncepcyjnej w architekturze warstwowej). W ten sposób sprzężenie nie jest tak bardzo zredukowane , jak przesunięte z komponentów, które są teoretycznie mniej wartościowe dla składników, które są teoretycznie bardziej wartościowe.

Osiąga się to poprzez projektowanie komponentów, których zewnętrzne zależności są wyrażone w postaci interfejsu, dla którego implementacja musi być zapewniona przez konsumenta komponentu. Innymi słowy, zdefiniowane interfejsy wyrażają to, czego potrzebuje komponent, a nie to, jak używasz komponentu (np. „INeedSomething”, a nie „IDoSomething”).

Zasada inwersji zależności nie odnosi się do prostej praktyki wyodrębniania zależności za pomocą interfejsów (np. MyService → [ILogger ⇐ Logger]). Chociaż powoduje to oddzielenie komponentu od szczegółów implementacji zależności, nie odwraca relacji między konsumentem a zależnością (np. [MyService → IMyServiceLogger] ⇐ Logger.

Znaczenie zasady odwracania zależności można sprowadzić do pojedynczego celu, jakim jest możliwość ponownego wykorzystania komponentów oprogramowania, które opierają się na zewnętrznych zależnościach w odniesieniu do części ich funkcjonalności (rejestrowanie, walidacja itp.)

W ramach tego ogólnego celu ponownego wykorzystania możemy wyróżnić dwa podtypy ponownego wykorzystania:

  1. Używanie komponentu oprogramowania w wielu aplikacjach z implementacjami zależnymi (np. Opracowałeś kontener DI i chcesz zapewnić rejestrowanie, ale nie chcesz łączyć swojego kontenera z określonym rejestratorem, tak że każdy, kto używa twojego kontenera, musi również użyj wybranej biblioteki logowania).

  2. Korzystanie z komponentów oprogramowania w kontekście ewolucji (np. Opracowano komponenty logiki biznesowej, które pozostają takie same w wielu wersjach aplikacji, w których szczegóły implementacji ewoluują).

W pierwszym przypadku ponownego wykorzystania komponentów w wielu aplikacjach, na przykład w przypadku biblioteki infrastruktury, celem jest zapewnienie klientom podstawowej infrastruktury bez łączenia ich z pod-zależnościami własnej biblioteki, ponieważ przyjmowanie zależności od takich zależności wymaga konsumenci również wymagają tych samych zależności. Może to być problematyczne, gdy konsumenci Twojej biblioteki zdecydują się użyć innej biblioteki do tych samych potrzeb infrastruktury (np. NLog vs. log4net) lub jeśli zdecydują się użyć późniejszej wersji wymaganej biblioteki, która nie jest wstecznie kompatybilna z wersją wymagane przez Twoją bibliotekę.

W drugim przypadku ponownego wykorzystania komponentów logiki biznesowej (tj. „Komponentów wyższego poziomu”) celem jest odizolowanie podstawowej implementacji domeny aplikacji od zmieniających się potrzeb szczegółów implementacji (np. Zmiana / aktualizacja bibliotek trwałości, bibliotek wiadomości , strategie szyfrowania itp.). W idealnym przypadku zmiana szczegółów implementacji aplikacji nie powinna powodować uszkodzenia składników hermetyzujących logikę biznesową aplikacji.

Uwaga: Niektórzy mogą sprzeciwić się opisaniu tego drugiego przypadku jako faktycznego ponownego wykorzystania, rozumując, że komponenty, takie jak komponenty logiki biznesowej używane w jednej rozwijającej się aplikacji, reprezentują tylko jedno użycie. Pomysł polega jednak na tym, że każda zmiana szczegółów implementacji aplikacji tworzy nowy kontekst, a zatem inny przypadek użycia, chociaż ostateczne cele można rozróżnić jako izolację i przenośność.

Chociaż przestrzeganie zasady odwracania zależności w tym drugim przypadku może przynieść pewne korzyści, należy zauważyć, że jej wartość w odniesieniu do nowoczesnych języków, takich jak Java i C #, jest znacznie ograniczona, być może do tego stopnia, że ​​nie ma ona znaczenia. Jak wspomniano wcześniej, DIP polega na całkowitym rozdzieleniu szczegółów implementacji na osobne pakiety. Jednak w przypadku rozwijającej się aplikacji zwykłe wykorzystanie interfejsów zdefiniowanych w zakresie domeny biznesowej będzie chronić przed koniecznością modyfikowania komponentów wyższego poziomu ze względu na zmieniające się potrzeby komponentów szczegółów implementacji, nawet jeśli szczegóły implementacji ostatecznie znajdują się w tym samym pakiecie . Ta część zasady odzwierciedla aspekty, które były związane z językiem w perspektywie, gdy zasada została skodyfikowana (tj. C ++), a które nie są istotne dla nowszych języków. To mówi,

Dłuższe omówienie tej zasady w odniesieniu do prostego użycia interfejsów, iniekcji zależności i wzorca Separated Interface można znaleźć tutaj . Dodatkowo można znaleźć tutaj dyskusję na temat tego, jak ta zasada odnosi się do języków dynamicznie typowanych, takich jak JavaScript .

Derek Greer
źródło
17
Dzięki. Widzę teraz, jak moja odpowiedź mija się z celem. Różnica między MyService → [ILogger ⇐ Logger] i [MyService → IMyServiceLogger] ⇐ Logger jest subtelna, ale ważna.
Patrick McElhaney
2
W tej samej linii jest to bardzo dobrze wyjaśnione tutaj: lostechies.com/derickbailey/2011/09/22/...
ejaenv
1
Jak bardzo chciałem, żeby ludzie przestali używać loggerów jako kanonicznego zastosowania do wstrzykiwania zależności. Szczególnie w połączeniu z log4net, gdzie jest prawie anty-wzorcem. Poza tym, świetne wyjaśnienie!
Casper Leon Nielsen
4
@ Casper Leon Nielsen - DIP nie ma nic wspólnego z DI Nie są to synonimy ani równoważne pojęcia.
TSmith
4
@ VF1 Jak stwierdzono w paragrafie podsumowującym, znaczenie zasady odwracania zależności dotyczy przede wszystkim ponownego wykorzystania. Jeśli John zwolni bibliotekę, która ma zewnętrzną zależność od biblioteki rejestrującej, a Sam chce użyć biblioteki Johna, Sam przejmuje zależność rejestrowania przejściowego. Sam nigdy nie będzie mógł wdrożyć swojej aplikacji bez wybranej biblioteki logowania Jana. Jeśli jednak John zastosuje się do DIP, Sam może dostarczyć adapter i użyć dowolnej wybranej biblioteki rejestrowania. DIP nie dotyczy wygody, ale sprzężenia.
Derek Greer
14

Projektując aplikacje oprogramowania, możemy brać pod uwagę klasy niskiego poziomu, które realizują podstawowe i podstawowe operacje (dostęp do dysku, protokoły sieciowe, ...), a klasy wysokiego poziomu - klasy, które hermetyzują złożoną logikę (przepływy biznesowe, ...).

Te ostatnie polegają na klasach niskiego poziomu. Naturalnym sposobem implementacji takich struktur byłoby pisanie klas niskopoziomowych, a gdy już mamy je do pisania złożonych klas wysokiego poziomu. Ponieważ klasy na wysokim poziomie są definiowane w kategoriach innych, wydaje się to logicznym sposobem. Ale to nie jest elastyczny projekt. Co się stanie, jeśli zajdzie potrzeba zastąpienia klasy niskiego poziomu?

Zasada odwrócenia zależności stwierdza, że:

  • Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obie powinny zależeć od abstrakcji.
  • Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.

Zasada ta ma na celu „odwrócenie” konwencjonalnego poglądu, że moduły wysokiego poziomu w oprogramowaniu powinny zależeć od modułów niższego poziomu. Tutaj moduły wysokiego poziomu są właścicielami abstrakcji (na przykład decydują o metodach interfejsu), które są implementowane przez moduły niższego poziomu. W ten sposób uzależniamy moduły niższego poziomu od modułów wyższego poziomu.

nikhil.singhal
źródło
11

Dobrze zastosowana inwersja zależności zapewnia elastyczność i stabilność na poziomie całej architektury aplikacji. Umożliwi to bezpieczniejszą i stabilniejszą ewolucję aplikacji.

Tradycyjna architektura warstwowa

Tradycyjnie interfejs użytkownika architektury warstwowej zależał od warstwy biznesowej, a to z kolei zależało od warstwy dostępu do danych.

Musisz zrozumieć warstwę, pakiet lub bibliotekę. Zobaczmy, jak wyglądałby kod.

Mielibyśmy bibliotekę lub pakiet dla warstwy dostępu do danych.

// DataAccessLayer.dll
public class ProductDAO {

}

I kolejna logika biznesowa biblioteki lub warstwy pakietu, która zależy od warstwy dostępu do danych.

// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO { 
    private ProductDAO productDAO;
}

Architektura warstwowa z odwróceniem zależności

Odwrócenie zależności wskazuje, co następuje:

Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obie powinny zależeć od abstrakcji.

Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.

Jakie są moduły wysokiego poziomu i niskiego poziomu? Myśląc o modułach, takich jak biblioteki lub pakiety, moduł wysokiego poziomu to te, które tradycyjnie mają zależności i niski poziom, od których zależą.

Innymi słowy, wysoki poziom modułu byłby miejscem wywołania akcji, a niski - miejscem wykonywania akcji.

Rozsądny wniosek, jaki można wyciągnąć z tej zasady, jest taki, że nie powinno być zależności między konkrecjami, ale musi istnieć zależność od abstrakcji. Ale zgodnie z przyjętym podejściem możemy niewłaściwie zastosować zależność od inwestycji, ale abstrakcję.

Wyobraź sobie, że dostosowujemy nasz kod w następujący sposób:

Mielibyśmy bibliotekę lub pakiet dla warstwy dostępu do danych, która definiuje abstrakcję.

// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{

}

I kolejna logika biznesowa biblioteki lub warstwy pakietu, która zależy od warstwy dostępu do danych.

// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO { 
    private IProductDAO productDAO;
}

Chociaż jesteśmy uzależnieni od abstrakcji, zależność między biznesem a dostępem do danych pozostaje taka sama.

Aby uzyskać odwrócenie zależności, interfejs trwałości musi być zdefiniowany w module lub pakiecie, w którym znajduje się ta logika lub domena wysokiego poziomu, a nie w module niskiego poziomu.

Najpierw zdefiniuj, czym jest warstwa domeny, a abstrakcją jej komunikacji jest zdefiniowana trwałość.

// Domain.dll
public interface IProductRepository;

using DataAccessLayer;
public class ProductBO { 
    private IProductRepository productRepository;
}

Po warstwie trwałości zależy od domeny, należy teraz odwrócić, jeśli zależność jest zdefiniowana.

// Persistence.dll
public class ProductDAO : IProductRepository{

}


(źródło: xurxodev.com )

Pogłębienie zasady

Ważne jest, aby dobrze przyswoić sobie tę koncepcję, pogłębiając cel i korzyści. Jeśli pozostaniemy mechanicznie i nauczymy się typowego repozytorium przypadków, nie będziemy w stanie zidentyfikować, gdzie możemy zastosować zasadę zależności.

Ale dlaczego odwracamy zależność? Jaki jest główny cel poza konkretnymi przykładami?

Zwykle pozwala to na częstsze zmiany rzeczy najbardziej stabilnych, które nie są zależne od rzeczy mniej stabilnych.

Łatwiej jest zmienić typ trwałości, czyli bazę danych lub technologię dostępu do tej samej bazy danych niż logika domeny lub akcje zaprojektowane do komunikacji z trwałością. Z tego powodu zależność jest odwrócona, ponieważ łatwiej jest zmienić trwałość, jeśli ta zmiana nastąpi. W ten sposób nie będziemy musieli zmieniać domeny. Warstwa domeny jest najbardziej stabilna ze wszystkich, dlatego nie powinna od niczego zależeć.

Ale nie ma tylko tego przykładu repozytorium. Istnieje wiele scenariuszy, w których ta zasada ma zastosowanie, i istnieją architektury oparte na tej zasadzie.

Architektury

Istnieją architektury, w których inwersja zależności jest kluczem do jej definicji. We wszystkich domenach jest najważniejszy i to abstrakcje wskażą, jaki protokół komunikacyjny pomiędzy domeną a pozostałymi pakietami czy bibliotekami jest zdefiniowany.

Czysta architektura

W czystej architekturze domena znajduje się w centrum i jeśli spojrzy się w kierunku strzałek wskazujących na zależności, widać, jakie warstwy są najważniejsze i stabilne. Warstwy zewnętrzne są uważane za niestabilne narzędzia, więc unikaj polegania na nich.


(źródło: 8thlight.com )

Architektura sześciokątna

Podobnie dzieje się z architekturą heksagonalną, w której domena znajduje się również w części centralnej, a porty są abstrakcjami komunikacji wychodzącej z domina na zewnątrz. Tutaj znowu widać, że domena jest najbardziej stabilna, a tradycyjna zależność jest odwrócona.

xurxodev
źródło
Nie jestem pewien, co to znaczy, ale to zdanie jest niespójne: „Ale zgodnie z przyjętym podejściem możemy niewłaściwie zastosować zależność od inwestycji, ale abstrakcję”. Może chciałbyś to naprawić?
Mark Amery
9

Dla mnie zasada odwrócenia zależności, opisana w oficjalnym artykule , jest naprawdę błędną próbą zwiększenia możliwości ponownego wykorzystania modułów, które są z natury mniej wielokrotnego użytku, a także sposobem na obejście problemu w języku C ++.

Problem w C ++ polega na tym, że pliki nagłówkowe zwykle zawierają deklaracje prywatnych pól i metod. Dlatego jeśli moduł C ++ wysokiego poziomu zawiera plik nagłówkowy modułu niskiego poziomu, będzie to zależeć od rzeczywistych szczegółów implementacji tego modułu. A to oczywiście nie jest dobre. Ale to nie jest problem w bardziej współczesnych językach powszechnie używanych dzisiaj.

Moduły wysokiego poziomu są z natury mniej przydatne do ponownego wykorzystania niż moduły niskiego poziomu, ponieważ te pierwsze są zwykle bardziej specyficzne dla aplikacji / kontekstu niż te drugie. Na przykład składnik implementujący ekran interfejsu użytkownika jest na najwyższym poziomie, a także bardzo (całkowicie?) Specyficzny dla aplikacji. Próba ponownego wykorzystania takiego komponentu w innym zastosowaniu przynosi efekt przeciwny do zamierzonego i może prowadzić jedynie do nadmiernej inżynierii.

Zatem stworzenie oddzielnej abstrakcji na tym samym poziomie komponentu A, który zależy od komponentu B (który nie zależy od A), może być wykonane tylko wtedy, gdy komponent A będzie naprawdę przydatny do ponownego wykorzystania w różnych aplikacjach lub kontekstach. Jeśli tak nie jest, zastosowanie DIP byłoby złym projektem.

Rogério
źródło
Nawet jeśli wysokopoziomowa abstrakcja jest przydatna tylko w kontekście tej aplikacji, może mieć wartość, gdy chcesz zastąpić rzeczywistą implementację kodem do testowania (lub udostępnić interfejs wiersza poleceń aplikacji zwykle dostępnej w sieci)
Przemek Pokrywka
3
DIP jest ważny w różnych językach i nie ma nic wspólnego z samym C ++. Nawet jeśli twój kod wysokiego poziomu nigdy nie opuści twojej aplikacji, DIP umożliwia powstrzymanie zmiany kodu podczas dodawania lub zmiany kodu niższego poziomu. Zmniejsza to zarówno koszty utrzymania, jak i niezamierzone konsekwencje zmian. DIP to koncepcja wyższego poziomu. Jeśli tego nie rozumiesz, musisz więcej googlować.
Dirk Bester,
3
Myślę, że źle zrozumiałeś, co powiedziałem o C ++. To była tylko motywacja do DIP; bez wątpienia jest to bardziej ogólne. Uwaga: oficjalny artykuł na temat DIP wyjaśnia, że ​​główną motywacją było wspieranie ponownego wykorzystania modułów wysokiego poziomu, poprzez uodpornienie ich na zmiany w modułach niskiego poziomu; bez potrzeby ponownego użycia, bardzo prawdopodobne jest, że będzie to przesada i nadmierna inżynieria. (Czy to przeczytałeś? Mówi również o kwestii C ++.)
Rogério
1
Czy nie przeoczasz odwrotnego zastosowania DIP? Oznacza to, że zasadniczo moduły wyższego poziomu implementują Twoją aplikację. Twoim głównym zmartwieniem nie jest ponowne użycie go, ale zmniejszenie uzależnienia od implementacji modułów niższego poziomu, tak aby aktualizowanie go, aby dotrzymało kroku przyszłym zmianom, izoluje aplikację od zniszczeń czasu i nowych technologii. Nie chodzi o ułatwienie wymiany systemu WindowsOS, ale po to, aby system WindowsOS był mniej zależny od szczegółów implementacji FAT / HDD, dzięki czemu nowsze NTFS / SSD można podłączyć do systemu WindowsOS bez lub z niewielkim wpływem.
knockNrod
1
Ekran interfejsu użytkownika zdecydowanie nie jest modułem najwyższego poziomu lub nie powinien być przeznaczony dla żadnej aplikacji. Moduły najwyższego poziomu to te, które zawierają reguły biznesowe. Moduł najwyższego poziomu nie jest również definiowany przez jego potencjał ponownego wykorzystania. Proszę sprawdzić proste wyjaśnienie Wujka Boba w tym artykule: blog.cleancoder.com/uncle-bob/2016/01/04/…
humbaba
8

Zasadniczo mówi:

Klasa powinna zależeć od abstrakcji (np. Interfejs, klasy abstrakcyjne), a nie od konkretnych szczegółów (implementacje).

martin.ra
źródło
Czy to może być takie proste? Są długie artykuły, a nawet książki, o których wspomniał DerekGreer? Właściwie szukałem prostej odpowiedzi, ale to niewiarygodne, jeśli jest tak prosta: D
Darius V
1
@ darius-v nie jest to kolejna wersja powiedzenia „używaj abstrakcji”. Chodzi o to, kto jest właścicielem interfejsów. Dyrektor mówi, że klient (komponenty wyższego poziomu) powinien zdefiniować interfejsy, a komponenty niższego poziomu powinny je implementować.
boran
6

Inni już podają dobre odpowiedzi i dobre przykłady.

Powodem, dla którego DIP jest ważny, jest to, że zapewnia on zasadę OO „luźno powiązany projekt”.

Obiekty w oprogramowaniu NIE powinny wchodzić w hierarchię, w której niektóre obiekty są obiektami najwyższego poziomu, zależnymi od obiektów niskiego poziomu. Zmiany w obiektach niskiego poziomu będą następnie przenikać do obiektów najwyższego poziomu, co sprawia, że ​​oprogramowanie jest bardzo wrażliwe na zmiany.

Chcesz, aby obiekty „najwyższego poziomu” były bardzo stabilne i nie były wrażliwe na zmiany, dlatego musisz odwrócić zależności.

Hace
źródło
6
Jak dokładnie to robi DIP, dla kodu Java lub .NET? W tych językach, w przeciwieństwie do C ++, zmiany w implementacji modułu niskopoziomowego nie wymagają zmian w modułach wysokiego poziomu, które go używają. Tylko zmiany w interfejsie publicznym byłyby widoczne, ale wtedy abstrakcja zdefiniowana na wyższym poziomie również musiałaby się zmienić.
Rogério
5

Znacznie jaśniejszym sposobem określenia zasady odwracania zależności jest:

Twoje moduły hermetyzujące złożoną logikę biznesową nie powinny bezpośrednio zależeć od innych modułów hermetyzujących logikę biznesową. Zamiast tego powinny polegać tylko na interfejsach do prostych danych.

To znaczy, zamiast implementować swoją klasę, Logicjak to zwykle robią ludzie:

class Dependency { ... }
class Logic {
    private Dependency dep;
    int doSomething() {
        // Business logic using dep here
    }
}

powinieneś zrobić coś takiego:

class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
    private Dependency dep;
    ...
}
class Logic {
    int doSomething(Data data) {
        // compute something with data
    }
}

Datai DataFromDependencypowinien mieszkać w tym samym module co Logic, a nie z Dependency.

Czemu to robić?

  1. Dwa moduły logiki biznesowej są teraz rozdzielone. Kiedy Dependencyzmiany, nie musisz ich zmieniać Logic.
  2. Zrozumienie tego, co Logicrobi, jest znacznie prostszym zadaniem: działa tylko na czymś, co wygląda jak ADT.
  3. Logicmożna teraz łatwiej przetestować. Możesz teraz bezpośrednio tworzyć instancje Dataz fałszywymi danymi i przekazywać je. Nie ma potrzeby tworzenia prób ani skomplikowanego tworzenia szkieletów testowych.
mattvonb
źródło
Myślę, że to nie jest w porządku. Jeśli DataFromDependency, który bezpośrednio się odwołuje Dependency, znajduje się w tym samym module co Logic, to Logicmoduł nadal bezpośrednio zależy od Dependencymodułu w czasie kompilacji. Według wujka Boba wyjaśnienie zasady , unikanie tego jest celem DIP. Zamiast tego, aby podążać za DIP, Datapowinien znajdować się w tym samym module co Logic, ale DataFromDependencypowinien znajdować się w tym samym module co Dependency.
Mark Amery
3

Odwrócenie kontroli (IoC) to wzorzec projektowy, w którym obiekt otrzymuje swoją zależność przez zewnętrzną strukturę, zamiast prosić strukturę o swoją zależność.

Przykład pseudokodu używający tradycyjnego wyszukiwania:

class Service {
    Database database;
    init() {
        database = FrameworkSingleton.getService("database");
    }
}

Podobny kod wykorzystujący IoC:

class Service {
    Database database;
    init(database) {
        this.database = database;
    }
}

Korzyści z IoC to:

  • Nie masz zależności od struktury centralnej, więc możesz to zmienić w razie potrzeby.
  • Ponieważ obiekty są tworzone przez iniekcję, najlepiej przy użyciu interfejsów, łatwo jest tworzyć testy jednostkowe, które zastępują zależności wersjami próbnymi.
  • Odsprzęganie kodu.
Staale
źródło
Czy zasada odwrócenia zależności i odwrócenie kontroli to to samo?
Peter Mortensen
3
„Inwersja” w DIP to nie to samo, co w przypadku inwersji sterowania. Pierwsza z nich dotyczy zależności czasu kompilacji, a druga przepływu kontroli między metodami w czasie wykonywania.
Rogério
Wydaje mi się, że w wersji IoC czegoś brakuje. Jak jest definiowany obiekt bazy danych, skąd się bierze. Próbuję zrozumieć, jak to nie tylko opóźnia określenie wszystkich zależności na najwyższym poziomie w zależności od olbrzymiego boga
Richard Tingle
1
Jak już powiedział @ Rogério, DIP to nie DI / IoC. Ta odpowiedź jest błędna IMO
zeraDev
1

Celem odwrócenia zależności jest stworzenie oprogramowania wielokrotnego użytku.

Chodzi o to, że zamiast dwóch zależnych od siebie fragmentów kodu, opierają się one na jakimś abstrakcyjnym interfejsie. Następnie możesz ponownie użyć dowolnego elementu bez drugiego.

Najczęściej osiąga się to poprzez odwrócenie kontenera sterowania (IoC), takiego jak Spring w Javie. W tym modelu właściwości obiektów są ustawiane poprzez konfigurację XML, zamiast wychodzenia obiektów i znajdowania ich zależności.

Wyobraź sobie ten pseudokod ...

public class MyClass
{
  public Service myService = ServiceLocator.service;
}

MyClass zależy bezpośrednio od klasy Service i klasy ServiceLocator. Potrzebuje obu, jeśli chcesz go używać w innej aplikacji. Teraz wyobraź sobie to ...

public class MyClass
{
  public IService myService;
}

Teraz MyClass opiera się na jednym interfejsie, interfejsie IService. Pozwolilibyśmy, aby kontener IoC faktycznie ustawił wartość tej zmiennej.

Tak więc teraz MyClass można łatwo ponownie wykorzystać w innych projektach, bez przynoszenia wraz z nią zależności tych dwóch pozostałych klas.

Co więcej, nie musisz przeciągać zależności MyService i zależności tych zależności, a ... cóż, masz pomysł.

Marc Hughes
źródło
2
Ta odpowiedź naprawdę nie dotyczy DIP, ale czegoś innego. Dwa fragmenty kodu mogą polegać na jakimś abstrakcyjnym interfejsie, a nie na sobie nawzajem, i nadal nie są zgodne z zasadą odwrócenia zależności.
Rogério
1

Jeśli możemy przyjąć za pewnik, że pracownik "wysokiego szczebla" w korporacji otrzymuje wynagrodzenie za wykonanie swoich planów i że plany te są dostarczane przez zagregowane wykonanie wielu planów pracowników "niskiego poziomu", to możemy powiedzieć generalnie okropny jest plan, jeśli opis planu pracownika wysokiego szczebla jest w jakikolwiek sposób powiązany z konkretnym planem dowolnego pracownika niższego szczebla.

Jeśli dyrektor wysokiego szczebla ma plan „skrócenia czasu dostawy” i wskazuje, że pracownik linii żeglugowej musi wypić kawę i robić ćwiczenia rozciągające każdego ranka, to plan ten jest silnie powiązany i ma niską spójność. Ale jeśli plan nie wspomina o żadnym konkretnym pracowniku, a w rzeczywistości po prostu wymaga, aby „podmiot, który może wykonywać pracę, jest przygotowany do pracy”, wówczas plan jest luźno powiązany i bardziej spójny: plany nie pokrywają się i można je łatwo zastąpić . Kontrahenci, czyli roboty, mogą z łatwością wymienić pracowników, a plan wysokiego szczebla pozostaje niezmieniony.

„Wysoki poziom” w zasadzie odwrócenia zależności oznacza „ważniejszy”.

mikrofon
źródło
1
To niezwykle dobra analogia. Tak jak w przypadku DIC komponenty wysokiego poziomu nadal zależą w czasie wykonywania od komponentów niskiego poziomu, w tej analogii wykonanie planów wysokiego poziomu zależy od planów niskiego poziomu. Ale tak jak w przypadku DIC to plany niskiego poziomu zależą w czasie kompilacji od planów wysokiego poziomu, w twojej analogii jest tak, że tworzenie planów niskiego poziomu zależy od planów wysokiego poziomu.
Mark Amery
0

Widzę, że powyższe odpowiedzi dają dobre wyjaśnienie. Chcę jednak podać proste wyjaśnienie za pomocą prostego przykładu.

Zasada odwrócenia zależności umożliwia programiście usunięcie zakodowanych na stałe zależności, dzięki czemu aplikacja staje się luźno powiązana i rozszerzalna.

Jak to osiągnąć: poprzez abstrakcję

Bez inwersji zależności:

 class Student {
    private Address address;

    public Student() {
        this.address = new Address();
    }
}
class Address{
    private String perminentAddress;
    private String currentAdrress;

    public Address() {
    }
} 

W powyższym fragmencie kodu obiekt adresu jest zakodowany na stałe. Zamiast tego, jeśli możemy użyć odwrócenia zależności i wstrzyknąć obiekt adresowy, przechodząc przez konstruktor lub metodę ustawiającą. Zobaczmy.

Z odwróceniem zależności:

class Student{
    private Address address;

    public Student(Address address) {
        this.address = address;
    }
    //or
    public void setAddress(Address address) {
        this.address = address;
    }
}
Sumanth Varada
źródło
-1

Powiedzmy, że mamy dwie klasy: Engineeri Programmer:

Class Engineer ma zależność od klasy Programmer, jak poniżej:

class Engineer () {

    fun startWork(programmer: Programmer){
        programmer.work()
    }
}

class Programmer {

    fun work(){
        //TODO Do some work here!
    }
}

W tym przykładzie klasa Engineerjest zależna od naszej Programmerklasy. Co się stanie, jeśli będę musiał zmienić Programmer?

Oczywiście muszę też zmienić Engineer. (Wow, w tym momencie też OCPjest naruszone)

Więc co musimy posprzątać ten bałagan? Właściwie odpowiedź brzmi: abstrakcja. Abstrakcji możemy usunąć zależność między tymi dwiema klasami. Na przykład mogę utworzyć Interfaceklasę for Programmer i od teraz każda klasa, która chce używać tej klasy, Programmermusi jej używać. InterfaceNastępnie zmieniając klasę Programmer, nie musimy zmieniać żadnych klas, które jej używały, Ze względu na abstrakcję, którą używany.

Uwaga: DependencyInjectionmoże pomóc nam zrobić DIPi SRPzbyt.

Ehsan
źródło
-1

Dodając do lawiny ogólnie dobrych odpowiedzi, chciałbym dodać własną małą próbkę, aby zademonstrować dobre i złe praktyki. I tak, nie jestem kimś, kto rzuca kamieniami!

Powiedzmy, że potrzebujesz małego programu do konwersji ciągu znaków na format base64 za pośrednictwem konsoli we / wy. Oto naiwne podejście:

class Program
{
    static void Main(string[] args)
    {
        /*
         * BadEncoder: High-level class *contains* low-level I/O functionality.
         * Hence, you'll have to fiddle with BadEncoder whenever you want to change
         * the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
         * problems with I/O shouldn't break the encoder!
         */
        BadEncoder.Run();            
    }
}

public static class BadEncoder
{
    public static void Run()
    {
        Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
    }
}    

DIP zasadniczo mówi, że komponenty wysokopoziomowe nie powinny być zależne od implementacji niskiego poziomu, gdzie „poziom” to odległość od I / O według Roberta C. Martina („Czysta architektura”). Ale jak wydostać się z tej kłopotliwej sytuacji? Po prostu uzależniając centralny Enkoder tylko od interfejsów, nie martwiąc się o ich implementację:

class Program
{
    static void Main(string[] args)
    {           
        /* Demo of the Dependency Inversion Principle (= "High-level functionality
         * should not depend upon low-level implementations"): 
         * You can easily implement new I/O methods like
         * ConsoleReader, ConsoleWriter without ever touching the high-level
         * Encoder class!!!
         */            
        GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter());        }
}

public static class GoodEncoder
{
    public static void Run(IReadable input, IWriteable output)
    {
        output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
    }
}

public interface IReadable
{
    string ReadInput();
}

public interface IWriteable
{
    void WriteOutput(string txt);
}

public class ConsoleReader : IReadable
{
    public string ReadInput()
    {
        return Console.ReadLine();
    }
}

public class ConsoleWriter : IWriteable
{
    public void WriteOutput(string txt)
    {
        Console.WriteLine(txt);
    }
}

Zauważ, że nie musisz dotykać GoodEncoder, aby zmienić tryb I / O - ta klasa jest zadowolona z interfejsów I / O, które zna; wszelkie niskopoziomowe implementacje IReadablei IWriteablenigdy nie będą im przeszkadzać.

John Silence
źródło
Nie sądzę, żeby to było odwrócenie zależności. W końcu nie odwróciłeś tutaj żadnej zależności; Twój czytelnik i pisarz nie polegają na GoodEncodertwoim drugim przykładzie. Aby stworzyć przykład DIP, musisz wprowadzić pojęcie o tym, co „jest właścicielem” interfejsów, które tutaj wyodrębniłeś - aw szczególności umieścić je w tym samym pakiecie co GoodEncoder, podczas gdy ich implementacje pozostają na zewnątrz.
Mark Amery
-2

Mówi o tym zasada odwrócenia zależności (DIP)

i) Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Obie powinny zależeć od abstrakcji.

ii) Abstrakcje nigdy nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.

Przykład:

    public interface ICustomer
    {
        string GetCustomerNameById(int id);
    }

    public class Customer : ICustomer
    {
        //ctor
        public Customer(){}

        public string GetCustomerNameById(int id)
        {
            return "Dummy Customer Name";
        }
    }

    public class CustomerFactory
    {
        public static ICustomer GetCustomerData()
        {
            return new Customer();
        }
    }

    public class CustomerBLL
    {
        ICustomer _customer;
        public CustomerBLL()
        {
            _customer = CustomerFactory.GetCustomerData();
        }

        public string GetCustomerNameById(int id)
        {
            return _customer.GetCustomerNameById(id);
        }
    }

    public class Program
    {
        static void Main()
        {
            CustomerBLL customerBLL = new CustomerBLL();
            int customerId = 25;
            string customerName = customerBLL.GetCustomerNameById(customerId);


            Console.WriteLine(customerName);
            Console.ReadKey();
        }
    }

Uwaga: Klasa powinna zależeć od abstrakcji, takich jak interfejs lub klasy abstrakcyjne, a nie od konkretnych szczegółów (implementacja interfejsu).

Rejwanul Reja
źródło