Kiedy NIE należy stosować zasady odwrócenia zależności?


43

Obecnie próbuję znaleźć SOLID. Zatem zasada inwersji zależności oznacza, że ​​dowolne dwie klasy powinny komunikować się za pośrednictwem interfejsów, a nie bezpośrednio. Przykład: Jeśli class Ama metodę, która oczekuje wskaźnika do obiektu typu class B, wówczas metoda ta powinna faktycznie oczekiwać obiektu typu abstract base class of B. Pomaga to również w otwarciu / zamknięciu.

Pod warunkiem, że dobrze to zrozumiałem, moim pytaniem byłoby, czy dobrą praktyką jest stosowanie tego do wszystkich interakcji klasowych, czy powinienem próbować myśleć w kategoriach warstwowych ?

Sceptycznie podchodzę do tego dlatego, że płacimy pewną cenę za przestrzeganie tej zasady. Powiedz, że muszę zaimplementować funkcję Z. Po analizie wniosku, że funkcja Zskłada się z funkcjonalnością A, Ba C. Tworzę fasady klasa Z, że poprzez interfejsy, wykorzystuje klas A, Bi C. Zaczynam kodowanie wdrażanie i w pewnym momencie sprawę, że zadanie Zfaktycznie składa się z funkcjonalnością A, Ba D. Teraz muszę usunąć Cinterfejs, Cprototyp klasy i napisać osobny Dinterfejs i klasę. Bez interfejsów tylko klasa musiałaby zostać wymieniona.

Innymi słowy, aby coś zmienić, muszę zmienić 1. dzwoniącego 2. interfejs 3. deklarację 4. implementację. W implementacji bezpośrednio sprzężonej z pytonem musiałbym zmienić tylko implementację.


13
Odwrócenie zależności jest po prostu techniką, więc powinno się ją stosować tylko w razie potrzeby ... nie ma ograniczenia co do stopnia, w jakim można ją zastosować, więc jeśli zastosujesz ją wszędzie, skończysz na śmietniku: jak w każdej innej sytuacji -specyficzna technika.
Frank Hileman,

Mówiąc najprościej, zastosowanie niektórych zasad projektowania oprogramowania zależy od możliwości bezlitosnego refaktoryzowania, gdy zmieniają się wymagania. Uważa się , że część interfejsu najlepiej uchwyci niezmienniki umowne projektu, podczas gdy kod źródłowy (implementacja) powinien tolerować częstsze zmiany.
rwong

