Gelber Grad

Prinzipien

Interface Segregation Principle (ISP)

Warum?
Leistungsbeschreibungen, die unabhängig von einer konkreten Erfüllung sind, machen unabhängig.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Das Interface Segregation Principle (ISP) ist ein weiteres SOLID Prinzip. Segregation bedeutet Abtrennung. Das Prinzip besagt, dass ein Client nicht von Details eines Service abhängig sein soll, die er gar nicht benötigt. Je weniger in dessen Interface enthalten ist, desto geringer ist die Kopplung zwischen den beiden Komponenten.

Stellen wir uns vor, wir müssten einen Stecker planen, mit dem ein Monitor an einen Computer angeschlossen werden soll. Wir entscheiden uns, einfach alle Signale die in einem Computer so anfallen, per Stecker zur Verfügung zu stellen. Der hat dann zwar einige Hundert Pins, aber dafür ist er maximal flexibel. Dummerweise ist damit die Kopplung ebenfalls maximal.

Beim Beispiel des Steckers ist es offensichtlich, dass eine Monitorverbindung nur jene Signale enthalten soll, die zur Darstellung eines Bildes auf dem Monitor erforderlich sind. Genauso verhält es sich mit Software Interfaces. Auch sie sollten so klein wie möglich sein, um unnötige Kopplung zu vermeiden. Und genau wie beim Monitorstecker sollte das Interface eine hohe Kohäsion haben: Es sollte nur Dinge enthalten, die wirklich eng zusammen gehören.

Um das Interface Segregation Principle anzuwenden, stehen die beiden Refaktorisierungen Extract Interface und Extract Superclass zur Verfügung.

Quellen

Quelle Autor Kurzbeschreibung
ObjectMentor Robert C. Martin Artikel zum Interface Segregation Principle von 1996, veröffentlicht im Engineering Notebook für The C++ Report

Dependency Inversion Principle (DIP)

Warum?
Punktgenaues Testen setzt Isolation von Klassen voraus. Isolation entsteht, wenn Klassen keine Abhängigkeiten von Implementationen mehr enthalten – weder zur Laufzeit, noch zur Übersetzungszeit. Konkrete Abhängigkeiten sollten deshalb so spät wie möglich entschieden werden. Am besten zur Laufzeit.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Auch das Dependency Inversion Principle (DIP) ist ein SOLID Prinzip. Es besagt folgendes:

  • High-Level Klassen sollen nicht von Low-Level Klassen abhängig sein, sondern beide von Interfaces.
  • Interfaces sollen nicht von Details abhängig sein, sondern Details von Interfaces.

Verwendet eine High-Level Klasse eine Low-Level Klasse unmittelbar, so ergibt sich eine starke Kopplung zwischen beiden. Spätestens beim Versuch, die High-Level Klasse isoliert zu testen, wird man auf Schwierigkeiten stoßen. Aus diesem Grund sollte die High-Level Klasse von einem Interface abhängig sein, das wiederum von der Low-Level Klasse implementiert wird. So kann die Low-Level Klasse im Unit Test durch ein Mockup ersetzt werden.

Um zur Laufzeit die invertierte, abstrakte Abhängigkeit mit einem konkreten Objekt aufzulösen, bieten sich im Prinzip drei Möglichkeiten:

  • mittels Konstruktorparameter „per Hand“
  • Einsatz eines Inversion of Control Containers (IoC Container) wie etwa Castle Windsor
  • Dependency Lookup

Im gelben Grad injizieren wir die Abhängigkeiten zunächst nur über die Parameter der Konstruktoren. Dies ist anfangs die einfachste Lösung und funktioniert mit einer handvoll Klassen ganz gut. Später im grünen Grad nutzen wir einen IoC Container und Dependency Lookup.

Quellen

Quelle Autor Kurzbeschreibung
objectmentor Robert C. Martin Artikel zum Dependency Inversion Principle von 1996, veröffentlicht im Engineering Notebook für The C++ Report

Liskov Substitution Principle (LSP)

