der gelbe grad

There are two ways of constructing a software design:
One way is to make it so simple that there are obviously no deficiencies,
and the other way is to make it so complicated that there are no obvious deficiencies.
(C. A. R. Hoare)

Eintrag 24.06.2018: Der gelbe Grad

Die Prinzipien

Information Hiding Principle:"So global wie nötig, so lokal wie möglich" - so ein Satz, den ich in meinem Delphi-Programmierkurs meinen Schülern vermittelt hatte. Global zugreifbare Elemente - auch als public-Deklarierte Attribute innerhalb Klassen - bergen das Risiko, dass eine unbedachte Veränderung einen unschönen Nebeneffekt haben kann. Daher ist die Frage immer, wie weit kann das Attribut "versteckt" werden. Ist private möglich? Muss es in abgeleiteten Klassen verfügbar sein (protected) oder braucht man es zugreifbar nach außen (public). Die Frage nach der Sichtbarkeit von Attributen/Methoden kann ganze Bücher mit Beispielen füllen. In einem aktuellen Projekt habe ich mich z.B. wissentlich dafür entschieden, dass ich die komplette SQL in einer entsprechenden Klasse nur intern halte und keine Modifizierung von außen zulasse. Hardcoded protected Methods sind dafür von einem von der Hauptklasse abgeleiteten Klasse zur Behandlung des SQLite-Dialektes generiert worden.

Ein weiteres Prinzip aus den SOLID-Principles ist das Liskov-Substitution-Principle - das Prinzip was auch mit dem folgenden, dem Principle of Least Astonishment verwand ist, geht nicht nur darauf ein, dass es keine Überraschungen geben soll, sondern sagt explizit aus, dass eine abgeleitete Klasse nicht die Funktionalität der Elternklasse einschränken soll. D.h. insbesondere, wenn in der Elternklasse nicht auf Exceptions behandelt wurde, darf die spezifizierte Klasse dies nun auch nicht um demjenigen, der die Klasse am Ende benutzt, dann nicht plötzlich ein anderes Verhalten im Fehlerfall zu geben - umgekehrt genauso: Wenn bisher Fehler abgehandelt wurden (z.B. Wertebereichsübersteigung) dann muss dies auch zukünftig so passieren. Es heißt also kurz gesagt: Eine abgeleitete Klasse darf erweitern, aber nicht einschränken.

Principle of Least Astonishment: Wie auch schon im Liskov Substitution Principle genannt, ist bei Vererbung ein Überraschungseffekt zu vermeiden. Wenn sich eine Klasse plötzlich anders verhält, z.B. der Wertebereich sich "verschiebt", weil man nicht mehr bei 0 anfangen will zu zählen, dann hat das in allen Implementierungen unsaubere Nebeneffekte. Das verkompliziert die Anwendung dann nur unnötigerweise und führt zu fehlerhaften Implementierungen. Wenn eine einheitliche Implementierung definiert wurde, dann sollte dieser auch weiter gefolgt werden, damit es beim Entwickler keine "Rüstzeiten" gibt, die er zunächst braucht, um sich in die Klasse / in das Projekt einzuarbeiten. Wird z.B. für "Suchen und Ersetzen" immer Strg+R genutzt, aber hier in einer einzigen Anwendung ist Strg+R für "Return to Start" implementiert, dann führt das nur zu unnötigen Ärgernissen. Man siehe auch Windows vs. Mac - Das @-Zeichen unter Windows führt über "Command+Q" zum Schließen des aktuellen Fensters/Programmes - da kommt beim Umstieg Freude auf.