@rwong Interfejs przechwytuje niezmienniki umowne tylko wtedy, gdy używasz języka obsługującego niezmienniki umowne. W popularnych językach (Java, C #) interfejs to po prostu zestaw sygnatur API. Dodanie zbędnych interfejsów tylko pogarsza wygląd.
Frank Hileman

Powiedziałbym, że źle to zrozumiałeś. DIP polega na unikaniu zależności czasu kompilacji od komponentu „wysokiego poziomu” do komponentu „niskiego poziomu”, aby umożliwić ponowne użycie komponentu wysokiego poziomu w innych kontekstach, w których można użyć innej implementacji dla niskiego poziomu komponent poziomu; odbywa się to poprzez utworzenie typu abstrakcyjnego na wysokim poziomie, który jest implementowany przez komponenty niskiego poziomu; więc zarówno elementy wysokiego, jak i niskiego poziomu zależą od tej abstrakcji. W końcu komponenty wysokiego i niskiego poziomu komunikują się przez interfejs, ale nie jest to istotą DIP.
Rogério,

Odpowiedzi:


87

W wielu kreskówkach lub innych mediach siły dobra i zła są często ilustrowane przez anioła i demona siedzącego na ramionach postaci. W naszej historii, zamiast dobra i zła, mamy SOLID na jednym ramieniu, a YAGNI (Nie będziesz go potrzebował!) Na drugim.

Maksymalne zasady SOLID najlepiej nadają się do dużych, złożonych, ultra konfigurowalnych systemów dla przedsiębiorstw. W przypadku mniejszych lub bardziej specyficznych systemów nie jest właściwe, aby wszystko było absurdalnie elastyczne, ponieważ czas spędzony na abstrakcji nie okaże się korzystny.

Przekazywanie interfejsów zamiast konkretnych klas czasami oznacza na przykład, że możesz łatwo zamienić odczyt z pliku na strumień sieciowy. Jednak w przypadku dużej liczby projektów oprogramowania taka elastyczność nigdy nie będzie potrzebna. Równie dobrze możesz po prostu przekazać konkretne klasy plików i nazwać to dniem i oszczędzić komórki mózgowe.

Częścią sztuki tworzenia oprogramowania jest dobre wyczucie tego, co może się zmienić w miarę upływu czasu, a co nie. Do rzeczy, które mogą się zmienić, użyj interfejsów i innych pojęć SOLID. Do rzeczy, które nie będą, użyj YAGNI i po prostu podaj konkretne typy, zapomnij o klasach fabrycznych, zapomnij o podłączaniu i konfiguracji środowiska wykonawczego itp. I zapomnij o wielu abstrakcjach SOLID. Z mojego doświadczenia wynika, że ​​podejście YAGNI okazało się być poprawne znacznie częściej niż nie.


19
Moje pierwsze wprowadzenie do SOLID było około 15 lat temu na nowym systemie, który budowaliśmy. Wszyscy piliśmy człowieka pomocnika koola. Gdyby ktoś wspominał coś, co brzmiało jak YAGNI, byliśmy jak „Pfffft ... plebeian”. Miałem zaszczyt (przerażenie?) Patrzeć, jak ten system ewoluował w ciągu następnej dekady. Stało się nieporęcznym bałaganem, którego nikt nie mógł zrozumieć, nawet my, założyciele. Architekci uwielbiają SOLID. Ludzie, którzy faktycznie zarabiają na życie, kochają YAGNI. Żadne z nich nie jest idealne, ale YAGNI jest bliższe ideału i powinno być twoim domyślnym, jeśli nie wiesz, co robisz. :-)
Calphool,

11
@NWard Tak, zrobiliśmy to w ramach projektu. Oszalałem z tym. Teraz nasze testy są niemożliwe do odczytania lub utrzymania, częściowo z powodu nadmiernego kpin. Ponadto, z powodu wstrzyknięcia zależności, trudno jest poruszać się po kodzie, gdy próbujesz coś wymyślić. SOLID nie jest srebrną kulą. YAGNI nie jest srebrną kulą. Zautomatyzowane testowanie nie jest srebrną kulą. Nic nie może Cię uratować przed ciężką pracą nad myśleniem o tym, co robisz i podejmowaniem decyzji o tym, czy pomoże ci to utrudnić, czy też twoją pracę.
jpmc26

22
Wiele sentymentów anty-SOLID. SOLID i YAGNI nie są dwoma końcami spektrum. Są jak współrzędne X i Y na mapie. Dobry system ma bardzo mało zbędnego kodu (YAGNI) ORAZ przestrzega zasad SOLID.
Stephen

31
Meh, (a) Nie zgadzam się z tym, że SOLID = przedsiębiorczość i (b) cały sens SOLID polega na tym, że jesteśmy bardzo słabymi predyktorami tego, co będzie potrzebne. Muszę się zgodzić z @Stephen tutaj. YAGNI mówi, że nie powinniśmy starać się przewidywać przyszłych wymagań, które nie są dzisiaj jasno określone. SOLID mówi, że powinniśmy oczekiwać, że projekt będzie ewoluował w czasie i zastosujemy pewne proste techniki, aby to ułatwić. Są one nie wykluczają się wzajemnie; obie są technikami dostosowywania się do zmieniających się wymagań. Prawdziwe problemy występują, gdy próbujesz projektować dla niejasnych lub bardzo odległych wymagań.
Aaronaught

