Suchen
Inside Wiki
Nützliche Links




 
phpforum.de bei Facebook
 
phpforum.de bei Twitter
 

Zurück   PHP Forum: phpforum.de > phpforum.de Wiki > phpforum.de Wiki

PHP Wiki Dieses Wiki sammelt Lösungen, zu Problemen, welche immer wieder im Forum auftauchen.

 
 
Artikel-Optionen Ansicht
  #1  

Standard API Design

 

Inhalte

Gutes API Design

Aufgrund der Tatsache, dass sich soviele Programmmierer in den unendlichen Möglichkeiten der OOP und den Design Patterns verhädern und schlussendlich gar manch prozeduraler Code einfacher zu warten wäre, gehen wir hier auf diese Problematik ein. Als Basis diente der englische Artikel (PDF) von Jeff Moore. Auf gehts:

Gängige Probleme mit Skripts und externen Bibliotheken:
- Macht nicht genau das, was wir brauchen
- Es macht zuviel
- Nicht konfigurierbar
- Schwierig davon abzuleiten (Vererbung)
- Zu wenig Support
- Zu wenig dokumentiert
- Unflexibler Aufbau
- API ändert zu oft

Demzufolge ist guter Code:
- Splittung der Aufgaben in Klassen und Methoden
- Konfigurierbar
- Dokumentiert
- Einfach verständlicher Aufbau (auch ohne Doc-Tags)
- Flexible Architektur
- Möglichst konstante API

Ein kleines Beispiel, das die Welt verbessern soll

Wir möchten eine (RSS-)Feed-Reader Komponente programmieren, die wir für alle Projekte unverändert übernehmen können.

PHP Quellcode:
class FeedReader
{}

$fr = new FeedReader();


Mögliche Probleme:
- Ein RSS-Feed einer externer URL wird gelesen
- Performance-Einbussen aufgrund langer Antwortzeiten

-> Basteln wir uns doch einen Cache:
PHP Quellcode:
class FeedReader
{
    protected $cache;

    function __construct()
    {
        $this->cache = new FeedCache();
    }
}

class FeedCache
{}

$fr = new FeedReader();


Wunderbar! Nun möchten wir den Cache mit folgenden Werten konfigurierbar haben:
- Maximales Alter des Caches
- Cache Verzeichnis

Also packen wir doch ein paar Konstanten mehr in unsere Konfigurationsdatei:
PHP Quellcode:
define('FEED_CACHE_DIR', '/cache');
define('FEED_CACHE_MAX_AGE', 3600); // 1 Stunde


Sieht hübsch aus, ist es aber nicht. Warum nicht?
- Global
- Was wenn für verschiedene Feeds verschiedene Konfigurationen gelten?
- Klasse kann nicht ohne Konstanten verwendet werden -> Abhängigkeit

Wir müssen also verschiedene Feeds unterschiedlich konfigurieren können - sprich pro Objekt eine Konfiguration ermöglichen. So vielleicht?
PHP Quellcode:
class FeedReader
{
    protected $cache;

    public function __construct($cacheDir, $maxAge)
    {
        $this->cache = new FeedCache($cacheDir, $maxAge);
    }
}

class FeedCache
{
    protected $cacheDir;
    protected $maxAge;

    public function __construct($cacheDir, $maxAge)
    {
        $this->cacheDir = $cacheDir;
        $this->maxAge = $maxAge;
    }
}

$fr = new FeedReader('/cache', 3600);


Juhu es funktioniert! Was gibts daran noch auszusetzen?
- Die Konstruktor von FeedReader wird mit Parametern gefüttert, die eigentlich gar nicht zu ihm gehören.
- FeedReader kann ohne FeedCache nicht mehr existieren -> Abhängigkeit
- Was wenn es mehrere Cache-Möglichkeiten wie z.B. Dateisystem, Datenbank, Memory o.ä. gibt?

Die Factory Methode

PHP Quellcode:
class FeedCache
{
    public static function factory($driver, $options = array())
    {
        $file = '/drivers/' . $driver . '.php';
        require_once $file;
        $classname = ucfirst($driver) . 'Cache';
        return new $classname($options);
    }
}

class CacheDriver
{
    protected $options;

    public function __construct(Array $options)
    {
        $this->options = $options;
    }
}

class FileCache extends CacheDriver
{}

class DatabaseCache extends CacheDriver
{}

class FeedReader
{
    protected $cache;

    protected $cacheDriver;

    protected $cacheOptions;

