design-patterns-dependency_injection.html 13.1 KB
Newer Older
1
<p><i>Dependency Injection</i> lässt sich am besten durch "Einbringen von Abhängigkeiten" übersetzen und ist in der Software-Entwicklung ein bekannter Begriff, bei dem es wie bei der Fabrikmethode um die Auslagerung von Konstruktoraufrufen zur Objekterzeugung geht. Die Ziel der Dependency Injection ist es, die Abhängigkeiten zwischen Klassen, die durch <code>import</code>-Anweisungen entstehen, zu reduzieren, und die Klassen dadurch zu entkoppeln. <a href="https://en.wikipedia.org/wiki/Loose_coupling">Lose Kopplung</a> von Klassen (oder Komponenten im Allgemeinen) hat den Vorteil, dass sich Änderungen einfacher durchführen lassen, da diese sich nur lokal auswirken.</p>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

<p>Dependency Injection ist kein GoF-Muster. Den Begriff hat Martin Fowler in seinem Artikel <a href="https://martinfowler.com/articles/injection.html">"Inversion of Control Containers and the Dependency Injection pattern"</a> geprägt <a href="#cite-Fow04">[Fow04]</a>. Er suchte nach einem Begriff für die Entkopplung bei der Objekterzeugung, der sich von dem allgemeinen Begriff <i>Inversion of Control (IoC)</i> (s. Kapitel <a href="#unit-spring">Spring</a>) abgrenzen lässt. Dependency Injection folgt dem <a href="https://en.wikipedia.org/wiki/Single_responsibility_principle">Prinzip der eindeutigen Verantwortlichkeit (engl. <i>Single-Responsibility-Prinzip</i>)</a> <a href="#cite-Mar18">[Mar18]</a>, das nach Robert Martin besagt, dass jede Klasse in der objekt-orientierten Programmierung nur eine wesentliche Aufgabe erfüllen soll und entsprechend dieser Aufgabe weiterentwickelt wird:</p>
<div class="cite">"There should never be more than one reason for a class to change." (Robert Martin)</div>

<p>Demzufolge ist die Situation im folgenden Code-Beispiel zu vermeiden: Die Klasse <code>MyClass</code> verwendet intensiv das Interface <code>MyLogger</code>, ist aber aufgrund eines direkten Konstruktoraufrufs auch von der konkreten Klasse <code>ConsoleLogger</code> abhängig, die dieses Interface implementiert. Wenn nun die konkrete Klasse ausgetauscht werden soll, muss die Klasse <code>MyClass</code> geändert werden, obwohl sich an ihrer eigentlichen Funktionalität nichts geändert hat.</p>

<pre><code class="language-java line-numbers">import MyLogger;
import ConsoleLogger; // this dependency should be avoided

class MyClass {
    MyLogger logger = new ConsoleLogger();
	
	// ...
}</code></pre> 

<p>Dependency Injection hat zum Ziel diese Abhängigkeit aufzulösen. Dies erfolgt im einfachsten Fall über einen Konstruktor oder über eine Setter-Methode.</p>

<pre><code class="language-java line-numbers">import MyLogger; // no import of concrete implementation required

class MyClass {
    MyLogger logger;
	
	MyClass(MyLogger logger) { this.logger = logger; } // constructor dependency injection
	
	void setLogger(MyLogger logger) { this.logger = logger; } // setter dependency injection
	
	// ...
}</code></pre> 

<p>Nun stellt sich die Frage, wer das Objekt erzeugt und von außen der Klasse <code>MyClass</code> injiziert. Allgemein ist anzustreben, die Konfiguration darüber, welche konkrete Implementierungsklasse je Interface genutzt werden soll, an einer zentralen Stelle zusammenzuführen, z.B. in einer Konfigurationsklasse oder in einer Konfigurationsdatei (z.B. XML, YAML). Eine derartige Konfiguration wird i.d.R. von einem Framework in seiner Funktion als <i>IoC-Container</i> vorgesehen und verwaltet. Die Ansätze je Framework können sich dabei leicht unterscheiden. Im Folgenden werden die Dependency Injection-Ansätze von  <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-dependencies">Spring</a> und <a href="https://github.com/google/guice/wiki/GettingStarted">Google Guice</a> vorgestellt. Beide nutzen die Java Reflection API, um zu ermitteln, welche konkrete Klasse an ein Interface gebunden werden kann. Guice spricht hier von <i>Binding</i>, während der entsprechende Begriff in Spring <i>Wiring</i> heißt.</p>

<h4>Spring</h4>