Warum?
Wer mit Erben zu tun hat, möchte keine Überraschungen erleben, wenn er mit Erblassern vertraut ist.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Auch das Liskov Substitution Principle (LSP) ist ein SOLID Prinzip. Es besagt, dass Subtypen sich so verhalten müssen wie ihr Basistyp. Dies klingt zunächst banal. Am Beispiel von Exceptions wird deutlich, welche Probleme entstehen, wenn das Prinzip verletzt wird: Löst der Basistyp bei der Ausführung einer Methode keine Exception aus, müssen alle Subtypen sich an diese Regel halten. Löst die Methode eines Subtyps dennoch eine Exception aus, würde dies bei Verwendern, die ein Objekt vom Basistyp erwarten, Probleme verursachen, weil sie nicht darauf vorbereitet sind. Wenn der Basistyp an der Stelle keine Exception auslöst, ist der Verwender nicht darauf eingestellt, Exceptions behandeln zu müssen.

Allgemeiner kann man das Prinzip auch so ausdrücken, dass ein Subtyp die Funktionalität eines Basistyps lediglich erweitern, aber nicht einschränken darf. Wenn eine Methode im Basistyp auf einem bestimmten Wertebereich definiert ist, darf der Subtyp diesen Wertebereich übernehmen oder auch erweitern, er darf ihn jedoch keinesfall einschränken.

Aus dem Liskov Substitution Principle ergibt sich ferner die Empfehlung, über Vererbung sehr genau nachzudenken. In den allermeisten Fällen ist die Komposition der Vererbung vorzuziehen (Favor Composition over Inheritance). Bei der Vererbung sollte man in jedem Fall über das Verhalten nachdenken, nicht nur über die Struktur. Statt Vererbung alsis-a Relation zu betrachten und dabei nur die (Daten-)Struktur zu bedenken, sollte man besser von einer behaves-as Relation ausgehen und das Verhalten der Klasse berücksichtigen.

Quellen

Quelle Autor Kurzbeschreibung
objectmentor Robert C. Martin Artikel zum Liskov Substitution Principle von 1996, veröffentlicht im Engineering Notebook für The C++ Report

Principle of Least Astonishment

Warum?
Wenn sich eine Komponente überraschenderweise anders verhält als erwartet, wird ihre Anwendung unnötig kompliziert und fehleranfällig.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Softwareentwicklung ist in hohem Maße ein kreativer Prozess. In diesem Prozess ist es wichtig, in den Fluss einzutauchen (engl. Flow). Wenn man diesen Zustand erreicht hat, sprudelt der Code nur so heraus. Jegliche Störung des Flow führt zu Unterbrechungen und letztlich dazu, dass in der zur Verfügung stehenden Zeit nur wenig Code produziert wird bzw. die Qualität des Code nicht optimal ist. Denn nach jeder Unterbrechung muss der Entwickler erst wieder Fahrt aufnehmen und erneut in den Fluss zu kommen. Überraschungen stellen Störungen dar. Sie führen zu Unterbrechungen und Fehlern. Dazu ein Beispiel: Ist die Tastenbelegung in der Entwicklungsumgebung so gewählt, dass eine übliche Tastenkombination wie z.B. Ctrl-C eine völlig andere Bedeutung hat, behindert dies den Entwickler. Ein Entwickler wird sich jedesmal ärgern, wenn er die „falsche“ Tastenkombination verwendet. Dies behindert kreatives Arbeiten.

Software sollte überraschungsarm implementiert sein. Wenn eine Abfragemethode namens GetValue() nicht nur einen Wert liefert, sondern gleichzeitig den Zustand des Systems ändert, wird der Entwickler diese Methode im besten Fall meiden, da er mit bösen Überraschungen rechnet. Im ungünstigen Fall fällt ihm dieses merkwürdige Verhalten nicht rechtzeitig auf. (Abfragemethoden die den Zustand ändern, verstoßen gegen das Command Query Separation Prinzip). Die testgetriebene Entwicklung fördert überraschungsarme Schnittstellen, da die Schnittstelle aus der Sichtweise ihrer Verwendung entworfen und implementiert wird.

Information Hiding Principle

Warum?
Durch das Verbergen von Details in einer Schnittstelle werden die Abhängigkeiten reduziert.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Beim Design einer Schnittstelle sollte man sich fragen, welche Details außen unbedingt sichtbar sein müssen. Mit Schnittstelle sind hier nicht nur Interfaces im objektorientierten Sinne gemeint, sondern auch implizite Schnittstellen. Jede Klasse hat zwangsläufig eine implizite Schnittstelle – sie enthält alle nach außen sichtbaren Details. Je mehr Details von außen sichtbar sind, desto höher ist die Kopplung zwischen der Klasse und ihren Verwendern. Benutzen die Verwender einer Klasse erstmal ein Detail, wird es schwerer, dieses Detail zu verändern. Dies steht der Wandelbarkeit der Software entgegen.

