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.

"L": Liskov substitution principle

Das Substitutionsprinzip von Liskov besagt, dass sich das erwartete Verhalten einer Implementierung nicht ändern darf, wenn man einen Typ durch einen Untertyp ersetzt.

Das Prinzip wird verletzt, wenn man eine Klasse von einer anderen ableitet und das Verhalten in der abgeleiteten Klasse so verändert, dass Annahmen über das Verhalten, die man aufgrund der Basisklasse hat, bei Verwendung der abgeleiteten Klasse nicht mehr zutreffen.

Die Folge der Verletzung des Prinzips ist, dass sich ein Programm unerwartet, beziehungsweise fehlerhaft verhält, wenn man eigentlich davon ausgeht, dass sich Typen durch ihre Untertypen ersetzen lassen und ein Untertyp im Spiel ist, der jedoch nicht den Erwartungen an das Verhalten seines Basistyps entsprechend implementiert ist.

Das klassische Beispiel hierzu, in dem das Prinzip verletzt wird, ist die Ableitung einer Klasse Circle von der Klasse Ellipse:

class Ellipse
{
  protected $height;
  protected $width;
}

class Circle extends Ellipse
{
  //...
}

Im mathematischen Sinn kann man die Beziehung zwischen den beiden Typen so sehen, wenn man den Kreis als Sonderfall einer Ellipse begreift, bei dem die Brennpunkte der Ellipse zusammenfallen.

Man findet Beispiele, in denen es das Interface der Klasse Ellipse ermöglicht, Höhe und Breite der Achsen zu setzen, also zum Beispiel die Höhe auf 4cm und die Breite auf 5cm. Da aufgrund der Symmetrie beim Kreis beide Achsen gleich lang sind, muss man in der abgeleiteten Klasse dafür sorgen, dass die Symmetrie beim Kreis immer erhalten bleibt:

class Ellipse
{
  public function setHeight($height)
  {
    $this->height = $height;
  }
  
  public function setWidth($width)
  {
    $this->width = $width;
  }
}

class Circle extends Ellipse
{
  public function setHeight($height)
  {
    $this->height = $height;
    $this->width = $height;
  }
  
  public function setWidth($width)
  {
    $this->height = $width;
    $this->width = $width;
  }
}

Das zerstört jedoch die Annahme, dass man bei einer Ellipse Höhe und Breite individuell, also unabhängig voneinander setzen kann! Verdächtig ist bei der Implementierung bereits die Manipulation der jeweils anderen Eigenschaft height und width.

Wenn nun ein Programm eine Ellipse benutzt und verschiedene Werte für Höhe und Breite verwendet, kann es sich unerwartet verhalten, sobald man anstelle einer Ellipse den Untertyp Circle verwendet, was den Regeln der Objektorientierung zufolge ja möglich sein sollte.

Man stelle sich vor, dass das Programm mit den Werten für Höhe und Breite außerhalb der Klassen Ellipse und Circle eine Flächenberechnung macht. Das Ergebnis weicht dann von der Flächenberechnung des Kreises, in dessen Implementierung die Grundannahme verletzt wird, dass man beides unabhängig voneinander setzen kann, ab:

class Ellipse
{
  //...
  
  public function getArea()
  {
    //Flächenberechnung einer Ellipse
    return $this->height/2 * $this->width/2 * pi();
  }
}

$ellipse = new Ellipse();

$ellipse->setHeight(4);
$ellipse->setWidth(5);

echo $ellipse->getArea(); //15.7

$circle = new Circle();

$circle->setHeight(4);
$circle->setWidth(5);

echo $circle->getArea(); //19.6 <== falsch, da die Höhe unbemerkt verändert wurde

Aus solchen Fällen mag man schlussfolgern, dass die "Realität" nicht 1:1 durch Quellcode abgebildet werden kann, sondern dass die Modellierung mitunter vom realen Sachverhalt abweichen muss. Es fragt sich, ob dies stimmt oder ob das Problem nicht doch in einer unzureichenden Modellierung liegt.

Tatsächlich darf man unter der getroffenen Annahme, dass Höhe und Breite verschieden gesetzt werden können, einen Kreis nicht als Spezialfall (Ableitung) einer Ellipse modellieren - Circle ist unter dieser Voraussetzung kein Untertyp von Ellipse.

Abstraktion ist per Definition kein Sachverhalt der Realität, weshalb das Grundprinzip der Vererbung per se unzureichend bei der exakten Abbildung realer Sachverhalte ist, obgleich die Möglichkeiten der Vererbung natürlich sehr hilfreich bei der Modellierung sind.

Die "korrekte" Modellierung ergibt sich vor dem Hintergrund der über das Verhalten der Basistypen gemachten Annahmen und muss bei Auftreten eines solchen Problems zu einem entsprechenden Refactoring führen, so dass das Substitutionsprinzip nicht weiter verletzt ist.

Die Verletzung des Prinzips ist in der Regel nicht in der vorhandenen Klassenhierarchie oder durch die Beziehung der Typen zueinander sichtbar, zeigt sich jedoch in unerwartetem Verhalten und damit in Form von Fehlfunktionen.

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