Refactoring - Software stimmig machen.

Refactoring - Software stimmig machen.

Inhalt


Topic:.SwEng_Refactoring.

.


1 Der evalutionäre Umgang mit Software

Topic:.SwEng_Refactoring..

Revolution oder Evolution? Die Situation ist oft folgende: Ein Programmierer bemerkt, dass Bezeichnungsschemata, Detailstrukturen oder einzelne Identifier nicht stimmig sind. Im Programmierer-Team hat man gegebenenfalls im Moment für solche Dinge keine Zeit. Zwei bis drei Lösungen gibt es:

Die Varinante b) gefällt aber auch nicht und wird meist bis auf die Zeit nach der dringend terminlichen Fertigstellung herausgeschoben. Danach gehts weiter in anderen Projekten....

Die dritte Lösung ist die zwischen den Extremen:


1.1 Lokales Refactoring (Neustrukturierung)

Topic:.SwEng_Refactoring...

Hat man sauber mit Schnittstellen gearbeitet (interface in Java) und ist die Abhängigkeit der Softwareteile geklärt, dann bestärken die Aussagen: "Interface ist nicht betroffen" und "Änderung im überschaubaren Rahmen" die Tatkraft, die Änderung in eigener Verantwortung durchzuführen. Selbstverständlich sollte man schauen, ob sich danach die Lesbarkeit der Dokumentation dieser Softwareteile verbessert hat (nur dann war der eigentliche Beweggrund zur Bereinigung gerechtfertigt.).


1.2 Der Compiler (Fehlermeldungen) muss alle nachzubessernden Stellen zeigen

Topic:.SwEng_Refactoring...

Wichtig für das Refactoring ist nun, dass der Compiler alle Stellen selbst finden kann, die nachgebessert werden müssen. Muss man per Hand aufpassen, dann kann man etwas vergessen. Dazu einige kleine Tricks:

 void setVal(int val);        //alte Variante, int wird übergeben.

 void setVal(float val);      //neue Variante, float wird übergeben, gleichzeitig ist der Wertebereich geändert.

 void setFloatVal(float val); //neue Variante mit geändertem Methodennamen,

In der ersten neuen Variante passt der Compiler automatisch, und damit falsch an. In der zweiten kann nicht automatisch angepasst werden. Man wird also per Compilerfehler an die Aufrufstelle geführt und wird richtig korrigieren. - Ein weiteres Beispiel mit bewußtem Tausch von Argumenten:

 void setValues(int val1, Type ref);  //alte Variante, int und eine Referenz wird übergeben.

 void setValues(Type ref, float val); //neue Variante, hier ist nebst dem float-Type einfach die Argumentreihenfolge gedreht.

 void setFloatVal(float val);         //neue Variante mit geändertem Methodennamen,

Möglicherweise ist die Drehung der Argumentreihenfolge auch gar nicht so ungewollt. Das wichtige beim Refactoring ist, dass der Compiler wiederum nicht automatisch falsch anpassen kann.


1.3 Änderung von Schnittstellen

Topic:.SwEng_Refactoring...

Soll eine Änderung an Schnittstellen erfolgen, dann weiß man gegebenenfalls überhaupt nicht, welche Softwareteile von der Änderung betroffen sind. Gegebenenfalls wird die Software aber in einem abgegrenzten Projekt verwendet, dann ist es eher überschaubar. Man sollte aber keine Furcht vor Änderungen haben, sonst manifestieren sich ungünstige Stukturen dauerhaft. Andererseits kann man Andere (Kollegen, externe Nutzer) nicht zur Anpassung zwingen, wenn sie in eigenen Problemen schwimmen. Daher kann folgendes Gebot gelten:

Es werden also die alte und die neue Form, möglicherweise ein paar Wochen später die noch neuere Form nebeneinander existieren. Freilich kann das einige Nutzer durcheinander bringen. Abhilfe schafft hier eine entsprechende Dokumentation, die auch auf ein warum verweist. Das Mittel @deprecated in Java gepaart mit Doku ist vollkommen ausreichend. In C/C++ hat man ggf. die Möglichkeit einer automatischen Dokumentationsgenerierung aus den Quellen und kann damit wenigstens informieren.

Die Message ist: Geduld üben und niemanden zu Änderungen zwingen, wenn er dafür im Moment keine Aufmerksamkeit haben kann.

Die zweite Message ist:Aushalten, das neue und alte Konzepte nebeneinanderstehen und verwechselbar sind.


