Jaka jest zasada inwersji zależności i dlaczego jest ważna?
oop
solid-principles
glossary
principles
dependency-inversion
Phillip Wells
źródło
źródło
Odpowiedzi:
Sprawdź ten dokument: Zasada odwrócenia zależności .
Zasadniczo mówi:
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.
źródło
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:
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).
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 .
źródło
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:
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.
źródło
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.
I kolejna logika biznesowa biblioteki lub warstwy pakietu, która zależy od warstwy dostępu do danych.
Architektura warstwowa z odwróceniem zależności
Odwrócenie zależności wskazuje, co następuje:
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ę.
I kolejna logika biznesowa biblioteki lub warstwy pakietu, która zależy od warstwy dostępu do danych.
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ść.
Po warstwie trwałości zależy od domeny, należy teraz odwrócić, jeśli zależność jest zdefiniowana.
(ź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.
źródło
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.
źródło
Zasadniczo mówi:
Klasa powinna zależeć od abstrakcji (np. Interfejs, klasy abstrakcyjne), a nie od konkretnych szczegółów (implementacje).
źródło
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.
źródło
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ę,
Logic
jak to zwykle robią ludzie:powinieneś zrobić coś takiego:
Data
iDataFromDependency
powinien mieszkać w tym samym module coLogic
, a nie zDependency
.Czemu to robić?
Dependency
zmiany, nie musisz ich zmieniaćLogic
.Logic
robi, jest znacznie prostszym zadaniem: działa tylko na czymś, co wygląda jak ADT.Logic
można teraz łatwiej przetestować. Możesz teraz bezpośrednio tworzyć instancjeData
z fałszywymi danymi i przekazywać je. Nie ma potrzeby tworzenia prób ani skomplikowanego tworzenia szkieletów testowych.źródło
DataFromDependency
, który bezpośrednio się odwołujeDependency
, znajduje się w tym samym module coLogic
, toLogic
moduł nadal bezpośrednio zależy odDependency
modułu w czasie kompilacji. Według wujka Boba wyjaśnienie zasady , unikanie tego jest celem DIP. Zamiast tego, aby podążać za DIP,Data
powinien znajdować się w tym samym module coLogic
, aleDataFromDependency
powinien znajdować się w tym samym module coDependency
.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:
Podobny kod wykorzystujący IoC:
Korzyści z IoC to:
źródło
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 ...
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 ...
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ł.
źródło
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”.
źródło
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:
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:
źródło
Odwrócenie zależności: zależy od abstrakcji, a nie konkrecji.
Odwrócenie kontroli: główna vs abstrakcja i jak główna jest spoiwem systemów.
Oto kilka dobrych postów, które mówią o tym:
https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/
https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/
https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/
źródło
Powiedzmy, że mamy dwie klasy:
Engineer
iProgrammer
:Class Engineer ma zależność od klasy Programmer, jak poniżej:
W tym przykładzie klasa
Engineer
jest zależna od naszejProgrammer
klasy. Co się stanie, jeśli będę musiał zmienićProgrammer
?Oczywiście muszę też zmienić
Engineer
. (Wow, w tym momencie teżOCP
jest 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ć
Interface
klasę for Programmer i od teraz każda klasa, która chce używać tej klasy,Programmer
musi jej używać.Interface
Nastę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:
DependencyInjection
może pomóc nam zrobićDIP
iSRP
zbyt.źródło
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:
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ę:
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 implementacjeIReadable
iIWriteable
nigdy nie będą im przeszkadzać.źródło
GoodEncoder
twoim 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.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:
Uwaga: Klasa powinna zależeć od abstrakcji, takich jak interfejs lub klasy abstrakcyjne, a nie od konkretnych szczegółów (implementacja interfejsu).
źródło