Dependency Inversion Principle: Zu gut Deutsch: Abhängigkeit-Umkehrungs-Prinzip. Dieses Prinzip beruht auch auf "Uncle Bob's" (Robert C. Martin) zu den SOLID-Principles zusammengefassten Prinzipien und gibt die Vorteile wieder, die sich daraus ergeben, wenn einzelne Klassen - hier insbesondere die s.g. "High-Level"-Klassen sowie die "Low-Level"-Klassen nicht direkt abhängig machen. Abhängigkeit sorgt oft für höheren Wartungsaufwand oder zu größeren Problemen bei Hinfälligkeit der Komponente gegen die man eine Abhängigkeit besitzt. Umgekehrt kann man sagen: Je weniger Abhängigkeiten man aufbaut, umso mehr Übersicht kann man haben und daher auch umso weniger Aufwand hat man um wieder in das Projekt einzutauchen. Nun geht es aber explizit um die OOP von Klassen. Eine Klasse "Unternehmen" kann z.B. als eine solche High-Level-Klasse angesehen werden. Nun hat das Unternehmen Attribute wie etwa "Geschäftsform" und "Name" mit den Methoden für das lesen und schreiben der Werte. Unklar ist allerdings die Rolle des Unternehmens für die eigene Geschäftstätigkeit. Es wäre also möglich nun ein Attribut zu definieren, was die Rolle wiedergibt: Ist es ein Lieferant, oder ein Kunde? Das hat allerdings ein praktischen Nachteil. Sobald der Lieferant bei uns auch einkaufen möchte, müssen wir ihn komplett neu anlegen. Anders wäre es, wenn wir es Objektorientiert versuchen. Zwei Attribute: Lieferantennummer und Kundennummer? Netter Versuch. Sobald allerdings für die Kunden noch etwas, wie eine Skonto-Definitions-ID, und für die Lieferanten eine Bonuskartennummer definiert werden muss, wird das Objekt etwas aufgebläht - ok, schon bei der Rollendefinition als zwei Attribute entspricht das nicht mehr der Thematik, dass nur eine Aufgabe mit der Klasse erfolgt werden soll. Aus Sichtweise des objektorientierten Designs Bietet sich also eine Ableitung von zwei Low-Level-Klassen Lieferunternehmen (oder Lieferant) und Kundenunternehmen an. Hier steckt der Teufel nun im Detail. Wenn wir dem Unternehmen zwei Attribute geben für die Ableitung in Lieferunternehmen und Kundenunternehmen, dann herrscht hier eine Abhängigkeit zwischen Unternehmen bzgl. seinen Ableitungen. Es ist somit geschickter, wenn die Klasse Unternehmen ein Interface definiert. Überall, wo man nur die Bausteine der High-Level-Klasse braucht, kann über das Interface die Klasse Unternehmen angesprochen werden. Die vom High-Level-Interface abgeleiteten Klassen können dann in den unterschiedlichen Kontexten entsprechend genutzt werden. Ein weiteres Beispiel kann die nach dem MVVM-Prinzip definierte Unabhängigkeit zwischen Datenmodell und der Darstellung sein. Eine High-Level-Klasse wäre für die generelle Abwicklung der Methoden zu einer Datenbank X als Interface definiert und implementiert nur die Hauptmethoden, die jede Datenbank "kann". Die Low-Level-Klassen definieren dann die Dialekte der einzelnen Datenbankhersteller. So kann man z.B. sicherstellen, dass bei Oracle die Systemzeit immer mit "sysdate", mit Sqlite allerdings mit "CURRENT_TIMESTAMP" abgefragt wird (ok, sehr fiktives Beispiel!!!! ;-) ).

 

Nachtrag 14.10.2018: Der gelbe Grad

Die Methoden

Automatisierte Unit Tests sind kurz gesagt, eine Investition in langfristige Qualität. Am Anfang sieht es so aus, als würde mehr Zeit aufgewendet, weil schlie├člich testen wir doch immer, was wir ändern. Was wir hier allerdings außer Acht lassen, ist die Tatsache, dass es bei Neuentwicklungen auch Seiteneffekte auf bestehende Bereiche der Anwendung haben kann. Wenn wir uns mit Testen befassen, finden wir auch schnell heraus, dass wir nicht alles testen können. Was wir allerdings machen können, ist durch viele, viele, viele Tests aufbauen, die uns Hinweise geben können, wo sich ein Defekt befindet.

Mockups (Testattrappen) sind quasi "Wegwerf-Produkte". Bei der Erfassung von Anforderungen ist es oft so, dass Personen, die miteinander kommunizieren, doch leider aneinander vorbei reden. Nach einem Gespräch scheint vielleicht jedem alles klar zu sein, doch spätestens, wenn wir bei der Implementierung sind, tauchen neue Fragen auf. Annahmen und Interpretationen helfen hier nun, einfach weiter zu machen. Der Kunde wird das schon so, oder so gemeint haben. Schließlich wollen wir nicht den Kunden verärgern oder zugeben, dass wir nicht alles richtig verstanden haben... oder?
Nein! Genau dieses "grübeln" ist es, was uns dann tatsächlich davon abhält, die richtige Entscheidung zu wählen: Den Kunden kontaktieren und ihm die neuen Informationen und Fragen weiterzuleiten. Weil der Fakt ist nicht, dass nicht alles besprochen wurde, sondern dass wir manche komplexen Prozesse oder Systeme garnicht ganz überblicken können und uns zur Erledigung einer komplexen Aufgabe ein iterativ inkrementieller Vorgehensansatz helfen kann. Das wohl bekannteste "Framework" ist ohne Zweifel SCRUM. Ein Ansatz, was Mockups bzw. Prototypen in den Vordergrund setzt, ist "Design-Thinking".

