Inhalt
Topic:.SwEng_Refactoring.
.
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:
a) Man belässt alles beim Alten. Der Nachteil: Die Unstimmigkeiten bleiben und werden noch weiter manifestiert. Weitere Programmteile kommen hinzu, die die unstimmigen Teile verwenden. Andere Programmteile richten sich nach den an sich unstimmigen Schema.
b) Es ist Zeit für Refactoring. Alles wird umgestellt. Das ist wie Revolution. (Erstmal stimmt gar nichts mehr, danach geht es weiter im alten Trott). Wichtig für Revolutionäres Refactoring ist, dass die Umstellung nicht gepaart ist mit Entwicklungsaufgaben. Es darf nichts am Algorithmus verändert werden, sondern nur bereinigt. Das Refactoring muss alles umfassen.
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:
Wird an einer Stelle eine änderungswürdige Unstimmigkeit erkannt, dann muss erstmal abgeschätzt werden, wie aufwändig diese Änderung im Blick auf die Gesamtsoftware ist. Diese Abschätzung ist oft nur grob, da das Problem ggf. nicht so zentral wichtig und (noch) nicht Chefsache ist, und damit ggf. ungenau.
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.).
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:
Wenn man die Reihenfolge von Argumenten einer Methode oder auch nur bestimmte Typen ändern will, dann sollte die neue Reihenfolge nicht mit automatischer Typkonvertierung mit der alten harmonisieren. Oder man sollte den Methodennamen etwas abändern. Damit wird man vom Compiler gezwungen, an den Aufrufstellen nachzubessern. Beispiel:
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.
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:
Schnittstellen können erweitert werden. Bisherige Nutzungen sind dann nicht betroffen. Man kann die alten unerwünschten Methoden
in Java als @deprecated
bezeichnen, dann fällt es bei der Nutzung auf ohne Zwang zur Änderung. In C/C++ gibt es ein solches Mittel nicht, hier hilft
nur verbale Kommunikation.
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.
Nach einiger Zeit werden auf grund von @deprecated
oder verbalen Hinweisen gegebenenfalls die anderen Nutzer die alten Schnittstellenmethoden nicht mehr benutzen. Das kann
1 Jahr oder länger dauern, bei umfangreichen Projekten und weniger strenger Führung. Man könnte sich dann trauen, die alten
Methoden wegzulöschen, und mal sehen was passiert.
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.
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?
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:
Entweder die Wiederverwendung wird in das Refactoring eingebunden - das ist möglicherweise organisations- und arbeitsaufwändig.
Oder Schnittstellen werden nicht geändert sondern nur erweitert. Die alten Schnittstellen müssen beibehalten werden. Sie können als @deprecated bezeichnet werden, aber man darf nicht damit rechnen, dass sie demnächst nicht mehr verwendet werden.
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.
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.
Entweder man gleicht sich dann an die geänderten Bezeichnungen an. Dann ist aber möglicherweise ein schwer überschaubarer Teil der anwendenden Software ebenfalls zu refactorieren,
oder man zieht die geänderten Bezeichnungen zwar nach, baut aber Adapter nach außen.
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.