Sven Rautenberg: OOP (in PHP) - 3 Fragen

Beitrag lesen

Moin!

ich entdecke gerade anhand dieses sehr guten Tutorials von Peter Kropff die tolle Welt von OOP in PHP.

Naja, ein eher mittelmäßiges Tutorial.

Die Beispiele sind allesamt vergleichbar mit "Auto extends Fahrzeug extends Metall". Das ist die klassische Methode, Vererbung zu erklären, geht aber nach meiner Meinung am Kern von OOP vorbei.

Sehr kritisch sehe ich beispielsweise, dass das Tutorial die Verwendung von statischen Methoden und Eigenschaften relativ unkritisch propagiert. Damit handelt man sich jede Menge Probleme ein, die dummerweise sehr unoffensichtlich sein können.

Das Problem was ich jetzt habe ist, dass in dem Text zwar alles super erklärt wird, aber wie man jetzt konkrete Probleme mit OOP umsetzt, steht eben nicht drin.

Das Tutorial erklärt die grundlegenden Konzepte der OOP. Du weißt also hinterher Dinge wie Vererbung, Konstruktoren, Interfaces, Sichtbarkeit etc, und wie diese Dinge technisch zusammenspielen können. Was nicht drinsteht - und das kriegt man vermutlich auch erst hin, wenn man selbst aktiv wird und seine eigenen Erfahrungen sammelt - ist wie man diese Kenntnis letztendlich in funktionsfähigen und verständlichen Code umsetzt.

Mal konkret formuliert: Folgender Tutorialcode wird dargeboten

class Getraenk  
{  
  private static $rechnung;  
  private static $getraenke = array();  
  private $promille;  
  
  public function bestellen($getraenk, $preis, $alkohol)  
  {  
    self::$rechnung   += $preis;  
    $this -> promille += $alkohol;  
    self::$getraenke[$getraenk] += 1;  
  }  
  public function getPromille()  
  {  
    return $this -> promille;  
  }  
  public static function getRechnung()  
  {  
    return self::$rechnung;  
  }  
  public static function getGetraenke()  
  {  
    return self::$getraenke;  
  }  
}

Wie oben schon kritisiert, werden hier statische Methoden und Eigenschaften verwendet.

Das Anwendungsbeispiel (mal verkürzt):

$dieter = new Getraenk;  
$doerte = new Getraenk;  
$dieter -> bestellen('Bier', 4.5, 0.3);  
$doerte -> bestellen('Wein', 8.0, 0.3);  
  
echo $dieter -> getPromille().'<br>';  
echo $doerte -> getPromille().'<br>';  
echo Getraenk::getRechnung().'<br>';  

Man sieht, dass es ZWEI Objekte vom Typ "Getraenk" gibt, die sich allerdings gegenseitig beeinflussen, denn jede Bestellung hat Auswirkungen auf die Gesamtrechnung.

Das Beispiel kann die Gefährlichkeit solcher Konzepte nicht verdeutlichen, weil sich die gesamte Anwendung der fragwürdigen Klasse ja auf das gewünschte Ziel hin entwickelt: Zwei Objekte sammeln Getränke, und am Ende steht die Gesamtrechnung zur Verfügung.

Was nicht gezeigt wird: Was passiert, wenn an einer ganz anderen Stelle im Code, für eine vollkommen andere Sache ebenfalls eine Getränkerechnung benötigt wird, und deshalb dieselbe Klasse zum Einsatz kommt?

Ich hoffe, du erkennst das Problem.

  • Ich habe eine Klasse "page", die die Verwaltung der Seiten übernimmt. Diese hat verschiedene Methoden, unter anderem "load", mit der man eine bestimmte Seite in den Speicher lädt. Wo aber gehört denn die Funktion (ich meine nicht "Subroutine") zum Auslesen der Seite aus den GET-Parametern hin? In die Klasse? Oder einfach außerhalb in das Script?

Ausgehend von den technischen Möglichkeiten von OOP kann man ja beliebig programmieren, nur sind manche Ansätze besser als andere, weil das spätere erneute Verstehen, das Verändern oder das Wiederverwenden des Codes einfacher ist.

Deshalb haben sich gewisse Grundsätze entwickelt, die man einhalten sollte, wenn man sich das Leben nicht unnötig schwer machen will: SOLID Dieses Akronym bedeutet:

* Single Responsibility: Eine Klasse sollte nur EINEN Grund haben, dass man sie ändert.
* Open/Closed Principle: Eine Klasse sollte offen für Erweiterung, aber geschlossen für Modifikation sein.
* Liskov Substitution: Eine Kindklasse sollte ohne Anpassungsnotwendigkeit als 1:1-Ersatz an allen Stellen verwendbar sein, an denen die Elternklasse schon funktioniert.
* Interface Segregation: Viele kleinere Interfaces für eine spezifische Aufgabe sind besser als ein großes Pauschal-Interface.
* Dependency Inversion: Die von einer Klasse benutzten weiteren Klassen dürfen nicht innerhalb dieser Klasse erzeugt werden, sondern kommen von außen hinein.

