Sven Rautenberg: OOP php: verschachtelte Klassen und ein Zugriff auf eine Eigensc

Beitrag lesen

Moin!

Anders Beispiel:
ich habe eine Klasse gebaut, die aus einer ASCII-Datei ein Objekt erstellt. Diese Ascii-Datei ist eine Art Steuerdatei um Einstellungen (Klimamodell) über unterschiedliche Plattformen und Programmiersprachen auszutauschen. Sie ist untergliedert durch Steuerzeichen, wie "#", "##", "&", EOL,...
Es ist ein Austauschformat, was wir definiert haben.

Die Klasse liest die Datei und erstellt ein php-Objekt daraus. Ebenso ist es fähig, diese Datei wieder zu schreiben. Manipulieren einer Steuerdatei würde es treffen. DIESE Klasse soll standalone fähig sein, da die spätere Verwendung in meinem CMS nicht zwingend ist. Sie liest also die ASCII Datei und prüft auf um die 40 Konsistenzfehler (neben Datei vorhanden auch: Anzahl Blöcke wie erwartet?, Anzahl Spalten in jeder Zeile gleich?, Zelle enthält Wert/Array?). Wenn eine(!) Exception anspringt wird die Eigenschaft des Objektes mit den Daten gelöscht und die Exception landet im ExceptionHandler(). Von dort delegiere ich sie an meine Site. Ich schreibe in den ExceptionHandler() groß rein:

"Achtung: hier bitte persönliche Einstellungen vornehmen, blabla..."

Ich habe das so gemacht, wie es da stand mit GLOBAL $Site;

Der Vorteil: die Klasse kann jeder verwenden und muss nur an einer einzigen Stelle (im ExceptionHandler()) Änderungen vornehmen. Notfalls liefere ich die Klasse aus mit echo $e; im ExceptionHandler().

Das ist genau der Punkt. Wenn deine Klasse wirklich gut designt wäre, müsste man sie nicht im Code an die eigenen Erfordernisse anpassen. Du zwingst den Anwender deiner Klasse, deren Code so zu ändern, dass das Behandeln von Problemfällen du der Infrastruktur der jeweiligen Anwendung passt.

Und das Grundsatzproblem bleibt: Innerhalb deiner Klasse wirfst du mit Exceptions, aber nach außen dringt davon nichts. Dabei kann diese Klasse gar nicht wissen, wie ihr Umfeld mit einem Problem arbeiten will.

Und aufgrund des in der Klasse global gültigen ExceptionHandlers gibt es auch keine Möglichkeit, das Behandeln von Problemen individuell für jeden Funktionsaufruf (derselben Funktion!) zu handhaben.

Das mag dir im Moment alles nicht so ganz bewußt sein, weil du selbst ja dein eigener Anwender der Klasse bist, und das Ändern des Codes das Leichteste von der Welt ist. Das ändert aber nichts an der Designproblematik. :)

So wär's besser:

class IrgendwasKlima {  
  
  public function einlesenOderSo() {  
    // fail:  
    throw new Exception('Das ist jetzt blöd...');  
  }  
}  
  
class Site {  
  
  private function gefangeneExceptionEinlagern(Exception $exception) {  
    $this->_exceptionLager[] = $exception;  
  }  
  
  
  public function foobar() {  
    // Irgendwoanders im Code deiner Site:  
    try {  
      $klima = new IrgendwasKlima;  
      $klima->einlesenOderSo();  
    }  
    catch (Exception $e) {  
      $this->gefangeneExceptionEinlagern($e);  
    }  
  }  
}

Diese Art hat diverse Vorteile:

1. Deine Klimaklasse kommuniziert mit ihrer Umwelt über ein eindeutiges, verlässliches Medium: Das Werfen einer Exception ist die standardisierte Methode, um "Game Over" zu signalisieren.

