
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)
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();
}
}
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.
- Zasada pojedynczej odpowiedzialności (Single-Responsibility Principle – SRP),
- Zasada otwarte – zamknięte (Open/Closed Principle – OCP),
- Zasada podstawiania Liskov (Liskov Substitution Principle – LSP),
- Zasada segregacji interfejsów (Interface Segregation Principle – ISP),
- Zasada odwracania zależności (Dependency Inversion Principle – DIP).