Upload
carsten-hetzel
View
91
Download
0
Embed Size (px)
Citation preview
Dependency InjectionEine Einführung mit grundlegenden Beispielen(c
) C
ars
ten
He
tze
l, 2
014
Was ist eigentlich das Problem?
(c)
Ca
rste
n H
etz
el,
20
14
Was ist eigentlich das Problem?
Wie üblich bei gängigen Prinzipien aus der Softwareentwicklung liegt dem Prinzip ein wiederkehrendes Problem zugrunde.
Schauen wir uns dazu folgende Klasse an:
(c) Carsten Hetzel, 20143
class UglyCoupling{
private $handle;
public function __construct(){
$filename = 'output.txt';$handle = @fopen($filename, 'w');if (!$handle) {
throw new \RuntimeException('Unable to open output file!');}$this->handle = $handle;
}
public function updateOrder($orderId, $newValue){
// ... update the order$output = 'The value of order ' . $orderId . ' has been changed to ' . $newValue . '!';$bytes = fwrite($this->handle, $output);if ($bytes === false) {
throw new \RuntimeException('Unable to write to output file!');}
}}
(c) Carsten Hetzel, 20144
Welche Probleme hat diese Klasse?
(c)
Ca
rste
n H
etz
el,
20
14
Welche Probleme hat diese Klasse?
Der Dateiname ist nicht änderbar
Es ist nicht klar, wo im Dateisystem die Datei ggf. erzeugt wird
Die Klasse UglyCoupling kann nicht genutzt werden, wenn die Datei nicht geschrieben werden kann
Die Klasse wirft Exceptions bei Fehlern, die mit ihrer eigentlichen Funktion nichts zu tun haben
Wenn weitere Dinge beim Aufruf von "updateOrder()" passieren sollen, müssen sie in "UglyCoupling" hinzugefügt werden
Tests erzeugen Dateien im Dateisystem, auch wenn das auf dem Testsystem vielleicht gar nicht gewünscht ist
(c) Carsten Hetzel, 20146
Was ist eigentlich das Problem?
Die Datei wird nicht geschlossen (bzw. erst, wenn der PHP-Prozess endet)
Jede Instanz der Klasse UglyCoupling öffnet die selbe Datei
Wenn der Inhalt des "outputs" oder das Ausgabeformat von Text auf PDF geändert werden soll, muss die Klasse UglyCoupling angepasst werden
...
(c) Carsten Hetzel, 20147
Was ist Dependency Injection?
(c)
Ca
rste
n H
etz
el,
20
14
Was ist Dependency Injection?
Dependency Injection fordert uns auf Ressourcen anzufordern, statt sie selber bereit zu stellen. Es gibt drei Wege Ressourcen anzufordern:
1. Constructor Injection
2. Setter Injection
3. Interface Injection
(c) Carsten Hetzel, 20149
Constructor Injection
Klassen, die Ressourcen für ihre Arbeit benötigen, fordern diese Ressourcen über ihren Konstruktor an.
Damit ist gewährleistet, dass die Klasse von Beginn an über die Ressourcen verfügt, die sie benötigt.
(c) Carsten Hetzel, 201410
Constructor Injection
class ConstructorInjection{
/*** @var Service*/private $service;
public function __construct(Service $service){
$this->service = $service;}
}
(c) Carsten Hetzel, 201411
Constructor Injection
Ein klarer Nachteil der Constructor Injection ist, dass sehr früh im Application Lifecycle Ressourcen bereitgestellt werden, die evtl. gar nicht zum Einsatz kommen.
Eine Konsequenz aus Constructor Injection ist, Ressourcen so kostengünstig wie möglich zu erstellen, damit keine überflüssige Arbeiten vorgenommen werden.
(c) Carsten Hetzel, 201412
Setter Injection
Benötigte Ressourcen werden über Set-Methoden bereitgestellt. Diese Methoden können ggf. deutlich nach der Erstellung des Anfordernden Objekts aufgerufen und dem Klienten bereitgestellt werden.
Dabei besteht aber auch das Risiko einen ungültigen Zustand, ein unvorhersehbares Verhalten oder sogar einen Fehler hervorzurufen.
Ggf. muss also die Verfügbarkeit der Ressourcen geprüft werden!
(c) Carsten Hetzel, 201413
Setter Injection
class SetterInjection{
/*** @var Service*/private $service;
/*** @param Service $service*/public function setService(Service $service){
$this->service = $service;}
}
(c) Carsten Hetzel, 201414
Interface Injection
Benötigt eine Klasse eine bestimmte Ressource, dann implementiert sie eine Interface, welches das Injizieren dieser Ressource anfordert.
D.h. das Interface fordert die Implementierung z.B. einer "inject"-Methode, die als Parameter die entsprechende Ressource erwartet.
(c) Carsten Hetzel, 201415
Interface Injection
class InterfaceInjection implements InjectSercvice{
/*** @var Service*/private $service;
public function injectService(Service $service){
$this->service = $service;}
}
(c) Carsten Hetzel, 201416
Was ist eigentlich mit einer "Ressource" gemeint?
(c)
Ca
rste
n H
etz
el,
20
14
Was sind „Ressourcen“?
Eine Ressource kann eigentlich alles mögliche sein - von einer einfachen Zahl bis zu einer komplexen Service-Klasse.
Es ist also keines Falls so, dass nur Services über Dependency Injection angefordert werden sollen.
Ein klassisches Beispiel sind Parameter für eine Datenbankverbindung.
(c) Carsten Hetzel, 201418
Aufgabe: Überarbeiten sie "UglyCoupling“
(c)
Ca
rste
n H
etz
el,
20
14
Aufgabe: „UglyCoupling“
Überarbeiten Sie die Klasse "UglyCoupling" so, dass die benötigten Ressourcen angefordert werden.
Bitte führen Sie diese Aufgabe in Teams durch und diskutieren Sie Ihre Ansätze.
Sie haben 5 Minuten Zeit!
(c) Carsten Hetzel, 201420
class UglyCoupling{
private $handle;
public function __construct() {$filename = 'output.txt';$handle = @fopen($filename, 'w');if (!$handle) {
throw new \RuntimeException('Unable to open output file!');}$this->handle = $handle;
}
public function updateOrder($orderId, $newValue) {// ... update the order$output = 'Order ' . $orderId . ' has been changed to ' . $newValue . '!';$bytes = fwrite($this->handle, $output);if ($bytes === false) {
throw new \RuntimeException('Unable to write to output file!');}
}}
(c) Carsten Hetzel, 201421
Lösungen?
(c)
Ca
rste
n H
etz
el,
20
14
Ist das besser?Bitte diskutieren Sie folgende Lösung!
(c)
Ca
rste
n H
etz
el,
20
14
class LessUglyCoupling{
private $fileObject;
public function __construct(\SplFileObject $fileObject){
$this->fileObject = $fileObject;}
public function updateOrder($orderId, $newValue){
// ... update the order$output = 'Order ' . $orderId . ' has been changed to ' . $newValue . '!';$bytes = $this->fileObject->fwrite($output);if ($bytes === null) {
throw new \RuntimeException('Unable to write to output file!');}
}}
(c) Carsten Hetzel, 201424
LessUglyCoupling
Naja, zumindest kann man jetzt den Dateinamen ändern, aber es wird immer noch in eine Datei ein Text geschrieben.
Außerdem liefert die Funktion "fwrite()" im Fehlerfall einen anderen Rückgabewert als die Methode SplFileObject::fwrite() (nämlich "false" statt "null")!
Darüber hinaus hat sich aber auch das Verhalten unseres Konstruktors geändert: Er wirft plötzlich keine Exceptionmehr! Für den Fall, dass man UnitTests für diese Klasse geschrieben hat, müssen wir diese spätestens jetzt anpassen.
(c) Carsten Hetzel, 201425
Eine Sauberere Lösung mit Konsequenzen!Was halten Sie von folgender Lösung?
(c)
Ca
rste
n H
etz
el,
20
14
Listen Sie Vor- und Nachteile auf!
class BetterCoupling{
private $updateOrderHandler;
public function __construct(UpdateOrderHandler $handler){
$this->updateOrderHandler = $handler;}
public function updateOrder($orderId, $newValue){
// ... update the order$this->updateOrderHandler->onOrderUpdate($orderId, $newValue);
}}
(c) Carsten Hetzel, 201427
Vorteile
Die Klasse "BetterCoupling" muss nicht mehr entscheiden, was beim Aufruf von "updateOrder" alles passiert. Diese Entscheidung trifft nun die Klasse "UpdateOrderHandler".
Wir müssen uns nicht mehr um die Behandlung von Fehlern kümmern, die uns eigentlich gar nicht interessieren.
Es ist kein fester Text mehr vorhanden.
...
(c) Carsten Hetzel, 201428
Nachteile
Wir brauchen eine zusätzliche Klasse "UpdateOrderHandler" und haben damit eine neue Abhängigkeit eingeführt.
Sollte man sich (idealer Weise) entschieden haben, dass "UpdateOrderHandler" ein Interface ist, dann haben wir dem System sogar noch weitere PHP-Dateien hinzugefügt.
Es muss auf jeden Fall ein Handler vorhanden sein, oder wir müssen die Implementierung wieder anpassen, so dass die Methode "onOrderUpdate()" nicht aufgerufen wird, wenn es keinen Handler gibt.
(c) Carsten Hetzel, 201429
Die beste Lösung!?Ist es möglich, dass wir ohne Abhängigkeiten auskommen?
(c)
Ca
rste
n H
etz
el,
20
14
Die beste Lösung?
class NoCoupling{
public function updateOrder($orderId, $newValue){
// ... update the order}
}
(c) Carsten Hetzel, 201431
Und was ist mit unserer Ausgabedatei?
(c)
Ca
rste
n H
etz
el,
20
14
Und was ist mit unserer Ausgabedatei?
class CouplingByInheritance extends NoCoupling{
public function updateOrder($orderId, $newValue){
parent::updateOrder($orderId, $newValue);// ... now do whatever you need to do!
}}
(c) Carsten Hetzel, 201433
CouplingByInheritance
Diese Klasse kann von uns frei gestaltet werden, ohne dass wir die zugrunde liegende Implementierung des fachlichen Problems ändern müssen.
Die Klasse "NoCoupling" kümmert sich also nur noch um das fachliche Problem und die abgeleitete Klasse kann irgend eine der bisher gezeigten Lösungsvarianten umsetzen.
(c) Carsten Hetzel, 201434
Wie werden die ganzen Ressourcen zusammengesetzt?
(c)
Ca
rste
n H
etz
el,
20
14
Wie werden die ganzen Ressourcen zusammengesetzt?
Das ist ja alles ganz schön, aber jetzt haben wir ein anders Problem:
Ich habe nichts gewonnen, wenn jetzt an der Stelle, an der ich früher "UglyCoupling" eingesetzt habe, eine ganze Reihe von anderen Klassen zusätzlich erzeugen muss!
Wo früher ...
(c) Carsten Hetzel, 201436
Vorher
class MyUglyApplication{
public function doSomething(){
// ...
$oderId = $this->providerOrderId();$newValue = $this->providerNewValue();
$myUglyClass = new UglyCoupling();$myUglyClass->updateOrder($oderId, $newValue);
}}
(c) Carsten Hetzel, 201437
Jetzt
class EvenMoreUglyApplication{
public function doSomething(){
// ...$filename = 'output.txt';$fileObject = new \SplFileObject($filename);$updateOrderHandler = new UpdateOrderHandler($fileObject);
$oderId = $this->providerOrderId();$newValue = $this->providerNewValue();
$myUglyClass = new CouplingByInheritance($updateOrderHandler);$myUglyClass->updateOrder($oderId, $newValue);
}}
(c) Carsten Hetzel, 201438
Refactoring des Ergebnisses
Im Client-Code (also unserer Anwendung) ist es scheinbar nicht besser sondern schlimmer geworden.
Der Code sieht darüber hinaus unleserlich aus.
Aber durch einfaches Refactoring lassen sich sehr schöne und saubere Methoden erstellen, die die einzelnen Ressourcen erstellen
(c) Carsten Hetzel, 201439
Refactoring des Ergebnisses
class MyInjectingApplication{
public function doSomething(){
// ...
$oderId = $this->providerOrderId();$newValue = $this->providerNewValue();
$coupledClass = $this->getCoupledClass();$coupledClass->updateOrder($oderId, $newValue);
}...
(c) Carsten Hetzel, 201440
Refactoring des Ergebnisses
...protected function getCoupledClass(){
$updateOrderHandler = $this->getUpdateOrderHandler();$coupledClass = new CouplingByInheritance($updateOrderHandler);return $coupledClass;
}
protected function getUpdateOrderHandler(){
$fileObject = $this->getFileObject();$updateOrderHandler = new UpdateOrderHandler($fileObject);return $updateOrderHandler;
}...
(c) Carsten Hetzel, 201441
Refactoring des Ergebnisses
...protected function getFileObject(){
return new \SplFileObject($this->getFilename());}
/*** @return string*/protected function getFilename(){
return 'output.txt';}...
(c) Carsten Hetzel, 201442
Refactoring des Ergebnisses
Auf diese Weise bleibt der Code leserlich, die Erstellung jeder einzelnen Ressourcen ist in jeweils einer Methode abgebildet und aus welchen Sub-Ressourcen eine angeforderte Ressource zusammengesetzt ist, kann jederzeit ganz gezielt geändert werden.
Der letzte verbleibende Schritt an dieser Stelle wäre zu entscheiden, welche der Ressourcen immer wieder aufs Neue oder nur einmal erstellt werden sollen.
(c) Carsten Hetzel, 201443
Dependency Injection und Anwendungen
(c)
Ca
rste
n H
etz
el,
20
14
Dependency Injection und Anwendungen
Während das gezeigte Beispiel den Ansatz verfolgt, dass die Anwendung selbst der Dependency Injection Container (also die Komponente, welche das System "zusammensetzt“) ist, gibt es natürlich eine Reihe von Frameworks, welche diese Aufgabe durch Konfigurationsdateien erledigen.
Interessant dabei ist, dass der jeweilige Dependency Injection Container (DIC) in der Regel die Konfigurationsdatei "auscompiliert", also in eine ausführbare Datei umwandelt, welche tatsächlich sehr ähnlich der oben vorgestellten Lösung ist.
(c) Carsten Hetzel, 201445
Dependency Injection und Anwendungen
In jedem Fall ermöglicht einem der oben vorgestellte Ansatz zu einem späteren Zeitpunkt die Anwendung relativ einfach auf einen DIC eines Frameworks umzustellen.
Die beteiligten Klassen fordern ja bereits ihre benötigten Ressourcen an, statt sie selber zu erstellen.
(c) Carsten Hetzel, 201446
Aufgabe: Anwendungen "Komponieren"
(c)
Ca
rste
n H
etz
el,
20
14
Aufgabe: Anwendungen "Komponieren"
Eine Anwendung zu Verwaltung von Rechnungen soll als einen Anwendungsfall folgendes Verhalten umsetzen:
Wenn der Anwender den Prozess "Rechnung Erstellen" (generateBill) aufruft wird
ein PDF der Rechnung erstellt und
ein Rechnungsbericht mit der Anzahl der Seiten der Rechnung ins Logfile geschrieben
Hinweise:
Erstelle "Kommandos" (Commands, siehe Command-Pattern), die Aufgaben kapseln
Lasse Commands die benötigten Ressourcen anfordern
Abstrahiere Teilinformationen (z.B. die Anzahl der Seiten des PDFs)(c) Carsten Hetzel, 201448
Präsentieren Sie Ihre Lösungen
(c)
Ca
rste
n H
etz
el,
20
14
(c) Carsten Hetzel, 201450