Zasada inwersji zależności - Dependency inversion principle

W projektowania obiektowego The zasada zależność inwersja jest formą specyficzna luźno sprzężenie oprogramowania modułów . Stosując się do tej zasady, konwencjonalne relacje zależności ustanowione z modułów ustalających zasady wysokiego poziomu do modułów zależności niskiego poziomu są odwracane, w ten sposób uniezależniając moduły wysokiego poziomu od szczegółów implementacji modułu niskiego poziomu. Zasada stanowi:

  1. Moduły wysokopoziomowe nie powinny importować niczego z modułów niskopoziomowych. Oba powinny zależeć od abstrakcji (np. interfejsów).
  2. Abstrakcje nie powinny zależeć od szczegółów. Szczegóły (realizacje konkretne) powinny zależeć od abstrakcji.

Narzucając, że zarówno obiekty wysokiego, jak i niskiego poziomu muszą zależeć od tej samej abstrakcji, ta zasada projektowania odwraca sposób, w jaki niektórzy ludzie mogą myśleć o programowaniu obiektowym.

Ideą punktów A i B tej zasady jest to, że projektując interakcję między modułem wysokiego poziomu a modułem niskiego poziomu, interakcję należy traktować jako abstrakcyjną interakcję między nimi. Ma to wpływ nie tylko na projekt modułu wysokopoziomowego, ale także na moduł niskopoziomowy: moduł niskopoziomowy powinien być zaprojektowany z myślą o interakcji i może być konieczna zmiana jego interfejsu użytkowania.

W wielu przypadkach myślenie o samej interakcji jako o abstrakcyjnym pojęciu pozwala na zmniejszenie sprzężenia komponentów bez wprowadzania dodatkowych wzorców kodowania, pozwalając jedynie na lżejszy i mniej zależny od implementacji schemat interakcji.

Kiedy odkryte abstrakcyjne schematy interakcji między dwoma modułami są ogólne i uogólnienie ma sens, ta zasada projektowania prowadzi również do następującego wzorca kodowania inwersji zależności.

Tradycyjny wzór warstw

W konwencjonalnej architekturze aplikacji komponenty niższego poziomu (np. warstwa narzędziowa) są projektowane do wykorzystania przez komponenty wyższego poziomu (np. warstwa polityki), co umożliwia budowanie coraz bardziej złożonych systemów. W tej kompozycji składniki wyższego poziomu zależą bezpośrednio od składników niższego poziomu, aby osiągnąć pewne zadanie. Ta zależność od składników niższego poziomu ogranicza możliwości ponownego wykorzystania składników wyższego poziomu.

Tradycyjny wzór warstw.png

Celem wzorca odwrócenia zależności jest uniknięcie tej wysoce powiązanej dystrybucji z zapośredniczeniem warstwy abstrakcyjnej i zwiększenie możliwości ponownego wykorzystania warstw wyższych/politycznych.

Wzór odwrócenia zależności

Po dodaniu warstwy abstrakcyjnej zarówno warstwy wysokiego, jak i niższego poziomu zmniejszają tradycyjne zależności od góry do dołu. Niemniej jednak koncepcja „inwersji” nie oznacza, że ​​warstwy niższego poziomu zależą bezpośrednio od warstw wyższego poziomu. Obie warstwy powinny zależeć od abstrakcji (interfejsów), które ujawniają zachowanie wymagane przez warstwy wyższego poziomu.

DIPLayersPattern.png

W bezpośrednim zastosowaniu odwracania zależności streszczenia należą do warstw wyższych/polityk. Ta architektura grupuje składniki wyższego poziomu/polityki i abstrakcje, które definiują niższe usługi w tym samym pakiecie. Warstwy niższego poziomu są tworzone przez dziedziczenie/implementację tych abstrakcyjnych klas lub interfejsów.