<span>Damit das Spring Framework die Objekte einer Klasse in Konstruktoren, in Methoden oder direkt in Attribute injizieren kann, sind diese dem Framework als sogenannte <i><a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html">Beans</a></i> bekanntzumachen. Spring definiert Beans wie folgt, wobei das Framework selbst die Funktion als IoC-Container übernimmt:</span>

<div class="cite"><a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html">"In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container." (Spring Docs)</a></div>

<p>Alle Klassen, die mit der Annotation <code>@Component</code> (aus dem Package <code>org.springframework.stereotype</code>) oder einer ihrer Spezialisierungen wie <code>@Service</code>, <code>@Repository</code> oder <code>@Controller</code> versehen sind, werden von Spring automatisch als Beans erkannt und registriert. Die registrierten Beans können zur Laufzeit über den Anwendungskontext erreicht werden. In dem folgenden Code-Beispiel soll ein Objekt der Klasse <code>CapsLockConsoleLogger</code> als Bean an anderen Stellen eingebunden werden können. Daher ist die Klasse als <code>@Component</code> annotiert (Zeile 1).</p>

<pre><code class="language-java line-numbers">@Component
class CapsLockConsoleLogger implements MyLogger {

    @Override
    public void log(String message) { System.out.println(message.toUpperCase()); }
}</code></pre> 

<p>In der folgenden Klasse <code>SpringClient</code> wird diese Bean in das Attribut <code>MyLogger logger</code> injiziert (Zeilen 4-5), d.h. genau an dieser Stelle findet die Dependency Injection statt. Obwohl kein Konstruktoraufruf zu sehen ist, kann der Logger später verwendet werden (Zeile 13). Die Annotation <code>@Autowired</code> sorgt dafür, dass das Framework per Reflection nach einer passenden Bean für das Interface <code>MyLogger</code> sucht und diese Bean an das Attribut bindet. Wichtig ist, dass genau eine passende Bean gefunden wird – und nicht keine oder mehrere.</p>

50
<p>Die Klasse <code>SpringClient</code> ist hier eine einfache <a href="https://spring.io/guides/gs/spring-boot/">Spring Boot-Anwendung</a>, die als solche annotiert ist (Zeile 1) und wie üblich gestartet wird (Zeile 8). Durch den Aufruf von <code>SpringApplication.run()</code> wird ein <code>ApplicationContext</code>-Objekt erzeugt, das den Spring IoC-Container repräsentiert und über das als zentraler Anwendungskontext alle Beans verwaltet werden und erreichbar sind.</p>
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181

<pre><code class="language-java line-numbers">@SpringBootApplication
class SpringClient { // this example is a Spring Boot application

    @Autowired
    MyLogger logger;

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(SpringClient.class, args);
    }

    @Bean
    CommandLineRunner run(ApplicationContext ctx) {
        return args -> logger.log("Hello World!");
    }
}</code></pre> 

<p>Wenn mehrere Beans dieselbe Schnittstelle erfüllen, muss über einen qualifizierenden Namen bestimmt werden, welche Bean das Framework injizieren soll. Dazu kann eine Konfigurationsklasse angelegt werden – wie z.B. die folgende Klasse <code>MyConfiguration</code>, in der die 3 Beans definiert sind (2 vom Typ <code>MyLogger</code>, 1 vom Typ <code>String</code>).</p>

<ul class="nav nav-tabs" id="di-tabs" role="tablist">
  <li class="nav-item"><a href="#di-tabs-config" class="nav-link active" data-toggle="tab" role="tab">MyConfiguration</a></li>
  <li class="nav-item"><a href="#di-tabs-logger" class="nav-link" data-toggle="tab" role="tab">TimestampConsoleLogger</a></li>
</ul>
<div class="tab-content" id="di-tabs-content">
  <div class="tab-pane show active" id="di-tabs-config" role="tabpanel">
	<pre><code class="language-java line-numbers">@Configuration
class MyConfiguration {

    @Bean
    MyLogger loggerA() { return new CapsLockConsoleLogger(); }

    @Bean
    MyLogger loggerB() { return new TimestampConsoleLogger(dateFormat()); }

    @Bean 
	String dateFormat() { return "yyyy-MM-dd HH:mm:ss"; }
}</code></pre> 
  </div>
  <div class="tab-pane" id="di-tabs-logger" role="tabpanel">
	<pre><code class="language-java line-numbers">@Component
class TimestampConsoleLogger implements MyLogger {

    SimpleDateFormat dateFormat;

    TimestampConsoleLogger(String pattern) { dateFormat = new SimpleDateFormat(pattern); }

