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.

"I": Interface segregation

Hier geht es darum, umfangreiche Interfaces die viele Methoden haben, auf mehrere, kleine Interfaces zu verteilen. Ein Interface mit vielen Methoden zwingt jede benutzende Implementierung dazu, alle Methoden des Interface zu implementieren.

Umfangreiche Interfaces sind ein Zeichen dafür, dass eine Klasse, die den Typ eines solchen großen Interface hat, zu viel tut und dass hierdurch zu "fette" Klassen, sogenannte "Blobs" entstehen. Umfangreiche Interfaces führen auch dazu, dass bestimmte Funktionalitäten unnötig oder aber unvollständig implementiert werden.

Schlanke Interfaces sind dagegen deutlich von Vorteil: Es herrschen mehr Übersichtlichkeit, Verständlichkeit, Flexibilität und Wiederverwendbarkeit. Schlanke Interfaces führen zu modularen, flexiblen Systemen, große Interfaces dagegen zu "Blobs".

Man betrachte zunächst die folgende Klasse mit unterschiedlichen Aufgaben:

class TheApplication
{
  public function printScreen(array $lines) { /* ... */ }
  
  public function sendEmails() { /* ... */ }
  
  public function organizeDocuments() { /* ... */ }
  
}

Nun kann man auf die Idee kommen, das public Interface dieser Klasse zu extrahieren, um verschiedene Implementierungen einer "Anwendung" zu ermöglichen. Wir tun das durch Extraktion des Interface "Application":

interface Application
{
  public function printScreen(array $lines);
  
  public function sendEmails();
  
  public function organizeDocuments();
}

Nun sind verschiedene Implementierungen dieser Anwendung denkbar:

class DesktopApplication implements Application
{
  //Implementierung aller Methoden hier
}

class MobileApplication implements Application
{
  //Implementierung aller Methoden hier
}

So weit so gut. Nun möchte man eine Klasse schreiben, die sich um das Organisieren der Dokumente der Anwendung kümmert. Diese neue Klasse soll in der Lage sein, eine Garbage Collection der Dokumente durchzuführen. Dazu müssen die Dokumente zunächst organisiert (gespeichert und geschlossen) werden, bevor dann die eigentliche Garbage Collection ausgeführt wird.

Dazu kann und will man sich natürlich der Instanz der Applikation bedienen, da diese das Organisieren der Dokumente durch die Methode organizeDocuments() bereits beherrscht. Also lässt sich die neue Klasse wie folgt schreiben:

class DocumentStorage
{
  protected $app;

  public function __construct(Application $app)
  {
    $this->app = $app;
  }
  
  public function cleanup()
  {
    //ordne alle Dokumente so wie für dier Platform richtig:
    $this->app->organizeDocuments();
    
    //führe hier jetzt die Garbage Collection aus...
  }
}

Das erscheint als gute Vorgehensweise, denn man nutzt in DocumentStorage die bereits vorhandene Fähigkeit der Application-Implementierungen, Dokumente zu organisieren.

Außerdem wird als Datentyp das Interface Application benutzt, wodurch die Klasse DocumentStorage auf verschiedenen Platformen zurecht kommt, denn die Methode organizeDocuments() ist in DesktopApplication und MobileApplication jeweils passend implementiert.

Man mag sich hierunter vorstellen, dass die Betriebssystemfunktionen für den Zugriff auf Dateien auf Desktop-Systemen anders angesteuert werden als auf mobilen Betriebssystemen. Natürlich ist das vereinfacht dargestellt.

Kommen wir zum Problem "fetter" Interfaces: Angenommen, dass organizeDocuments() für einen bestimmten Anwendungsfall anders arbeiten muss: Ein Mac ist kein Desktop-System, so wie dieses bereits implementiert ist, sondern der Zugriff auf Dateien muss hier anders gelöst werden.

Es wird damit notwendig, das gesamte Interface Application in einer neuen Klassen zu implementieren, welche auf das System Macbook abgestimmt ist. Dabei ist man nun gezwungen, auch die sonstigen Methoden des Interface zu implementieren, obwohl nur eine Methode, beziehungsweise ein spezieller Verantwortungsbereich betroffen ist.

Man endet dann mit einer wenig zufrieden stellenden Lösung, vielleicht so:

