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

SOLID: Ein Überblick

Das Kunstwort SOLID benennt wesentliche Grundprinzipien der objektorientierten Softwareentwicklung, die zu besserer Software führen, wenn man Ihnen folgt.

Unter anderem die Design Patterns der GoF richten sich nach diesen Prinzipien. Es sind jedoch ganz allgemein fünf wesentliche Prinzipien der objektorientierten Entwicklung, deren Berücksichtigung etliche Vorteile bringt.

Neben einer kurzen Beschreibung des jeweiligen Prinzips möchte ich hier Beispiele in PHP zeigen, welche die Anwendung dieser Prinzipien deutlich machen.

Die SOLID-Prinzipien sind weniger ein akademisches Thema, sondern beschreiben vielmehr auf Erfahrung basierende, handfeste Kriterien des praktischen Software-Designs in der täglichen Arbeit, die einen nicht zu unterschätzenden Einfluss auf Entwicklungskosten und -möglichkeiten haben, und deren Berücksichtigung entscheidend dafür ist, wie gut sich eine Software implementieren, langfristig weiterentwickeln und später warten lässt.

Um den praktischen Nutzen der Anwendung dieser Prinzipien zu zeigen bemühe ich mich nachfolgend um Code, der, wenn auch vereinfacht, aus dem konkreten Entwicklungsalltag stammen könnte.

"O": Open for extension, closed for modification

Das Prinzip besagt, Code so zu gestalten, dass er durch sein Design erweiterbar ist, aber Veränderungen an bereits vorhandenen Code nicht notwendig sind, um Erweiterungen zu implementieren.

Das einfachste Beispiel für die Berücksichtigung dieses Prinzips ist die Verwendung von Interfaces. Nehmen wir eine Klasse, die Reports im XLSX-Format erstellt:

class Report
{
  protected $handle;

  public function __construct($filename)
  {
    $this->handle = $this->openFile($filename);
  }
  
  public function __destruct()
  {
    $this->closeFile();
  }
  
  protected function openFile($filename) { /* ... */ }
  
  protected function closeFile() { /* ... */ }
  
  protected function printRow(array $row) { /* ... */ }

  public function generate(\Iterator $data)
  {
    foreach ($data as $row) {

		$this->printRow($row);    
    }
  }
}

Der Methode generate() wird ein Iterator übergeben. Über diesen wird iteriert und die einzelnen Datensätze zeilwenweise in eine XLSX geschrieben:

$report = new Report('/data/report.xlsx');

$data = [
  [1, 8.76, 'X'],
  [2, 7.99, 'Y'],
  [3, 15.00, 'P'],
];

$report->generate(new \ArrayIterator($data));

Mithilfe der Klasse ArrayIterator kann ein Report aus einem Array erstellt werden. Das erscheint zunächst als Umweg, denn schließlich hätte man generate() auch wie folgt deklarieren können:

public function generate(array $data)
{
  //...
}

Obwohl die Verwendung des Datentyps array flexibel wirkt, muss hier allerdings immer ein Array im richtigen Format übergeben werden.

Was nun, wenn das Ergebnis einer Datenbankabfrage als Report ausgegeben werden soll? Bei Verwendung von generate(array $data) müsste das Resultset zuerst vollständig abgerufen und in ein Array gespeichert werden, bevor man es an generate() übergeben kann.

Bei großen Resultsets kann dies unnötig viel Speicher verbrauchen und dem Prozess unter Umständen sogar der verfügbare Speicher ausgeben. Zudem muss man sich zusätzlich um die Konvertierung des Resulsets der Datenbankabfrage in ein Array kümmern:

$result = $mysqli->query('SELECT * FROM table');

$data = [];

while (true == $row = $mysql->fetch_row()) {

  $data[] = $row;
}

$report->generate($data);

Die Verwendung des Iterator in generate() macht es dagegen notwendig und möglich, eine Klasse zu verwenden, die das Iterator-Interface implementiert:

class MySQLiResultset implements \Iterator
{
  protected $result;
  
