Kapselung durch Konvention
Als ich letztens die Bootstrap-Datei für MVCLite
programmiert habe, stellte sich die Frage, wie ich gleichartige, damit
meine ich die gleiche Intention, optimal kapseln kann. Die Intention
ist auch sehr schnell beschrieben: Ich möchte die Applikation
initialisieren. Die erste Idee war natürlich eine simple PHP-Datei, in
der man alles machen kann, was man machen möchte. Vom Aufbau her wäre
die Bootstrap-Datei der www/config.php
ähnlich gewesen. Wieso nicht gleich die www/config.php als
Bootstrap-Datei benutzen? Wäre natürlich möglich gewesen, aber diese
Datei soll für das wirklich Essentielle zuständig sein. Die
Bootstrap-Datei soll hingegen die Feineinstellungen übernehmen.
Außerdem wollte ich einen eleganten objektorientierten Weg gehen. Also
muss ich auf jeden Fall eine (oder mehrere) Klassen nutzen. Mein erster
Einfall war natürlich eine Methode, die den gesamten Code zum
Initialisieren enthält und dann lediglich aufgerufen wird. Aber habe
ich damit überhaupt was gewonnen gegenüber einer reinen PHP-Datei ohne
objektorientierten Rahmen? Nein, sicherlich nicht. Also weiter
grübeln...
Nach kurzem Grübeln fiel mir das Kommando-Entwurfsmuster ein. Ja, das würde auf jeden Fall elegant und objektorientiert sein. Ein kurzes Beispiel verdeutlicht den Einsatz des Kommando-Entwurfsmuster (Command Pattern).
Erstmal das Interface, das in jede Bootstrap-Klasse implementiert werden muss.
// Initializable.php
interface Initializable
{
public function init ();
}
Jetzt folgen einige konkrete Bootstrap-Klassen für Tätigkeiten, die in der momentan von MVCLite verwendeten Bootstrap-Klasse ausgeführt werden.
Zuerst kommt die Klasse, die den X-Powered-By-Header sendet.
// Bootstrap/PoweredBy.php
class Bootstrap_PoweredBy implements Initializable
{
public function init ()
{
header('X-Powered-By: ' . MVCLite::NAME . ' ' . MVCLite::VERSION);
}
}
Und nun die Klasse, welche die standardmäßige Route setzt.
// Bootstrap/Route.php
class Bootstrap_Route implements Initializable
{
public function init ()
{
MVCLite::getInstance()->setRoute(new MVCLite_Request_Route_Standard());
}
}
Jetzt fehlt nur noch die eigentliche Bootstrap-Datei, welche die anderen Bootstrap-Dateien aufruft um die Applikation zu initialisieren.
// Bootstrap.php
class Bootstrap
{
private $_commands = array();
public function __construct ()
{
$this->_commands[] = new Bootstrap_PoweredBy();
$this->_commands[] = new Bootstrap_Route();
}
public function init ()
{
foreach($this->_commands as $obj)
{
$obj->init();
}
}
}
So einfach ist das Kommando-Entwurfsmuster implementiert. Es ist
sehr einfach erweiterbar. Man muss für jede neue Initialisierung eine
neue Klasse schreiben und sie dann lediglich einbinden. Natürlich kann
man so was automatisieren, aber das ist in diesem Fall irrelevant. Ein
weiterer Vorteil des Kommando-Entwurfsmuster ist das dynamische
Hinzufügen von Kommando-Objekten. Klingt auf jeden Fall gut.
Aber brauche ich diesen Effekt überhaupt? Ich bezweifle es. Und sind so
viele Klassen mit vielleicht ein paar Zeilen effektivem Code notwendig?
Das bezweifle ich auch. Also suchen wir eine andere Möglichkeit.
Wieso regeln wir das ganze nicht per Konvention? Überall hört man doch
vom Hype "Konvention über Konfiguration". Wie bewerkstelligt man so was
eigentlich? Es ist viel einfacher als man vllt. denken mag. Man denkt
sich eine Regel aus, nach der etwas passieren soll und implementiert
Code, die diese Regel ausführt. Meine Regel für die Bootstrap-Datei
lautet: Jede Methode, die mit "init" beginnt, soll beim Starten der
Applikation ausgeführt werden. Klingt einfach? Genau das ist die
Intention. Konventionen müssen einfach sein, sonst werden sie
kontraproduktiv. Oder muss man etwas mit
"initComponentXAfterComponentY" anfangen können? Das klingt kompliziert
und ist es auch. Solche Konstrukte zerstören die Vorteile von
Konventionen. Sie müssen von Natur aus einfach erlernbar und anwendbar
sein. Es ist nichts dagegen einzuwenden, Konventionen zu erweitern,
aber dadurch darf die Simplizität nicht beschädigt werden.
Hier ein Auszug aus der aktuellen Bootstrap-Datei, die dasselbe bewirkt wie der vorangegangene Code:
class Bootstrap
{
public function initPoweredBy ()
{
header('X-Powered-By: ' . MVCLite::NAME . ' ' . MVCLite::VERSION);
}
public function initRoute ()
{
MVCLite::getInstance()->setRoute(new MVCLite_Request_Route_Standard());
}
}
Man hat nun sämtliche Methoden für die Initialisierung in einer
Klasse. Zwar kann man die Initialisierungen zur Laufzeit zwar nicht
erweitern, aber das ist in meinen Augen nicht notwendig. Es ist nur
noch nötig eine weitere Methode hinzuzufügen und schon hat man seine
Bootstrap-Datei erweitert. Sollte es zu viel Code werden, könnte man
einige Initialisierungen auslagern, sodass die Klasse kompakter wird.
Aber das sind Probleme, die noch in weiter Ferne sind.
Die Implementation zum Aufrufen der Bootstrap-Datei könnte – Code wurde aus der aktuellen MVCLite.php entnommen - folgendermaßen aussehen:
public function bootstrap ()
{
// Code der Kürze halber weggelassen
foreach(get_class_methods($bootstrap) as $method)
{
if(substr($method, 0, 4) != 'init')
{
continue;
}
$bootstrap->$method();
}
}
Dies war ein konkretes Beispiel bei der man Konventionen verwenden
kann. Aber es gibt noch genügend andere Fälle, in denen dieses
Unterfangen nützlich sein kann. Es war auch ein Beispiel, das zeigte,
dass Entwurfsmuster einen Weg, aber nicht immer den optimalen Weg,
darstellen.
Ich will jedoch jetzt noch mit einem kleinen Beispiel aufwarten,
welches ich in der nächsten Zeit implementieren werde. Es geht dabei um
eine Klasse, die eine Zeile in der Datenbank darstellt. Jede der
Spalten einer Zeile ist einem bestimmten Schema unterworfen. Z.B.
dürfen INT-Felder nur Ziffern enthalten, die bis zu einem gewissen
Bereich gehen. Ich möchte, dass nur validierte Informationen in die
Datenbank gehen. Bisher habe ich so was durch eine validate()-Methode
realisiert, was für mich allerdings keine sonderlich elegante Lösung
darstellte. Deshalb habe ich mir nach dem letzten Brainstorming
vorgenommen, den Ansatz der Konventionen für Validierung zu nutzen. Die
Regel lautet: "Führe alle Methoden aus, die mit validate beginnen und
sammle deren etwaige Fehlermeldungen."
Es ist eine Tabelle mit folgenden Feldern gegeben:
- id (INT)
- name (VARCHAR(32))
- published (ENUM('Y', 'N'))
Daraus könnte folgender Codeschnipsel folgen:
public function validateId ($row)
{
$result = array();
if(isset($row['id']) && !is_numeric($row['id']))
{
$result[] = 'Ungueltige Id.';
}
return $result;
}
public function validateName ($row)
{
$result = array();
if(!isset($row['name']) || strlen($row['name']) < 3)
{
$result[] = 'Name ist zu kurz.';
}
else if(strlen($row['name']) > 32)
{
$result[] = 'Name ist zu lang.';
}
return $result;
}
public function validatePusblish ($row)
{
$result = array();
if(isset($row['published']) && !in_array($row['published'], array('Y', 'N')))
{
$result[] = 'Ungueltiger Status.';
}
return $result;
}
Eine validate()-Methode, die in der Vaterklasse implementiert ist, könnte so aussehen:
public function validate ()
{
$prefix = 'validate';
$len = strlen($prefix);
$row = $this->getColumns();
$result = array();
foreach(get_class_methods($this) as $method)
{
if($method == $prefix || substr($method, 0, $len) != $prefix)
{
continue;
}
foreach($this->{$method}($row) as $message)
{
$result[] = $message;
}
}
return $result;
}
Wie bereits erwähnt, ist dies nur ein weiteres praktisches Beispiel, wo man es sinnvoll einsetzen könnte. Ich würde jetzt gerne allgemeiner werden über solche Konventionen, aber außer der Tatsache, dass Konventionen einfach gehalten werde müssen, gibt es nichts Allgemeines über Konventionen zu sagen, denn sie können in sehr vielen Formen vorkommen. Z.B. ist das PEAR-Schema eine Konvention und die Regeln, die auf diese Schemata zutreffen, sind ganz andere als die bisher genannten. Man kann mit Konventionen auch zwei Entitäten verknüpfen, sodass der Name einer Tabelle aus dem Namen einer Klasse – und logischerweise auch umgekehrt – ableitbar sind. Beispiele gibt es viele und ich habe gerade mal an der Oberfläche gekratzt, sodass es noch viele Anwendungsmöglichkeiten gibt. Wer sich für dieses Thema interessiert, der sollte sich in den Code anderer exzellenter Entwickler vertiefen um weitere Anwendungsmöglichkeiten zu erfahren.