8
„możesz łatwo zamienić odczyt z pliku na strumień sieciowy” - to dobry przykład, w którym zbyt uproszczony opis DI prowadzi ludzi na manowce. Ludzie czasem myślą (w efekcie): „ta metoda użyje File, więc zamiast tego zajmie się wykonaniem IFilezadania”. Wówczas nie mogą łatwo zastąpić strumienia sieciowego, ponieważ przesadzili z interfejsem, a są IFilemetody, których metoda nawet nie używa, które nie dotyczą gniazd, więc gniazdo nie może zaimplementować IFile. Jedną z rzeczy, dla których DI nie jest srebrną kulą, jest wymyślenie odpowiednich abstrakcji (interfejsów) :-)
Steve Jessop

11

Słowami laika:

Stosowanie DIP jest łatwe i przyjemne . Niepoprawne wykonanie projektu przy pierwszej próbie nie jest wystarczającym powodem rezygnacji z DIP.

  • Zwykle IDE pomagają w przeprowadzaniu tego rodzaju refaktoryzacji, niektóre nawet pozwalają wyodrębnić interfejs z już zaimplementowanej klasy
  • Prawidłowe zaprojektowanie za pierwszym razem jest prawie niemożliwe
  • Normalny przepływ pracy polega na zmianie i ponownym przemyśleniu interfejsów w pierwszych etapach rozwoju
  • W miarę rozwoju oprogramowania dojrzewa i będziesz miał coraz mniej powodów, aby modyfikować interfejsy
  • W zaawansowanym etapie interfejsy (projekt) będą dojrzałe i prawie się nie zmienią
  • Od tego momentu zaczniesz czerpać korzyści, ponieważ Twoja aplikacja jest otwarta na skalowanie.

Z drugiej strony programowanie z interfejsami i OOD może przywrócić radość czasami nieaktualnemu rzemiosłu programowania.

Niektórzy twierdzą, że to dodaje złożoności, ale myślę, że opossite jest prawdą. Nawet w przypadku małych projektów. Ułatwia testowanie / kpiny. To sprawia, że ​​twój kod ma mniej, jeśli jakieś caseinstrukcje lub są zagnieżdżone ifs. Zmniejsza złożoność cykliczną i sprawia, że ​​myślisz na nowo. Dzięki temu programowanie jest bardziej podobne do projektowania i produkcji w świecie rzeczywistym.


5
Nie wiem, jakie języki lub IDE są używane przez OP, ale w VS 2013 jest śmiesznie prosta praca z interfejsami, wyodrębnianie interfejsów i ich implementacja, i krytyczne, jeśli używa się TDD. Nie ma dodatkowych kosztów związanych z programowaniem przy użyciu tych zasad.
stephenbayer

Dlaczego ta odpowiedź mówi o DI, jeśli pytanie dotyczyło DIP? DIP to koncepcja z lat 90., a DI z 2004 r. Są one bardzo różne.
Rogério

1
(Mój poprzedni komentarz miał na celu inną odpowiedź; zignoruj ​​go.) „Programowanie interfejsów” jest znacznie bardziej ogólne niż DIP, ale nie polega na tym, aby każda klasa implementowała osobny interfejs. Ułatwia to „testowanie / kpowanie” tylko wtedy, gdy narzędzia testujące / kpiące mają poważne ograniczenia.
Rogério

@ Rogério Zwykle podczas korzystania z DI nie każda klasa implementuje osobny interfejs. Jeden interfejs implementowany przez kilka klas jest powszechny.
Tulains Córdova

@ Rogério Poprawiłem odpowiedź, za każdym razem, gdy wspominałem DI, miałem na myśli DIP.
Tulains Córdova

9

Użyj inwersji zależności tam, gdzie ma to sens.

Jednym z przeciwnych przykładów jest klasa „string” zawarta w wielu językach. Reprezentuje prymitywną koncepcję, zasadniczo tablicę znaków. Zakładając, że możesz zmienić tę klasę podstawową, nie ma sensu używać DI tutaj, ponieważ nigdy nie będziesz musiał zamieniać stanu wewnętrznego na coś innego.

Jeśli masz grupę obiektów używanych wewnętrznie w module, które nie są narażone na inne moduły lub ponownie użyte gdziekolwiek, prawdopodobnie nie warto używać DI.

