Kategorie-Archiv: Clean Code

Es ist einfach so zu programmieren, dass ein Computer damit etwas anfangen kann. Viel schwieriger ist es, es auch anderen Menschen zugänglich zu machen.

Das erweiterte Specification Pattern

oop clean code specification pattern

Wenn ich Objekte entwerfe, dann kommt es oft vor, dass sie nach bestimmten Kriterien eingeteilt werden müssen. User müssen z.B. in Admin, Moderatoren und Benutzer eingeteilt werden, News nach Kategorien usw. Bleiben wir einmal bei den Benutzern. Wir haben einen abgesperrten Bereich, den nur Admins und Moderatoren betreten dürfen. Oft beginnt es dann mit solch einfachen Konstrukten:

if ($user->isModerator()
    || $user->isAdmin()
) {
    // Zugriff gestattet
}

Diese Konstrukte funktionieren natürlich erstmals. Aber dann ändern sich unsere Anforderungen. Neben den Administratoren und den Moderatoren sollen jetzt auch Stamm-User Zugriff auf den Bereich haben:

if ($user->isModerator()
    || $user->isAdmin()
    || $user->countPostings() > 200
) {
    // Zugriff gestattet
}

Damit das ganze mehr Aussagekraft bekommt, könnten (und sollten) wir ein paar neue Methoden schaffen:

if ($user->isOperator()
    || $user->isPremiumUser()
) {
    // Zugriff gestattet
}

Wir wollen aber noch einen Schritt weiter gehen, indem wir uns eigene Specification-Objekte erstellen:

interface UserSpecification {
    public isSatisfiedBy(User $user);
}

class OperatorSpecification implements UserSpecification {
    public function isSatisfiedBy(User $user) {
        return $user->isAdmin() || $user->isModerator();
    }

    public function or(UserSpecification $spec) {
        return new OrSpecification($this, $spec);
    }
}

class PremiumUserSpecification implements UserSpecification {
    public function isSatisfiedBy(User $user) {
        return $user->countPostings() > 200;
    }
}

class OrSpecification implements UserSpecification {
    private $specA;
    private $specB;

    public function __construct(
        UserSpecification $specA,
        UserSpecification $specB
    ) {
        $this->specA = $specA;
        $this->specB = $specB;
    }

    public function isSatisfiedBy(User $user) {
        return $this->specA->isSatisfiedBy($user)
            || $this->specB->isSatisfiedBy($user);
    }
}
$specification = new OperatorSpecification();
$specification = $specification->or(new PremiumUserSpecification());
if ($specification->isSatisfiedBy($user)) {
    // Zugriff gestatten
}

Auf diese Art und Weise bleiben unsere Spezifikationen klein und übersichtlich und vor allem wartbar und die Komposition (hier am Beispiel der OrSpecification) ermöglicht es uns, mit relativ wenigen Specifications auszukommen und die vorhandenen zu nutzen anstatt ständig neue zu erstellen. Zudem wird unser User-Objekt nun nicht durch unnötig viele is*() Methoden aufgebläht. Das ist auch schon alles zum Specification Pattern.

Das erweiterte Specification Pattern

Lasst uns nun schauen, wie wir die bisherigen Specifications erweitern können, um noch mehr herauszuholen. Oft will man nicht nur überprüfen können, ob ein Benutzer zu einer bestimmten Gruppe gehört, sondern wir wollen uns genau diese Gruppe an Benutzern aus der Datenbank holen. In der Regel steht der Code dafür in einer Mapper– oder Repository-Klasse, die oft so aufgebaut ist:

class UserRepository {
    public function findAdmins() { /* … */ }
    public function findMods() { /* … */ }
    public function findOperators() { /* … */ }
    public function findPremiumUsers() { /* … */ }
    public function findOperatorsAndPremiumUsers() { /* … */ }
}

Das ist jetzt etwas überspitzt dargestellt, aber es wird denke ich klar, worauf ich hinaus will. Diese Mapper/Repositories wachsen und wachsen und enthalten am Ende eine lange Liste von find*()-Methoden, die sich oft nur marginal unterscheiden.

Hier kommen jetzt wieder unsere Specifications ins Spiel. Wir erweitern sie um eine match()-Methode, welche in unserem Fall (Doctrine ORM) einen QueryBuilder und den Alias des aktuellen Objekts übergeben bekommt. Je nachdem was zur Kommunikation mit der Datenbank eingesetzt wird, muss das natürlich angepasst werden.

