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.

Zugriff auf die Konfiguration

zf2-logo konfiguration Die Konfiguration im Zend Framework 2 zu füllen, ist nicht schwierig. Wie aber kann man auf die Konfiguration zugreifen?

Quick and dirty geht das ganz einfach:

namespace MyModule;

class Module
{
    public function getConfig()
    {
        return array(
            'my-module' => array(
                'foo' => 'bar'
            )
        );
    }
}
// Zugriff z.B. im Controller:
$config = $this->getServiceLocator()->get('Config');
echo $config['my-module']['foo']; // gibt 'bar' aus

Das funktioniert, ist aber nicht wirklich schön. Spätestens wenn man sein Modul auch anderen zugänglich machen will, müssen die sich auf die Suche nach unseren Konfigurationsmöglichkeiten machen. Wir können natürlich eine Dokumentation mit allen Möglichkeiten anlegen, aber eine Dokumentation zu schreiben macht nicht wirklich Spaß und wenn Dokumentation und Quelltext getrennt voneinander liegen, veraltet die Dokumentation schneller als man glaubt.

Wir brauchen also eine sauberer Lösung, um auf unsere Konfiguration zuzugreifen. Es bietet sich an, die Konfiguration in einem Objekt zu kapseln. Das könnte z.B. so aussehen:

namespace MyModule\Options;

use Zend\Stdlib\AbstractOptions;

class ModuleOptions extends AbstractOptions
{
    protected $foo;

    public function setFoo($value)
    {
        $this->foo = $value;
    }

    public function getFoo()
    {
        return $foo;
    }
}

Wir haben hier eine einfache Klasse, die eine Getter- und eine Setter-Methode für foo besitzt. Warum erweitern wir dafür Zend\Stdlib\AbstractOptions? Dort ist ein Konstruktor definiert, der es uns ermöglicht, die Optionen als Array zu übergeben und so direkt zu setzen. Das sieht dann ungefähr so aus:

namespace MyModule;

use MyModule\Options\ModuleOptions;
use Zend\ServiceManager\ServiceLocatorInterface as ServiceLocator;

class Module
{
    public function getConfig()
    {
        return array(
            'my-module' => array(
                'foo' => 'bar'
            )
        );
    }

    public function getServiceConfig()
    {
        return array(
            'factories' => array(
                'MyModule\Options\ModuleOptions' => function (ServiceLocator $services) {
                    $config = $services->get('Config');
                    return new ModuleOptions($config['my-module']);
                }
            )
        );
    }
}
// Zugriff z.B. im Controller:
$config = $this->getServiceLocator()->get('MyModule\Options\ModuleOptions');
echo $config->getFoo(); // gibt 'bar' aus

Wie ihr seht, haben wir durch diese paar Zeilen mehr einen sauber definierten Zugriff und einen zentralen Punkt im Code, wo sämtliche Konfigurationsmöglichkeiten für unser Modul dokumentiert sind und wo wir Standard-Werte setzen können. Das allein sollten euch diese paar Zeilen Code wert sein :-)

ZF2 Kurztipp: Modulspezifische Layouts

zf2 kurztipps modulspezifische layouts Eine häufige Frage beim Einsatz des Zend Framework 2 ist: Wie kann ich für ein bestimmtes Modul ein anderes Layout nutzen. Rob Allen hat genau dafür ein Modul geschrieben, das ihr bequem über Composer installieren könnt.

Nun müsst ihr das Modul nur noch aktivieren, indem ihr in der application.config.php Folgendes eintragt:

return array(
    'modules' => array(
        'EpdModuleLayouts',
        // eure anderen Module
    ),
    // […]
);

Jetzt steht euch das Modul zur Verfügung und ihr könnt z.B. in einer autoload/config/layouts.php oder auch in eurer module.php die gewünschten Layouts angeben:

return array(
    'module_layouts' => array(
        'MyModule' => 'layout/my-specific-layout',
    ),
);

Für alle Module, die nicht explizit angegeben werden, wird übrigens das Standard-Layout geladen.

Verschiedene Environments mit dem ZF2

zf2 environments Die meisten Anwendungen laufen mittlerweile in verschiedenen Environments. Entwickler, die vom ZF1 auf das ZF2 umsteigen, suchen oft nach einer Möglichkeit, für die verschiedene Environments verschiedene Einstellungen zu nutzen. Im ZF1 war dies innerhalb der Datei application.ini möglich. Im ZF2 ist diese Datei nicht mehr vorhanden. Es müssen andere Möglichkeiten gesucht werden, Variablen abhängig von der Environment zu setzen.

Da die Konfiguration mittlerweile Plain-PHP ist, liegt natürlich das Abfragen einer Umgebungsvariable nahe. Das könnte dann zum Beispiel so aussehen:

return array(
    'db' => array(
        'driver' => 'Pdo',
        'dsn'            => 'mysql:dbname=example;host=localhost',
        'username'       => getenv('APP_ENV') == 'dev'
                          ? 'root'
                          : 'admin',
        'password'       => getenv('APP_ENV') == 'dev'
                          ? ''
                          : '123456',
        'driver_options' => array(
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
        ),
    )
);

Das funktioniert zwar, allerdings liefert das ZF2 von Hause aus eine bessere Möglichkeit mit: das config/autoload-Verzeichnis. Das Vorgehen ist ganz einfach: Man lege eine Datei mit den globalen Einstellungen ab, die auf global.php endet. Sie enthält die Einstellungen, die in allen Environments gleich sind. Für unser Beispiel sieht das so aus:

return array(
    'db' => array(
        'driver' => 'Pdo',
        'dsn'            => 'mysql:dbname=example;host=localhost',
        'driver_options' => array(
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
        ),
    )
);

Ergänzt wird das ganze dann durch eine Datei, die auf local.php endet. Sie enthält die umgebungsspezifischen Konfigurationen. Für unsere dev-Umgebung sähe das so aus:

return array(
    'db' => array(
        'username'       => 'root',
        'password'       => '',
    )
);

Die dazugehörige .gitignore sorgt dann dafür, dass die Datei mit den umgebungsspezifischen Konfigurationen nicht im Repository landet. Dieses Vorgehen hat zwei Vorteile: Zum einen kann jeder Entwickler beliebige Konfigurationen verwenden, ohne dass sich die Konfigurationsdateien unnötig aufblähen, zum anderen landen keine sicherheitskritischen Daten im Repository.

Es ist also nicht besonders schwierig, verschiedene Environments im ZF2 zu konfigurieren. Meiner Meinung nach ist es sogar einfacher geworden, wenn man das Prinzip einmal verstanden hat.

ZF2-Kurztipp: Environment mit ZFTool testen

zf2 kurztipps zftool Während sich das Zend_Tool im Zend Framework 1 noch hauptsächlich darauf beschränkt hat, Controller und andere Klassen anzulegen, bringt sein Nachfolger, das ZFTool-Modul, für das Zend Framework 2 auch die Möglichkeit mit, seine Environment automatisch zu überprüfen. Das ganze nennt sich Diagnostics und funktioniert wahlweise über die Kommandozeile oder das Webinterface. In der Anleitung wird nicht nur das Einbinden bestehender Tests erklärt, sondern auch, wie man mit wenigen Zeilen Code eigene Tests schreibt – anschauen lohnt sich.

yet another php blog