Są dwa miejsca, w których moim zdaniem powinno się automatycznie stosować DI:

  1. W modułach przeznaczonych do rozbudowy. Jeśli głównym celem modułu jest jego rozbudowa i zmiana zachowania, sensowne jest, aby piec DI od samego początku.

  2. W modułach, które refaktoryzujesz w celu ponownego użycia kodu. Być może kodujesz klasę, aby coś zrobić, a potem uświadomisz sobie, że dzięki refaktorowi możesz wykorzystać ten kod gdzie indziej i trzeba to zrobić . To świetny kandydat na DI i inne zmiany rozszerzalności.

Klucze tutaj są używane tam, gdzie jest to potrzebne, ponieważ wprowadzi dodatkową złożoność i upewnij się, że mierzysz to za pomocą wymagań technicznych (punkt pierwszy) lub ilościowego przeglądu kodu (punkt drugi).

DI jest doskonałym narzędziem, ale jak każde * narzędzie, może być nadużywane lub niewłaściwie używane.

* Wyjątek od powyższej zasady: piła szablasta to idealne narzędzie do każdego zadania. Jeśli to nie rozwiąże problemu, usunie go. Na stałe.


3
Co jeśli „twoim problemem” jest dziura w ścianie? Piła tego nie usunie; pogorszyłoby to sytuację. ;)
Mason Wheeler,

3
@MasonWheeler z mocną i przyjemną w użyciu piłą, „dziura w ścianie” może zmienić się w „drzwi”, co jest przydatnym atutem :-)

1
Nie możesz użyć piły do ​​wykonania łaty na dziurę?
JeffO,

Chociaż istnieją pewne zalety posiadania Stringtypu, który nie jest rozszerzalny dla użytkownika , istnieje wiele przypadków, w których alternatywne reprezentacje byłyby pomocne, gdyby typ miał dobry zestaw operacji wirtualnych (np. Skopiowanie podłańcucha do określonej części short[], raport, czy podłańcuch zawiera lub może zawierać tylko ASCII, spróbuj skopiować podciąg, który prawdopodobnie zawiera tylko ASCII, do określonej części byte[]itp.) Szkoda, że ​​struktury nie mają zaimplementowanych użytecznych interfejsów.
supercat

1
Dlaczego ta odpowiedź mówi o DI, jeśli pytanie dotyczyło DIP? DIP to koncepcja z lat 90., a DI z 2004 r. Są one bardzo różne.
Rogério

5

Wydaje mi się, że w pierwotnym pytaniu brakuje części sensu DIP.

Sceptycznie podchodzę do tego dlatego, że płacimy pewną cenę za przestrzeganie tej zasady. Powiedzmy, że muszę zaimplementować funkcję Z. Po analizie stwierdzam, że funkcja Z składa się z funkcjonalności A, B i C. Tworzę fascynującą klasę Z, która poprzez interfejsy używa klas A, B i C. Zaczynam kodować implementacja i w pewnym momencie zdaję sobie sprawę, że zadanie Z faktycznie składa się z funkcjonalności A, B i D. Teraz muszę zeskrobać interfejs C, prototyp klasy C i napisać osobny interfejs D i klasę. Bez interfejsów tylko klasa wymagałaby fali.

Aby naprawdę skorzystać z DIP, należy najpierw utworzyć klasę Z i wywołać funkcjonalność klas A, B i C (które nie zostały jeszcze opracowane). Daje to interfejs API dla klas A, B i C. Następnie przechodzisz do tworzenia klas A, B i C i wypełniasz szczegóły. Powinieneś skutecznie tworzyć abstrakcje, których potrzebujesz, tworząc klasę Z, całkowicie w oparciu o to, czego potrzebuje klasa Z. Możesz nawet pisać testy w klasie Z, zanim jeszcze klasy A, B lub C zostaną napisane.

Pamiętaj, że DIP mówi, że „Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji”.

Po ustaleniu, czego potrzebuje klasa Z i sposobie, w jaki chce uzyskać to, czego potrzebuje, możesz wypełnić szczegóły. Oczywiście, czasem trzeba będzie wprowadzić zmiany w klasie Z, ale w 99% przypadków tak nie będzie.