interface UserSpecification {
    // …
    public function match(QueryBuilder $qb, $dqlAlias);
}

class OperatorSpecification implements UserSpecification {
    // …
    public function match(QueryBuilder $qb, $dqlAlias) {
        return $qb->expr()->orX(
            $qb->expr()->eq($dqlAlias . '.admin', 1),
            $qb->expr()->eq($dqlAlias . '.moderator', 1)
        );
    }
}

class PremiumUserSpecification implements UserSpecification {
    // …
    public function match(QueryBuilder $qb, $dqlAlias) {
        return $qb->expr()->gt($dqlAlias . '.numPostings', 200);
    }
}

Und in unserem Repository sieht das dann so aus:

class UserRepository {
    public function findSatisfying(UserSpecification $specification) {
        $qb    = $this->createQueryBuilder('u');
        $expr  = $specification->match($qb, 'u');
        $query = $qb->where($expr)->getQuery();
        return $query->getResult();
    }
}

Durch diese Änderung sparen wir es uns nicht nur, zahlreiche find*()-Methoden zu erstellen, unser Mapper/Repository erfüllt nun auch das Open-Closed-Prinzip.

Testbarkeit

Ein weiterer Vorteil: Es lässt sich nun einfach prüfen, ob die Specifications hinsichtlich Datenbank und Objekten die gleiche Specification darstellen:

$specification = /* … */;
foreach ($repository->findSatisfying($specification) as $each) {
    $this->assertTrue($specification->isSatisfiedBy($each));
}

Zusammenfassung

Die Specifications bieten uns einen zentralen Ort, wo wir sowohl die Datenbankabfrage modifizieren als auch die Objekte prüfen können, wodurch die Wahrscheinlichkeit sinkt, dass bei eventuellen Änderungen eine der beiden Seiten vergessen wird. Sie vereinfachen zudem unsere Mapper bzw. Repositories und erhöhen so unsere Code-Qualität.

Named Constructor

oop clean code named constructor Um eine Methode leicht verständlich zu machen, gibt es viele Möglichkeiten. Man kann die Parameter benennen oder den Methodennamen selbst dazu nutzen. Hat man zwei Methoden, die fast das Selbe machen, dabei aber unterschiedlich Parameter erwarten, kann man dies anhand des Namens deutlich machen:

class Date
{
    public function setTimestamp($value)
    {
        // ...
    }

    public function setAtomFormattedDate($value)
    {
        // ...
    }

    public function setRssFormattedDate($value)
    {
        // ...
    }

    
    // ...
    
}

Was aber macht man, wenn man den Wert schon im Konstruktor übergeben will? Weiterlesen

Der vollständige Konstruktor

oop clean code constructor

Vor einigen Tagen habe ich in einem Artikel beschrieben, wie man mit vielen Parametern in Methoden umgehen kann. Beim Konstruktor handelt es sich allerdings um einen Sonderfall, denn im Gegensatz zu normalen Methoden haben die einzelnen Parameter oft nicht viel miteinander zu tun und lassen sich deswegen nicht zu Parameterobjekten zusammenfassen. Um den Code dennoch lesbar zu halten, benötigen wir andere Mittel.

Weiterlesen

‚ vs. „

Im Netz gibt es zahlreiche Vergleiche zwischen single- und double quotes. Die Meisten basieren auf verschiedenen Benchmarks und stellen die Performance-Unteschiede zwischen den beiden Möglichkeiten dar. Da ich die minimalen Performance-Unterschiede für nicht relevant halte, möchte ich heute einen etwas anderen Vergleich vorstellen.

Weiterlesen

(Zu) viele Parameter

oop clean code parameter

Oft entwirft man Methoden, die viele Parameter benötigen. Nachdem noch 1-2 Features zu der Klasse hinzugefügt wurden, stellt man dann fest, dass es eindeutig zu viele Parameter geworden sind. Folgende Methode dient uns als Beispiel:

public function send(
    $fromStreet, $fromPostalCode, $fromCity, $fromCountry,
    $toStreet,   $toPostalCode,   $toCity,   $toCountry
) {
    //..
}

Das mag bei der Definition noch einigermaßen übersichtlich sein, doch spätestens bei der Nutzung der Methode geht die Übersicht verloren (und das obwohl die Reihenfolge der Parameter sich hier einfach ableiten lässt):

$obj->send('Musterstraße 74', '22041', 'Hamburg', 'Deutschland', 'Musterweg 42', '8471', 'Leibnitz', 'Österreich');

Weiterlesen