1.4 Geänderte Schnittstellen bei nicht geänderter Implementierung

Topic:.SwEng_Refactoring...

In C sind Methoden-Schnittstellen mit Prototypen in Headerfiles und Datenschnittstellen mit struct-Definitionen in Headerfiles repräsentiert. Wenn Headerfiles geändert werden, dann sollten alle nutzenden Sources neu compiliert werden, dann stimmt alles.

Möglicherweise wird man aber nicht neu compilieren oder ältere Libraries einfach einziehen. Wie kann man das verhindern? Während eines Entwicklungsprozesses nur schwer. Bei einer Softwarelieferung nach außen selbstverständlich ja, indem man dann alles neu compiliert und auf Konsistenz achtet.

Ein Ausweg ist die nicht-Nutzung von Libraries, grundsätzlich alles aus Quellen vollständig compilieren. Aber Libraries haben auch Vorteile (einfache Nutzbarkeit, Anwender braucht nicht fremde Quellen mit möglichen Unstimmigkeiten).

Ein C-Compiler/Linker findet C-Routinen mit geänderten Argumenten nicht heraus. Ergebnis ist ein oft schlecht findbares Softwarefehlverhalten. Abhilfe ist hier nur, neue Routinen mit neuen Namen zu erfinden und die alten erstmal @deprecated zu belassen.

Bezüglich Datenstrukturen hilft folgender Trick: Neue Daten werden hinten angehangen. Damit stimmen alte Libraries + alte Nutzung. Probleme gibts bei alten Libraries und neuer Nutzung. Diese wird ebenfalls formell nicht erkannt.

In C++ kommt bei Interfaces ein weiterer Effekt hinzu: Die virtuellen Methoden sind implementierungstechnisch in einer Sprungleiste angeordnet. Und zwar (meist) in Reihenfolge wie sie in der class-Definition aufgeführt sind. Hängt man neue Methoden hinten an, so vertragen sich alte Nutzungen mit alter Lib trotz eines neuen Headerfiles. Das ist so wie bei hinten ergänzten Daten. Damit kann man erstmal erweitern, ohne alle Kollegen zur Neucompilierung zu zwingen.

Eine Bereinigung, ein Revolutionäres Refactoring, sollte dann erfolgen, wenn alle Softwareentwicklungen entsprechend fortgeschritten sind und eine Neucompilierung über alles erfolgen kann.

In Java sieht das Problem wie folgt aus:

Man kann neue java-Files von Interfaces haben, aber alte jar-Files beim compilieren und/oder beim Ablauf benutzen. TODO: Wie reagiert dann Java?


2 Refactoring und Reusing (Wiederverwendung)

Topic:.SwEng_Refactoring..

Hat man die Situation, dass ein Softwarestand irgendwo anders wiederverwendet wurde, dann ist ein Refactoring die erste Ursache, dass die Software auseinanderläuft. Zu verhindern ist das nur mit zwei Maßnahmen:

Die Schlussfolgerung aus diesem Dilemma kann nur sein:

Schnittstellen von vornherein gut überlegt und eher defensiv festlegen, nur das anbieten, was absehbar notwendig ist. Damit können Schnittstellen ein Refactoring gut überleben.


2.1 Paralleles Nachziehen von Quellen

Topic:.SwEng_Refactoring...

Bei Wiederverwendung gibt es die Möglichkeit der Parallelpflege: Der eigene Softwarestand wird selbst gepflegt, aber es wird ab und zu ein Vergleich mit dem Original durchgeführt. Es gibt dazu schöne Tools der Differenzanzeige von Texten. Was gefällt oder passt, wird dann übernommen. Das ist allemal besser als ein vollständiges Auseinanderlaufen mit dem Original.

Aber der textuelle Vergleich ist dann schwierig, wenn Bezeichnungen formal geändert wurden. Dann ist in fast jeder Zeile womöglich ein Unterschied, der eigentlich kein richtiger ist. Das zu überschauen fällt schwer.

Letzteres ist in C/C++ mit defines möglich:

 #define alte_Bezeichnung neue_Bezeichnung

Dann sieht der Compiler die alte Bezeichnung (nebst der neuen), verwendet aber die neue. Folglich müssen die anwendenden Quellen nicht sofort oder gar nicht nachgezogen werden. Da beide Bezeichnungen nebeneinanderstehen, kann die anwendende Software eine Umstellung nach und nach machen.