Praktiken

Automated Unit Tests

Warum?
Nur automatisierte Tests werden auch wirklich konsequent ausgeführt. Je punktgenauer sie Code testen, desto besser.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Im orangen Grad haben wir Integrationstests eingeführt, nun geht es um Unit Tests. Im Gegensatz zu Integrationstests wird bei Unit Tests eine einzelne Funktionseinheit (vor allem Klassen, aber auch Methoden oder Komponenten) isoliert getestet. Dazu ist es erforderlich, diese Funktionseinheit von ihren Abhängigkeiten befreien zu können. Sollen Unit Tests im Nachhinein für bestehenden Code ergänzt werden, sind häufig Refaktorisierungen erforderlich. Wir haben durch die Integrationstests die Sicherheit, dass wir dabei keine Fehler einbauen.

Automatisierte Tests bieten zweifachen Nutzen:

  • Sie sparen Zeit
  • Sie nehmen Angst

Je stärker eine Codebasis in Veränderung begriffen ist, desto eher ist die Zeitersparnis zu spüren. Denn wo Code sich verändert, muss immer wieder Neues und auch Altes (Regressionstests) getestet werden. Da spart Automatisation einfach Zeit. Und je komplexer der Code, desto größer ist die Angstreduktion. Denn wenn komplexer Code verändert werden soll – um Funktionalität hinzuzufügen, ihn zu optimieren oder schlicht zu korrigieren –, da besteht hohe Gefahr, ungewollt Fehler einzuführen. Kleinschrittige automatisierte Tests decken diese jedoch auf, sodass kein Grund zur Angst besteht, zu „verschlimmbessern“.

Siehe auch unter Tools.

Mockups

Warum?
Ohne Attrappen keine einfach kontrollierbaren Tests.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

In der Regel verwenden Komponenten andere Komponenten. Will man eine Komponente isoliert testen, müssen diese Abhängigkeiten abgetrennt werden. Dabei interessiert uns nun ausschließlich die Funktionalität der zu testenden Komponente (System Under Test (SUT)). Und es interessiert uns, wie die Komponente mit den anderen interagiert.

Beim Isolieren verwenden wir sogenannte Mockups. Diese werden anstelle der echten Komponenten verwendet. So interagiert das System Under Test während der Tests mit gut kontrollierbaren Attrappen statt mit realen Komponenten.

Die Literatur kennt noch andere Bezeichnungen für Attrappen wie Stub, Dummy oder Fake, die teilweise synonym zu Mockup benutzt werden, aber durchaus für unterschiedliche Funktionsweisen stehen. Bevor man ein Mock Framework wie z.B. Rhino Mocks verwendet, sollte man ein Mockup zunächst „per Hand“ implementieren. Dies hilft, den Mechanismus zu verstehen.

Siehe auch unter Tools.

Code Coverage Analysis

Warum?
Traue nur Tests, von denen du weißt, dass sie auch wirklich das Testareal abdecken.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Unit Tests sollten nach Möglichkeit alle Pfade durch unseren Code abdecken. Nur so gewinnen wir das Vertrauen, dass der Code korrekt arbeitet. Um in Erfahrung zu bringen, welche Codebereiche bislang nicht durch Tests abgedeckt sind, bedienen wir uns der Code Coverage Analyse. Diese dient dazu, Bereiche im Code aufzudecken, die noch nicht während der automatisierten Tests ausgeführt werden.

Unit Tests sollten eigentlich 100% des zu testenden Codes abdecken. Zwar bedeutet das nicht automatisch, dass genügend Tests existieren, doch weniger als 100% Code Coverage zeigen an, dass es noch Taschen von Code gibt, über die überhaupt noch keine Korrektheitsaussage gemacht werden kann. 100% Codeabdeckung sind deshalb immer anzustreben.