2. Der Verwender deiner Klimaklasse kann unabhängig von dieser bestimmen, was mit einer Exception passieren soll. Einlagern und Ansammeln, oder direkt im Einzelfall behandeln - vollkommen egal.

3. Die Klimaklasse braucht keine Referenz auf das globale Site-Objekt.

4. Das Fangen von Exceptions funktioniert auch, wenn das im Konstruktor der Klimaklasse passiert.

5. Die Klimaklasse muss nicht verändert werden, wenn sich die Anforderungen für das Behandeln von Exceptions in der Site-Klasse verändert oder in einer anderen Klasse ganz andere Anforderungen bestehen.

Wenn du keine Lust hast, die ganzen try/catch-Blöcke aus den bisherigen Klassen zu entfernen, dann änderst du deinen globalen ExceptionHandler auf:

private function ExceptionHandler($e){  
  throw $e  
}

Das Setzen eines Exception-Handlers für nicht gefangene Exceptions wäre zu überlegen. Ich bin halt skeptisch, dass es sinnvoll ist, mehr als eine Exception pro Aufruf ansammeln zu lassen, um die man sich nicht sinnvoll im Code kümmern konnte.

Das verstehe ich nicht ganz:

Gibst du der Site im Konstruktor oder in einer separaten Methode ein Datenbankobjekt zur Verwendung, dann muss die Datenbankklasse nicht mehr zwingend "DB" heißen, sie muss nur noch sämtliche Methoden implementieren, die Site aufruft. Und um das sicherzustellen, wird man sehr wahrscheinlich ein Interface definieren, welche als Kontrollinstanz dafür sorgt, dass sämtliche Methoden, die zur Verfügung gestellt werden müssen, auch implementiert sind. Umgekehrt dokumentiert das Interface, welche Methoden man überhaupt benutzen darf, bzw. auf welche Methoden man sich verlassen kann.

Vielleicht geht es mit Code deutlicher:

// Das Interface definiert, welche Funktionen alle Klassen anbieten MÜSSEN, wenn sie das Interface implementieren:  
  
interface Db_Queryfunktionen_Interface {  
  public function connect();  
  public function query($sql);  
}  
  
// Beispielhaft eine Mysql-Klasse  
class Db_Mysql implements Db_Queryfunktionen_Interface {  
  public function connect() {  
    $this->db = new mysqli(...);  
  }  
  public function query($sql) {  
    return $this->db->query($sql);  
  }  
}  
  
// Beispielhaft eine Oracle-Klasse  
class Db_Oracle implements Db_Queryfunktionen_Interface {  
  public function connect() {  
    $this->oracle = oci_connect(...);  
  }  
  public function query($sql) {  
    $stmt = oci_parse($this->oracle, $sql);  
    return oci_execute($stmt);  
    // Code hier nicht auf die Goldwaage legen von wegen Korrektheit eines Oracle-Querys. ;)  
  }  
}  
  
// Und nun die große Magie:  
class IrgendwasMitDatenbank {  
  // Der Konstruktor fordert eine Klasse, die das Interface implementiert.  
  public function __construct(Db_Queryfunktionen_Interface $db) {  
    $this->_db = $db;  
  }  
  public function getIrgendwasFromDb() {  
    // Weil das DB-Objekt garantiert die zwei Methoden "connect" und "query" implementiert,  
    // kann man diese Methoden garantiert aufrufen, egal welche Datenbank man wirklich nutzt.  
    $this->_db->connect();  
    $this->_db->query("SELECT * FROM table");  
  }  
}  
  
// Objekt mit Oracle-Datenbank verbinden  
$obj = new IrgendwasMitDatenbank(new Db_Oracle);  
// Funzt ;)  

Wenn du aber innerhalb von try eine Exception wirfst, um in den catch-Block derselben Funktion "weiter unten" zu gelangen, ist das erstmal ein Mißbrauch von Exceptions zur Programmflusssteuerung. Für sowas gibt es ehrlichere Methoden.
Also zu meiner Verteidigung: ich verwende die Exceptions nie, um in den catch Block weitere Anweisungen zu verarbeiten. Dort steht stets nur
$this->ExceptionHandler($e);. :)