Odwrócenie zależności i własności zachęca do ponownego wykorzystania warstw wyższych/politycznych. Wyższe warstwy mogłyby korzystać z innych implementacji niższych usług. Gdy komponenty warstwy niższego poziomu są zamykane lub gdy aplikacja wymaga ponownego użycia istniejących usług, często pośredniczy między usługami a abstrakcjami adapter .

Uogólnienie wzorca inwersji zależności

W wielu projektach zasada i wzorzec inwersji zależności są traktowane jako jedna koncepcja, którą należy uogólnić, tj. zastosować do wszystkich interfejsów między modułami oprogramowania. Są ku temu co najmniej dwa powody:

  1. Łatwiej jest postrzegać zasadę dobrego myślenia jako wzorzec kodowania. Po zakodowaniu klasy abstrakcyjnej lub interfejsu programista może powiedzieć: „Wykonałem pracę abstrakcji”.
  2. Ponieważ wiele narzędzi do testowania jednostkowego opiera się na dziedziczeniu w celu wykonania mockowania , regułą stało się stosowanie ogólnych interfejsów między klasami (nie tylko między modułami, gdy ma sens stosowanie ogólności).

Jeśli używane narzędzie do szyderstwa opiera się tylko na dziedziczeniu, konieczne może być szerokie zastosowanie wzorca odwrócenia zależności. Ma to poważne wady:

  1. Samo zaimplementowanie interfejsu przez klasę nie wystarcza do zmniejszenia sprzężenia; tylko myślenie o potencjalnej abstrakcji interakcji może prowadzić do mniej powiązanego projektu.
  2. Implementacja ogólnych interfejsów w całym projekcie utrudnia zrozumienie i utrzymanie. Na każdym kroku czytelnik będzie zadawał sobie pytanie, jakie są inne implementacje tego interfejsu, a odpowiedź brzmi: tylko mocki.
  3. Uogólnienie interfejsu wymaga więcej kodu instalacyjnego, w szczególności w fabrykach, które generalnie opierają się na frameworku wstrzykiwania zależności.
  4. Generalizacja interfejsu ogranicza również użycie języka programowania.

Ograniczenia generalizacji

Obecność interfejsów do realizacji wzorca odwrócenia zależności (DIP) ma inne konsekwencje projektowe w programie zorientowanym obiektowo :

  • Wszystkie zmienne składowe w klasie muszą być interfejsami lub abstraktami.
  • Wszystkie pakiety klas konkretnych muszą łączyć się tylko przez pakiety interfejsu lub klasy abstrakcyjnej.
  • Żadna klasa nie powinna pochodzić z konkretnej klasy.
  • Żadna metoda nie powinna zastępować metody zaimplementowanej.
  • Wszystkie instancje zmiennych wymagają zaimplementowania wzorca twórczego, takiego jak metoda fabryki lub wzorzec fabryki , lub użycia struktury wstrzykiwania zależności .

Ograniczenia dotyczące naśladowania interfejsu

Korzystanie z narzędzi do prześmiewania opartych na dziedziczeniu wprowadza również ograniczenia:

  • Statyczne widoczne z zewnątrz elementy członkowskie powinny systematycznie polegać na wstrzykiwaniu zależności, co znacznie utrudnia ich implementację.
  • Wszystkie testowalne metody powinny stać się implementacją interfejsu lub przesłonięciem abstrakcyjnej definicji.

Przyszłe kierunki