class MacbookApplication implements Application
{
  public function printScreen(array $lines)
  {
    throw new Exception('not implemented');
  }
  
  public function sendEmails()
  {
    throw new Exception('not implemented');
  }
  
  public function organizeDocuments()
  {
    //organisiere die Dokumente so wie es auf dem Macbook richtig ist...
  }
}
$storage = new DocumentStorage(new MacbookApplication());

$storage->cleanup();

Dieser Ansatz funktioniert, aber man kommt in die Verlegenheit, alle anderen Methoden zusätzlich implementieren zu müssen, ohne dass das hier Sinn macht. Es entsteht eine Anwendungsimplementierung, die dem Vertrag ihres Interface überhaupt nicht gerecht wird, sprich, gar nicht als Anwendungsimplementierung funktioniert und damit in dieser Form wenig sinnvoll ist.

Es bieten sich zwar Vererbung oder Decorator-Pattern an, um eine konsistente Lösung zu erhalten, aber eigentlich möchte man sich gar nicht um die restliche Funktionalität kümmern, die bezüglich der Problemstellung hier nichts zu suchen hat. Zudem macht es wenig Sinn, dass die neue Klasse DocumentStorage Zugriff auf alle Methoden einer Application hat, also beispielsweise auch E-Mail versenden könnte.

Ursache für dieses Dilemma ist das Interface Application, das zu groß geraten ist und drei verschiedene Zuständigkeiten abdeckt, womit wir bei der Art von Problem sind, dem das "Schnittstellenaufteilungsprinzip" entgegen wirkt. Das Prinzip "Interface Segregation" bringt uns dazu, das große Interface auf mehrere kleinere Interfaces aufzuteilen:

interface Printer
{
  public function printScreen(array $lines);
}

interface Sender
{
  public function sendEmails();
}

interface DocumentManager
{
  public function organizeDocuments();
}

Damit sind Ausgabe-, Sender- und Dokumentenmanagement-Funktionalitäten sauber getrennt. Bei einer Neuentwicklung würde man Printer, Sender und DocumentManager unabhängig voneinander entwickeln und diese in der Anwendung als Bausteine zusammensetzen.

Es entstünde dadurch auch die Möglichkeit, unterschiedliche Implementierungen miteinander zu kombinieren. Bei einer zu groß geratenen Schnittstelle dagegen wären die Implementierungen hart miteinander gekoppelt. Bei Bedarf kann das außerdem zu unsauberen Hilfskonstrukten führen, die in einem Wartungsalbtraum enden.

Der bestehenden Ansatz, der in Form der Klasse TheApplication ja bereits fertig entwickelt vorliegt, lässt sich dem Schnittstellenaufteilungsprinzip folgend so auf den richtigen Weg bringen:

class TheApplication implements Printer,Sender,DocumentManager
{
  public function printScreen(array $lines) { /* ... */ }
  public function sendEmails() { /* ... */ }
  public function organizeDocuments() { /* ... */ }
}

class DocumentStorage
{
  protected $documentManager;

  public function __construct(DocumentManager $documentManager)
  {
    $this->documentManager = $documentManager;
  }
  
  public function cleanup()
  {
    $this->documentManager->organizeDocuments();
    
    //...
  }
}
$app = new TheApplication();

$storage = new DocumentStorage($app);

$storage->cleanup();

Auf diesem Weise ist die Klasse DocumentStorage nur noch vom Interface DocumentManager abhängig, da lediglich dieser Datentyp dem Konstruktor übergeben werden kann.

Wenn hier dann später einmal eine andere Implementierung benutzt werden soll als die bereits vorhandene, kann man sich darauf beschränken, das Interface DocumentManager zu implementieren und sieht sich nicht unnötigerweise mit den sonstigen Funktionalitäten der Anwendung konfrontiert.

Da die refaktorierte Klasse MyApplication nun alle drei Interfaces implementiert, kann sie dennoch weiterverwendet werden. DocumentStorage dagegen kann nur noch auf die Funktionalität von DocumentManager zugreifen und nicht auf die anderen Methoden der übergebenen Instanz von TheApplication, was gut ist, da zugunsten der Modularität Abhängigkeiten reduziert wurden.

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