    public function __construct($cacheDriver, Array $cacheOptions = array())
    {
        $this->cache = FeedCache::factory($cacheDriver, $cacheOptions);
    }
}

$fr = new FeedReader('file', array('cacheDir' => '/cache', 'maxAge' => 3600));

Sieht ja schon richtig professionell aus...

Wir haben aber immer noch Probleme:
- FeedReader ist immer noch abhängig vom Cache
- Konstruktor vom FeedReader wird immer noch mit Parametern für den FeedCache gefüttert.
- Das $options Array kompliziert die ganze Sache und verunstaltet die API von FeedReader und FeedCache.
- Wir machen die Dateinamen von den Klassennamen abhängig
- Komplizierte Driver-Klassen werden schwierig zu integrieren sein
- Aufwändige Dokumentationsarbeit wird die Folge sein...

Das gelbe vom Ei ist es also nicht... (das heisst natürlich nicht, dass Factories schlecht sind!)

Gibt es noch eine andere Möglichkeit?

Wie wärs mit einer Klasse pro Cache-Konfiguration mittels Vererbung?
PHP Quellcode:
class FeedCache
{
    protected $maxAge;

    public function __construct($maxAge)
    {
        $this->maxAge = (int) $maxAge;
    }
}

class FileFeedCache extends FeedCache
{
    protected $cacheDir;

    public function __construct($maxAge, $cacheDir)
    {
        parent::__construct($maxAge);
        $this->cacheDir = $cacheDir;
    }
}

class FeedReader
{
    protected $cache;

    public function __construct()
    {
        $this->cache = $this->createCache();
    }

    protected function createCache()
    {
        return null;
    }
}

class FeedReaderWithFileCache extends FeedReader
{
    protected function createCache()
    {
        return new FileFeedCache('/cache', 3600);
    }
}

$fr = new FeedReaderWithFileCache();


Eleganter gehts also wirklich nicht. Oder?
- Das Problem der Abhängigkeit besteht weiterhin
- Komplizierter Aufbau
- Der Benutzer muss alle Kinder von FeedReader kennen
- Die Konfiguration ist nicht dem Objekt zugeordnet

Irgendwie kommen wir nicht weiter...

Back to the roots

PHP Quellcode:
class FeedReader
{
    protected $cache;

    function __construct()
    {
        $this->cache = new FeedCache();
    }
}

class FeedCache
{}

$fr = new FeedReader();

Wie können wir den FeedCache konfigurieren?

Woran scheiterten wir?

- FeedCache durch FeedReader konfigurieren ist schlecht
- FeedCache über FeedReader erstellen lassen ist schlecht
- Für jeden Cache eine eigene Kinderklasse von FeedReader ist schlecht

Die Lösung

Kopple die Klassen voneinander und füttere den FeedReader mit einem beliebigen Cache mit einem fixen Interface:
PHP Quellcode:
interface Cache
{}

class FileCache implements Cache
{
    protected $cacheDir;
    protected $maxAge;

    public function __construct($cacheDir, $maxAge)
    {
        $this->cacheDir = $cacheDir;
        $this->maxAge = $maxAge;
    }
}

class DbCache implements Cache
{
    //...
}

class NoCache implements Cache
{
    //...
}

class FeedReader
{
    protected $cache;

    function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }
}

$fr = new FeedReader(new FileCache('/cache', 3600));


Alle Probleme gelöst?

Haben wir einen einfachen Aufbau? Ja.
Sind alle Klassen und deren Objekte individuell Konfigurierbar? Ja.
Sind alle Abhängigkeiten zwischen den verschiedenen Cache-Systemen und dem FeedReader beseitigt? Ja.
Funktioniert der FeedReader auch ohne Cache? Ja, mithilfe der NoCache Klasse. Aber wäre da folgendes nicht Einfacher?
PHP Quellcode:
function __construct(Cache $cache = null)

Möglich, jedoch schaffen wir wieder Abhängigkeiten: Der FeedReader dürfte dann nur die Cache-Funktionen aufrufen, wenn sein $cache nicht null ist. Wenn wir all diese Prüfungen uns ersparen können und diese in der NoCache Klasse handeln können, sollten wir dies ev. auch tun! Denn: Low Coupling ist auch für Unit-Tests von Vorteil. Man sollte immer zusehen, dass seine Komponenten soweit wie möglich beim Testen auch nur mit Mock-Objekten auskommen.

