SOLID: Dependency Inversion Principle (Zasada odwracania zależności)

Wprowadzenie

W dzisiejszym artykule wyjaśnię koncepcję zasady odwracania zależności, która reprezentuje literę “D” w akronimie 5 zasad SOLID. Na wstępie przyjrzymy się definicji zasady, a nastepnie zostanie ona omówiona na przykładzie.

DIP – Dependency Inversion Principle (Zasada odwracania zależności)

"Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji."

Definicja sugeruje, że zasada odwraca sposób zarządzania zależnościami w aplikacjach. Zamiast projektować moduł wyższego poziomu (klasy, które zawierają złożoną logikę zależną od modułów niskiego poziomu) jako bezpośrednio zależny od modułów niższego poziomu (klasy implementujące podstawowe operacje np. odczyt pliku), powinniśmy zaprojektować go tak, aby opierał się na abstrakcji a nie rzeczywistej implementacji.
W skrócie – chodzi o to aby klasy, które zawierają logikę biznesową, nie były zależne od klas, które nie odgrywają kluczowej roli. Spójrzmy na przykład.

Przykład zastosowania

Poniżej znajduje się przykład, który narusza zasadę odwrócenia zależności. Mamy klasę WorkerManager, która jest klasą wysokiego poziomu, oraz klasę niskiego poziomu Worker. Do naszej aplikacji musimy dodać nowy moduł, aby modelować zmiany w strukturze firmy determinowane zatrudnieniem nowych wyspecjalizowanych pracowników. W tym celu stworzyliśmy nową klasę SuperWorker.

Załóżmy, że klasa WorkerManager jest bardzo złożona i ma dużo logiki biznesowej. Dodając nowy rodzaj pracownika musielibyśmy wprowadzić wiele modyfikacji:

  • musimy wprowadzić modyfikacje do klasy WorkerManager (pamiętajmy, że jest to złożona klasa i będzie to wymagało czasu i wysiłku),
  • każda modyfikacja może mieć wpływ na istniejące funkcje klasy,
  • konieczne jest powtórzenie testów.

Wszystkie te problemy mogą zająć dużo czasu i spowodować błędy w innych funkcjonalnościach aplikacji. Spójrzmy na błędny kod.

//przykład łamiący zasadę DIP

public class Worker {
    public void work() {
        //work logic
    }
}

public class SuperWorker {
    public void work() {
        //work logic
    }
}

public class WorkerManager {
    private final Worker worker;
    private final SuperWorker superWorker;

    public WorkerManager() {
        this.worker = new Worker();
        this.superWorker = new SuperWorker();
    }

    public void manageWorker() {
        worker.work();
    }

    public void manageSuperWorker() {
        superWorker.work();
    }
} 

Sytuacja wyglądałaby inaczej, gdyby aplikacja została zaprojektowana zgodnie z zasadą odwrócenia zależności. Oznacza to, że projektujemy klasę WorkerManager, interfejs Worker oraz klasę SimpleWorker, implementującą interfejs Worker. Kiedy musimy dodać klasę SuperWorker, wystarczy zaimplementować dla niej interfejs Worker. Brak dodatkowych zmian w istniejących klasach.

//przykład z uwzględnieniem zasady DIP

public interface Worker {
    void work();
}

public class SimpleWorker implements Worker {
    @Override
    public void work() {
        //work logic
    }
}

public class SuperWorker implements Worker {
    @Override
    public void work() {
        //work logic
    }
}

public class WorkerManager {
    private final Worker worker;

    //wstrzykiwanie zależności w konstruktorze
    public WorkerManager(Worker worker) {
        this.worker = worker;
    }

    public void manageWorker() {
        worker.work();
    }
}
 
W zrefaktoryzowanym kodzie warstwa abstrakcji jest dodawana za pośrednictwem interfejsu Worker, z wykorzystaniem wstrzykiwania zależności (ang. dependency injection), które zostało opisane niżej. Dzięki zastosowaniu zasady DIP klasa WorkerManager nie wymaga już konieczności wprowadzania zmian podczas dodawania nowych typów pracowników. Jednocześnie zminimalizowaliśmy ryzyko występowania błędów w pozostałych funkcjonalnościach klasy.

Wstrzykiwanie zależności

Wstrzykiwanie zależności to technika, która pozwala udostępnienie klasie obiektów utworzonych poza nią. Wstrzykiwanie zależności możemy wykonać na trzy sposoby:

  • w konstruktorze (jak na powyższym przykładzie),
  • w metodach, jako argumenty,
  • w setterach.

Dzięki automatycznemu wstrzykiwaniu zależności, klasa wysokiego poziomu nie wie nic o klasach niskiego poziomu i operuje na nich niejawnie, a nie bezpośrednio.

Podsumowanie

Stosując zasadę DIP oddzielamy klasy wysokiego poziomu od klas konkretnych, dzięki czemu nie działają one z nim bezpośrednio, a używają interfejsów jako warstwy abstrakcyjnej.
Zgodnie z zasadą odwrócenia zależności, tworzenie klas niskiego poziomu za pomocą operatora new() jest zabronione. Jeżeli jest to koniecznego, można wykorzystać do tego wzorce projektowe, np. metoda fabrykująca czy fabryka abstrakcyjna. Wzorzec metody szablonowej jest dobrym przykładem zastosowania zasady DIP.
Używanie metody DIP wiąże się ze zwiększonym wysiłkiem podczas tworzenia kodu, jednak na pewno wysiłek ten zaowocuje w elastycznej i łatwej w utrzymaniu aplikacji.

 

Leave a Comment

Your email address will not be published. Required fields are marked *