Dependency Inversion Principle – DIP

Dependency Inversion Principle – DIP

Zasada odwracania zależności, jest krytyczna dla tworzenie luźno powiązanego, łatwego w utrzymaniu oprogramowania. Zasada ta to piąta i ostatnia z zasad SOLID.

Zasada inwersji zależności (DIP) mówi, że:

Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych,
obie grupy modułów powinny zależeć od abstrakcji.
Abstrakcje nie powinny zależeć od szczegółów rozwiązania,
to szczegóły powinny zależeć od abstrakcji.

Mówiąc prościej wszystkie obiekty powinny zależeć od warstwy abstrakcji a nie konkretnej klasy. Idąc dalej, w deklaracji klasy, funkcji czy metody nie powinniśmy bezpośrednio używać klas a jedynie naszych interfejsów lub klas abstrakcyjnych.

Głównym celem stosowania zasady DIP jest oddzielenie wysokopoziomowych komponentów aplikacji od komponentów niskopoziomowych. Pomiędzy komponentami wysokopoziomowymi a niskopoziomowymi powinna się znajdować warstwa abstrakcji, pozwalająca na uniezależnienie warstw. Dzięki takiemu rozwiązaniu możliwa jest zamiana jednej warstwy bez wprowadzania zmian na innym poziomie.

Przykładowo warstwa biznesowa aplikacji korzysta z warstwy dostępu do danych. Aby usunąć bezpośrednie zależności pomiędzy warstwami, część biznesowa nie może posiadać referencji bezpośrednio do warstwy dostępu do danych. Referencje powinny być wstrzykiwane przez konstruktor, co w zależności od wstrzykniętego typu pozwala nam operować na innej warstwie dostępu do danych.

Projektując aplikacje, możemy brać pod uwagę klasy niskopoziomowe które realizują podstawowe operacje (dostęp do dysku, …), i klasy wysokopoziomowe, które hermetyzują złożoną logikę (przepływy biznesowe, …). Te ostatnie polegają na klasach niskopoziomowych. Naturalnym sposobem implementacji takich struktur byłoby pisanie klas niskopoziomowych, a gdy już je mamy to pisanie złożonych klas wysokiego poziomu.

Pokaże teraz przykład, który narusza zasadę DIP.

Mamy klasę Manager, która jest klasą na wysokim poziomie, oraz klasę niskiego poziomu zwaną Sms. I teraz musimy dodać nowy moduł do naszej aplikacji, aby modelować zmiany w strukturze firmy determinowane nowym sposobem wysyłania wiadomości. W tym celu stworzyliśmy nową klasę Email. Załóżmy, że klasa Manager jest dość złożona i zawiera bardzo złożoną logikę i teraz musimy to zmienić, aby wprowadzić obsługę nowej klasy Email.

DIP – zły przykład

public class Sms
{
   public void Send()
   {
      // ....sending
   }
}

public class Manager
{
   Sms _sms;

   public void SetSms(Sms sms)
   {
      _sms = sms;
   }

   public void Manage()
   {
      sms.Send();
   }
}
	
public class Email 
{
   public void Send()
   {
      //.... sending
   }
}

Jakie mamy wady:
– musimy zmienić klasę Manager, pamiętamy, że jest to klasa złożona, a wprowadzenie zmian będzie wymagało czasu i wysiłku.
– może to mieć wpływ na niektóre bieżące funkcje klasy Manager.
– testy jednostkowe należy powtórzyć.

Wszystkie te problemy mogą zająć dużo czasu, aby je rozwiązać i mogą powodować nowe błędy w starej funkcjonalności. Sytuacja byłaby inna, gdyby aplikacja została zaprojektowana zgodnie z zasadą odwracania zależności. Oznacza to, że projektujemy klasę Manager, interfejs ISend oraz klasę Sms która implementuje interfejs ISend. Kiedy musimy dodać klasę Email, wszystko, co musimy zrobić, to zaimplementować dla niej interfejs ISend. Brak dodatkowych zmian w istniejących klasach.

Teraz kod, który obsługuje zasadę DIP.