Wie verhält sich das ganze wenn wir keine selbst geschriebene Cache-Klasse verwenden möchten?
PHP Quellcode:
class 3rdPartyCache
{
    //...
}

class CacheAdapter extends 3rdPartyCache implements Cache
{}

Und schon haben wir einen externen Cache, den unser FeedReader verwenden kann.

Ok, soweit sollten wir die optimale Lösung gefunden haben.

Aber was ist mit den Dingern hier?

- Singleton
- Registry

Gehen wir davon aus wir möchten alle Errors unseres FeedReaders mithilfe eines zentralen Loggers festhalten. Versuchen wir es doch mal mit der statischen Registry und dem Singleton:

PHP Quellcode:
class FeedReader
{
    protected $cache;
    protected $logger;

    function __construct(Cache $cache)
    {
        $this->cache = $cache;
        $this->logger = Registry::get('logger');
    }
}

PHP Quellcode:
class FeedReader
{
    protected $cache;
    protected $logger;

    function __construct(Cache $cache)
    {
        $this->cache = $cache;
        $this->logger = Logger::getInstance();
    }
}

Fällt Dir etwas auf?

Wo liegt der Unterschied zu den folgenden Kandidaten, die uns Probleme verschafft haben?
PHP Quellcode:
// $_GLOBALS
$_GLOBALS['cacheDir'] = '/cache';
...
$this->cacheDir = $_GLOBALS['cacheDir'];

// KONSTANTEN
define('CACHE_DIR', '/cache');
...
$this->cacheDir = CACHE_DIR;

// global
$logger = new Logger();
...
global $logger;
$this->logger = $logger;


Ausser dem OOP-Kleidchen ist hier kein wirklicher Unterschied festzustellen. Wie wir sehen schaffen wir mit statischen Registries, Singletons o.ä. keine wirkliche Abhilfe, obwohl diese durchaus sehr bequem sein können. Die Frage ist auch, ob GLOBAL wirklich böse ist oder nicht. Mit einem einfachen JA/NEIN ist dies nicht zu beantworten und kann von Fall zu Fall verschieden sein. Grundsätzlich kann aber durchaus gesagt werden, dass der Namensraum jeweils so klein wie möglich gehalten werden sollte. Das Hauptproblem der statischen Registries ist, dass sie eben statisch sind - sprich ihre statischen Eigenschaften sind eben nicht an ein Objekt gebunden, da es kein Objekt gibt. Man kann es also wirklich mit global vergleichen.

Zurück zu unserem Beispiel. Wir möchten den FeedReader mit dem Logger erweitern. Warum machen wir das nicht einfach genau so mit dem Cache?
PHP Quellcode:
class FeedReader
{
    protected $cache;
    protected $logger;

    function __construct(Cache $cache, Logger $logger)
    {
        $this->cache = $cache;
        $this->logger = $logger;
    }
}

$fr = new FeedReader(
        new FileCache('/cache', 3600),
        new Logger()
      );


Soweit so gut – eine einfache, klare Sache! Natürlich könnte man den Logger-Parameter hier auch als optional deklarieren oder wie beim Cache mit einer NoLogger Klasse arbeiten.

Gehen wir weiter... wo wird der FeedReader überhaupt instanziert? Phu... das könnte z.B. irgendwo in einem Controller (siehe MVC) sein. Aber... die anderen Controller und z.B. deren SOAP-Reader verwenden vielleicht ja auch diesen Logger?! Muss ich den Logger dann durch alle ActionController durchschleusen? Also so?
PHP Quellcode:
interface Logger
{}

class MyLogger implements Logger
{}


class Dispatcher
{
    //...
    $logger = new MyLogger();
    $actionControllerClass = 'NewsController';
    $this->actionController = new $actionControllerClass($logger);
    //...
}

class NewsController extends ActionController
{
    private $logger;

    public function __construct(Logger $logger)
    {
        parent::__construct();
        $this->logger = $logger;
    }

    public function test()
    {
        $feedReader = new FeedReader(
                          new FileCache('/cache', 3600),
                          $this->logger
                      );
    }
}


Im Gegensatz zur statischen Registry:
PHP Quellcode:
public function test()
    {
        $this->feedReader = new FeedReader(
                              new FileCache('/cache', 3600),
                              Registry::get('logger');
                            );
    }

entsteht wie wir sehen ein Mehraufwand, da alle Parameter, die von mehreren Objekten verwendet werden, immer mitgeschleift werden müssen. Im obigen Falle wird der Logger sogar übergeben, obwohl ev. ein aufgerufener ActionController ihn gar nicht braucht. Nungut, lieber etwas zuviel übergeben als etwas zu wenig. Aber irgendwie ist das immer noch nicht 100% befriedigend...