Zasady to sposoby myślenia. Wzorce są powszechnymi sposobami rozwiązywania problemów. Wzorcom kodowania może brakować funkcji języka programowania.

  • Języki programowania będą nadal ewoluować, aby umożliwić im wymuszanie silniejszych i bardziej precyzyjnych umów użytkowania w co najmniej dwóch kierunkach: wymuszanie warunków użytkowania (warunki przed, po i niezmienne) oraz interfejsy oparte na stanie. To prawdopodobnie zachęci i potencjalnie uprości silniejsze zastosowanie wzorca odwrócenia zależności w wielu sytuacjach.
  • Coraz więcej narzędzi do szyderstwa używa teraz wstrzykiwania zależności, aby rozwiązać problem zastępowania członków statycznych i niewirtualnych. Języki programowania prawdopodobnie ewoluują, aby generować kod bajtowy zgodny z mockingiem. Jednym z kierunków będzie ograniczenie użycia członków niewirtualnych. Drugim będzie generowanie, przynajmniej w sytuacjach testowych, kodu bajtowego umożliwiającego mockowanie bez dziedziczenia.

Realizacje

Dwie popularne implementacje DIP wykorzystują podobną architekturę logiczną, ale mają różne implikacje.

Bezpośrednia implementacja pakuje klasy zasad z klasami abstraktów usług w jednej bibliotece. W tej implementacji komponenty wysokopoziomowe i niskopoziomowe są dystrybuowane do oddzielnych pakietów/bibliotek, gdzie interfejsy definiujące zachowanie/usługi wymagane przez komponent wysokopoziomowy są własnością i istnieją w bibliotece komponentu wysokopoziomowego. Implementacja interfejsu komponentu wysokiego poziomu przez komponent niskiego poziomu wymaga, aby pakiet komponentów niskiego poziomu był zależny od komponentu wysokiego poziomu w celu kompilacji, odwracając w ten sposób konwencjonalną relację zależności.

Inwersja zależności.png

Rysunki 1 i 2 ilustrują kod z tą samą funkcjonalnością, jednak na Rysunku 2 zastosowano interfejs do odwrócenia zależności. Kierunek zależności można wybrać, aby zmaksymalizować ponowne wykorzystanie kodu strategii i wyeliminować zależności cykliczne.

W tej wersji DIP zależność komponentu warstwy niższej od interfejsów/abstraktów w warstwach wyższego poziomu utrudnia ponowne wykorzystanie komponentów warstwy niższej. Ta implementacja zamiast tego „odwraca” tradycyjną zależność od góry do dołu na przeciwną, od dołu do góry.

Bardziej elastyczne rozwiązanie wyodrębnia abstrakcyjne komponenty do niezależnego zestawu pakietów/bibliotek:

DIPLayersPattern v2.png

Podział każdej warstwy na własny pakiet zachęca do ponownego wykorzystania dowolnej warstwy, zapewniając solidność i mobilność.

Przykłady

Moduł genealogiczny

System genealogiczny może przedstawiać relacje między ludźmi jako wykres bezpośrednich relacji między nimi (ojciec-syn, ojciec-córka, matka-syn, matka-córka, mąż-żona, żona-mąż itp.). Jest to bardzo wydajne i rozszerzalne, ponieważ łatwo jest dodać byłego męża lub opiekuna prawnego. Mechanizmy powinny implementować swój interfejs, a nie politykę. Schemat jest błędny.

Jednak niektóre moduły wyższego poziomu mogą wymagać prostszego sposobu przeglądania systemu: każda osoba może mieć dzieci, rodziców, rodzeństwo (w tym przyrodnich braci i siostry lub nie), dziadków, kuzynów i tak dalej.

W zależności od zastosowania modułu genealogicznego, przedstawienie wspólnych relacji jako odrębnych właściwości bezpośrednich (ukrycie wykresu) sprawi, że sprzężenie między modułem wyższego poziomu a modułem genealogicznym będzie znacznie lżejsze i pozwoli całkowicie zmienić wewnętrzną reprezentację relacji bezpośrednich bez wpływu na korzystające z nich moduły. Pozwala również na osadzenie dokładnych definicji rodzeństwa lub wujów w module genealogicznym, egzekwując w ten sposób zasadę pojedynczej odpowiedzialności .

