CodeGym/Kursy Java/Moduł 3/Inwersja zależności

Inwersja zależności

Dostępny

9.1 Odwrócenie zależności

Pamiętasz, jak kiedyś powiedzieliśmy, że w aplikacji serwerowej nie można po prostu tworzyć strumieni new Thread().start()? Tylko kontener powinien tworzyć wątki. Teraz rozwiniemy ten pomysł jeszcze bardziej.

Wszystkie obiekty powinny być również tworzone tylko przez kontener . Oczywiście nie mówimy o wszystkich obiektach, a raczej o tzw. obiektach biznesowych. Są one również często określane jako pojemniki. Nóżki tego podejścia wyrastają z piątej zasady SOLID, która wymaga pozbycia się klas i przejścia na interfejsy:

  • Moduły najwyższego poziomu nie powinny zależeć od modułów niższego poziomu. Zarówno te, jak i inne powinny opierać się na abstrakcjach.
  • Abstrakcje nie powinny zależeć od szczegółów. Implementacja musi zależeć od abstrakcji.

Moduły nie powinny zawierać odniesień do konkretnych implementacji, a wszelkie zależności i interakcje między nimi powinny być budowane wyłącznie w oparciu o abstrakcje (czyli interfejsy). Istotę tej reguły można zapisać w jednym zdaniu: wszystkie zależności muszą mieć postać interfejsów .

Pomimo swojej fundamentalnej natury i pozornej prostoty, zasada ta jest najczęściej łamana. Mianowicie za każdym razem, gdy używamy operatora new w kodzie programu/modułu i tworzymy nowy obiekt określonego typu, to zamiast zależności od interfejsu powstaje zależność od implementacji.

Wiadomo, że nie da się tego uniknąć i gdzieś trzeba tworzyć obiekty. Ale przynajmniej musisz zminimalizować liczbę miejsc, w których to się robi iw których klasy są wyraźnie określone, a także zlokalizować i wyizolować takie miejsca, aby nie były rozproszone w całym kodzie programu.

Bardzo dobrym rozwiązaniem jest szalony pomysł skoncentrowania tworzenia nowych obiektów w obrębie wyspecjalizowanych obiektów i modułów – fabryk, lokalizatorów usług, kontenerów IoC.

W pewnym sensie taka decyzja jest zgodna z zasadą pojedynczego wyboru, która mówi: „Gdy system oprogramowania musi obsługiwać wiele alternatyw, ich pełna lista powinna być znana tylko jednemu modułowi systemu” .

Dlatego jeśli w przyszłości konieczne będzie dodanie nowych opcji (lub nowych implementacji, jak w przypadku tworzenia nowych obiektów, które rozważamy), to wystarczy zaktualizować tylko moduł, który zawiera te informacje, a wszystkie pozostałe moduły pozostaną nienaruszone i będą mogły kontynuować swoją pracę jak zwykle.

Przykład 1

new ArrayList Zamiast pisać coś w stylu , sensowne byłoby List.new(), aby JDK zapewniał poprawną implementację liścia: ArrayList, LinkedList, a nawet ConcurrentList.

Na przykład kompilator widzi, że istnieją wywołania obiektu z różnych wątków i umieszcza tam implementację wątkowo bezpieczną. Albo za dużo insertów na środku arkusza, wtedy implementacja będzie oparta na LinkedList.

Przykład 2

Stało się to już na przykład z rodzajami. Kiedy ostatnio napisałeś algorytm sortowania do sortowania kolekcji? Zamiast tego teraz wszyscy używają metody Collections.sort(), a elementy kolekcji muszą obsługiwać interfejs Comparable (porównywalny).

Jeśli sort()przekażesz do metody kolekcję zawierającą mniej niż 10 elementów, całkiem możliwe jest posortowanie jej za pomocą sortowania bąbelkowego (sortowanie bąbelkowe), a nie sortowania szybkiego.

Przykład 3

Kompilator już obserwuje, w jaki sposób łączysz łańcuchy i zastąpi Twój kod kodem StringBuilder.append().

9.2 Odwrócenie zależności w praktyce

Teraz najciekawsze: zastanówmy się, jak możemy połączyć teorię z praktyką. W jaki sposób moduły mogą poprawnie tworzyć i odbierać swoje „zależności” i nie naruszać odwrócenia zależności?

Aby to zrobić, projektując moduł, musisz sam zdecydować:

  • co moduł robi, jaką funkcję pełni;
  • wtedy moduł potrzebuje ze swojego otoczenia, czyli z jakimi obiektami/modułami będzie miał do czynienia;
  • A jak go zdobędzie?

Aby zachować zgodność z zasadami Odwracania Zależności, zdecydowanie musisz zdecydować, jakich obiektów zewnętrznych używa Twój moduł i w jaki sposób będzie otrzymywał do nich odwołania.

A oto następujące opcje:

  • moduł sam tworzy obiekty;
  • moduł pobiera obiekty z pojemnika;
  • moduł nie ma pojęcia, skąd pochodzą obiekty.

Problem polega na tym, że aby stworzyć obiekt, trzeba wywołać konstruktor określonego typu, w efekcie moduł będzie zależny nie od interfejsu, ale od konkretnej implementacji. Ale jeśli nie chcemy, aby obiekty były tworzone jawnie w kodzie modułu, możemy użyć wzorca Factory Method .

