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

Logger injizieren

Im Buch Software-Sanierung: Weiterentwicklung, Testen und Refactoring bestehender Software wird unter anderem eine interessante Technik beschrieben, Abhängigkeiten durch Ableitung aufzulösen (Object Seam, Kapitel 11). Prinzipiell entsteht zwar eine Abhängigkeit durch die Einführung einer Ableitung; wenn allerdings die abgeleitete Klasse abhängig von einer anderen (oder wie hier von einem externen Dienst) ist, dann kann man in der abgeleiteten Klassen alle relevanten Methoden überschreiben, ein anderes Verhalten implementieren und so die bestehende Abhängigkeit auflösen. Instanzen der abgeleiteten Klasse lassen sich dann natürlich überall dort injizieren, wo bislang nur die ursprüngliche Klasse verwendet wird.

Im Buch wird diese Technik an einem Anwendungsfall demonstriert, bei dem eine Abhängigkeit zu einem LDAP-Dienst besteht, der nicht direkt getestet werden kann. Es wird eine Ableitung dieser Klasse implementiert und dadurch eine Testschicht für einen Unittest eingezogen. In den überschriebenen Methoden können dann beispielsweise die Zugangsdaten für die connect()-Methode überprüft werden, und der eigentlich Connect zum LDAP-Server kann umgangen werden (aufgelöste Abhängigkeit).

Dieselbe Technik für das Debugging

Das Codebeispiel im Buch ist in Java programmiert. Ich möchte hier die grundsätzlichee Technik kurz mit PHP skizzieren und zeigen, wie sie - im Gegensatz zum Unittesting - für das Debugging gute Dienste leisten kann.

Als Beispiel dient hier eine Datenbankverbindung. Das Codebeispiel ist so einfach wie möglich gehalten und benutzt deshalb kein bekanntes Framework, damit das allgemeine Prinzip deutlich wird. Es geht dabei nicht darum, eine Datenbankverbindung zu implementieren. Eine Datenbankklasse als Beispiel zu verwenden, ist natürlich ein "alter Hut". Das wird aber sicherlich helfen, eben das zu ignorieren, und das Wesentliche, nämlich die Technik, um die es hier geht, zu sehen.

Szenario

Bei einer bestehenden Anwendung kommt es immer wieder zu Verbindungsproblemen mit der Datenbank. Die Anwendung ist gewachsen und besitzt keine vernünftige Fehlerbehandlung und keinen Logging-Mechanismus. Die Verbindungprobleme treten unvorhersehbar auf - ein Entwickler "debuggt" die Anwendung mehrfach, kann aber keinen Fehler finden, weil die Verbindung während der Fehlersuche funktioniert. Das Ziel ist es hierbei, den Verbindungsaufbau zur Datenbank zu protokollieren, um einen Fehler aufzuspüren.

Lösungsansatz

Was wir brauchen, ist das automatische "Einfangen" und protokollieren des Fehlers. Die Lösung besteht grundsätzlich darin, den Verbindungsaufbau zu überprüfen (geschieht in der vorliegenden Anwendung nicht und nur unzureichend) und dann die Situation zu protokollieren. Das entstehende Logfile kann dann untersucht werden, um der Ursache der unregelmäßig auftretenden Verbindungsprobleme auf die Spur zu kommen.

Naive Umsetzung

Wir wollen davon ausgehen, dass niemand in dieser Situation, die schnell gelöst werden muss, die gesamte Anwendung mit einer ordentlichen Fehlerbehandlung versieht und vollständig umbaut. Naheliegend ist es dagegen, den bestehenden Code vorübergehend "aufzubohren", um die auftretenden Fehler zu protokollieren. Das würde bedeuten, die Fehlerbehandlung und -protokollierung in die Datenbankklasse hinein zu programmieren. Das ist nicht optimal: Die zentrale Datenbankklasse wird aus Zeitnot heraus verändert (es gibt ein akutes Problem); folglich wird der erweiterte Code nicht so durchdacht sein, als wenn man die Erweiterung in Ruhe durchführen könnte, und vielleicht führt er sogar zu neuen Problemen (Was passiert zum Beispiel, wenn das Logfile nicht schreibbar ist?). Nicht schlimm ist diese Vorgehensweise, wenn der Code nicht wieder entfernt oder refaktorisiert wird, sobald das Verbindungsproblem behoben ist. Die Versuchung ist allerdings groß, das eingebaute Logging drin zu lassen. Später wird vielleicht an anderer Stelle ein Logging eingeführt, und die Anwendung degeneriert weiter ...

Lösung durch Ableitung

In einem Satz ausgedrückt: Datenbankklasse ableiten, Verbindungsmethode überschreiben, dort auf Fehler prüfen und protokollieren, und die abgeleiteten Klasse vorübergehend im System verwenden (injizieren). Wie sieht das aus? Hier der PHP-Code:

Bestehende Anwendung

class DB
{
  public function __construct()
  {
    //...
  }

  public function getErrorMessage()
  {
    //...
  }

  public function connect($host, $user, $passwd)
  {
    //...
  }  
}

class Application
{
  public function initDatabase(DB $db)
  {
    //Anm.: Die Verbindung schlächt manchmal fehl; keiner weiß, warum ...
    $db->connect();
  }
  
  public function run()
  {
    //...
  }
}

//main
$db = new DB();
$app = new Application();
$app->initDatabase($db);
$app->run();

Einen Logger injizieren

interface Loggable
{
  public function log($message);
}

class FileLogger implements Loggable
{
   public function log($message)
   {
     // $message an Logfile hängen
   }
}

class LoggingDB extends DB
{
  protected $_logger;

  public function __construct(Loggable $logger)
  {
    $this->_logger = $logger;
    parent::__construct();
  }
  
  public function connect($host, $user, $passwd)
  {
    if (!parent::connect($host, $user, $passwd)) {
      $errorMessage = $this->getErrorMessage();
      $errorMessage .= " (host=$host, user=$user, passwd=$passwd)";
      $this->_logger->log($errorMessage);
    }
  }
}

//main
$db = new LoggingDB(new FileLogger());  // <== Einzige Änderung
$app = new Application();
$app->initDatabase($db);
$app->run();

Fazit

Lediglich durch Ableitung und Instanziierung der abgeleiteten Klasse anstelle der Basisklasse wird das Logging hinzugefügt. Das bestehende System wird nicht verändert. Sobald die Ursache der Verbindungsprobleme auf diese Weise gefunden und beseitigt wurde, kann wieder die ursprüngliche Datenbankklasse verwendet werden. Alle erfolgten Änderungen sind dann lediglich zur Beseitigung des Problems erfolgt, und die Anwendung bleibt frei von Ad-Hoc-Debugging-Code.

Sollte man tatsächlich ein solches Problem in einer Zend-Framework-Anwendung haben und diesen Lösungsansatz darauf übertragen, dann würde man den Logger von Zend_Db_Adapter_Abstract ableiten und darin den eigentlichen Adapter kapseln (Decorator). Das müsste man so tun, damit die Factory()-Methode in Zend_Db benutzt werden kann und die Lösung transparent funktioniert. Das eigentliche Logging würde man natürlich mit Zend_Log realisieren oder bei Bedarf Zend_Log_Writer_Abstract selbst implementieren.

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