Wreszcie, jeśli podejście pierwszego rozszerzalnego uogólnionego grafu wydaje się najbardziej rozszerzalne, użycie modułu genealogicznego może pokazać, że bardziej wyspecjalizowana i prostsza implementacja relacji jest wystarczająca dla aplikacji i pomaga stworzyć bardziej wydajny system.

W tym przykładzie wydzielenie interakcji między modułami prowadzi do uproszczonego interfejsu modułu niższego poziomu i może prowadzić do prostszej jego implementacji.

Klient zdalnego serwera plików

Wyobraź sobie, że musisz zaimplementować klienta na zdalnym serwerze plików (FTP, przechowywanie w chmurze ...). Możesz myśleć o tym jako o zestawie abstrakcyjnych interfejsów:

  1. Połączenie/Rozłączenie (może być potrzebna warstwa trwałości połączenia)
  2. Interfejs tworzenia/zmiany nazwy/usuwania/usuwania folderów/tagów
  3. Tworzenie/zastępowanie plików/zmiana nazwy/usuwanie/odczyt interfejsu
  4. Wyszukiwanie plików
  5. Równoczesne zastępowanie lub usuwanie rozwiązania
  6. Zarządzanie historią plików...

Jeśli zarówno pliki lokalne, jak i pliki zdalne oferują te same abstrakcyjne interfejsy, każdy moduł wysokiego poziomu korzystający z plików lokalnych i w pełni implementujący wzorzec odwrócenia zależności będzie mógł bezkrytycznie uzyskiwać dostęp do plików lokalnych i zdalnych.

Funkcjonalność dysku lokalnego zazwyczaj korzysta z folderów, podczas gdy magazyn zdalny może używać folderów lub tagów. Musisz zdecydować, jak je ujednolicić, jeśli to możliwe.

Na zdalnym pliku być może będziemy musieli użyć tylko tworzenia lub zastępowania: zdalna aktualizacja plików niekoniecznie ma sens, ponieważ losowa aktualizacja jest zbyt wolna w porównaniu z losową aktualizacją lokalnego pliku i może być bardzo skomplikowana w implementacji). Na zdalnym pliku możemy potrzebować częściowego odczytu i zapisu (przynajmniej wewnątrz modułu zdalnego pliku, aby umożliwić wznowienie pobierania lub wysyłania po przerwaniu komunikacji), ale losowy odczyt nie jest dostosowany (z wyjątkiem sytuacji, gdy używana jest lokalna pamięć podręczna).

Wyszukiwanie plików może być podłączane: wyszukiwanie plików może opierać się na systemie operacyjnym lub w szczególności w przypadku wyszukiwania znaczników lub pełnego tekstu, może być zaimplementowane w różnych systemach (osadzone w systemie operacyjnym lub dostępne osobno).

Wykrywanie współbieżnego zastępowania lub usuwania rozdzielczości może mieć wpływ na inne abstrakcyjne interfejsy.

Projektując klienta zdalnego serwera plików dla każdego interfejsu koncepcyjnego, musisz zadać sobie pytanie o poziom usług wymaganych przez moduły wysokiego poziomu (niekoniecznie wszystkie) i nie tylko o to, jak zaimplementować funkcje zdalnego serwera plików, ale może o to, jak utworzyć plik usługi w Twojej aplikacji kompatybilne między już zaimplementowanymi usługami plików (pliki lokalne, istniejący klienci w chmurze) a nowym klientem zdalnego serwera plików.

Po zaprojektowaniu wymaganych abstrakcyjnych interfejsów klient zdalnego serwera plików powinien zaimplementować te interfejsy. A ponieważ prawdopodobnie ograniczyłeś niektóre funkcje lokalne istniejące w pliku lokalnym (na przykład aktualizacja pliku), być może będziesz musiał napisać adaptery dla lokalnych lub innych istniejących używanych modułów zdalnego dostępu do plików, z których każdy oferuje te same abstrakcyjne interfejsy. Musisz także napisać własny program do wyliczania dostępu do plików, który pozwoli na odzyskanie wszystkich systemów kompatybilnych z plikami dostępnych i skonfigurowanych na Twoim komputerze.