Nicht sehr kreativ. ;) Exceptions immer gleich zu behandeln, nämlich irgendwo anzulagern, erlaubt ja nicht, den Fehlerfall bestmöglich abzumildern, wenn das möglich ist.

Die Gestaltung von Exception-Klassen und deren Vererbung ist ein gar nicht mal simples Thema. Im Gegensatz zu anderen Klassen müssen alle Exceptions, die man selbst gestaltet, von der in PHP integrierten Klasse "Exception" erben.
Ich habe keine Exception-Klasse definiert und möchte das auch nicht. Ich nutze von den Exceptions nur die Message und den Errorcode.

Ist in Ordnung. Aber es bleibt die Frage, ob "nur" Errorcode nicht zu unbeschreibend ist. Im Prinzip kannst du beim Werfen der Exception ja beliebige Codes eingeben, die mit der Message nichts zu tun haben müssten.

Wenn du dir eigene Exception-Klassen definierst, kannst du den Code in jeder eigenen Exception fest definieren, erhältst zusätzlich noch einen netten, beschreibenden Namen, könntest die Message um beschreibende Standardtextkomponenten anreichern etc.

Und erst damit kriegst du auch die Möglichkeit, auf unterschiedliche Exceptions mit unterschiedlichen catch-Blöcken zu reagieren. Das mag überdimensioniert klingen, aber ich finde es ziemlich gut, wenn man das einsetzen kann, wenn man es braucht.

Es gibt Leute, die hiervon ausgehend Exception-Typen basierend auf der Art des aufgetretenen Problems werfen, beispielsweise die InvalidArgumentException für fehlerhafte Parameter (als Kind der "LogicException"), und die UnexpectedValueException für das Auftreten unerwarteter Werte (als Kind der "RuntimeException"). Dieser Ansatz versucht, die auftretenden Probleme nach Typen zu gruppieren (die genannten Exceptions sind in PHP über die Standard PHP Library "SPL" verfügbar), so dass man in der Lage ist, je nach Fehlertyp die eine oder andere Fehlerbehebung zu versuchen, oder dem User eine entsprechende Fehlermeldung zu übermitteln.

Das klingt äußerst interessant. Kennst Du einen Beitrag im Internet (Tutorial, Ähnliches) wo dieses Prinzip vorgestellt wird. Ich verstehe es näcmlich leider nicht ganz:

Mein persönlicher Favorit hingegen ist, dass Exceptions nach den Klassen gruppiert vererbt werden, die thematisch als Module in einer Software auftreten: Eine Datenbankklasse "Db" wirft eine Db_Exception. Das darin enthaltene Modul Db_Connector wirft eine Db_Connector_Exception. Der Db_Connector_Mysql wirft eine Db_Connector_Mysql_Exception. Und die Exceptions erben in einer Kette jeweils voneinander. Damit wäre man in der Lage, auf einer sehr hohen Programmebene beim Aufruf der Methoden der Klasse "Db" mit try/catch alle Db_Exceptions zu fangen (man weiß dann, dass irgendwas in der Datenbankabfrage schief gelaufen ist und man kein Ergebnis bekommt), inklusive aller Db_Connector_Exception (weil z.B. der konfigurierte Datenbanktreiber nicht geladen werden konnte) und aller Db_Connector_Mysql_Exception (z.B. für fehlerhafte SQL-Querys).

Innerhalb der Db-Klasse würde man mit try/catch alle Db_Connector_Exception fangen können, in Db_Connector wären das Db_Connector_Mysql_Exception, etc... Wobei das Fangen nur Sinn ergibt, wenn man ein passendes Alternativprogramm anbieten kann.

Ich fand ganz spontan das hier: http://www.php.de/tutorials/45124-php-exceptions-teil-1-a.html

- Sven Rautenberg