Ich gebe gern zu, dass diese Prinzipien sehr abstrakt klingen mögen für den Anfänger.

Deshalb mal als Gedankenansatz: "eine Klasse "page", die die Verwaltung der Seiten übernimmt" bedeutet für dich was genau? Was heißt "Verwaltung"?

In meiner Vorstellung enthält "Verwaltung" sehr viele verschiedene Aufgaben, die alle nicht gesammelt in eine einzige Klasse gehören. Beispielsweise wird es wohl einen Mechanismus geben, eine Seite aus dem persistenten Speicherbereich ins RAM zu laden. Die Seite wird also irgendeine Methode namens "load()" enthalten, die das macht. Aber was macht die Methode dann genau? Beachte: Ich sprach vom "persistenten Speicherbereich" - das ist erstmal alles mögliche, beispielsweise eine Datei auf Festplatte, oder eine Tabelle in einer Datenbank. Oder eine externe, per HTTP erreichbare Ressource von einem anderen Server.

Du würdest für dein konkretes System natürlich erstmal nur eine einzige Methode zum Laden des Seiteninhalts benötigen - aber wenn du z.B. die Datenbankabfrage konkret in die load-Methode deiner Klasse "page" schreibst, dann hast du dich für diese Klasse eindeutig festgelegt, dass der persistente Speicher eben "Datenbank" ist. Das ist aber ein Verstoß gegen das erste SOLID-Prinzip: Single Responsibility - eine Klasse wird nur aus EINEM Grund geändert. Deine Klasse "page" hat den primären Grund zu existieren und geändert zu werden, weil sie in deinem System eine auszugebende Seite repräsentiert. Und wenn in dieser Klasse Datenbankcode steht, hat sie dadurch noch einen zweiten Grund, geändert werden zu müssen: Wenn sich die Datenbank verändert, oder der Datenspeicher ganz allgemein.

Aus demselben Grund wird die Klasse "page" keinerlei Code zur Auswertung von Request-Parametern haben - das ist Aufgabe einer anderen Klasse, die nur zu diesem Zweck existieren sollte.

  • Und wo packt man die Umleitung auf Fehlerseiten hin?

Das ist ebenfalls Aufgabe einer ganz anderen Klasse. Insgesamt gehört sowas in ein Konstrukt, welches man als MVC kennt. Und alles, was für das Auswerten des Request und das Generieren des zugehörigen Responses zuständig ist, ist in den diversen Frameworks, die weitverbreitet sind, schon recht umfassend und gut anpassbar realisiert.

Hatte ich dir schon mal empfohlen, dir irgendeins dieser Frameworks anzusehen? Einfach nur mal, um die Konzepte, die man dort findet, überhaupt mal kennenzulernen und zu verstehen?

Sollte die Klasse selbstständig bei Fehlern statt der gewünschten die Fehlerseite laden, oder sollte sie nur einen Fehler zurückgeben und das Hauptprogramm lässt dann die Klasse die Fehlerseite laden?

Eine Klasse "page" wird garnicht angesprochen, wenn sie selbst von einem Request nicht angesprochen wird, weil der Request dann ja offenbar eine andere Seite meinte - und nichtexistente Seiten zeichnen sich dadurch aus, dass sie nicht existieren, deswegen auch keine zugehörige Klasse "page".

Irgendein vorgeschalteter Mechanismus muss ja erstmal erkennen, ob die vom Request gemeinte Seite existiert.

  • Die letzte Frage, die eigentlich aus den anderen beiden hervorgeht ist, ob man sowas wie Session-Initialisierung, erstellen der page-Instanz, usw. in die Klasse CMS* als Methode packen sollte, die dann im Hauptprogramm aufgerufen wird, oder ob das einfach im Hauptprogramm stehen sollte.

Es gibt keine Klasse "CMS". Siehe oben: Single Responsibility.

Und ja, eine Session ist auch etwas, was man in eine Klasse verpacken muss.

* Diese Klasse stellt ein paar statische Methoden zur Verfügung, die die eigentliche Funktionalität des CMS ausmachen: Dateien mit gleichzeitigem Locking laden und dann das enthaltene JSON parsen und zurückgeben, etc. etc.

Um Gottes Willen!*

- Sven Rautenberg

[*] Kannst du natürlich erstmal so programmieren, um dann hoffentlich festzustellen, dass sowas extreme Probleme nach sich ziehen wird, wenn der Code irgendwann mal geändert werden muss.