Nigdy nie będzie klasy D, ponieważ doszedłeś do wniosku, że Z potrzebuje A, B i C, zanim zostaną napisane. Zmiana wymagań to zupełnie inna historia.


5

Krótka odpowiedź brzmi „prawie nigdy”, ale w rzeczywistości jest kilka miejsc, w których DIP nie ma sensu:

  1. Fabryki lub budowniczych, których zadaniem jest tworzenie obiektów. Są to zasadniczo „węzły liści” w systemie, który w pełni obejmuje IoC. W pewnym momencie coś musi faktycznie stworzyć twoje obiekty i nie może zależeć od niczego innego, aby to zrobić. W wielu językach kontener IoC może to zrobić za Ciebie, ale czasami musisz to zrobić w staromodny sposób.

  2. Implementacje struktur danych i algorytmów. Zasadniczo w tych przypadkach istotne cechy, które optymalizujesz (takie jak czas działania i złożoność asymptotyczna), zależą od używanych typów danych. Jeśli wdrażasz tabelę skrótów, naprawdę musisz wiedzieć, że pracujesz z tablicą do przechowywania, a nie z listą połączoną, a tylko sama tabela wie, jak poprawnie przydzielić tablice. Nie chcesz także przekazywać zmiennej tablicy, a osoba dzwoniąca złamie tablicę skrótów, bawiąc się jej zawartością.

  3. Klasy modeli domen . Wprowadzają one logikę biznesową i (przez większość czasu) sensowne jest mieć tylko jedną implementację, ponieważ (przez większość czasu) tworzysz oprogramowanie tylko dla jednej firmy. Chociaż niektóre klasy modeli domen mogą być konstruowane przy użyciu innych klas modeli domen, zazwyczaj dzieje się tak w poszczególnych przypadkach. Ponieważ obiekty modelu domeny nie zawierają żadnej funkcji, którą można by z powodzeniem wyśmiewać, DIP nie daje korzyści w zakresie testowania ani konserwacji.

  4. Wszelkie obiekty, które są dostarczane jako zewnętrzny interfejs API i muszą tworzyć inne obiekty, których szczegółów implementacji nie chcesz udostępniać publicznie. Obejmuje to ogólną kategorię „projektowanie bibliotek różni się od projektowania aplikacji”. Biblioteka lub framework może swobodnie korzystać z DI wewnętrznie, ale w końcu będzie musiał wykonać jakąś rzeczywistą pracę, w przeciwnym razie nie będzie to bardzo przydatna biblioteka. Powiedzmy, że tworzysz bibliotekę sieciową; tak naprawdę nie chcesz, aby konsument mógł zapewnić własną implementację gniazda. Możesz użyć wewnętrznie abstrakcji gniazda, ale API, które udostępniasz dzwoniącym, stworzy własne gniazda.

  5. Testy jednostkowe i podwójne testy. Podróbki i odcinki powinny zrobić jedną rzecz i zrobić to po prostu. Jeśli masz fałszywkę, która jest wystarczająco złożona, aby martwić się o to, czy zrobić wstrzyknięcie zależności, to prawdopodobnie jest zbyt złożona (być może dlatego, że implementuje interfejs, który jest również zbyt skomplikowany).

Może być więcej; są to te, które widzę dość często.


Co powiesz na „za każdym razem, gdy pracujesz w dynamicznym języku”?
Kevin

Nie? Robię dużo pracy w JavaScript i nadal działa tam równie dobrze. „O” i „I” w SOLID mogą jednak być nieco rozmyte.
Aaronaught

Huh ... Uważam, że pierwszorzędne typy Pythona w połączeniu z pisaniem kaczym sprawiają, że jest to mniej konieczne.
Kevin