Gdy to zrobisz, Twoja aplikacja będzie mogła zapisywać swoje dokumenty lokalnie lub zdalnie w sposób przezroczysty. Mówiąc prościej, moduł wysokiego poziomu korzystający z nowych interfejsów dostępu do plików może być używany niewyraźnie w scenariuszach lokalnego lub zdalnego dostępu do plików, dzięki czemu można go ponownie wykorzystać.

Zauważ, że wiele systemów operacyjnych zaczęło implementować tego rodzaju funkcje i twoja praca może być ograniczona do dostosowania twojego nowego klienta do tych już abstrakcyjnych modeli.

W tym przykładzie myślenie o module jako zbiorze abstrakcyjnych interfejsów i dostosowywanie innych modułów do tego zestawu interfejsów pozwala zapewnić wspólny interfejs dla wielu systemów przechowywania plików.

Kontroler widoku modelu

Przykład DIP

Pakiety UI i ApplicationLayer zawierają głównie konkretne klasy. Kontrolery zawierają streszczenia/typy interfejsów. Interfejs użytkownika ma instancję ICustomerHandler. Wszystkie paczki są fizycznie oddzielone. W ApplicationLayer znajduje się konkretna implementacja, której użyje klasa Page. Instancje tego interfejsu tworzone są dynamicznie przez Fabrykę (ewentualnie w tym samym pakiecie Kontrolery). Konkretne typy, Page i CustomerHandler, nie zależą od siebie; oba zależą od ICustomerHandler.

Bezpośrednim efektem jest to, że interfejs użytkownika nie musi odwoływać się do ApplicationLayer ani żadnego konkretnego pakietu, który implementuje ICustomerHandler. Klasa betonu zostanie załadowana za pomocą odbicia . W każdej chwili konkretną implementację można zastąpić inną konkretną implementacją bez zmiany klasy UI. Inną interesującą możliwością jest to, że klasa Page implementuje interfejs IPageViewer, który może być przekazany jako argument do metod ICustomerHandler. Wtedy konkretna implementacja mogłaby komunikować się z interfejsem użytkownika bez konkretnej zależności. Ponownie oba są połączone interfejsami.

Powiązane wzory

Przykładem wzorca adaptera może być również zastosowanie zasady inwersji zależności . Oznacza to, że klasa wysokiego poziomu definiuje swój własny interfejs adaptera, który jest abstrakcją, od której zależą inne klasy wysokiego poziomu. Dostosowana implementacja również zależy koniecznie od tej samej abstrakcji interfejsu adaptera, podczas gdy może być zaimplementowana przy użyciu kodu z jego własnego modułu niskopoziomowego. Moduł wysokopoziomowy nie jest zależny od modułu niskopoziomowego, ponieważ wykorzystuje on funkcjonalność niskopoziomową jedynie pośrednio przez interfejs adaptera, wywołując na interfejsie metody polimorficzne, które są zaimplementowane przez zaadaptowaną implementację i jej moduł niskopoziomowy.

Różne wzorce, takie jak Plugin , Service Locator lub Dependency injection są wykorzystywane w celu ułatwienia dostarczania w czasie wykonywania wybranej implementacji komponentu niskiego poziomu do komponentu wysokiego poziomu.

Historia

Zasada inwersji zależności została postulowana przez Roberta C. Martina i opisana w kilku publikacjach, w tym w artykule Object Oriented Design Quality Metrics: an analysis of dependencies , artykule opublikowanym w C++ Report w maju 1996 roku zatytułowanym The Dependency Inversion Principle oraz książkach Agile Tworzenie oprogramowania, zasady, wzorce i praktyki oraz zasady, wzorce i praktyki Agile w języku C# .

Zobacz też

Bibliografia

Zewnętrzne linki