1. Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. I jedne, i drugie powinny zależeć od abstrakcji. 2. Abstrakcje nie powinny zależeć od szczegółów. To szczegóły powinny zależeć od abstrakcji.
Robert C. Martin
W tradycyjnej architekturze moduły wysokiego poziomu składają się z modułów niższych poziomów. To tak zwane sztywne powiązania (tight coupling). W takiej architekturze moduły wyższych poziomów zależą od modułów niższych poziomów. Każda zmiana wprowadzona w module niższego poziomy wymusza zmianę w modułach wyższych poziomów. Ta zależność utrudnia ponowne wykorzystanie komponentów wyższego poziomu.
Celem zasady odwrócenia zależności jest zmniejszenie tych powiązań (loose coupling). Obie warstwy komunikują się za pomocą interfejsów które udostępniają metody wymagane przez moduły wyższego poziomu – dodatkowej warstwy abstrakcji. Często robi się to za pomocą wstrzykiwania zależności (dependency injection).
Rozważmy przykładową sytuację. Mamy klasę Displayer (moduł niższego poziomu) odpowiedzialną za wyświetlanie na ekranie:
public class Displayer {
public void out() {
System.out.println("Wyświetl na ekranie");
}
}
Klasa ComputerSystem (moduł wyższego poziomu) składa się między innymi z obiektu klasy Displayer – jest od niego zależna:
public class ComputerSystem {
private Displayer displayer;
public ComputerSystem() {
this.displayer = new Displayer();
}
public void out () {
this.displayer.out();
}
}
Utwórzmy klasę Printer odpowiedzialną za drukowanie i dodajmy ją do Systemu:
public class Printer{
public void out() {
System.out.println("Wydrukuj na drukarce");
}
}
public class ComputerSystem {
private Displayer displayer;
private Printer printer;
public ComputerSystem() {
this.displayer = new Displayer();
this.printer = new Printer();
}
public void out (String device) {
switch (device) {
case "SCREEN":
this.displayer.out();
break;
case "PRINTER":
this.printer.out();
break;
default:
break;
}
}
}
Możemy stworzyć obiekt klasy ComputerSystem i wywołać na nim metodę out:
public class Runner {
public static void main (String[] args) {
ComputerSystem system = new ComputerSystem();
system.out("PRINTER");
}
}
Powyższy przykład działa, ale ma 3 wady:
Dodanie każdego nowego urządzenia wymaga zmiany w istniejącym kodzie klasy ComputerSystem – łamie zasadę otwarte-zamknięte,
Klasa ComputerSystem zależy od klas Displayer i Printer – łamie zasadę odwrócenia zależności,
Klasy są ściśle powiązane, klasa ComputerSystem nie może istnieć bez Displayer i Printer.
Poprawiona implementacja
Stwórzmy interfejs z metodą out i zaimplementujmy go w klasach Displayer i Printer:
public interface Device {
public void out();
}
public class Displayer implements Device{
@Override
public void out() {
System.out.println("Wyświetl na ekranie");
}
}
public class Printer implements Device{
@Override
public void out() {
System.out.println("Wydrukuj na drukarce");
}
}
W samych klasach nic się właściwie nie zmieniło. Wcześniej zawierały metodę out, teraz implementują metodę out z interfejsu.
W konstruktorze klasy ComputerSystem dodajmy jako parametr obiekt implementujący interfejs Device – to może być Dispalyer lub Printer ponieważ obie klasy implementują interfejs. To jest wstrzykiwanie zależności (dependency injection):
public class ComputerSystem {
private Device device;
public ComputerSystem(Device device) {
this.device = device;
}
public void out() {
this.device.out();
}
}
Kiedy przekazujemy obiekt klasy Displayer lub Printer do konstruktora – ta specyficzna implementacja interfejsu jest zapisywana do pola device. Program sam rozpozna klasę obiektu przechowywanego w device i uruchomi metodę z właściwej klasy – polimorfizm:
public class Runner {
public static void main (String[] args) {
Printer printer = new Printer();
ComputerSystem system = new ComputerSystem(printer);
system.out();
}
}
Zalety kodu zgodnego z zasadą odwrócenia zależności
Zasada odwrócenia zależności ułatwia rozbudowę aplikacji. Wystarczy, że nowe moduły niższego rzędu będą implementowały interfejs pośredniczący w komunikacji żeby móc je przekazywać jako argumenty.
Zasada odwrócenia zależności daje większą kontrolę użytkownikowi. Nie musi on polegać na sztywnych zależnościach, zamiast tego może łączyć moduły wyższego i niższego rzędu za pomocą interfejsu pośredniczącego.
Moduły wyższego poziomu są bardziej uniwersalne dzięki temu, że nie są zależne od modułów niższego poziomu.