DIP nie ma absolutnie nic wspólnego z systemem typów. W jaki sposób „typy pierwszej klasy” są unikalne dla Pythona? Kiedy chcesz przetestować coś w izolacji, powinieneś zastąpić podwójny test jego zależnościami. Te duplikaty testowe mogą być alternatywnymi implementacjami interfejsu (w językach o typie statycznym) lub mogą być anonimowymi obiektami lub alternatywnymi typami, które mają takie same metody / funkcje (pisanie kaczką). W obu przypadkach nadal potrzebujesz sposobu, aby faktycznie zastąpić instancję.
Aaronaught

1
Python Kevin nie był pierwszym językiem, który posiadał dynamiczne pisanie lub luźne próby. Jest to również całkowicie nieistotne. Pytanie nie brzmi, jaki jest typ obiektu, ale jak / gdzie ten obiekt jest tworzony. Kiedy obiekt tworzy własne zależności, jesteś zmuszany do testowania jednostek, które powinny być szczegółami implementacji, poprzez robienie okropnych rzeczy, takich jak usuwanie konstruktorów klas, o których publiczny interfejs API nie wspomina. A zapominanie o testowaniu, zachowaniu mieszania i budowie obiektów prowadzi po prostu do ciasnego połączenia. Pisanie kaczką nie rozwiązuje żadnego z tych problemów.
Aaronaught

2

Niektóre znaki, że możesz stosować DIP na zbyt mikro poziomie, gdzie nie zapewnia to wartości:

  • masz parę C / CImpl lub IC / C, z tylko jedną implementacją tego interfejsu
  • podpisy w interfejsie i implementacji są zgodne jeden na jeden (naruszając zasadę DRY)
  • często zmieniasz C i CImpl w tym samym czasie.
  • C jest wewnętrzną częścią twojego projektu i nie jest udostępniane poza projektem jako biblioteka.
  • jesteś sfrustrowany przez F3 w Eclipse / F12 w Visual Studio, zabierając cię do interfejsu zamiast rzeczywistej klasy

Jeśli to właśnie widzisz, być może lepiej będzie, jeśli Z zadzwoni bezpośrednio do C i pominie interfejs.

Nie myślę też o dekorowaniu metod za pomocą szkieletu wstrzykiwania zależności / dynamicznego proxy (Spring, Java EE) w taki sam sposób, jak w prawdziwym SOLID DIP - jest to bardziej szczegół implementacji sposobu, w jaki dekoracja metody działa w tym stosie technologicznym. Społeczność Java EE uważa to za ulepszenie polegające na tym, że nie potrzebujesz par Foo / FooImpl, tak jak kiedyś ( odniesienie ). Natomiast Python obsługuje dekorację funkcji jako pierwszorzędną funkcję językową.

Zobacz także ten post na blogu .


0

Jeśli zawsze odwracasz swoje zależności, to wszystkie twoje zależności są do góry nogami. Co oznacza, że ​​jeśli zacząłeś od niechlujnego kodu z węzłem zależności, to właśnie to (naprawdę) masz, po prostu odwrócone. Właśnie tam pojawia się problem, że każda zmiana implementacji musi również zmienić jej interfejs.

Punktem inwersji zależności jest selektywne odwracanie zależności, które powodują splątanie rzeczy. Ci, którzy powinni przejść z A do B do C, nadal to robią, to ci, którzy przechodzili z C do A, teraz przechodzą z A do C.

Wynikiem powinien być wykres zależności od cykli - DAG. Istnieją różne narzędzia , które sprawdzą tę właściwość i narysują wykres.

Aby uzyskać pełniejsze wyjaśnienie, zobacz ten artykuł :

Istotą prawidłowego zastosowania zasady inwersji zależności jest:

Podziel kod / usługę /…, na której polegasz, na interfejs i implementację. Interfejs restrukturyzuje zależność w żargonie używającego go kodu, implementacja implementuje go pod względem podstawowych technik.

Wdrożenie pozostaje tam, gdzie jest. Ale interfejs ma teraz inną funkcję (i używa innego żargonu / języka), opisując coś, co można zrobić za pomocą kodu. Przenieś do tego pakietu. Nie umieszczając interfejsu i implementacji w tym samym pakiecie, zależność (kierunek) odwraca się od użytkownik → implementacja do implementacji → użytkownik.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.