Zasady SOLID #5 – Dependency Inversion Principle

rollercoaster
W wielu opracowaniach o podstawach programowania obiektowego można przeczytać, iż obiektowość ma w pewnym stopniu naśladować świat rzeczywisty. Zasada, którą poznamy w tym wpisie, nauczy nas jak ograniczyć zależności pomiędzy klasami. Przekonamy się, że niezależność jest pożądaną cechą zarówno w świecie rzeczywistym, jak i tym programistycznym.

Wprowadzenie

Zasada SOLID numer 5 nosi nazwę Zasada Odwrócenia Zależności (ang. Dependency Inversion Principle). Zgodnie z jej treścią, moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych, zależności między nimi powinny wynikać z abstrakcji. Natomiast abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.
Tworząc oprogramowanie, powinniśmy ograniczać zależności pomiędzy klasami. Możemy to osiągnąć poprzez zastosowanie interfejsów. Gdy zajdzie konieczność wprowadzenia zmian w implementacji, zazwyczaj nie musimy dokonywać zmian w interfejsie. Gdyby moduł wysokiego poziomu zależał bezpośrednio od implementacji, to jej zmiana mogłaby spowodować konieczność wprowadzenia zmian w tym module. Interfejsy zapewniają nam większą stabilność modułów.

Objaśnienie zasady

Komputer to maszyna, która bez możliwości współpracy z urządzeniami dodatkowymi byłaby praktycznie bezużyteczna. Jak inaczej byłaby możliwa praca na komputerze bez możliwości podłączenia np. myszy, klawiatury, monitora, drukarki itd.? Z tego względu komunikacja z urządzeniami peryferyjnymi jest zagadnieniem bardzo istotnym.
Spójrzmy w jaki sposób taka komunikacja zachodzi. Mamy w komputerze przeróżne porty: USB, VGA, HDMI, PS/2, Jack 3,5mm i wiele innych. Umożliwiają one podpięcie danego urządzenia do komputera. I te porty właśnie są przykładem zastosowania Zasady Odwrócenia Zależności. Komputer nie jest zależny bezpośrednio od urządzeń dodatkowych. Porty te są właśnie interfejsami będącymi w pewnym sensie pośrednikami pomiędzy komputerem a urządzeniem. Jeżeli dany komponent nie jest już potrzebny, to nic nie stoi na przeszkodzie, aby odłączyć go od portu i w razie potrzeb podpiąć coś innego.
Wyobraźmy sobie teraz sytuację, w której wszystkie komponenty nie byłyby podpinane do komputera poprzez porty, lecz należałoby je przylutować bezpośrednio do płyty głównej. Tak wyglądałaby współpraca komputera z urządzeniami peryferyjnymi gdyby nie było „interfejsów”, czyli portów. Komputer bezpośrednio zależałby od tych urządzeń. Ten przykład może nam uzmysłowić jak istotnym zagadnieniem jest Zasada Odwrócenia Zależności.

Przykład

Tym razem jako przykład niech posłuży oprogramowanie parkomatu. Jego zadaniem będzie przetworzyć płatność, a następnie wydrukować bilet parkingowy.

Kod niezgodny z DIP

public class ParkingMeter {

    private Payment payment = new Payment();

    public void printTicket() {
        payment.pay();
        System.out.println("printing parking ticket...");
    }
}

public class Payment {

    public void pay() {
        System.out.println("Paid using coins");
    }
} 
Jak widać na przykładzie powyżej, klasa ParkingMeter jest główną klasą programu, która drukuje bilet parkingowy w metodzie printTicket(), po uprzednim uregulowaniu płatności. Za płatności jest odpowiedzialna klasa Payment i jej metoda pay().
Powyższy kod nie jest zgodny z Zasadą Odwróćenia Zależności. Klasa ParkingMeter jest klasą wyższego rzędu. Klasa ta jest uzależniona od klasy niższego rzędu, Payment. Wszelkie zmiany dokonane w klasie Payment mogą mieć bezpośredni wpływ na działanie klasy ParkingMeter.
W tym kodzie występuje także drugi problem. Klasa ParkingMeter obsługuje tylko jeden rodzaj płatności, gdyż posiada referencję do obiektu klasy odpowiadającej za płatność monetami. Gdybyśmy chcieli zmienić rodzaj płatności to musielibyśmy zmodyfikować klasę ParkingMeter lub Payment (lub obydwie). Tak więc mamy tu również złamaną Zasadę Otwarte-Zamknięte.

Kod zgodny z DIP

public class ParkingMeter {

    private IPayment payment;

    public ParkingMeter(IPayment payment) {
        this.payment = payment;
    }

    public void printTicket() {
        payment.pay();
        System.out.println("printing parking ticket...");
    }
}

public interface IPayment {

    void pay();
}

public class CoinPayment implements IPayment {

    @Override
    public void pay() {
        System.out.println("Paid using coins");
    }
}

public class BanknotePayment implements IPayment {

    @Override
    public void pay() {
        System.out.println("Paid using banknotes");
    }
}

public class CreditCardPayment implements IPayment {

    @Override
    public void pay() {
        System.out.println("Paid using credit card");
    }
} 
W tej wersji oprogramowania, klasa PaymentMethod już nie jest zależna od konkretnej implementacji, ale od interfejsu. Dzięki temu, zmniejszyliśmy zależności występujące w programie.
Dodatkowo, wykorzystaliśmy tu wzorzec projektowy Wstrzykiwanie Zależności, dzięki czemu możemy w konstruktorze klasy wysłać dowolny obiekt implementujący interfejs IPayment. Daje nam to możliwość obsługi dowolnego rodzaju płatności bez konieczności modyfikacji istniejącego kodu. Powoduje to także, iż zachowujemy Zasadę Podstawienia Liskov, gdyż metoda printTicket() w klasie ParkingMeter może wykorzystywać wszystkie implementacje interfejsu IPayment.

Podsumowanie

Zasada Odwrócenia Zależności jest stosunkowo prosta do zaimplementowania, a daje nam bardzo dużo możliwości. Redukujemy zależności poprzez wprowadzenie abstrakcji. DIP ułatwia nam dalsze rozszerzanie oprogramowania. Oprócz tego, Zasada Odwrócenia Zależności jest ściśle powiązana z innymi regułami SOLID, dzięki czemu stosowanie się do niej ułatwia nam zachowanie zgodności z innymi zasadami.
Tym artykułem kończę serię o zasadach SOLID. Warto je znać, gdyż są to jedne z najważniejszych zasad programowania obiektowego. Uczą nas w jaki sposób pisać dobry i czytelny kod, łatwy w zrozumieniu i utrzymaniu.
Streszczając krótko całą serię o zasadach SOLID:
  • twórz takie jednostki oprogramowania (klasy, metody itp.), aby były odpowiedzialne tylko za jedną rzecz,
  • twórz taki kod, aby był łatwy do rozszerzania bez konieczności modyfikacji tego już napisanego,
  • jeżeli funkcja korzysta z obiektu danej klasy, spraw, aby mogła korzystać też z obiektów wszystkich klas dziedziczących po tej klasie,
  • twórz na tyle małe interfejsy, aby obiekty nie implementowały metod, których nie potrzebują,
  • twórz taki kod, aby moduły wysokopoziomowe zależały od abstrakcji, a nie bezpośrednio od modułów niskopoziomowych.
Zasady SOLID na przestrzeni lat zostały bardzo spopularyzowane. Stały się fundamentem dobrego i przejrzystego kodu, a stosowanie się do nich świadczy o profesjonalizmie programisty.

Znajdź mnie również na:

arrow