W tym nowym projekcie nowa warstwa abstrakcji jest dodawana poprzez interfejs ISend. Teraz problemy z wcześniejszego kodu zostały rozwiązane (biorąc pod uwagę brak zmian w logice wysokiego poziomu):

– klasa menedżera nie wymaga zmian podczas dodawania klasy Email.
– zminimalizowane ryzyko wpłynięcia na starą funkcjonalność obecną w klasie Manager, ponieważ jej nie zmieniamy.
– nie ma potrzeby ponownego przeprowadzania testów jednostkowych dla klasy Manager.

DIP – przykład

public interface ISend
{
   void Send();
}

public class Sms :ISend
{
   public void Send()
   {
      // ....sending
   }
}

public class Email : ISend
{
   public void Send()
   {
      //.... sending
   }
}

public class Fax : ISend
{
   public void Send()
   {
      //.... sending
   }
}

public class Manager
{
   ISend _send;

   public void SetSms(ISend send) 
   {
      _send = send;
   }

   public void Manage()
   {
      _send.Send();
   }
}

Wniosek

Zastosowanie tej zasady oznacza, że klasy wysokiego poziomu nie działają bezpośrednio z klasami niskiego poziomu, używają interfejsów jako warstwy abstrakcyjnej. W tym przypadku tworzenie instancji nowych obiektów niskiego poziomu w klasach wysokiego poziomu, jeśli jest to konieczne nie może być wykonane przy użyciu operatora new. Zamiast tego można użyć odpowiednich wzorców projektowych, takich jak metoda fabryczna czy fabryka abstrakcyjna…

Oczywiście, stosowanie tej zasady oznacza większy wysiłek, zaowocuje to większą liczbą klas i interfejsów do utrzymania, bardziej złożonym kodem, ale bardziej elastycznym. Zasada ta nie powinna być stosowana na ślepo do każdej klasy lub każdego modułu. Jeśli mamy funkcjonalność klasy, która z dużym prawdopodobieństwem pozostanie niezmieniona w przyszłości, nie ma potrzeby stosowania tej zasady.

Zastanówmy się teraz, jakie mogą być skutki zależności modułów wysokopoziomowych od niskopoziomowych, czyli co by się działo, gdybyśmy nie postępowali według pierwszej części definicji zasady odwracania zależności.

Moduły wysokopoziomowe z natury rzeczy zawierają modele biznesowe i decyzje strategiczne aplikacji w największym stopniu odpowiadają za funkcjonowanie aplikacji. Gdyby okazało się, iż zależą one od modułów niskopoziomowych, to zmiany elementów niskopoziomowych mogłyby mieć wpływ na funkcjonowanie modułów wysokopoziomowych, a co za tym idzie wymuszałyby zmiany na wyższych poziomach.

Dodatkowo, gdy moduły wysokopoziomowe zależą od niskopoziomowych, ponowne ich wykorzystanie staje się trudne. Jeśli jednak odwrócimy tę zależność w drugą stronę, to bardzo łatwo będzie można wielokrotnie je wykorzystywać.

Jeśli chodzi o zależność od abstrakcji, pisany przez nas kod nie powinien być uzależniony od konkretnej klasy, zależności takie powinny kończyć się na interfejsach, klasach abstrakcyjnych. Zasadę zależności od abstrakcji można streścić w trzech punktach:

  • Żadna zmienna nie powinna zawierać referencji do konkretnej klasy.
  • Żadna klasa nie powinna dziedziczyć po konkretnej klasie.
  • Żadna metoda nie powinna przykrywać metody zaimplementowanej w którejkolwiek z klas bazowych.

Podstawowymi zaletami zasady DIP jest to, że właściwe jej zastosowanie jest kluczowe, jeśli chcemy tworzyć kod wielokrotnego użytku. Ma ona również duży wpływ na odporność naszego kodu na przyszłe zmiany, ponieważ zgodnie z tą zasadą abstrakcje, oraz szczegóły są od siebie odizolowane, co z kolei wpływa na to, że tworzony kod jest dużo prostszy w konserwacji.

3 comments

  1. Łał! W końcu dostałem stronę internetową, z której faktycznie jestem w stanie uzyskać cenne informacje dotyczące moich studiów i wiedzy.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *