Kontakt
Telefon +49 2161 17 58 83
Mobil +49 179 72 66 112
E-Mail info@ulrich-borchers.de

Abhängigkeiten mit der Factory-Methode reduzieren

In diesem Artikel möchte ich an einem Beispiel zeigen, wie das Pattern 'Factory Method' eingesetzt werden kann, um die Abhängigkeiten zwischen Schichten zu reduzieren und zusätzliche Flexibilität zu schaffen.

Stellen wir uns einen objektorientierten Wrapper für Datenbanktabellen vor. Im Zend Framework sind das die Klassen, die von Zend_Db_Table_Abstract abgeleitet werden. Der Einfachheit und Unabhängigkeit halber soll diese Basisklasse ganz einfach 'Table' heißen. Die Wrapperklassen werden von der Basisklasse abgeleitet und benennen im Wesentlichen Primärschlüssel und Tabellenname. Die Funktionalitäten für Lesen, Schreiben, Ändern und Löschen (CRUD) wird von der Basisklasse bereit gestellt.

Ich möchte die Factory-Methode und den Vorteil deren Verwendung an einem konkreten Beispiel zeigen, nämlich der Implementierung von Checklisten: Eine Checkliste hat Items, die abzuhaken sind. Eine solche Checkliste kann mehrfach instanziiert werden. Dazu ein Beispiel für das Beispiel: Bevor ein Flugzeug startet, wird eine umfangreiche Checkliste durchgegangen. Die einzelnen Punkte auf dieser Liste sind deren Items: Funktioniert der Höhenmesser? Befinden sich links und rechts jeweils eine Tragfläche? usw. Diese Checkliste kann instanziiert werden: Vor jedem Start entsteht eine Instanz. Die Checkliste wurde für Flug A-123 2010-08-10 12:18 durchgegangen, für Flug OSBL-666 2010-09-11 11:14 usw. - das sind jeweils die Instanzen.

Es existieren dazu vier Tabellen: Eine für die Checklisten, eine für deren Items, eine für Instanzen der Checklisten und schließlich eine für Instanzen der Items. Der objektorientierte Wrapper besteht dann aus vier Klassen, die alle von 'Table' abgeleitet sind: Table_List, Table_List_Items, Table_List_Instance und Table_List_Instance_Items.

Die Implementierung des Wrappers:

abstract class Table
{
    protected $_db;
    protected $_primaryKey = 'id';
    protected $_tableName;
    
    public function __construct(DB_Interface $db)
    {
        $this->_db = $db;
    }
  
    public function insert($row)       { /* ... */ }
    public function update($id, $data) { /* ... */ }
    public function delete($id)        { /* ... */ }
    public function get($id)           { /* ... */ }
}

class Table_List extends Table
{
    $_tableName = 'list';
}

class Table_List_Items extends Table
{
    $_tableName = 'list_items';
}

class Table_List_Instace extends Table
{
    $_tableName = 'list_instance';
}

class Table_List_Instance_Items extends Table
{
    $_tableName = 'list_instance_items';
}

Nehmen wir an, es gibt eine Klasse, die alle fachlichen Verantwortlichkeiten für die Verwaltung der Checklisten vereint. Diese Klasse kapselt den objektorientierten Wrapper für die Datenbank und sieht so aus:

class List_Manager
{
    public function createList($caption, array $items)
    {
        $db = Registry::get('db');
        $table = new Table_List();
        $id = $table->insert(array('caption' => $caption));
        if (!$id) {
            return false;
        }
        return $this->createListItems($id, $items);
    }
    
