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.