    @Override
    public void log(String message) { System.out.println(dateFormat.format(new Date()) + "\t" + message); }
}</code></pre> 
  </div>
</div>

<p>Die Beans können über eine <code>@Qualifier</code>-Annotation und ihren Methodennamen wie folgt injiziert werden.</p>
<pre><code class="language-java line-numbers">@Autowired @Qualifier("loggerA")
MyLogger logger;</code></pre> 

<p>Alternativ kann eine Bean über ihren Methodennamen beim Anwendungskontext abgerufen werden.</p>
<pre><code class="language-java line-numbers">logger = ctx.getBean("loggerA", MyLogger.class); // request bean from application context
logger.log("Hello World!");</code></pre> 

<p>Anstatt in der Konfigurationsklasse <code>MyConfiguration</code> können die Beans auch in einer entsprechenden XML-Konfigurationsdatei definiert werden, welche dann als Anwendungskontext zu laden oder diesem hinzuzufügen ist.</p>

<ul class="nav nav-tabs" id="di2-tabs" role="tablist">
  <li class="nav-item"><a href="#di2-tabs-xml" class="nav-link active" data-toggle="tab" role="tab">beans.xml</a></li>
  <li class="nav-item"><a href="#di2-tabs-client" class="nav-link" data-toggle="tab" role="tab">SpringXMLConfigClient</a></li>
</ul>
<div class="tab-content" id="di2-tabs-content">
  <div class="tab-pane show active" id="di2-tabs-xml" role="tabpanel">
	<pre><code class="language-xml line-numbers"><!-- <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	   
    <bean id="loggerA" class="CapsLockConsoleLogger"/>

    <bean id="loggerB" class="TimestampConsoleLogger">
        <constructor-arg ref="dateFormat"/>
    </bean>

    <bean id="dateFormat" class="java.lang.String">
        <constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
    </bean>
	
</beans> --></code></pre> 
  </div>
  <div class="tab-pane" id="di2-tabs-client" role="tabpanel">
	<pre><code class="language-java line-numbers">class SpringXMLConfigClient {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        MyLogger logger = ctx.getBean("loggerA", MyLogger.class);
        logger.log("Hello World!");
    }
}</code></pre> 
  </div>
</div>

<h4>Google Guice</h4>

<p>Google Guice bietet eine bewährte Alternative für Dependency Injection, falls in einem Projekt bewusst ohne Spring gearbeitet werden soll. Als Guice in 2008 durch Google veröffentlicht wurde, war es das erste Framework, das Dependency Injection in Java mittels Annotationen möglich machte. In Guice wird die Konfigurationsklasse, die Interfaces an konkrete Implementierungsklassen bindet als Modul bezeichnet.</p>

<div class="cite"><a href="https://github.com/google/guice/wiki/GettingStarted">"Guice uses bindings to map types to their implementations. A module is a collection of bindings specified using fluent, English-like method calls." (Guice Docs)</a></div>

<p>Der folgende Beispiel-Code zeigt das Modul <code>MyModule</code>, das das Interface <code>MyLogger</code> an seine Implementierung <code>CapsLockConsoleLogger</code> bindet.

<pre><code class="language-java line-numbers">class MyModule extends AbstractModule { // a Guice configuration module

    @Override
    protected void configure() {
        bind(MyLogger.class).to(CapsLockConsoleLogger.class);
		// more bindings follow here ...
    }
}</code></pre>

<p>Diese Konfigurationsklasse wird in dem folgenden Code-Beispiel verwendet, um ein <code>Injector</code>-Objekt zu erzeugen (Zeile 9). Den Objekten, die später über die Methode <code>getInstance</code> dieses <code>Injector</code>-Objekts erzeugt werden (Zeile 12), können ihre Abhängigkeiten über die Annotation <code>@Inject</code> injiziert werden – wie z.B. bei dem Attribut <code>MyLogger logger</code> (Zeilen 3-4).</p> 

<pre><code class="language-java line-numbers">class GuiceClient {

    @Inject
    MyLogger logger;

    public static void main(String[] args){

        // create an injector based on module configuration
        Injector injector = Guice.createInjector(new MyModule());

        // create objects using the injector
        GuiceClient client = injector.getInstance(GuiceClient.class);
        client.logger.log("Hello World!");
    }
}</code></pre>

<p>Die gezeigten Code-Beispiele bezüglich Dependency Injection finden sich im Verzeichnis <a href="/patterns/dependency-injection" class="repo-link">/patterns/dependency-injection</a> des Modul-Repository.</p>