Mittwoch, 21. August 2013

Fazit "Software Sanierung" von Sebastian Kübeck - Teil 1a, Einführung und Design Prinzipien

Ohne konkrete Zahlen herauszukramen, stelle ich hier die Behauptung auf, dass ein Software Entwickler in seinem Berufsleben viel mehr mit existierendem Code arbeitet als dass er regelmäßig die berühmte "Grüne Wiese" bestellt.
 Dem Pragmatismus der Pragmatic Programmers folgend, versuche ich aus diesem Fakt eine Tugend zu machen, mit dem Ziel "Master Of Legacy Code" zu werden. Und ganz ehrlich, sich einen fremden, alten, nicht Unit-getesteten Code zu eigen machen - das ist doch im Grunde herausfordernder als immer nur ein neues Town House auf die planierte Fläche zu setzen.

Dieser Logik folgend stürzte ich mich neulich enthusiastisch auf das Buch "Software Sanierung" von Sebastian Kübeck, erschienen  2009 im mitp-Verlag. Um es vorweg zu nehmen: hiermit gestehe ich, dass in der Vergangenheit meine Unit-Tests häufig eher externe Tests waren.

Der erste Teil des Buches ist ein Crash-Kurs im benötigten Handwerkszeug:

  • Objektorientierung
  • Tests inklusive Abgrenzung der verschiedenen Test-Arten
  • Wichtige Design Patterns
  • Refactoring Patterns
  • Fehlerbehandlung (Exceptions)

Da dieses Blog mein öffentliches Gehirn sein soll, hier die Liste der Dinge, die ich als wichtig in diesem ersten Teil empfand:

Natürliche versus Künstliche Komplexität
Die natürliche Komplexität beschreibt letztendlich die Grund-Komplexität des implementierten Fachprozesses. "Der kleinstmögliche Umfang an Informationen, die notwendig sind um [ eine Problemstellung ] vollständig zu beschreiben, definiert die natürliche Komplexität des Problems."


Die natürliche Komplexität lässt sich nur verringern in dem man Features aus einer Anwendung wieder ausbaut und sie damit wieder vereinfacht.

Nun zur künstlichen Komplexität: "...der Ballast .., der nötig ist um Programme unter den gegebenen Rahmenbedingungen und mit den Kenntnissen der Programmierer zu realisieren."

Die künstliche Komplexität kann man direkt verringern, zum Beispiel durch gutes Design, Hochsprachen oder die Verwendung von Bibliotheken anstelle von eigenen Implementierungen. Ein guter Entwickler erhöht also bei einer Programmerweiterung die künstliche Komplexität nur um das wirklich notwendige Minimum - so weit zumindest die Theorie.

Sanieren statt Wegreißen
In der Einleitung werden ein paar gute Argumente für eine Sanierung bestehender Software angeführt, besonders wichtig finde ich diesen hier (in eigenen Worten):
Häufig ist die Software selbst die einzig verbliebene, aktuelle Spezifikation des Fachprozesses. Wissensträger sind teilweise nicht mehr verfügbar, die existierende Dokumentation ist lückenhaft. Allein das Programm beinhaltet das gesammelte Wissen der letzten x Jahre/ Jahrzehnte.

UML Klassendiagramme sind zwar schick aber...
...spiegeln nicht die Interaktion der Objekte wieder. Außerdem entsteht kein Programm aus einer Klassen/ Objekt-Beschreibung. Das scheint wohl noch aus der Zeit zu kommen, wo man Klassendiagramm gemalt hat und sich dann den Code per Knopfdruck generiert hat. 
Viel näher am Software-Entwicklungsprozess sind die Interaktionsdiagramme die erst später in die UML aufgenommen wurden. Das Nützlichste aus meiner Sicht ist das Kommunikationsdiagramm - letztendlich eine Formalisierung der Kästchen mit Pfeilen die man sowieso gern zur Visualisierung verwendet.

Interface-Aufteilungsprinzip
"Interfaces sollten nur so viele Methoden haben, wie für die Ausführung einer Aufgabe unbedingt nötig sind. Können zusätzliche Methoden zur Verfügung gestellt werden, sollte man das Interface aufteilen."

Liskov Substituitions-Prinzip
"If it looks like a duck, quacks like a duck, but needs batteries – you probably have the wrong abstraction" (Link)

Das Web ist voll mit Erklärungen dieses Prinzips.Grundsätzlich soll jede Erweiterung einer Klasse die Elternklasse vollständig ersetzen.

"Der Nachteil der Verletzung des Liskov-Substitutionsprinzips liegt in der Erwartungshaltung an eine Erweiterung einer Klasse. Da man dank der Polymorphie unter Umständen nur it der Elternklasse arbeitet, ohne zu wissen, dass man es eigentlich mit einer Ableitung zu tun hat, ist es äußerst unangenehm, wenn sich diese Klasse ganz anders verhält als die Elternklasse."

Für mich wird dieses Prinzip durch ein Beispiel am besten deutlich. Robert Martin hat dies sinngemäß einmal so erklärt: Mathematisch ist ein "Quadrat" ein "Rechteck". Man ist also versucht auch eine Klasse "Quadrat" von einer Basisklasse "Rechteck" abzuleiten.
Die Methoden "setX()"und "setY()" machen bei einem Rechteck durchaus Sinn - beim einem Quadrat allerdings nicht wirklich. Hier setzt der Aufruf einer Methode alle vier Seiten. Die Abstraktion "ein Quadrat ist ein Rechteck" passt hier also unter objektorientierter Betrachtungsweise schlecht.

Abhängigkeits-Inversionsprinzip
Auf diesem Prinzip bauen fast alle Refactorings des Buches auf. Es sagt aus, "dass Klassen möglichst nicht von konkreten Implementierungen anderer Klassen, sondern von deren Interfaces abhängig sein sollen".
In der Praxis bietet es sich an eine starre Kopplung z.B. an die JDBC-Klassen durch ein eigenes Interface aufzulösen, Die Produktionsimplementierung des Interfaces ist letztendlich ein Wrapper (ja, ich weiss, es heißt "Delegation") um die JDBC-Klassen.
Die Testimplementierung nutzt das gleiche Interface, emuliert aber Aktionen wie "getLastName()" mittels Hashmap.

Wenn nun der Ursprungsklasse während der Laufzeit eine andere Datenbank-Klasse mittels z.B. "setDatabase()" Methode "injiziert" wird, spricht man von "Dependency Injection".

Änderungsvektoren (einer Klasse)
Im Laufe der Zeit wird häufig eine Klasse durch verschiedene Änderungswünsche (Anforderungen) in verschiedene Richtungen getrieben. Diese verschiedenen Richtungen nennt man Änderungsvektoren.

Single-Responsibility Principle
Eine Klasse sollte nur einen dieser Vektoren implementieren. Also z.B. sich nur um Datenbankaktionen kümmern und keine Berechnungen durchführen.

Das Gleiche gilt für Methoden: checkAndStoreData() wird besser zu "checkData()" und "storeData()". Dieses Prinzip ist universell und gilt genauso für C Dateien und Funktionen.


1 Kommentar: