Tell, Don‘t Ask!Erstellung aussagekräftiger Schnittstellen(c
) C
ars
ten
He
tze
l, 2
014
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.
(c) Carsten Hetzel, 20142
Aufgabe: Welche Programmierprinzipien kennen Sie noch?
(c)
Ca
rste
n H
etz
el,
20
14
Programmierprinzipien
- Information Hiding- Least Knowledge Principle- Lose Kopplung, starke Bindung (Kohärenz)- Die SOLID-Prinzipien- Programmiere gegen Interfaces, nicht gegen Implementierungen- Verwende Abhängigkeiten zu Abstraktionen, nicht zu konkreten Klassen- Don't Repeat Yourself (DRY)- Kapsle variable Bestandteile (Strategie-Pattern)- Bevorzuge Komposition statt Vererbung- Das "Hollywood Prinzip" (Don't call us, we call you!)- Strebe lose gekoppelte Systeme an- Keep It Simple and Stupid (KISS)
(c) Carsten Hetzel, 20144
Ein einfaches Beispiel
(c)
Ca
rste
n H
etz
el,
20
14
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
}
(c) Carsten Hetzel, 20146
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
(c) Carsten Hetzel, 20147
Ein einfaches Beispiel
Was ist mit folgender Änderung?
if ($myClass->getStatus() == MyClass::PREMIUM_USER) {
// Do something ...
} else {
// Do something else
}
(c) Carsten Hetzel, 20148
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 sogargeradezu rechtfertigen weitere Bedingungen im Client Code zu fromulieren.
(c) Carsten Hetzel, 20149
Ein einfaches Beispiel
Wenn wir die Abfrage in eine Methode verlagern (siehe Abfragemethode/QueryMethod), dann wird derCode 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.
(c) Carsten Hetzel, 201410
Ein komplexeres Beispiel
(c)
Ca
rste
n H
etz
el,
20
14
Ein komplexeres Beispiel
Stellen Sie sich folgende Situation vor: Sie verwenden ein Framework, welches Ihnen dieVerarbeitung von Formularen erlaubt. Diese Klasse sieht etwa wie folgt aus:
class Form{
// ...public function getValue($key){
// ...return $value;
}
public function addFormElement($key, FormElement$element)
{// ...
}}
(c) Carsten Hetzel, 201412
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:
(c) Carsten Hetzel, 201413
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 KlasseKonstanten an, welche dann beim Auslesen verwendet werden können.
(c) Carsten Hetzel, 201414
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?
(c) Carsten Hetzel, 201415
Ein komplexeres Beispiel
Schlecht lesbar
Nicht wiederverwendbar
Fehleranfällig
...
(c) Carsten Hetzel, 201416
Konkretisierungen können auch die API erweitern
(c)
Ca
rste
n H
etz
el,
20
14
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 {// ...
}// ...
}}
(c) Carsten Hetzel, 201418
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 eineTelefonnummer - 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.
(c) Carsten Hetzel, 201419
Vermitteln der Intention
(c)
Ca
rste
n H
etz
el,
20
14
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:
(c) Carsten Hetzel, 201421
Vermitteln der Intention
class BetterContactController{
// ...
public function createNewContact(){
// ...$form = new ContactForm();// ...if ($form->isValid()) {
$contact = new Contact();$form->fillContact($contact);$contact->save();
} else {// ...
}// ...
}}
(c) Carsten Hetzel, 201422
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?
(c) Carsten Hetzel, 201423
Vermitteln der Intention
class ContactController{
// ...
public function createNewContact(){
// ...$contact = new Contact();$form = new ContactForm($contact);// ...if ($form->isValid()) {
$contact->save();} else {
// ...}// ...
}}
(c) Carsten Hetzel, 201424
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. DerClient-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 Contactdurchgeführt werden. Es ist viel sinnvoller die Validierung derAttribute eines Modells dem Modell zu überlassen, schließlich können Eingabedaten von allen möglichen Stellen des Systems und unterschiedlichsten Schnittstellen her kommen.
(c) Carsten Hetzel, 201425
Von verstreuter Business-Logikzu Value-Objects
(c)
Ca
rste
n H
etz
el,
20
14
Von verstreuter Business-Logikzu 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älternausgetauscht werden.
Wie würden Sie dieses Problem lösen?
(c) Carsten Hetzel, 201427
Von verstreuter Business-Logikzu 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);
}
}
(c) Carsten Hetzel, 201428
Von verstreuter Business-Logikzu 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:
(c) Carsten Hetzel, 201429
Von verstreuter Business-Logikzu 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;
}
}
(c) Carsten Hetzel, 201430
Von verstreuter Business-Logikzu Value-Objects
Entsprechend sieht die Bottle-Klasse folgendermaßen aus:
class Bottle
{
// ...
public function reduceBy(Transfusion $t)
{
$this->liters -= $t->getLiters();
}
// ...
}
(c) Carsten Hetzel, 201431
Von verstreuter Business-Logikzu 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?
(c) Carsten Hetzel, 201432
Aufgabe: Markierung von "besonderen" Rechnungsposten
(c)
Ca
rste
n H
etz
el,
20
14
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.
(c) Carsten Hetzel, 201434
Vielen Dank für Ihre Aufmerksamkeit!
(c) Carsten Hetzel, 201435
Top Related