2. Tell, Don't Ask - Erstellung
aussagekräftiger Schnittstellen
Es gibt eine Vielzahl von Programmierprinzipien im Bereich
objektorientiert Softwareentwicklung aber keines hilft wie
"Tell, Don't Ask!" dabei, aussagekräftige Schnittstellen für
Klassen zu erstellen.
"Tell, Don't Ask!" (TDA) besagt, dass wir statt Objekte nach
ihrem Zustand zu fragen und diesen anschließend
auszuwerten, die Klasse selber aussagekräftige Methoden
anbieten lassen sollen, die die Auswertung oder Verarbeitung
übernehmen.
2 (c) Carsten Hetzel, 2014
6. Ein einfaches Beispiel
Eine der häufigsten Anwendungen dieses Prinzips ist die
Kapselung von Statusabfragen.
Welche Nachteile hat z.B. folgender Code:
if ($myClass->getStatus() == 10) {
// Do something ...
} else {
// Do something else
}
6 (c) Carsten Hetzel, 2014
7. Ein einfaches Beispiel
if ($myClass->getStatus() == 10) {
// Do something ...
} else {
// Do something else
}
Die Bedeutung des Werts 10 ist nicht klar - der Code ist also
undurchsichtig (Opazität/Opacity hoch)
Wenn wir diese Abfrage häufiger benötigen, verteilt sie sich über
den gesamten Client Code
Sollte sich Abfrage ändern (z.B. ein weiterer Status zu beachten
sein), müssen wir viele Stellen anpassen
7 (c) Carsten Hetzel, 2014
8. Ein einfaches Beispiel
Was ist mit folgender Änderung?
if ($myClass->getStatus() == MyClass::PREMIUM_USER) {
// Do something ...
} else {
// Do something else
}
8 (c) Carsten Hetzel, 2014
9. Ein einfaches Beispiel
if ($myClass->getStatus() == MyClass::PREMIUM_USER) {
// Do something ...
} else {
// Do something else
}
Jetzt verstehen wir zwar die Bedeutung der Abfrage besser,
aber die anderen Probleme bestehen weiterhin.
Schlimmer noch: Wir haben die Klasse um Elemente
(Konstanten) erweitert, die dazu verleiten wenn nicht sogar
geradezu rechtfertigen weitere Bedingungen im Client Code
zu fromulieren.
9 (c) Carsten Hetzel, 2014
10. Ein einfaches Beispiel
Wenn wir die Abfrage in eine Methode verlagern (siehe
Abfragemethode/QueryMethod), dann wird der
Code kompakt und aussagekräftig:
if ($myClass->isPremiumUser()) {
// Do something ...
} else {
// Do something else
}
Sogar eine Veränderung der Bedingung kann nun ohne
Anpassung des Client Codes vorgenommen werden.
10 (c) Carsten Hetzel, 2014
12. Ein komplexeres Beispiel
Stellen Sie sich folgende Situation vor: Sie verwenden ein Framework, welches Ihnen die
Verarbeitung von Formularen erlaubt. Diese Klasse sieht etwa wie folgt aus:
class Form
{
// ...
public function getValue($key)
{
// ...
return $value;
}
public function addFormElement($key, FormElement
$element)
{
// ...
}
}
12 (c) Carsten Hetzel, 2014
13. Ein komplexeres Beispiel
Einer Form können Sie also verschiedene Elemente wie z.B.
ein Eingabefeld oder eine Checkbox hinzufügen.
Um ihre Dialoge wieder verwenden zu können entscheiden
Sie sich konkrete Ableitungen dieser Klasse zu erstellen, z.B.
ein Kontaktformular, um von neuen Kunden Kontaktdaten
aufzunehmen:
13 (c) Carsten Hetzel, 2014
14. Ein komplexeres Beispiel
class ContactForm extends Form
{
const KEY_NAME = 'name';
const KEY_AGE = 'age';
const KEY_EMAIL = 'email';
// ...
public function __construct()
{
$nameInput = $this->createNameInputField();
$this->addFormElement(self::KEY_NAME, $nameInput);
$ageInput = $this->createAgeInputField();
$this->addFormElement(self::KEY_AGE, $ageInput);
// ...
}
}
Um Fehler beim Zugriff auf die einzelnen Form-Felder zu vermeiden bietet die Klasse
Konstanten an, welche dann beim Auslesen verwendet werden können.
14 (c) Carsten Hetzel, 2014
15. Ein komplexeres Beispiel
Der Controller sieht etwa folgendermaßen aus:
class UglyContactController
{
// ...
public function createNewContact()
{
// ...
$form = new ContactForm();
// ...
if ($form->isValid()) {
$contact = new Contact();
$contact->setName($form->getValue(ContactForm::KEY_NAME));
$contact->setAge($form->getValue(ContactForm::KEY_AGE));
$contact->setEMail($form->getValue(ContactForm::KEY_AGE));
$contact->save();
} else {
// ...
}
// ...
}
}
Was halten Sie von dieser Lösung? Welche Probleme sehen Sie?
15 (c) Carsten Hetzel, 2014
16. Ein komplexeres Beispiel
Schlecht lesbar
Nicht wiederverwendbar
Fehleranfällig
...
16 (c) Carsten Hetzel, 2014
18. Konkretisierungen können auch die API
erweitern
Da wir sowieso eine separate ContactForm-Klasse haben, können wir uns die Verwendung der
Konstanten sparen, indem wir direkt entsprechende Getter anbieten:
class BetterContactController
{
public function createNewContact()
{
// ...
$form = new ContactForm();
// ...
if ($form->isValid()) {
$contact = new Contact();
$contact->setName($form->getName());
$contact->setAge($form->getAge());
$contact->setEMail($form->getEMail());
$contact->save();
} else {
// ...
}
// ...
}
}
18 (c) Carsten Hetzel, 2014
19. Konkretisierungen können auch die API
erweitern
Diese Version des Controllers ist schon deutlich einfacher zu
lesen und Fehler werden schneller entdeckt. Aber wenn wir
die Klasse Contact erweitern - z.B. um eine
Telefonnummer - dann müssen wir alle Controller nach
solchen Code-Stellen durchsuchen und sie anpassen.
Verwenden wir die Klasse ContactForm, dann müssen wir
auch den Code-Block mit den Setter-Aufrufen kopieren und
haben einen klassischen Fall von Code-Duplizierung.
19 (c) Carsten Hetzel, 2014
21. Vermitteln der Intention
Was soll denn an der Stelle passieren, wenn die
Eingabedaten des Formulars korrekt waren und gespeichert
werden können?
Im Bereich mit den Setter-Aufrufen sollen die Daten aus dem
Formular in die Contact-Instanz übertragen werden. Eine
Methode wie "transferToContact“ oder "fillContact()"
verdeutlichen die Intention besser:
21 (c) Carsten Hetzel, 2014
22. Vermitteln der Intention
class BetterContactController
{
// ...
public function createNewContact()
{
// ...
$form = new ContactForm();
// ...
if ($form->isValid()) {
$contact = new Contact();
$form->fillContact($contact);
$contact->save();
} else {
// ...
}
// ...
}
}
22 (c) Carsten Hetzel, 2014
23. Vermitteln der Intention
Inzwischen sieht der Code leserlich aus. Aber will man
wirklich explizit ausdrücken, dass die Daten aus dem
Formular in den Kontakt übertragen werden?
Eigentlich würden wir doch erwarten, dass das automatisch
passiert, oder?
23 (c) Carsten Hetzel, 2014
24. Vermitteln der Intention
class ContactController
{
// ...
public function createNewContact()
{
// ...
$contact = new Contact();
$form = new ContactForm($contact);
// ...
if ($form->isValid()) {
$contact->save();
} else {
// ...
}
// ...
}
}
24 (c) Carsten Hetzel, 2014
25. Vermitteln der Intention
Im Grunde hat sich nur folgendes geändert: Die Methode
"fillContact()" muss nicht mehr explizit aufgerufen werden,
sondern gehört quasi zum Lebenszyklus des Formulars. Der
Client-Code ist schlanker geworden und entspricht eher
unseren Erwartungen.
Darüber hinaus haben wir einen weiteren Vorteil erreicht: Die
Validierung der Eingabedaten kann nun von Contact
durchgeführt werden. Es ist viel sinnvoller die Validierung der
Attribute eines Modells dem Modell zu überlassen,
schließlich können Eingabedaten von allen möglichen Stellen
des Systems und unterschiedlichsten Schnittstellen her
kommen.
25 (c) Carsten Hetzel, 2014
27. Von verstreuter Business-Logik
zu Value-Objects
Angenommen Sie sollen ein System von Behältern
beschreiben, welche Flüssigkeiten aufnehmen können (z.B.
Flaschen).
In diesem System kann es beliebig viele Behälter geben, die
Menge an Wasser soll aber immer gleich bleiben.
Wasser kann immer nur zwischen zwei Behältern
ausgetauscht werden.
Wie würden Sie dieses Problem lösen?
27 (c) Carsten Hetzel, 2014
28. Von verstreuter Business-Logik
zu Value-Objects
class Bottle
{
private $liters;
// ...
}
class AppController
{
public function transfuseAction($amount, $sourceBottleId,
$targetBottleId)
{
// ..
$source = $this->getBottleById($sourceBottleId);
$target = $this->getBottleById($targetBottleId);
$source->setLiters($source->getLiters() - $amount);
$target->setLiters($target->getLiters() + $amount);
}
}
28 (c) Carsten Hetzel, 2014
29. Von verstreuter Business-Logik
zu Value-Objects
Welche Probleme hat diese Lösung?
Was wollen wir eigentlich machen?
Finden wir eine sprechendere Lösung, die auch direkt die
Rahmenbedingungen des Systems einhält:
29 (c) Carsten Hetzel, 2014
30. Von verstreuter Business-Logik
zu Value-Objects
class Transfusion
{
private $liters;
public function __construct($liters, Bottle $source, Bottle
$target)
{
$source->reduceBy($this);
$target->fillBy($this);
}
public function getLiters()
{
return $this->liters;
}
}
30 (c) Carsten Hetzel, 2014
31. Von verstreuter Business-Logik
zu Value-Objects
Entsprechend sieht die Bottle-Klasse folgendermaßen aus:
class Bottle
{
// ...
public function reduceBy(Transfusion $t)
{
$this->liters -= $t->getLiters();
}
// ...
}
31 (c) Carsten Hetzel, 2014
32. Von verstreuter Business-Logik
zu Value-Objects
Und abschließend der Controller:
class AppController
{
public function transfuseAction($amount, $sourceBottleId,
$targetBottleId)
{
// ..
$source = $this->getBottleById($sourceBottleId);
$target = $this->getBottleById($targetBottleId);
$transfusion = new Transfusion($amount, $source,
$target);
}
}
Welche Vorteile hat diese Lösung?
32 (c) Carsten Hetzel, 2014
34. Aufgabe: Markierung von "besonderen"
Rechnungsposten
Auf dem Bildschirm sollen alle Posten einer Rechnung
aufgelistet und diejenigen Posten mit einem "X" markiert
werden, die einen Wert von über 100€ haben.
34 (c) Carsten Hetzel, 2014
35. Vielen Dank für Ihre Aufmerksamkeit!
35 (c) Carsten Hetzel, 2014