  public function __construct($result)
  {
    $this->result = $result;
  }
  
  //Implementierung der Iterator-Methoden:
  //next() holt den nächsten Datensatz per mysqli::fetch_row()
}

Die Verwendung sieht dann so aus:

$resultset = new MySQLiResultset(  $mysqli->query('SELECT * FROM table') );

$report->generate($resultset);

Da MySQLiResultset das Iterator-Interface implementiert, kann generate() eine Instanz dieser Klasse übergeben werden.

Weil die Daten erst in der Iteration abgerufen werden, verschiebt sich das Lesen der Daten auf den Zeitpunkt, zu dem die Daten tatsächlich benötigt werden. Das könnte man sozusagen als "Lazy Reading" bezeichnen.

Und da die Datensätze zudem einzeln in der Iteration gelesen werden, entfällt die Notwendigkeit, ein gegebenenfalls speicherintensives Array zu erzeugen und möglicherweise auch noch unnötig im Speicher vorhalten zu müssen, bevor es tatsächlich benötigt wird.

Das speicherschonende Verhalten der MySQLiResultset-Klasse ist ein positiver Nebeneffekt, den ich am Rande mit aufzeigen wollte. Eigentlich geht es hier ja aber um das "Open/Closed-Prinzip":

Die Verwendung des Iterator macht beliebige Implementierung möglich, die sich selbst darum kümmern, die passenden Daten bereitzustellen: Wenn es andere Datenstrukturen und/oder andere Datenquellen gibt, aus denen Reports erstellt werden sollen, wird deren Iteration dort implementiert, wo auch die Daten behandelt werden. Die Report-Klasse muss sich dagegen nicht um die technischen Details kümmern.

Man könnte nun beispielsweise auch Daten aus einer PostgreSQL-Datenbank oder aus einem Solr-Index holen und dafür entsprechende Resultset-Klassen bereitstellen, die nahtlos mit der Report-Klasse zusammenspielen, ohne dass man diese erweitern müsste.

Und man könnte eine bereits vorhandene, interne Datenstruktur, die anders strukturiert ist als für das XLSX-Dokument benötigt, durch Implementierung des Iterator-Interface in eine zeilenweise Repräsentation transformieren lassen, die man dann ebenfalls an generate() übergibt.

Durch Implementierung des Iterator-Interface können die Datenquellen erweitert werden ("open for extension"). Gleichzeitig findet die Erweiterung der Funktionalität in den zusätzlichen Implementierungen statt, wobei die Report-Klasse nicht verändert wird ("closed for modification").

Die Verletzung des Prinzips in diesem Beispiel könnte so aussehen:

class Report
{
  public function generateFromMySQLi($result)
  {
    //...
  }
  
  public function generateFromArray(array $data)
  {
  	//...
  }
}

Natürlich wird man hier versuchen, möglichst keinen Code zu duplizieren und Gemeinsamkeiten der Algorithmen in gemeinsam genutzten Methoden unterzubringen.

Aber abgesehen davon, dass nun eine Abhängigkeit von konkreten Datentypen besteht, müsste man den Code in Report erweitern, wenn weitere Datenquellen hinzukommen, wodurch die Klasse Report dem Risiko ausgesetzt ist, dass hier Defekte eingebaut werden und ihr Verhalten unbeabsichtigt verändert wird.

Für die Anwendung des Open/Closed-Prinzips sind unterschiedlichste Beispiele denkbar und das konsequente Programmieren gegen Interfaces ist nur eine mögliche Anwendung des Prinzips. Ein anderes Beispiel ist die Verwendung einer Template-Methode, die in einer abgeleiteten Klassen überschrieben werden kann (open), wobei jedoch der Algorithmus der Basisklasse, der die Template-Methode aufruft, als final deklariert ist (closed).

Abschließend noch einmal der wesentliche Punkt beim Open/Closed-Prinzip: Code wird so gestaltet, dass er Erweiterungen in einer Forum zulässt, bei welcher der bereits bestehende, zu erweiternde Code selbst nicht verändert oder erweitert werden muss.

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