In der Praxis zeigt es sich jedoch, dass 100% Codeabdeckung nicht immer mit unmittelbar vertretbarem Aufwand erreicht werden können. Wie auch sonst im Leben kann die Mühe für die letzten 2,3,4 Prozent überproportional wachsen. Deshalb kann es nach genauer Analyse der Abdeckungslage akzeptabel sein, mit weniger als 100% zufrieden zu sein.

Unterhalb von 90% ist die Abdeckung dann allerdings so löchrig, dass sie als unprofessionell anzusehen ist. Wer also mit automatischen Tests beginnt, sollte immer auch gleichzeitig die Codeabdeckung messen. Sonst lässt sich keine Aussage über die Qualität der Tests machen.

Für die Messung der Codeüberdeckung gibt es zwei einfache Kennzahlen, die als C0- und C1-Kennzahlen bezeichnet werden. Die C0-Kennzahl misst die Anweisungsüberdeckung, wogegen die C1-Kennzahl die Entscheidungsüberdeckung bzw. die Zweigüberdeckung misst.

C0 = (Anzahl der getesteten Anweisungen / Anzahl der gesamten Anweisungen) * 100%
C1 = (Anzahl der getesteten Entscheidungen bzw. Zweige / Anzahl der gesamten Entscheidungen bzw. Zweige) * 100%

C1 ist dabei die stärkere Kennzahl, da 100% Entscheidungsüberdeckung bzw. Zweigüberdeckung 100% Anweisungsüberdeckung impliziert. Der Umkehrschluss gilt nicht.

Der Anweisungsüberdeckungstest sowie der Zweigüberdeckungstest arbeiten auf Basis eines Kontrollflussgraphen, siehe http://de.wikipedia.org/wiki/Kontrollflussgraph, während der Entscheidungsüberdeckungstest direkt auf dem Quellcode basiert. Die Testverfahren Anweisungsüberdeckungstest und Zweigüberdeckungstest sind sehr gut unter http://de.wikipedia.org/wiki/Kontrollflussorientierte_Testverfahren beschrieben.

Siehe auch unter Tools.

Partizipation in Professional Events

Warum?
Am besten lernen wir von anderen und in Gemeinschaft.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Um nicht nur „im eigenen Saft zu schmoren“, ist es wichtig, regelmäßig mit anderen Softwareentwicklern zu diskutieren und Erfahrungen auszutauschen. Um dabei auch über den Tellerrand zu blicken, sollte der Austausch mit Entwicklern außerhalb des eigenen Teams, der täglichen Routine, erfolgen. Gut geeignet sind User Groups, die sich in allen Regionen Deutschlands finden lassen.

Bei den regionalen User Groups steht der Erfahrungsaustausch im Vordergrund. Der ist wichtig. Je länger der aber innerhalb derselben Gruppe stattfindet, je besser man die Gesprächspartner kennt, desto mehr gleichen sich die Meinungen auch in einer User Group wieder an. Deshalb ist es wichtig, immer wieder auch über diesen Tellerrand hinaus zu schauen. Neues Gedankenfutter und Diskussionen mit ganz anderen Entwicklern bieten dafür überregionale Entwicklerkonferenzen.

Für Gedankenaustausch und Inspiration sollte ein CCD also drei Ebenen im Blick behalten: das eigene Entwicklerteam, die regionale User Group und die überregionale Konferenz. Jede Ebene hat dabei ihren eigenen Rhythmus: täglich, monatlich, jährlich.

Links:

Complex Refactorings

Warum?
Es ist nicht möglich, Code direkt in der ultimativen Form zu schreiben.
Wandelbarkeit   
Korrektheit   
Produktionseffizienz   
Kontinuierliche Verbesserung   
Single Developer

Bereits im roten Grad sind einfache Refaktorisierungen eingeführt worden. Doch Umbenennen und Methode extrahieren reichen nicht aus, um den Code zu verbessern – oft sind größere Eingriffe erforderlich. Die Einteilung in einfache und komplexe Refaktorisierungen ist sinnvoll, weil komplexe Refaktorisierungen nur mit vorhandenen automatisierten Tests effizient und risikolos zu bewerkstelligen sind. Ohne Tests wäre nach dem Refaktorisieren nicht bekannt, ob der Code immer noch korrekt ist.

Siehe auch unter refactoring-legacy-code.net sowie unter Tools.

Weiter geht es beim grünen Grad