Kapselung durch Konvention

Friday, 28 September 2007, 08:55 von Blackflash

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:

  1. id (INT)
  2. name (VARCHAR(32))
  3. 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.

Kommentare


Kommentiere!

Your Name:


Your Email:


Your URL:


Spam Prevention:
Enter the text above into the box below.
If you are unable to read it, refresh the page.


Your Comment: