Wstrzykiwanie zależności

Wzorce projektowe

  |   7 min czytania

Zastosowanie

Wstrzykiwanie zależności (ang. Dependency Injection) (wzorzec architektoniczny) jest wzorcem, który pozwala na eliminacje bezpośrednich zależności między elementami systemu. Wzorzec ten jest realizacją paradygmatu Odwrócenie Sterowania (ang. Inversion of Control), który polega na zamianie odpowiedzialności kodu, tzn. to kod frameworka wywołuje kod programisty. Dzięki podejściu odwrócenia zależności testowanie jest ułatwione ze względu na modularność i możliwe wstrzykiwanie zaślepek (mock) do testowanego kodu. Realizacja wzorca następuje poprzez przekazanie zainicjalizowanych obiektów do klas, które z nich korzystają, dzięki czemu komponenty są ze sobą luźno powiązane (loose coupling) - nie przejmują odpowiedzialności innych obiektów. Wstrzykiwanie zależności może odbywać się m.in. poprzez konstruktor (Constructor Injection, metodę set (Setter Injection), interfejs (Interface Injection) czy też refleksje (Field Injection). Ponadto można wykorzystać tzw. Kontener DI (DI Container), który przejmuje odpowiedzialność inicjalizowania i wstrzykiwania obiektów w odpowiednie miejsca w odpowiednim czasie pod warunkiem zdefiniowania reguł powiązań.

Ograniczenia

Korzystając ze Wstrzykiwanie zależności należy mieć na uwadze, że zastosowany w sposób nieumiejętny może doprowadzić do tworzenia cyklicznych zależności co jest sygnałem, że jakieś klasy nie spełniają zasady pojedynczej odpowiedzialności. Istnieje także ryzyko stworzenia super klasy poprzez wstrzyknięcie zbyt dużej ilości zależności. Z uwagi na możliwe zagrożenia refleksja nie jest zalecena do realizacji Wstrzykiwania zależności.

Użycie

Ze względu na swoją charakterystykę i spełnienie trzech zasad SOLID użycie Wstrzykiwania zależności wydaje się naturalnym sposobem tworzenia większości aplikacji. Znacząco ułatwia testowanie, utrzymanie i rozwój kodu. Niektóre architektury realizujące paradygmat Odwrócenia terowania wymuszają użycie Wstrzykiwania zależności (np. Spring).

Implementacja

Instancje klas, które mają zostać przekazane jako zależności do innego obiektu, są tworzone na zewnątrz poza jego ciałem. Wstrzyknięcie utworzonych zależności następuje poprzez konstruktor docelowego obiektu oraz jego metody.

Wstrzykiwanie zależności diagram

Poniższy listing przedstawia realizacja wzorca na trzy sposoby.

public class ConstructorInjection {

    private Dependency1 dependency1;
    private Dependency2 dependency2;

    public ConstructorInjection(Dependency1 dependency1, Dependency2 dependency2) {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
    }
}

public class SetterInjection {

    private Dependency1 dependency1;
    private Dependency2 dependency2;

    public void setDependency1(Dependency1 dependency1) {
        this.dependency1 = dependency1;
    }

    public void setDependency2(Dependency2 dependency2) {
    	this.dependency2 = dependency2;
    }
}

public class InterfaceInjection implements IInjection {

    private Dependency1 dependency1;
    private Dependency2 dependency2;

    @Override
    public void injectDependency1(Dependency1 dependency1) {
    	this.dependency1 = dependency1;
    }

    @Override
    public void injectDependency2(Dependency2 dependency2) {
    	this.dependency2 = dependency2;
    }
}

interface IInjection {

    void injectDependency1(Dependency1 dependency1);
    void injectDependency2(Dependency2 dependency2);
}

Tak stworzone klasy mogą zostać zainicjalizowane różnymi obiektami typu Dependency1 i Dependency2, co przedstawia poniższy listing.

Dependency1 dependency1 = new Dependency1("A");
Dependency2 dependency2 = new Dependency2(10);
ConstructorInjection constructorInjection = new ConstructorInjection(dependency1, dependency2);

SetterInjection setterInjection = new SetterInjection();
setterInjection.setDependency1(new Dependency1(20), new Dependency2("B"));

InterfaceInjection interfaceInjection = new InterfaceInjection();
interfaceInjection.injectDependency1(new Dependency1(30), new Dependency2("C"))

Poniższy listing przedstawia implementacje klasy o podobnych właściwościach bez użycia wzorca.

public class WithoutInjection {

    private Dependency1 dependency1;
    private Dependency2 dependency2;

    public WithoutInjection() {
    	this.dependency1 = new Dependency1(10);
    	this.dependency2 = new Dependency2("A");
    }
}

Jak łatwo zauważyć obiekty klas Dependency1 oraz Dependency2 przyjmują jeden stan zgodny z wewnętrzną implementacją klasy WithoutInjection co mocno wiąże zależności tej klasy.

Przykład

W wielu miejscach aplikacji zachodzi potrzeba logowania przepływu operacji, interakcji użytkownika oraz napotkanych błędów. W tym celu używane są obiekty typu Logger. Jeśli użytkownik jest podłączony do internetu to logi wysyłane są bezpośrednio na serwery. W przeciwnym razie logi zapisywane są do pliku w pamięci urządzenia. Działanie aplikacji jest poddawane testom, zgodnie z którymi proces logowania ma zostać wyjęty z testów komponentów systemu. W tym celu tworzona jest zaślepka. Poniższy listing przedstawia implementacje komponentu ComponentWithLogs wykorzystującego Wstrzykiwania zależności przez konstruktor oraz logerów aplikacji.

public class ComponentWithLogs {

    private Logger logger;

    public ComponentWithLogs(Logger logger) {
    	this.logger = logger;
    }

    public void operation1() {
    	logger.logClickEvent(getClickEvent());
    	try {
    		//do stuff
    		logger.logState(getState());
    	}
    	catch (Exception exception) {
    		logger.logError(getError(exception));
    	}
    }

    //other methods
}

public class NetworkLogger implements Logger {

    @Override
    public void logState(AppState appState) {
    	NetworkManager.send(appState);
    }

    @Override
    public void logClickEvent(ClickEvent clickEvent) {
    	NetworkManager.send(clickEvent);
    }

    @Override
    public void logError(Error error) {
    	NetworkManager.send(error);
    }
}

public class FileLogger implements Logger {

    @Override
    public void logState(AppState appState) {
    	FileManager.write(appState);
    }

    @Override
    public void logClickEvent(ClickEvent clickEvent) {
    	FileManager.write(clickEvent);
    }

    @Override
    public void logError(Error error) {
    	FileManager.write(error);
    }
}

public class MockLogger implements Logger {

    @Override
    public void logState(AppState appState) {
    	Console.log(appState.toString());
    }

    @Override
    public void logClickEvent(ClickEvent clickEvent) {
    	Console.log(clickEvent.toString());
    }

    @Override
    public void logError(Error error) {
    	Console.log(error.toString());
    }
}

interface Logger {

    void logState(AppState appState);
    void logClickEvent(ClickEvent clickEvent);
    void logError(Error error);
}

Użycie Wstrzykiwania zależności logerów dla komponentu ComponentWithLogs aplikacji może odbywać się w nasępujący sposób.

ComponentWithLogs component;
if(isInternetConnection)
    component = new ComponentWithLogs(new NetworkLogger());
else
    component = new ComponentWithLogs(new FileLogger());

//test class
ComponentWithLogs componentTest = new ComponentWithLogs(new MockLogger());

//do stuff

Biblioteki

Popularnym frameworkiem dla Androida realizujący wstrzykiwanie zależności jest Dagger 2. Jest to Kontener DI, który ułatwia wstrzykiwanie zależności w całej aplikacji. Innym przykładem jest Butter Knife, którego zadaniem jest wiązanie widoków do obiektów.