3. 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:
3 (c) Carsten Hetzel, 2014
4. 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!');
}
}
}
4 (c) Carsten Hetzel, 2014
6. 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
6 (c) Carsten Hetzel, 2014
7. 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
...
7 (c) Carsten Hetzel, 2014
9. 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
9 (c) Carsten Hetzel, 2014
10. 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.
10 (c) Carsten Hetzel, 2014
11. Constructor Injection
class ConstructorInjection
{
/**
* @var Service
*/
private $service;
public function __construct(Service $service)
{
$this->service = $service;
}
}
11 (c) Carsten Hetzel, 2014
12. 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.
12 (c) Carsten Hetzel, 2014
13. 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!
13 (c) Carsten Hetzel, 2014
14. Setter Injection
class SetterInjection
{
/**
* @var Service
*/
private $service;
/**
* @param Service $service
*/
public function setService(Service $service)
{
$this->service = $service;
}
}
14 (c) Carsten Hetzel, 2014
15. 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.
15 (c) Carsten Hetzel, 2014
16. Interface Injection
class InterfaceInjection implements InjectSercvice
{
/**
* @var Service
*/
private $service;
public function injectService(Service $service)
{
$this->service = $service;
}
}
16 (c) Carsten Hetzel, 2014
17. Was ist eigentlich mit
einer "Ressource"
gemeint?
(c) Carsten Hetzel, 2014
18. 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.
18 (c) Carsten Hetzel, 2014
20. 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!
20 (c) Carsten Hetzel, 2014
21. 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!');
}
}
}
21 (c) Carsten Hetzel, 2014
23. Ist das besser?
Bitte diskutieren Sie folgende Lösung!
(c) Carsten Hetzel, 2014
24. 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!');
}
}
}
24 (c) Carsten Hetzel, 2014
25. 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 Exception
mehr! Für den Fall, dass man UnitTests für diese Klasse
geschrieben hat, müssen wir diese spätestens jetzt anpassen.
25 (c) Carsten Hetzel, 2014
26. Eine Sauberere Lösung
mit Konsequenzen!
Was halten Sie von folgender Lösung?
(c) Carsten Hetzel, 2014
27. 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);
}
}
27 (c) Carsten Hetzel, 2014
28. 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.
...
28 (c) Carsten Hetzel, 2014
29. 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.
29 (c) Carsten Hetzel, 2014
30. Die beste Lösung!?
Ist es möglich, dass wir ohne Abhängigkeiten auskommen?
(c) Carsten Hetzel, 2014
31. Die beste Lösung?
class NoCoupling
{
public function updateOrder($orderId, $newValue)
{
// ... update the order
}
}
31 (c) Carsten Hetzel, 2014
32. Und was ist mit unserer
Ausgabedatei?
(c) Carsten Hetzel, 2014
33. 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!
}
}
33 (c) Carsten Hetzel, 2014
34. 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.
34 (c) Carsten Hetzel, 2014
35. Wie werden die ganzen
Ressourcen
zusammengesetzt?
(c) Carsten Hetzel, 2014
36. 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 ...
36 (c) Carsten Hetzel, 2014
37. Vorher
class MyUglyApplication
{
public function doSomething()
{
// ...
$oderId = $this->providerOrderId();
$newValue = $this->providerNewValue();
$myUglyClass = new UglyCoupling();
$myUglyClass->updateOrder($oderId, $newValue);
}
}
37 (c) Carsten Hetzel, 2014
38. 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);
}
}
38 (c) Carsten Hetzel, 2014
39. 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
39 (c) Carsten Hetzel, 2014
40. Refactoring des Ergebnisses
class MyInjectingApplication
{
public function doSomething()
{
// ...
$oderId = $this->providerOrderId();
$newValue = $this->providerNewValue();
$coupledClass = $this->getCoupledClass();
$coupledClass->updateOrder($oderId, $newValue);
}
...
40 (c) Carsten Hetzel, 2014
41. 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;
}
...
41 (c) Carsten Hetzel, 2014
42. Refactoring des Ergebnisses
...
protected function getFileObject()
{
return new SplFileObject($this->getFilename());
}
/**
* @return string
*/
protected function getFilename()
{
return 'output.txt';
}
...
42 (c) Carsten Hetzel, 2014
43. 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.
43 (c) Carsten Hetzel, 2014
45. 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.
45 (c) Carsten Hetzel, 2014
46. 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.
46 (c) Carsten Hetzel, 2014
48. 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)
48 (c) Carsten Hetzel, 2014