Mit Dependency Injection wird bewirkt, dass eine Klasse selber keine
Abhängigkeiten mehr verwalten muss, da diese von aussen zur Verfügung gestellt
werden. Dadurch soll es möglich werden, die Module, von welchen eine Klasse abhängig
ist, frei zu wechseln, ohne die Klasse selber zu verändern.
Eine Abhängigkeit kann mittels Dependency Injection an drei verschiedenen
Stellen eingeführt werden. Es ist möglich, die Abhängigkeit bei der
Konstruktion des Clients zu erstellen, hierbei wird die Abhängigkeit in den
Konstruktor eingeführt, eine zweite Möglichkeit ist es, die Abhängigkeit mit
einer Methode zu setzen, welche nach dem Konstruktor aufgerufen wird.
Die letzte Möglichkeit ist es, die Abhängigkeit direkt im Feld einzuführen, sodass
weder ein Konstruktor noch eine spezifische Methode benötigt wird.
Die Dependency Injection ist eine Erweiterung der Dependency Inversion unter dem
SOLID Prinzip, welche die Abhängigkeit von unten nach oben bringt.
Der Service ist der Teil der Applikation, welcher an einer anderen Stelle
genutzt werden soll, da dieser einen Teil der benötigten Geschäftslogik
implementiert. Damit die Applikation möglichst modular bleibt und mit dem
Dependency Inversion Principle übereinstimmt, wird ein Service nie direkt in
einem Client genutzt. Anstelle dessen implementiert ein Service ein Interface,
welches wiederum von Clients verwendet werden kann.
Der Client bezeichnet den Teil der Applikation, welcher einen Service
konsumieren und benutzen soll. Die Abhängigkeit soll hier “eingespritzt”
werden. Dadurch kann der Client die Funktionalitäten des Interface, welche vom
Service implementiert werden, einfach nutzen ohne sich darum kümmern zu müssen,
welche Implementation er jetzt benutzt.
Das Interface ist die Abstraktion zwischen Client und Service. Es definiert eine
Schnittstelle, wodurch ein Client weiss, welche Methoden ihm auf einem Service
zur Verfügung stehen und macht es für einen Service klar, welche Methoden
implementiert werden müssen.
Die Aufgabe des Injektors ist es, die jeweiligen Services mittels der
Interfaces in die Clients “einzuspritzen”, damit diese sie benutzen
können. Der Injector ist im Vergleich zur Dependency Inversion die einzig neue
Rolle, welche zu vor noch nicht benötigt wurde.
Contexts and Dependency Injection (CDI) ist ein Java-Standard, welcher das Prinzip
der Dependency Injection erweitert. CDI ermöglicht es, die Abhängigkeiten der
verschiedenen Module automatisiert zu injizieren, wodurch es nicht mehr nötig ist,
die benötigten Abhängigkeiten manuell mitzugeben. CDI entscheidet anhand des Programmcodes
und einer Konfiguration, welche Abhängigkeit wo benötigt wird.
Um dieses Prinzip zu verdeutlichen, gibt es hier ein kleines Beispiel in Java.
Dabei soll eine Applikation erstellt werden, welche genutzt werden kann, um mit
unterschiedlichen Kaffeemaschinen Kaffee zu brühen.
Für die Ausgangslage gibt es zwei Kaffeemaschine, die BasicCoffeeMachine und die PremiumCoffeeMachine.
Beide Maschinen haben eine Method, um Kaffee zu brühen, jedoch kann die PremiumCoffeeMachine neben dem
Filterkaffee auch noch Espresso brühen.
import java.util.Map;
public class BasicCoffeeMachine {
private Configuration config;
private Map<CoffeeSelection, GroundCoffee> groudnCoffee;
private BrewingUnit brewingUnit;
public BasicCoffeeMachine(Map<CoffeeSelection, GroundCoffee> coffee) {
this.config = new Configuration(30, 480);
this.groundCoffee = coffee;
this.brewingUnit = new BrewingUnit();
}
public Coffee brewFilterCoffee() {
GroundCoffee groundCoffee = this.groundCoffee.get(CoffeeSelection.FILTER_COFFEE);
return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee, this.config.getQuantityWater());
}
public void addGroundCoffee(CoffeeSelection sel, GroundCoffee newBeans) throws CoffeeException {
GourndCoffee existingCoffee = this.groundCoffee.get(sel);
if (existingCoffee != null) {
if (existingCoffee.getName().equals(newBeans.getName())) {
existingCoffee.setQuantity(existingCoffee.getQuantity() + newBeans.getQuantity());
} else {
throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection.");
}
} else {
this.groundCoffee.put(sel, coffee);
}
}
}
import java.util.HashMap;
import java.util.Map;
public class PremiumCoffeeMachine {
private Map<CoffeeSelection, Configuration> configMap;
private Map<CoffeeSelection, CoffeeBean> beans;
private Grinder grinder
private BrewingUnit brewingUnit;
public PremiumCoffeeMachine(Map<CoffeeSelection, CoffeeBean> beans) {
this.beans = beans;
this.grinder = new Grinder();
this.brewingUnit = new BrewingUnit();
this.configMap = new HashMap<>();
this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480));
this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28));
}
public Coffee brewEspresso() {
Configuration config = configMap.get(CoffeeSelection.ESPRESSO);
GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee())
return this.brewingUnit.brew(CoffeeSelection.ESPRESSO, groundCoffee, config.getQuantityWater());
}
public Coffee brewFilterCoffee() {
Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE);
GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee());
return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee, config.getQuantityWater());
}
public void addCoffeeBeans(CoffeeSelection sel, CoffeeBean newBeans) throws CoffeeException {
CoffeeBean existingBeans = this.beans.get(sel);
if (existingBeans != null) {
if (existingBeans.getName().equals(newBeans.getName())) {
existingBeans.setQuantity(existingBeans.getQuantity() + newBeans.getQuantity());
} else {
throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection.");
}
} else {
this.beans.put(sel, newBeans);
}
}
}
Der erste Schritt zur Dependency Injection ist die Abstraktion der öffentlichen Methoden.
Dazu muss ein Interface für die Kaffeemaschine erstellt werden. Da nicht jede Kaffeemaschine
Espresso brühen kann, gibt es hier zwei Interfaces, ein Grundlegendes für alle Kaffeemaschine,
welches Filterkaffee brühen kann und ein zweites für die Premium Kaffeemaschine, welche auch
Espresso brühen kann. Die BasicCoffeeMachine kann dann das einfachere Interface implementieren
und die PremiumCoffeeMachine implementiert einfach beide.
public interface CoffeeMachine {
Coffee brewFilterCoffee();
}
public interface EspressoMachine {
Coffee brewEspresso();
}
Refactoring
import java.util.Map;
public class BasicCoffeeMachine implements CoffeeMachine {
private Configuration config;
private Map<CoffeeSelection, GroundCoffee> groundCoffee;
private BrewingUnit brewingUnit;
public BasicCoffeeMachine(Map<CoffeeSelection, GroundCoffee> coffee).
this.groundCoffee = coffee;
this.brewingUnit = new BrewingUnit();
this.config = new Configuration(30, 480);
}
@Overrride
public Coffee brewFilterCoffee() {
GroundCoffee groundCoffee = this.groundCoffee.get(CoffeeSelection.FILTER_COFFEE);
return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee, this.config.getQuantityWater());
}
public void addGroundCoffee(CoffeeSelection sel, GroundCoffee newCoffee) throws CoffeeException {
GroundCoffee existingCoffee = this.groundCoffee.get(sel);
if (existingCoffee != null) {
if (existingCoffee.getName().equals(newCoffee.getName())) {
existingCoffee.setQuantity(existingCoffee.getQuantity() + newCoffee.getQuantity())
} else {
throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection.")
}
} else {
this.groundCoffee.put(sel, newCoffee)
}
}
}
import java.util.HashMap;
import java.util.Map;
public class PremiumCoffeeMachine implements CoffeeMachine, EspressoMachine {
private Map<CoffeeSelection, Configuration> configMap;
private Map<CoffeeSelection, CoffeeBean> beans;
private Grinder grinder
private BrewingUnit brewingUnit;
public PremiumCoffeeMachine(Map<CoffeeSelection, CoffeeBean> beans) {
this.beans = beans;
this.grinder = new Grinder();
this.brewingUnit = new BrewingUnit();
this.configMap = new HashMap<>();
this.configMap.put(CoffeeSelection.FILTER_COFFEE, new Configuration(30, 480));
this.configMap.put(CoffeeSelection.ESPRESSO, new Configuration(8, 28));
}
@Override
public Coffee brewEspresso() {
Configuration config = configMap.get(CoffeeSelection.ESPRESSO);
GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.ESPRESSO), config.getQuantityCoffee())
return this.brewingUnit.brew(CoffeeSelection.ESPRESSO, groundCoffee, config.getQuantityWater());
}
@Override
public Coffee brewFilterCoffee() {
Configuration config = configMap.get(CoffeeSelection.FILTER_COFFEE);
GroundCoffee groundCoffee = this.grinder.grind( this.beans.get(CoffeeSelection.FILTER_COFFEE), config.getQuantityCoffee());
return this.brewingUnit.brew(CoffeeSelection.FILTER_COFFEE, groundCoffee, config.getQuantityWater());
}
public void addCoffeeBeans(CoffeeSelection sel, CoffeeBean newBeans) throws CoffeeException {
CoffeeBean existingBeans = this.beans.get(sel);
if (existingBeans != null) {
if (existingBeans.getName().equals(newBeans.getName())) {
existingBeans.setQuantity(existingBeans.getQuantity() + newBeans.getQuantity());
} else {
throw new CoffeeException("Only one kind of coffee supported for each CoffeeSelection.");
}
} else {
this.beans.put(sel, newBeans);
}
}
}
Da nun beide Klassen ihre Interfaces implementieren, kann die Kaffeeapplikation nach dem Dependency Injection
Prinzip erstellt werden. Die Applikation muss jetzt nicht mehr selber verwalten, welche Kaffeemaschine sie
benötigt, sondern kann einfach die Interfaces, welche sie benötigt über ihren Konstruktor anfordern.
Wodurch die richtige Kaffeemaschine bei der Erstellung der Kaffeeapplikation injiziert werden kann.
public class CoffeeApp {
private CoffeeMachine coffeeMachine;
public CoffeeApp(CoffeeMachine coffeeMachine) {
this.coffeeMachine = coffeeMachine;
}
public Coffee prepareCoffee() throws CoffeeException {
Coffee coffee = this.coffeeMachine.brewFilterCoffee();
System.out.println("Coffee is ready");
return coffee;
}
}
import java.util.HashMap;
import java.util.Map;
public class CoffeeAppStarter {
public static void main(String[] args) {
Map<CoffeeSelection, CoffeeBean> beans = new HashMap<CoffeeSelection, CoffeeBean>();
beans.put(CoffeeSelection.ESPRESSO, new CoffeeBean("My favorite espresso bean", 1000));
beans.put(CoffeeSelection.FILTER_COFFEE, new CoffeeBean("My favorite filter coffee bean", 600));
PremiumCoffeeMachine machine = new PremiumCoffeeMachine(beans);
CoffeeApp app = new CoffeeApp(machine);
try {
app.prepareCoffee();
} catch (CoffeeException e) {
e.printStackTrace();
}
}
}