Die Code Coverage Analyse ist ein Ansatz, bei dem man versucht wird, den kompletten Quellcode mit Unit-Tests zu überdecken. Dabei wird die Art der Code-Überdeckung eingeteilt in Anweisungs-, Zweig- und Pfadüberdeckung. Die Anweisungsüberdeckung (Auch C0-Überdeckungstest genannt) besagt, wie viele Anweisungen durch einen Test überdeckt werden. Verschiedene if- else-if und else Logiken werden dabei au&sszlig;er Acht gelassen. Dabei geht es dann in dem Zweigüberdeckungstest (auch C1-Überdeckungtest genannt). Hier kann es aber auch sein, dass es mehrere if-Abschnitte gibt, die hintereinander und nicht ineinander verschachtelt sind. Nehmen wir mal an, wie haben zwei if-Anweisungen mit je einem else-Teil, die hintereinander abgelaufen werden. Eine Mögliche Abfolge wäre if-if und else-else. Dabei wären alle Voraussetzungen für einen Zweigüberdeckungstest gelegt. Was aber, wenn wir bei dem ersten if ein Objekt nicht setzen, was aber nur beim zweiten else genutzt wird? Da wäre der if-else quasi nie getestet und würde irgendwann im Produktiv-System auf einen Fehler stoßen. Um auch diese Tests abzudecken gibt es die Klasse der Pfad-überdeckungstests (auch C2-Überdeckungtest genannt). Wer die C2-Tests ernst nimmt, hat im Grunde, sobald eine Schleife im Spiel ist, enorm viel Zeit einzuplanen. Um hier eine Erleichterung zu haben, wurde diese Test-Klasse nochmal in drei Teile unterteilt. C2a steht für die Abdeckung aller Pfade - auch das Durchlaufen aller Schleifen, was in möglicherweise sehr vielen Testfällen resultiert. C2b unterscheidet nochmal nach "Boundary" und "Interior-Test" und gibt die Beschränkung auf die Anzahl der Durchläufe in den Schleifen fest. Beim Boundary-Test müssen alle Pfade innerhalb der Schleife abgearbeitet werden und dem Interior reicht die Abdeckung, die bei zwei Durchläufen der Schleife möglich sind. Der C2c-Test setzt die Anzahl der Durchläufe der Schleifen auf eine natürliche Zahl n fest, egal ob in der Schleife alle Pfade abgedeckt wurden. Wenn wir noch genauer in die Test-Welt einsteigen wollen, dann werden wir uns auch mit der Frage beschäftigen dürfen, in wie weit ein Pfad allein Aussage darüber gibt, ob eine Kombination aus Bedingungen (z.B. a || b) zu einem guten Test führt. Die Antwort liefert der C3- bzw. Bedingungs-Überdeckungtest. Hier werde ich nun nicht auf die Details eingehen, da hier die Anzahl der Tests auf ein nicht mehr überschaubare Zahl explodieren lassen kann.

Die Teilnahme an Fachveranstaltungen hilft nicht nur, sich einmal abseits des Büros mit anderen Leuten sich zu treffen, die gleiche oder ähnliche Interessen besitzen und damit die Möglichkeit "Networking" zu betreiben, sondern auch einmal andere Ansichten, Methoden oder Lösungen, die einem selbst Inspiration geben können einen noch besseren Weg einzuschlagen. Es gibt in vielen größeren Städten eine Usergroup oder ein Stammtisch, der sich für einen Gedankenaustausch regelmäßig trifft.

Komplexe Refaktorisierungen - nunja, im roten Grad haben wir einfache Refaktorisierungen bereits kennen gelernt. Bei Komplexeren, geht es um mehr als nur Umbenennen und Extraktion von Methoden. Komplexe Refaktorisierungen haben nur in Zusammenarbeit mit dem Aufbau von automatisierten Tests Sinn, da die Änderungen sonst zu risikobehaftet sind. Im Clean-Code-Guide wird nun auf die Website refactoring-legacy-code.net/ verwiesen, auf der die Mikado-Methode eingeführt und beschrieben wird. Komplizierte Probleme gehören zu denjenigen, die durch einfaches Nachdenken gelöst werden können. Die Mikado-Methode gibt die Möglichkeit mit komplexen Problemen umgehen zu können. Dabei wird die Möglichkeit des Reverts nach einer Änderung genutzt, wenn sich in Tests neue Fehler nachweisen lassen.