Tell, Don't Ask! Erstellung aussagekräftiger Schnittstellen

Von am

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.

Aufgabe: Welche Programmierprinzipien kennen Sie noch?

  • 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)

Ein einfaches Beispiel

Eine der häufigsten Anwendungen dieses Prinzips ist die Kapselung von Statusabfragen. Welche Nachteile hat z.B. folgender Code:

<?php

if ($myClass->getStatus() == 10) {
    // Do something ...
} else {
    // Do something else
}

Nachteile:

  • 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

Was ist mit folgender Änderung?

<?php

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 abzufragen.

Wenn wir die Abfrage in eine Methode verlagern (siehe Abfragemethode/QueryMethod), dann wird der Code kompakt und aussagekräftig:

<?php

if ($myClass->isPremiumUser()) {
    // Do something ...
} else {
    // Do something else
}

Sogar eine Veränderung der Bedingung kann nun ohne Anpassung des Client Codes vorgenommen werden.

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:

<?php

class Form
{
    // ...

    public function getValue($key)
    {
        // ...
        return $value;
    }

    public function addFormElement($key, FormElement $element)
    {
        // ...
    }
}

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:

<?php

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.

Der Controller sieht etwa folgendermaßen aus:

<?php

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?

  • Schlecht lesbar
  • Nicht wiederverwendbar
  • Fehleranfällig

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:

<?php

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 {
            // ...
        }
        // ...
    }
}

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.

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:

<?php

class BetterContactController
{
    // ...

    public function createNewContact()
    {
        // ...
        $form = new ContactForm();
        // ...
        if ($form->isValid()) {
            $contact = new Contact();
            $form->fillContact($contact);
            $contact->save();
        } else {
            // ...
        }
        // ...
    }
}

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?

<?php

class ContactController
{
    // ...

    public function createNewContact()
    {
        // ...
        $contact = new Contact();
        $form = new ContactForm($contact);
        // ...
        if ($form->isValid()) {
            $contact->save();
        } else {
            // ...
        }
        // ...
    }
}

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 mehr 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.

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 dem System kann es beliebig viele Behälter geben, die Menge an Wasser aber immer gleich bleiben. Wasser kann immer nur zwischen zwei Behältern ausgetauscht werden.

Wie würden Sie dieses Problem lösen?

<?php

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);
    }
}

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:

<?php

class Transfusion
{
    private $liters;

    public function __construct($liters, Bottle $source, Bottle $target)
    {
        $source->reduceBy($this);
        $target->fillBy($this);
    }

    public function getLiters()
    {
        return $this->liters;
    }
}

Entsprechend sieht die Bottle-Klasse folgendermaßen aus:

<?php

class Bottle
{
    // ...

    public function reduceBy(Transfusion $t)
    {
        $this->liters -= $t->getLiters();
    }

    // ...
}

Und abschließend der Controller:

<?php

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?

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.