    public function createListItems($listId, array $items)
    {
        $db = Registry::get('db');
        $table = new Table_List_Items();
        foreach ($items as $caption) {
        if (!$table->insert(array('list_id' => $listId, 'caption' => $caption))9 {
            return false;
        }
        return true;
    }
    
    public function createListInstance($caption, $listId)
    {
    	$db = Registry::get('db');
    	$table = new Table_List_Instance_Items();
    	// ...
    }
}

So weit die gängige Vorgehensweise.

Statische Struktur mit direkten Abhängigkeiten:

List_Manager mit direkten Abhängigkeiten

Dass die Klasse List_Manager die Benutzung des Wrappers zentralisiert, ist wartungsfreundlich. Im Diagramm wird jedoch deutlich, dass sie Abhängigkeiten zu ALLEN Tabellenklassen besitzt. Bei Änderungen an den Tabellenklassen müssen auch immer Änderungen an List_Manager in Betracht gezogen werden - das betrifft nicht nur die Benennung der einzelnen Klassen, sondern deren - gegebenenfalls individuell gestaltete - Schnittstellen. Letzteres ist besonders ungünstig für die Flexibilität.

Durch Einführung einer Factory-Methode in der Basisklasse lässt sich die Instanziierung der einzelnen Tabellenklassen vereinheitlichen und vereinfachen:

abstract class Table
{
    //...
    
    /**
     * @return Table
     */
    public static function factory($identifier)
    {
        switch ($identifier) {
            case 'list':
                return new Table_List($this->_db);
                break;
            case 'list_items':
                return new Table_List_Items($this->_db);
                break;
            case 'list_instance':
                return new Table_List_Instance($this->_db);
                break;
            case 'list_instance_items':
                return new Table_List_Instance_Items($this->_db);
                break;
            default:
                throw new Exception('invalid type');
        }
    }
}

Die Factory-Methode lässt sich in PHP natürlich flexibler implementieren. In dieser Form ist sie jedoch für dieses Beispiel anschaulicher. Entscheidend ist, dass der Code in List_Manager auf diese Weise einfacher wird und dass keine direkte Abhängigkeit mehr von den konkreten Tabellenklassen besteht. Die Klassen können umbenannt und der Initialisierungscode wird so zusammengefasst und kann bei Bedarf später zentral verändert werden, falls notwendig.

Aufgelöste Abhängigkeiten durch factory()-Methode:

Reduzierte Abhängigkeit mit factory()-Methode

Je mehr Wrapperklassen es gibt und je umfangreicher List_Manager dadurch wird, umso mehr wird sich diese Entkopplungstechnik bewähren. List_Manager sieht in diesem einfachen Beispiel bei Verwendung der Factory-Methode zwar noch recht ähnlich aus wie zuvor. Trotzdem lässt sich die gewonnene Flexibilität erahnen. Außerdem sind die Abhängigkeiten zu den Klassen Registry und DB_Interface ($db ist eine Instanz davon) rückstandsfrei eliminiert!

class List_Manager
{
    public function createList($caption, array $items)
    {
        $table = Table::factory('list');
        $id = $table->insert(array('caption' => $caption));
        if (!$id) {
            return false;
        }
        return $this->createListItems($id, $items);
    }
    
    public function createListItems($listId, array $items)
    {
        $table = Table::factory('list_items');
        foreach ($items as $caption) {
        if (!$table->insert(array('list_id' => $listId, 'caption' => $caption)) {
            return false;
        }
        return true;
    }
    
    public function createListInstance($caption, $listId)
    {
    	$table = Table::factory('list_instance_items');
    	// ...
    }
}

Nun können wir die Factory-Methode mächtiger gestalten, indem wir für Table ein zusätzliches Interface einführen. Das sieht auf den ersten Blick überkompliziert aus, aber es eröffnet weitere Flexibilität:

interface Table_Interface
{
    public function insert($row);
    public function update($id, $data);
    public function delete($id);
    public function get($id);
}

class Table implements Table_Interface
{
    //...
}

class Table
{
    //...
    
    /**
     * @return Table_Interface
     */
    public static function factory($identifier)
    {
        //....
    }
}

Da wir hier PHP programmieren, ist der Rückgabetyp 'Table_Interface' natürlich nur eine Konvention. Es wird aber dadurch deutlich, dass die Factory-Methode nicht zwangsläufig eine (abgeleitete) Instanz der Klasse 'Table' zurück geben muss. Insofern sich die Implementierung von List_Manager an diesem Interface orientiert, kann die Factory-Methode nun beliebige Implementierungen zurück geben, zum Beispiel:

class OldTableManager implements Table_Interface
{
    //...
}

abstract class Table
{
    public static function factory($identifier)
    {
        switch ($identifier) {
	            case 'list':
	                return new OldTableManager();
	                break;
	            
	            //...
        }
    }
}

Um dieses Konzept durchzusetzen (nicht jeder Programmierer hält das Konzept 'Schnittstelle' oder die Verwendung der Factory-Methode konsequent durch :-P ), forcieren wir die Schnittstelle Table_Interface durch Verwendung von final für die Methoden der Basisimplementierung. Außerdem unterbinden wir die direkte Instanziierbarkeit der Tabellenklassen, indem wir den Konstruktor als protected deklarieren:

abstract class Table
{
    //...
    
    protected function __construct(DB_Interface $db)
    {
        $this->_db = $db;
    }
    
    public static function factory($identifier)
    {
        //...
    }
  
    final public function insert($row)       { /* ... */ }
    final public function update($id, $data) { /* ... */ }
    final public function delete($id)        { /* ... */ }
    final public function get($id)           { /* ... */ }
}

Nun können alle konkreten Tabellenklassen nur noch mit der Factory-Methode instanziiert werden. Außerdem ist sichergestellt, dass die Schnittstelle der CRUD-Methoden immer exakt gleich ist. Dies verhindert also, dass diese Methoden individuell überschrieben werden. Es kann kein Wildwuchs Einzug halten, da die Veränderung der Parameterlisten nicht mehr möglich ist. Wie schon erwähnt, können wir mit PHP nicht die Typisierung erzwingen, so dass in List_Manager wirklich nur das Table_Interface benutzt werden könnte. Trösten wir uns damit, dass auch in Java dieses Konzept mittels Typecast leicht durchbrochen werden kann.

Trotzdem ist es in PHP möglich, die Einhaltung der Schnittstelle sicher zu stellen, wenn auch mit einem Trick, der für produktive Umgebungen aufgrund zu erwartender Performanceeinbußen nicht zu empfehlen ist. Nachfolgende Wrapperklasse könnte aber durchaus während der Entwicklung eingesetzt werden. Sie wieder auszubauen ist dank Factory-Methode ganz leicht möglich:

class Table_Debug_Wrapper implements Table_Interface
{
    private $_allowedMethods = array('insert', 'delete', 'update', 'get');
    private $_table;

    public function __construct(Table_Interface $table)
    {
        $this->_table = $table;
    }
    
    public function __call($method, $args)
    {
    	if (!in_array($method, $this->_allowedMethods)) {
    	    throw new Exception('Nono.');
    	return call_user_func_array(array($this->_table, $method), $args);
    }
}

abstract class Table
{
    //...
    
    /**
     * @return Table_Interface
     */
    public static function factory($identifier)
    {
        $db = Registry::get('db');
        switch ($identifier) {
            case 'list':
                return new Table_Debug_Wrapper(new Table_List($db));
                break;
            //...
        }
    }
}

Fazit

Die Factory-Methode hat sich in PHP bewährt, um die konkrete Implementierung einer Schnittstelle zu abstrahieren. Insbesondere in Datenbankadaptern wird das Pattern bevorzugt eingesetzt. Ich hoffe, dass ich in diesem Artikel zeigen konnte, wie das Pattern 'Factory Method' eingesetzt werden kann, um Abhängigkeiten zwischen Schichten zu reduzieren, mehr Flexibilität zu schaffen und Schnittstellen zu forcieren.

Ich möchte abschließend noch erwähnen, dass ich eine neue Abhängigkeit eingebaut habe, die unschön ist. Wer sie erkennt, möge mir doch gerne Bescheid geben. Ich freue mich darauf, über Möglichkeiten zu diskutieren, wie diese Unstimmigkeit ausgeräumt werden kann. Gute Vorschläge werde ich gerne erwähnen und auch darstellen!

Zurück

Hands On PHP

Harter Hut

Programmiertes - Jenseits von Prosa.

Zend Certified Engineer
Zend Certified Engineer ZF
Oracle Certified Professional, MySQL 5.6 Developer
Sun Certified Java Programmer (SCJP)
Sun Certified Web Component Developer (SCWCD)
RSS
tl_files/open_clip_art/symbole/rss-icon.png  Abonnieren