Moment mal... probieren wirs doch mal mit einer nicht-statischen Registry.
PHP Quellcode:
interface Logger
{}

class MyLogger implements Logger
{}

class Dispatcher
{
    //...
    $registry = new Registry();
    $registry->set('logger', new MyLogger());

    $actionControllerClass = 'NewsController';
    $this->actionController = new $actionControllerClass($registry);
    //...
}

class NewsController extends ActionController
{
    private $registry;

    public function __construct(Registry $registry)
    {
        parent::__construct();
        $this->registry = $registry;
    }

    public function test()
    {
        $feedReader = new FeedReader(
                              new FileCache('/cache', 3600),
                              $this->registry->get('logger')
                          );
    }
}

Der Vorteil liegt auf der Hand: Der NewsController hat nichts mehr mit dem Logger direkt zu tun. Er erhält lediglich eine Registry mit einem Haufen an Objekten die beliebig verwendet werden können. Wir kommen der Sache also langsam näher, denn auch der globale Raum ist verschwunden, den wir bei der statischen Registry bemengelten. Und? Stört überhaupt noch etwas? Eigentlich nicht. Testen wir mal ob das Konzept auch mit einem DbCache, der eine allgemeine Datenbank-Verbindung verwenden soll, hält:

PHP Quellcode:
interface Logger
{}

class MyLogger implements Logger
{}

interface DB
{}

class MyDB implements DB
{}

class DbCache implements Cache
{
    private $db;
    private $maxAge;

    public function __construct(DB $db, $maxAge)
    {
        $this->db = $db;
        $this->maxAge = $maxAge;
    }
}

class Dispatcher
{
    //...
    $registry = new Registry();
    $registry->set('logger', new MyLogger());
    $registry->set('db', new MyDB());

    $actionControllerClass = 'NewsController';
    $this->actionController = new $actionControllerClass($registry);
    //...
}

class NewsController extends ActionController
{
    private $registry;

    public function __construct(Registry $registry)
    {
        parent::__construct();
        $this->registry = $registry;
    }

    public function test()
    {
        $feedReader = new FeedReader(
                              new DbCache($this->registry->get('db'), 3600),
                              $this->registry->get('logger')
                          );
    }
}

Scheint also wirklich zu klappen, eine saubere Sache

Konfiguration

Was ist wenn wir die maximale Cache-Zeit in einem allgemeinen Config-Objekt unterbringen möchten, damit wir uns nicht immer durch den Code der Controller durchschlagen müssen um die Zeit anzupassen?
PHP Quellcode:
class Dispatcher
{
    //...
   
    $config = new Config();
    $config->set('feed_cache_max_age', 3600);

    $registry = new Registry();
    $registry->set('config', $config);
    $registry->set('logger', new MyLogger());
    $registry->set('db', new MyDB());

    $actionControllerClass = 'NewsController';
    $this->actionController = new $actionControllerClass($registry);
    //...
}

class NewsController extends ActionController
{
    private $registry;

    public function __construct(Registry $registry)
    {
        parent::__construct();
        $this->registry = $registry;
    }

    public function test()
    {
        $feedReader = new FeedReader(
                              new DbCache($this->registry->get('db'),
                                          $this->registry->get('config')->get('feed_cache_max_age')),
                              $this->registry->get('logger')
                          );
    }
}

Scheint also auch kein Problem zu sein. Die FeedReader API bleibt immer noch unangetastet!

Weitere Gedanken

Muss der NewsController überhaupt wissen, was der FeedReader benötigt bzw. ob der jetzt einen Logger braucht und welchen Cache er wie verwenden soll? Muss das Sache des NewsController sein im Falle einer Allgemeinen Konfiguration? Wenn nicht, wie wäre dies zu lösen? Gehen wir mal davon aus, das auch ein allgemeiner Cache verwendet werden soll. Z.B. ein DbCache. Dieser ist also unabhängig vom FeedReader und gilt für die gesamte Applikation.

Dependency Injection / IoC Container / Component Manager
PHP Quellcode:
interface Logger
{}

class MyLogger implements Logger
{}

class Dispatcher
{
    //...
    $config = new Config();
    $config->set('cache_max_age', 3600);