„Najważniejsze jest to, że zamiast bezpośrednio tworzyć instancję obiektu za pomocą new, zapewniamy klasie klienta interfejs do tworzenia obiektów. Ponieważ taki interfejs zawsze można zastąpić odpowiednim projektem, uzyskujemy pewną elastyczność podczas korzystania z modułów niskiego poziomu w modułach wysokiego poziomu” .

W przypadkach, gdy konieczne jest utworzenie grup lub rodzin powiązanych obiektów, zamiast metody fabrycznej używana jest Fabryka abstrakcyjna .

9.3 Korzystanie z Lokalizatora usług

Moduł pobiera potrzebne przedmioty od tego, który już je posiada. Zakłada się, że system posiada repozytorium obiektów, w którym moduły mogą „umieszczać” swoje obiekty i „pobierać” obiekty z repozytorium.

Takie podejście jest realizowane przez wzorzec Service Locator , którego główną ideą jest to, że program ma obiekt, który wie, jak uzyskać wszystkie zależności (usługi), które mogą być wymagane.

Główną różnicą w stosunku do fabryk jest to, że Service Locator nie tworzy obiektów, ale faktycznie zawiera już obiekty instancyjne (lub wie, gdzie / jak je zdobyć, a jeśli tworzy, to tylko raz przy pierwszym wywołaniu). Fabryka przy każdym wywołaniu tworzy nowy obiekt, do którego otrzymujesz pełną własność i możesz z nim robić, co chcesz.

Ważne ! Lokalizator usług tworzy odniesienia do tych samych już istniejących obiektów . Dlatego musisz bardzo uważać na przedmioty wydawane przez Lokalizator usług, ponieważ ktoś inny może z nich korzystać w tym samym czasie co Ty.

Obiekty w Service Locator mogą być dodawane bezpośrednio przez plik konfiguracyjny iw zasadzie w dowolny dogodny dla programisty sposób. Sam lokalizator usług może być klasą statyczną z zestawem metod statycznych, singletonem lub interfejsem i może być przekazywany do wymaganych klas za pośrednictwem konstruktora lub metody.

Lokalizator usług jest czasami nazywany antywzorcem i jest odradzany (ponieważ tworzy niejawne połączenia i daje tylko pozory dobrego projektu). Możesz przeczytać więcej od Marka Seamana:

9.4 Wstrzykiwanie zależności

Moduł w ogóle nie dba o zależności „wydobywające”. Określa tylko to, co jest mu potrzebne do działania, a wszystkie niezbędne zależności są dostarczane (wprowadzane) z zewnątrz przez kogoś innego.

To się nazywa - Dependency Injection. Zazwyczaj wymagane zależności są przekazywane jako parametry konstruktora (Constructor Injection) lub poprzez metody klasowe (Setter injection).

Takie podejście odwraca proces tworzenia zależności – zamiast samego modułu, tworzeniem zależności steruje ktoś z zewnątrz. Moduł z aktywnego emitera obiektów staje się pasywny – to nie on tworzy, tylko inni tworzą dla niego.

Ta zmiana kierunku nazywana jest Odwróceniem Kontroli lub Zasadą Hollywood – „Nie dzwoń do nas, my zadzwonimy do Ciebie”.

Jest to najbardziej elastyczne rozwiązanie, dające modułom największą autonomię . Można powiedzieć, że tylko on w pełni realizuje „zasadę pojedynczej odpowiedzialności” – moduł powinien być całkowicie skupiony na tym, aby dobrze wykonywać swoją pracę i nie martwić się o nic innego.

Zapewnienie modułowi wszystkiego co niezbędne do pracy to osobne zadanie, którym powinien zająć się odpowiedni „specjalista” (zwykle za zarządzanie zależnościami i ich implementację odpowiada określony kontener, kontener IoC).

Właściwie wszystko jest tu jak w życiu: w dobrze zorganizowanej firmie programiści programują, a biurka, komputery i wszystko, czego potrzebują do pracy, kupuje i zapewnia kierownik biura. Lub, jeśli użyjesz metafory programu jako konstruktora, moduł nie powinien myśleć o przewodach, ktoś inny jest zaangażowany w składanie konstruktora, a nie same części.

Nie będzie przesadą stwierdzenie, że wykorzystanie interfejsów do opisu zależności między modułami (Dependency Inversion) + poprawne tworzenie i wstrzykiwanie tych zależności (przede wszystkim Dependency Injection) to kluczowe techniki decouplingu .

Służą jako fundament, na którym luźne powiązanie kodu, jego elastyczność, odporność na zmiany, ponowne użycie, bez którego wszystkie inne techniki nie mają większego sensu. To podstawa luźnego sprzężenia i dobrej architektury.

Zasada Inversion of Control (wraz z Dependency Injection i Service Locator) została szczegółowo omówiona przez Martina Fowlera. Istnieją tłumaczenia obu jego artykułów: „Inversion of Control Containers and the Dependency Injection pattern” oraz „Inversion of Control” .

Komentarze
  • Popularne
  • Najnowsze
  • Najstarsze
Musisz się zalogować, aby dodać komentarz
Ta strona nie ma jeszcze żadnych komentarzy