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.

"S": Single Responsibility

Eine Klasse soll genau eine Verantwortlichkeit haben. Das bedeutet zum einem, dass sie nicht für verschiedene, fachliche Anforderungen gleichsam zuständig sein sollte. Das Prinzip gilt für Module, Subsysteme und sonstige Systemkomponenten gleichermaßen, ist also nicht auf Objektorientierung beschränkt.

Zum anderen sollte eine fachliche Zuständigkeit nur an genau einer Stelle umgesetzt sein, wenn man das Prinzip ernst nimmt. Mit andern Worten: Mehrere Lösungen für dieselbe Sache an unterschiedlichen Stellen darf es möglichst nicht geben. Einzige Ausnahme ist, dass dieselbe Lösung verschiedene technische Ausprägungen haben kann, wozu es mehrere Implementierungen desselben Typs geben kann. Aber auch hier gibt es die Verantwortlichkeit dann nur an einer Stelle, nämlich beim Basistyp.

Das "Verantwortlichkeitsprinzip" beinhaltet also auch die Forderung nach starker (funktionaler) Kohäsion ... oder noch einmal anders gesagt, richtet es sich nach dem "DRY-Prinzip" (Don't Repeat Yourself).

Das "Verantwortlichkeitsprinzip" für Softwarebausteine hat erhebliche Vorteile wenn man es befolgt. Zur Verdeutlichung zunächst die Verletzung des Prinzips am Beispiel "Logging":

class APIService
{
  public function serviceRequest()
  {
    $fp = fopen('/var/log/apilog.log', 'a');
    fwrite($fp, 'start servicing incoming request');
    
    //...
    
    fwrite($fp, 'finished servicing incoming request');
    fclose($fp);
  }
}

class Database
{
  public function query($sql)
  {
  	$fp = fopen('/var/log/queries.log', 'a');
    fwrite($fp, 'executing sql query: ' . $sql);
    fclose($fp);
    
    //...
  }
}

Hier kümmern sich zwei Klassen um dieselbe Sache, nämlich um Logging. Man kann sich leicht vorstellen, dass diese Vorgehensweise bedeutet, dass das auf diese Weise praktizierte Logging in der gesamten Codebasis verteilt ist: Dateizugriffe passieren dezentral an etlichen Stellen und sind unmöglich in ihrer Gesamtheit kontrollierbar. Spätestens wenn man beeinflussen möchte, was unter welchen Bedingungen geloggt wird oder in welchem Verzeichnis die Logfiles verwaltet werden, ist man mühselig damit beschäftigt, die gesamte Codebasis zu bearbeiten.

Folgt man dem Prinzip der alleinigen Verantwortlichkeit (single responsibility), dann wird man das Logging an genau einer Stelle implementieren und kann es dann dort zentral steuern, beeinflussen und verändern. An allen anderen Stellen wird diese Funktionalität dann lediglich benutzt. Möglich wäre eine Logger-Klasse:

class Logger
{
  protected $basepath = '/var/log/';

  protected $channels = [
  
    'api'     => 'apilog.log',
    'queries' => 'queries.log',
  ];
  
  protected static $instance;
  
  final protected function __construct() {}
  
  final protected function __clone() {}
  
  public static function getInstance()
  {
    if (false == self::$instance instanceof Logger) {
      self::$instance = new Logger();
    }
    return self::$instance;
  }

  public function log($message, $channel, $type)
  {
    //...
  }
}

Überall dort, wo Informationen zu protokollieren sind, wird diese Klasse dann benutzt, ohne die interne Funktionsweise in Form der Dateizugriffe zu wiederholen:

class APIService
{
  public function serviceRequest()
  {
    Logger::getInstance()->log('start servicing incoming request', 'api');
    
    //...
    
    Logger::getInstance()->log('finished servicing incoming request', 'api');
  }
}

class Database
{
  public function query($sql)
  {
    Logger::getInstance()->log('executing sql query: ' . $sql, 'api');
    
    //...
  }
}

Die Klasse Logger ist hier als Singleton skizziert, um Probleme beim Parallelzugriff auf dieselben Dateien und mit zu vielen unnötig geöffneten Dateihandles zu vermeiden.

Durch das Extrahieren der Klasse Logger entstehen mehrere Vorteile. Zunächst einmal ist bereits augenscheinlich, dass der Code in APIService und Database schlanker und lesbarer geworden ist.

Die Zentralisierung (Single Responsibility) des Loggings in der Klasse Logger bietet jedoch im Gegensatz zur ersten Lösung einige weitere positive Effekte:

  • Die verschiedenen Logfiles können auf diese Weise in einer einzigen Klasse verwaltet werden. Die Festlegung und Änderung der Dateinamen beispielsweise passiert bei Bedarf nur an genau einer Stelle, nämlich in der Klasse Logger.
  • Das Basisverzeichnis der Logfiles kann an genau einer Stelle beeinflusst werden. Es entsteht auch die Möglichkeit, dieses zur Laufzeit zu verändern.
  • Der technische Umgang mit den Logfiles wird zentral gehandhabt. Hier ließe es sich nun beispielsweise mithilfe von flock() realisieren, dass auch parallele Prozesse dasselbe Logfile benutzen können. Diese Erweiterung kann man dann an genau einer Stelle vornehmen. Ohne dies würden parallel zugreifende Prozesse die Protokolldatei zerstören und die Synchronisation von Zugriffen wäre mit erheblichem Aufwand und riskantem Eingriff in die Programmierung verbunden.
  • Der Logger lässt sich um die Möglichkeit erweitern, das Logging für einzelne Logtypen (info, error, warning ...) gezielt zu steuern oder es auch komplett ein- und auszuschalten.
  • Das technische Backend, in welches protokolliert wird, kann verändert werden: Logging in eine Datenbank statt in eine Datei wird möglich und auch beides zugleich ist denkbar, ohne etwas an den loggenden Klassen Database und APIService ändern zu müssen.
  • Möchte man das Verhalten des Loggings konfigurierbar machen, kann man die Logger-Klasse entsprechend erweitern und muss auch hierfür nur genau diese eine Klasse anfassen - der Code in den benutzenden Klassen bleibt unberührt.

Dieses Beispiel macht deutlich, dass durch Anwendung des "Single Responsibility"-Prinzips nicht nur lesbarer und wartbarer Code entsteht, sondern dass die Implementierung zusätzlicher Features hierdurch überhaupt erst in den Bereich des Möglichen rücken kann. Man sieht, wieviel Einfluss die Vorgehensweise und Orientierung an einem bewährten Prinzip auf Qualität und Möglichkeiten bereits bei geringem Codeumfang hat.

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