    $di = new DIContainer();
    $di->register('Config', $config);
    $di->register('Logger', new MyLogger());
    $di->register('DB', new MyDB());
    $di->register('Cache',
                  new DbCache($di->getInstance('DB'),
                      $di->getInstance('Config')->get('cache_max_age')
                  )
                 );

    $actionControllerClass = 'NewsController';
    $this->actionController = new $actionControllerClass($di);
    //...
}

class NewsController extends ActionController
{

    private $di;

    public function __construct(DIContainer $di)
    {
        parent::__construct();
        $this->di = $di;
    }

    public function test()
    {
        $feedReader = $this->di->getInstance('FeedReader');
    }
}


Wie funktioniert dieser DIContainer?
Sobald ein Objekt von "FeedReader" via DIContainer mittels getInstance('FeedReader') angefordert wird, ermittelt dieser mittels Reflection, welche Parameter die Klasse erwartet. Wenn alle erwarteten Parameter im DIContainer abgespeichert sind, kann er diese beim Instanzieren der Klasse "FeedReader" automatisch dessen Konstruktor übergeben. Das dazugehörige Script:
PHP Quellcode:
class DIContainer
{
    private $container;

    public function __construct()
    {
        $this->container = array();
    }

    public function register($name, $object)
    {
        $this->container[$name] = $object;
    }

    public function getInstance($name)
    {
        $args = array();
        $rc = new ReflectionClass($name);
        foreach ($rc->getConstructor()->getParameters() as $key => $param) {
            if (!$pc = $param->getClass()) {
                // throw Exception etc.
                return false;
            }
            $pcn = $pc->getName();
            if (!isset($this->container[$pcn])
             || !$this->container[$pcn] instanceof $pcn) {
                // throw Exception etc.
                return false;
            }
            $args[$key] = $this->container[$pcn];
        }
        return $rc->newInstanceArgs($args);
    }
}

Die übergabe via Konstruktoren ist nur eine von vielen Möglichkeiten. Richtige DIContainer bieten da reichlich mehr Varianten. Sogar die Möglichkeit, dass der DIContainer von sich aus fehlende Objekte erzeugt, ist nicht selten. Zend_Di vom Zend Framework befindet sich derzeit noch in der Entwicklungsphase. Eine weitere OpenSource-Variante ist der ComponentManager des Typo3 Frameworks Flow3.

Fazit

Nicht-statische Registries und DI Container sind zwar ähnlich und doch verschieden. Die DI Varianten der obig genannten Frameworks versprechen viel – für die ZF-Freunde ist es im Moment zwar noch etwas zu früh, jedoch sollte man sie im Auge behalten.

Lesenswertes

How to Design a Good API and Why it Matters
Procata.com/talks
Inversion of Control Containers and the Dependency Injection pattern

OFFTOPIC:

Bestimmt der längste Artikel im Wiki Auch ist es kein einfaches Thema. Über eine Diskussion und Kritik würde ich mich sehr freuen!


Mitwirkende: puritania
Erstellt von puritania, 28.03.2008 am 15:04
Zuletzt bearbeitet von puritania, 02.05.2008 am 19:02
0 Kommentare , 6316 Betrachtungen

Dieser Text steht unter der GNU-Lizenz für freie Dokumentation


 

Lesezeichen

Stichworte
oop

Artikel-Optionen
Ansicht

Forumregeln
Es ist Ihnen nicht erlaubt, neue Themen zu verfassen.
Es ist Ihnen nicht erlaubt, auf Beiträge zu antworten.
Es ist Ihnen nicht erlaubt, Anhänge hochzuladen.
Es ist Ihnen nicht erlaubt, Ihre Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.

Gehe zu
Ähnliche Themen
Thema Autor Forum Antworten Letzter Beitrag
Gute Lösung? B4bUcK3 PHP 6 25.11.2007 18:45
Gute Links zu PHP nils_langner Sonstiges 5 04.11.2007 20:48
[OOP]Gute PHP Quellcodes Thomas B Sonstiges 3 25.09.2007 09:00
Suche gute IDE maddinel Editoren & Entwicklungsumgebungen 5 17.09.2005 14:42
gute XML Klasse Sentinel Skriptsuche 1 30.06.2005 16:39


Alle Zeitangaben in WEZ +2. Es ist jetzt 13:23 Uhr.


Powered by vBulletin® Version 3.8.8 (Deutsch)
Copyright ©2000 - 2018, Jelsoft Enterprises Ltd.
Powered by NuWiki v1.3 RC1 Copyright ©2006-2007, NuHit, LLC