Die Wartung der Software (siehe Kap. 22) findet auf Wunsch oder im Auftrag der Anwender statt. Sie haben ein starkes Interesse daran, dass die Software korrigiert, angepasst und erweitert wird. Das Reengineering dagegen dient nicht (direkt) dem Anwender, sondern ermöglicht oder erleichtert die Wartung. Ein Reengineering ist spätestens dann erforderlich, wenn die Software verändert werden muss, aber nicht (mit vertretbarem Aufwand) verändert werden kann, weil die Wartungsqualität schlecht ist.
Das Reengineering ist also eine Maßnahme, die dem Kunden nur indirekt dient, indem sie das System in einen wartbaren Zustand zurückversetzt und damit die Wartung im Auftrag des Kunden möglich macht. Wenn der Holzfäller den Holzeinschlag unterbricht, um die Axt nachzuschleifen, dann betreibt er Reengineering. Diese Unterbrechung wäre unnötig, wenn die Axt entweder immer scharf bliebe oder wenn er sie durch besonderes Geschick oder einen speziellen Mechanismus bei jedem Schlag ein wenig nachschärfen könnte.
Software Engineering ist ein Lehr- und Forschungsgebiet, das vor allem durch Erfahrungswissen gekennzeichnet ist. Der Mangel an harten Fakten hat Forscher immer wieder veranlasst, die Essenz der Aussagen zu suchen und als Gesetze zu formulieren. So haben Endres und Rombach (2003) solche Gesetze (die oft nur Beobachtungen sind) in einem kompakten Buch zusammengetragen.
Am Anfang dieser Entwicklung stand 1977 Halsteads Buch über »Software Science« (siehe Abschnitt 14.4.4). Wenige Jahre später verfolgten Lehman und Belady mit ihren »Laws of Software Evolution« ein ähnliches Ziel. (Zum Begriff der Evolution siehe Abschnitt 9.5.2.) Sie behaupteten, die »Naturgesetze« gefunden zu haben, die bestimmen, wie sich Software in der Wartung verändert. Der Anspruch war zu hoch, physikalisch streng lässt sich das Verhalten von Personen und Organisationen nicht fassen. Aber wenigstens zwei der fünf Hypothesen werden durch Beobachtungen in der Praxis gestützt.
law of continuing change — A system that is used undergoes continuing change until it becomes more economical to replace it by a new or restructured system.
law of increasing entropy — The entropy of a system increases with time unless specific work is executed to maintain or reduce it.
Lehman, Belady (1985)
Diese Aussagen gelten für die P- und E-Programme in der Taxonomie von Lehman, also für Software, deren Anforderungen nicht stabil sind, sondern sich mit der Zeit ändern (Abschnitt 9.3). Alle Anwendungsprogramme fallen in diese Kategorie.
Software-Evolution ist die wiederholte Anpassung eines bestimmten Software-Systems an veränderte Anforderungen und Umgebungsbedingungen. Mit der Evolution ist unvermeidlich ein Qualitätsverlust verbunden; wenn sie aber unterbleibt, wird die Software nutzlos, weil sie den veränderten Anforderungen nicht mehr entspricht.
Das erste »Gesetz« sagt aus, dass die Änderung stattfindet, das zweite fügt hinzu, dass der Effekt dieser Änderung negativ ist, wenn man dem nicht mit zusätzlichem Aufwand entgegenwirkt. David Parnas hat für diesen Effekt den Begriff Software-Alterung (software aging; Parnas, 1994) eingeführt. Alternde Software zeigt die folgenden Symptome:
Die Software wird fehleranfällig.
Es wird schwieriger und damit teurer, sie an neue Anforderungen anzupassen.
Die Leistung (die Effizienz) lässt nach.
Die Architektur degeneriert.
Zwei wichtige Gründe für diese Symptome sind, dass Komponenten durch die wiederholten Modifikationen immer größer und komplexer werden und dass neue Komponenten irgendwie in die dafür eigentlich ungeeignete Architektur integriert und mit den existierenden Komponenten verbunden werden. Die der ursprünglichen Spezifikation angemessene Architektur passt nicht mehr zur aktuellen Aufgabe; was als Fabrikhalle geplant und tauglich war, ist durch eine Reihe von Umbauten zu einem schlechten Wohnhaus geworden.
Parnas leitet daraus die Empfehlung ab, Software so zu konstruieren, dass sie weiterentwickelt und angepasst werden kann. Dieser Ansatz wird als Design for Change bezeichnet. Eine Architektur, die zumindest einigermaßen für zukünftige Anpassungen gerüstet ist, entsteht, wenn die Prinzipien des Software-Entwurfs – insbesondere Information Hiding und Trennung von Zuständigkeiten – eingehalten werden (siehe dazu Abschnitt 17.3). Doch auch wenn man diese Entwurfsprinzipien beherzigt, kann der Prozess der Software-Alterung nicht aufgehalten, sondern lediglich verlangsamt werden.
Lehman und Belady beschreiben den Alterungsprozess, dem die Software unterliegt, mit der Metapher der Entropie. Dieser Begriff stammt aus der Thermodynamik: Die Entropie ist ein Maß für die Unordnung der Moleküle. Abbildung 23–1 zeigt das Prinzip: In einem Behälter mit zwei durch einen Schieber getrennten Kammern sind alle Gasmoleküle in der linken Kammer gefangen. Wenn der Schieber geöffnet wird, verteilen sich die Moleküle nach den Regeln der Statistik, also ungefähr gleichmäßig, ein Zustand geringer Ordnung, also hoher Entropie, stellt sich ein. Um den ursprünglichen, geordneten Zustand wiederherzustellen, muss man von außen Einfluss nehmen, Energie zuführen, indem man die rechte Kammer wieder leer pumpt.
Abb. 23–1 Das Entropie-Prinzip der Thermodynamik
Weil wir wissen, dass die Qualität einer Software im Laufe ihrer Evolution abnimmt, ist es ratsam, rechtzeitig Gegenmaßnahmen zu ergreifen, wenn die Software lange eingesetzt werden soll. Wenn wir beim Bild der Fabrikhalle bleiben, sehen wir, dass daraus niemals eine gute Wohnarchitektur werden kann. Aber der radikale Rat »Abreißen und neu bauen« ist meist wertlos, weil die Halle jetzt verfügbar ist und Wohnungen jetzt gebraucht werden.
Eine wichtige Gegenmaßnahme zum schleichenden Qualitätsverlust ist das Reengineering. Dabei wird die Software so verändert, dass ihre Qualität verbessert wird, ohne dass sich ihre Funktionalität ändert.
Abb. 23–2 Beispiel für den Alterungsprozess von Software
Abbildung 23–2 zeigt, wie der Alterungsprozess einer Software verlaufen kann. Beim Abschluss der Entwicklung, wenn Release 1 freigegeben ist, hat die Software eine angemessene Qualität erreicht. Durch Korrekturen und Erweiterungen nimmt die Qualität bis zum Release 3 stetig ab. Dann wird wegen der wachsenden Probleme entschieden, ein Reengineering durchzuführen; die Qualität steigt wieder an (Release 4), wenn auch nicht auf das ursprüngliche Niveau. Durch die nachfolgenden Veränderungen sinkt sie wieder ab. Nachdem Release 5 freigegeben wurde, wird entschieden, kein weiteres Reengineering durchzuführen, sondern eine Neuentwicklung zu starten, nach deren Abschluss das existierende System abgelöst wird.
Dieses Beispiel zeigt, wie wichtig es ist, den qualitativen Zustand von Software zu kennen, damit wichtige Entscheidungen vorbereitet und getroffen werden können. Dazu brauchen wir geeignete Kennzahlen, beispielsweise die Zahl der gemeldeten Fehler pro Release, die Kosten für die Fehlerkorrektur oder die Kosten für die Umsetzung neuer Anforderungen. Auch Kennzahlen über die Qualitätsveränderungen von Code und Architektur, wie sie mit modernen Messwerkzeugen ermittelt werden können, sind hilfreich. Alle Kennzahlen müssen natürlich regelmäßig ermittelt und ausgewertet werden.
Die Aussagen zur Software-Evolution können folgendermaßen zusammengefasst werden: Wir müssen Software immer wieder anpassen, und dabei wird die Software qualitativ schlechter. Natürlich tragen Unzulänglichkeiten der Wartung (oberflächliche Analyse der Software vor der Veränderung, zunehmende Redundanz im Code, mangelnde Dokumentation) zu diesem Effekt bei. Aber im Grunde kann man dem Trend nicht ausweichen, denn die Strukturen waren für einen bestimmten Zweck und bestimmte Bedingungen gewählt worden, und diese verändern sich.
Schließlich können wir die Software nicht mehr warten, weil ihre Architektur nicht mehr trägt und ihre Funktionalität durch eine Reihe von Reparaturen und Ergänzungen implementiert ist, die wir nicht mehr überblicken können. Eigentlich haben wir es mit zwei verschiedenen Effekten zu tun: Zum einen ist die Architektur objektiv schlechter als zu Beginn. Zum anderen sind die Rahmenbedingungen schlechter geworden: Die Köpfe, in denen wichtige Informationen über das System gespeichert sind, stehen nicht mehr zur Verfügung, die Dokumente sind veraltet, die Werkzeuge, mit denen das System entwickelt wurde, funktionieren nicht mehr. Reverse Engineering und Reengineering sind Maßnahmen, die in dieser Situation helfen sollen.
Die Begriffe Reverse Engineering und Reengineering werden oft – fälschlicherweise – synonym verwendet. Wir übernehmen die Definitionen von Chikofsky und Cross:
Reverse Engineering is the process of analyzing a system to identify the system’s components and their interrelationships and create representations of the system in another form or at a higher level of abstraction.
Re-structuring is a transformation from one form of representation to another at the same relative level of abstraction. The new representation is meant to preserve the semantics and external behavior of the original.
Reengineering is the examination and alteration of a subject system to reconstitute it in a new form and the subsequent implementation of the new form.
Forward Engineering is the traditional process of moving from high-level abstractions and logical, implementation-independent designs to the physical implementation of a system.
Chikofsky, Cross (1990)
Reverse Engineering ist dann das Mittel der Wahl, wenn eine Software, die wir nicht mehr verstehen, weiterentwickelt werden muss. Indem wir Techniken des Reverse Engineerings einsetzen, wollen wir auf der Basis der vorhandenen Dokumente einer Software – das ist oft nur noch der Code – die Architektur und sogar die Anforderungen wiederfinden. Typisch geht man bis zum Entwurf zurück, der durch Restrukturierung (Re-structuring) aufgeräumt, vereinfacht, modernisiert wird.
Anschließend können wir durch Forward Engineering den gewünschten Zustand herstellen. Forward Engineering ist also die Neuimplementierung der Software auf Basis der Ergebnisse, die wir durch Reverse Engineering und Re-structuring erreicht haben. Dabei sind die Freiheiten im Vergleich zu einer Neuentwicklung »auf der grünen Wiese« erheblich eingeschränkt, weil das Resultat zur alten Software kompatibel sein muss, auch weil man natürlich versucht, vorhandene Software-Einheiten mit möglichst geringen Modifikationen weiter zu nutzen.
Die Kombination aus Reverse Engineering, Re-structuring und Forward Engineering bezeichnet man als Reengineering. Reengineering ist also eine Veränderung der Software, die auf die Verbesserung der Wartbarkeit zielt. Andere nichtfunktionale Qualitäten, insbesondere die Effizienz, können dabei ebenfalls verbessert werden. Die Funktionalität bleibt unangetastet. Keipinger (2000) beschreibt die Situation durch die Formel
Reengineering = Reverse Engineering + Δ + Forward Engineering
Er akzeptiert dabei aber, dass das Δ über die Restrukturierung hinausgeht (siehe Abschnitt 23.2.4).
Ein Reengineering ist immer dann notwendig, wenn man die Software weder unverändert weiternutzen noch komplett wegwerfen und ersetzen kann oder will. Man muss, das ist die typische Ausgangssituation, ein altes Software-System, zu dem es keine oder nur noch veraltete separate Dokumente gibt,
erweitern,
auf eine neue System-Software (z. B. Betriebssystem, Middleware) oder Programmiersprache portieren,
partiell ersetzen oder einer erheblichen Veränderung unterziehen.
Bevor die Software verändert werden kann, muss sie in einen wartbaren Zustand versetzt werden. Die Abbildung 23–3 (angelehnt an Demeyer, Ducasse, Nierstrasz, 2003, S. 7) verdeutlicht den Zusammenhang zwischen Reverse Engineering, Restrukturierung und Forward Engineering.
Abb. 23–3 Vorgehensweise beim Reengineering
Beim Reverse Engineering werden aus den noch verfügbaren Informationen (typischerweise der Code und veraltete Dokumente) abstraktere Modelle hergeleitet. Dazu müssen die grobe Ablaufstruktur, die Datenstrukturen, die Module und die Aufrufbeziehungen identifiziert werden. In extremen Fällen ist nur noch der Objektcode vorhanden, weil der Quellcode durch Schlamperei (fehlende Konfigurationsverwaltung) verloren gegangen ist. Dann entsteht die folgende Sequenz von Dokumenten:
Objektcode → Quellcode → Feinentwurf → Systementwurf → Spezifikation
Für das Reverse Engineering brauchen wir Techniken und Visualisierungsformen, um große und hoch komplexe Systeme zu verstehen. Zwei wichtige Techniken sind die Nachdokumentation und die Wiederentdeckung der Architektur (design recovery). Chikofsky und Cross definieren:
Redocumentation is the creation or revision of a semantically equivalent representation within the same relative abstraction level.
Design recovery recreates design abstractions from a combination of code, existing documentation (if available), personal experience, and general knowledge about problem and application domains.
Chikofsky, Cross (1990)
Bei der Nachdokumentation geht es in der Praxis darum, den vorhandenen Code in einer übersichtlichen Form darzustellen, um ihn leichter verstehen zu können. So versucht man, archaischen Code (älterer FORTRAN-Dialekt, PL/I, COBOL, Assembler) daraufhin zu analysieren, welche höheren Konstrukte darin enthalten sind. So können beispielsweise Verzweigungen, Schleifen, Parameter, schwieriger auch Abstrakte Datentypen erkannt werden. Bei den Code-Analysen werden statische oder dynamische Analysewerkzeuge eingesetzt. Diese Werkzeuge generieren in der Regel grafische Repräsentationen oder tabellarische Übersichten der untersuchten Aspekte, also beispielsweise der Aufrufbeziehungen zwischen Prozeduren oder der Benutzt-Beziehungen zwischen Modulen. Anspruchsvollere und spezifische Werkzeuge sind dringend erforderlich, aber rar und bis heute Forschungsgegenstand (z.B. im Bauhaus-Projekt, siehe Eisenbarth, Koschke, Simon, 2002).
Das Ziel des Design Recovery ist, die Architektur des Systems wiederzufinden. Dafür muss der Code eingehend untersucht und analysiert werden; die Nachdokumentation des Codes ist also der erste Schritt beim Design Recovery. Die Ergebnisse der Code-Analyse werden anschließend genutzt, um höhere Abstraktionen zu erzeugen oder Bereiche im System zu identifizieren, die abgetrennt und möglicherweise wiederverwendet werden können. Die Erfahrung zeigt jedoch, dass Design Recovery nur dann erfolgreich sein kann, wenn neben dem Code auch noch Wissen über die Anwendungsentwicklung verfügbar ist, da es sonst sehr schwer, wenn nicht unmöglich ist, die im Code enthaltenen Abstraktionen und Entwurfsentscheidungen zu entdecken und zu beschreiben.
Auf Code-Ebene kann man restrukturieren, um Lesbarkeit und Wartbarkeit zu verbessern. Dabei sind die Möglichkeiten zur Automatisierung begrenzt. Voll automatisch kann man mit den entsprechenden Werkzeugen eine Querübersetzung durchführen, also z. B. von FORTRAN nach ADA. Damit wird die Struktur aber nicht besser; man kann nur erreichen, dass ein alter Compiler nicht mehr benötigt wird und dass die weitere Wartung und die eigentliche Restrukturierung in der modernen Sprache stattfinden.
Der Traum von einer automatischen Strukturverbesserung hat sich bis heute nicht erfüllen lassen. Wenn in alten Programmiersprachen Strukturen (Unterprogramme, verschiedene Schleifen usw.) nicht elegant dargestellt werden konnten, sind die Programmierer auf Ersatzstrukturen ausgewichen; das können Werkzeuge teilweise erkennen; meist aber waren die Programmierer nicht moderner als ihre Sprachen, sie haben also keine Abstrakten Datentypen in ihren COBOLProgrammen versteckt, einfach darum, weil sie die Abstrakten Datentypen nicht kannten. Und im Allgemeinen kann man nichts aus einem Programm herausdestillieren, was die Entwickler nicht bewusst hineingesteckt haben.
Bislang sind wir davon ausgegangen, dass die Funktionalität beim Reengineering grundsätzlich unverändert bleibt. Das erscheint oft sehr umständlich, weil man ja meist mit dem Reengineering unter dem Druck einer Wartungsaufgabe begonnen hat. Ist es unter diesen Umständen wirklich sinnvoll, einen Zustand zu verbessern, um ihn dann sofort wieder erheblich zu ändern?
Tatsächlich hat das große Vorteile. Denn solange die Funktionalität unverändert bleibt, kann man das neue System gegen das alte testen; eventuell kann man sogar einzelne Komponenten austauschen, wenn diese »steckerkompatibel« sind. Im Regelfall sollte man also versuchen, das Reengineering strikt gegen die Wartung abzugrenzen. Da eine (brauchbare) Spezifikation fehlt, ist der Vergleich mit dem alten System durch Regressionstests oft die einzige Möglichkeit, das neue System zu validieren.
Dieser Ansatz stößt aber an seine Grenzen, wenn Alt und Neu ohnehin nicht verträglich sind, wenn also eine andere Funktionalität gebraucht wird, die mit dem alten System inkompatibel ist. In diesem Falle kann das neue System wahrscheinlich die alten Anforderungen gar nicht erfüllen. Man muss dann das Reverse Engineering so weit treiben, dass auch die Spezifikation wiedergewonnen wird, sodass die Prüfung des neuen Systems eine solide Grundlage hat.
Ganz grundsätzlich stellt sich vor dem Reengineering die Frage, ob der sehr beträchtliche Aufwand erbracht werden kann und gerechtfertigt ist. Wenn die Vorbedingungen, wie sie am Beginn von Abschnitt 23.2.2 genannt sind, wirklich bestehen, gibt es keine Alternative. Trotzdem werden viele Reengineering-Projekte abgebrochen, weil die Kosten auch pessimistische Schätzungen übersteigen; dann zeigt sich, dass es doch andere Möglichkeiten gibt. Eventuell kann man das System eine begrenzte Zeit lang völlig unverändert weiternutzen, um währenddessen mit Hochdruck an einem Nachfolger zu arbeiten. Das kann durchaus die billigere Lösung sein. Leider ist eine rationale Entscheidung dieser Frage kaum möglich, denn es fehlt bislang eine Grundlage, um die Kosten des Reengineerings mit ausreichender Genauigkeit zu schätzen.
Die Erfahrungen mit Reengineering-Projekten werden selten dokumentiert; eine interessante Ausnahme ist das A-7E-Projekt, das im Auftrag der US Navy unter der Leitung von David Parnas durchgeführt wurde. Heninger (1980) öffnet dem Leser ein Fenster zur Dokumentation, die den Umfang einiger Telefonbücher hat. Hintergrund des Projekts war die Ersetzung einer Software, für die keine brauchbare Dokumentation verfügbar war; die neue Software sollte aber das exakt gleiche Verhalten wie die alte zeigen. Der Artikel zeigt, welche Techniken man anwenden kann und welcher Aufwand notwendig ist, um eine Spezifikation zu rekonstruieren.
Eine spezielle Art der Restrukturierung, die einer definierten Vorgehensweise folgt, ist das Refactoring. Martin Fowler, der Refactoring populär gemacht hat, definiert es folgendermaßen:
Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure. It is a disciplined way to clean up code that minimizes the chances of introducing bugs. In essence when you refactor you are improving the design of the code after it has been written.
Martin Fowler (1999, S. xvi)
Das Refactoring wird präventiv eingesetzt: Wenn eine notwendige Änderung mit den bestehenden Strukturen kollidiert, also die Qualität der Architektur beeinträchtigen würde, ändert man zunächst die Strukturen so, dass die folgende Änderung keinen negativen Effekt hat. Beim Refactoring wird das System in einem systematischen Prozess durch kleine Umbaumaßnahmen verbessert, ohne dass sich das beobachtbare Verhalten verändert. Um dabei Fehler zu vermeiden, muss das veränderte System nach jedem Schritt angemessen getestet werden. Refactoring ist somit die Iteration kleiner Verbesserungsschritte zusammen mit entsprechenden Tests. Die prinzipielle Vorgehensweise beim Refactoring zeigt Abbildung 23–4.
Nachdem die Schwachstellen identifiziert sind, wird die Zielstruktur definiert und der Bereich lokalisiert, der von den beabsichtigten Änderungen betroffen ist. Dann muss festgelegt werden, welche Umbauten durchgeführt werden sollen. Bevor jedoch etwas verändert werden darf, müssen entsprechende Testfälle definiert werden, die im Laufe des Prozesses angepasst und erweitert werden. Ein Umbauschritt wird durchgeführt, anschließend wird geprüft, ob dabei die Funktionalität nicht verändert wurde. Diese Schritte werden so lange wiederholt, bis der geplante Umbau abgeschlossen ist.
Wichtig ist, dass die einzelnen Umbauschritte und die dabei vorgenommenen Änderungen klein und überschaubar sind und dass nach jedem Schritt die festgelegten Tests durchgeführt werden. Nur so kann einigermaßen sichergestellt werden, dass die Funktion, das Verhalten, gleich bleibt; eine Garantie gibt es in keinem Fall.
Die Technik des Refactorings wurde für objektorientierte Programme entwickelt, sie ist aber nicht darauf beschränkt. Refactoring ist ein zentraler Bestandteil aller agilen Entwicklungsprozesse (siehe Abschnitt 10.6.4) und wird in solchen Projekten jederzeit durchgeführt, wenn Bedarf besteht; die verbessernde Wartung ist also in die Entwicklung integriert. Dadurch sollen Code und Architektur stets auf einem angemessenen Qualitätsniveau bleiben und nicht degenerieren.
Natürlich wäre es sinnvoll, Schwachstellen im Code und in der Architektur gar nicht erst entstehen zu lassen. Das gelingt aber in der Praxis nicht immer. Refactoring ist darum wichtig und notwendig. Wir behandeln nachfolgend das Refactoring auf Code-Ebene (Abschnitt 23.3.1) und das Architektur-Refactoring (Abschnitt 23.3.2).
Abb. 23–4 Vorgehensweise beim Refactoring
Wenn der Code Probleme macht, sich gegen die Weiterentwicklung sträubt, wenn er »stinkt« (englisch code smell), ist ein Refactoring sinnvoll. Dies fällt typischerweise dann auf, wenn eine Änderung (eine Erweiterung oder eine Korrektur) ansteht. Beim Refactoring wird ein Teil des Codes so umgebaut, dass die Änderung anschließend problemlos und ohne Schaden für die Struktur vorgenommen werden kann.
Kent Beck und Martin Fowler haben für objektorientierte Programme eine Reihe typischer Code-Schwächen und deren Auswirkungen beschrieben (Fowler, 1999). Dazu gehören: redundanter Code, zu lange Methoden und Klassen, der Gebrauch von CASE-Anweisungen, um Objekte zu unterscheiden, usw. Fowler führt weiterhin vor, wie der Code umgebaut und verbessert werden kann. Dabei zeigt sich, dass häufig ähnliche Schritte durchzuführen sind (z. B. Aufteilen einer Klasse in mehrere Klassen oder das Verlagern einer Methode in eine andere Klasse). Diese grundlegenden Umbauschritte (sie werden als Refactorings bezeichnet) lassen sich in Form präziser Anleitungen – Fowler nennt sie »mechanics« – formulieren. Er hat in seinem Buch mehr als siebzig Refactorings beschrieben und folgendermaßen gruppiert:
Umbau auf Methodenebene
Diese Refactorings helfen, die Methoden so zu schneidern, dass sie nicht zu lang sind, dass kein redundanter Code darin steckt und dass Variablen nur einem einzigen Zweck dienen.
Umbau der Verantwortlichkeiten zwischen Objekten
Weil es fast unmöglich ist, die Zuständigkeiten zwischen den Objekten auf Anhieb richtig zu entwerfen, braucht man diese Refactorings, um die Zuständigkeiten zu verlagern. Dazu gehört, dass Methoden verschoben werden oder dass aus einer bestehenden eine neue Klasse extrahiert wird.
Umbau der Datenorganisation
Mit diesen Refactorings können die Datenstrukturen von Klassen verändert werden. So kann eine einfache Variable durch ein Objekt ersetzt werden, wenn nicht nur ein Wert, sondern mehrere zusammengehörende Werte benötigt werden.
Vereinfachen von Bedingungsausdrücken
In diese Gruppe fallen Refactorings, um komplexe Bedingungsausdrücke aufzuspalten und um CASE-Anweisungen, in denen je nach Objekttyp unterschiedliches Verhalten ausgewählt wird, durch eine polymorphe Konstruktion zu ersetzen.
Vereinfachen von Methodenaufrufen
Mit diesen Refactorings können Methodensignaturen so umgebaut werden, dass sie insgesamt einfacher und angemessener werden. Dazu zählen das Umbenennen von Methoden, das Einfügen und Löschen von Parametern, aber auch das Ersetzen eines Parameters durch einen Methodenaufruf.
Umbau von Vererbungshierarchien
Diese Refactorings helfen, Änderungen in einer Vererbungshierarchie durchzuführen. Beispiele sind das Verschieben von Methoden und Variablen innerhalb der Hierarchie oder das Einfügen neuer, extrahierter Klassen, auch das Zusammenfassen mehrerer Klassen in einer Klasse.
Einige dieser Änderungen können mit Hilfe von Werkzeugen durchgeführt werden; die Java-Entwicklungsumgebungen INTELLIJ IDEA (JetBrains, o.J.) und ECLIPSE (Eclipse, o.J.) bieten beispielsweise eine komfortable Unterstützung dafür. Ihre Verwendung hilft, Fehler zu vermeiden.
Während Fowler Refactorings beschreibt, die Schwachstellen im Code betreffen, gehen Roock und Lippert (2004) auf Schwachstellen ein, die auf der ArchitekturEbene objektorientiert konstruierter Systeme liegen. Sie bezeichnen diese analog als »architecture smells« und ordnen sie gemäß dem grundlegenden ArchitekturMetamodell in die folgenden Gruppen ein:
Schwachstellen in Benutzungsgeflechten
Das sind alle Anomalien der Benutzt-Beziehungen, z. B. unbenutzte Klassen oder zyklische Beziehungen zwischen Klassen.
Schwachstellen in Vererbungshierarchien
Beispiele sind parallele Vererbungshierarchien (d. h., symmetrisch zur Hierarchie der fachlichen Klassen gibt es Hierarchien von Klassen, die spezielle Aspekte der fachlichen Klassen realisieren) oder zu tiefe Hierarchien.
Schwachstellen in Paketen
In diese Gruppe fallen zu große und zu kleine Pakete, zu tiefe oder unausgewogene Schachtelung oder Aufrufzyklen zwischen Paketen.
Schwachstellen in Subsystemen
Auch hier geht es um eine ausgewogene Struktur, also um die angemessene Größe der Subsysteme, aber ebenso um die Nutzung der angebotenen Schnittstellen.
Schwachstellen in Schichten
Dazu gehören beispielsweise Aufwärtsreferenzen zwischen Schichten oder das Durchbrechen der Schichtung.
Können Schwachstellen im Code noch durch erfahrene Entwickler erkannt werden, indem sie den Code lesen, so lassen sich Schwachstellen der Architektur nur erkennen, wenn eine aktuelle und vollständige Architekturbeschreibung vorliegt. Da diese oft nicht existiert, wird ein Analysewerkzeug wie der SOTOGRAPH (Bischofberger, Wolf, 2004) benötigt, das auf der Basis des Codes Aussagen über die Qualität der Architektur macht, indem es die Beziehungen zwischen den zentralen Elementen der Architektur (Klassen, Subsysteme, Schichten) analysiert und visualisiert.
Werden Architektur-Schwachstellen identifiziert, dann muss umgebaut werden. Das ist nicht einfach, und es geht – anders als auf Code-Ebene – sicher nicht automatisch. Ein Katalog angemessener Lösungen ist nicht in Sicht, vielleicht unmöglich. Hier ist also das architektonische Wissen und die Erfahrung der Software-Architekten gefragt. Wichtig ist auch hier, dass der architektonische Umbau geplant und in überschaubaren Schritten vollzogen wird, die jeweils separat getestet werden.
Wird Software über viele Jahre eingesetzt und gewartet, dann wird ihre Struktur immer schlechter. Zudem sind große Teile alter Software obsolet, sie wurden für längst vergessene Geräte geschrieben oder für Aufgaben, die es nicht mehr gibt. Niemand wagt aber, die eigentlich überflüssigen Teile zu »amputieren«, denn die Auswirkungen sind nicht absehbar. Die Wartung solcher Software ist extrem schwierig, aufwändig und riskant. Im Englischen werden solche Systeme mit dem Euphemismus »Legacy Software« bezeichnet; Legacy (Erbschaft) ist ja eigentlich ein positiv besetzter Begriff. Auf Deutsch kommt das Wort »Erblast« dem Sinn wohl näher; es drückt aus, dass die Menschen, die mit und an diesem System arbeiten, nicht für seine Entstehung und seinen Zustand verantwortlich sind, aber die Probleme trotzdem lösen müssen.
legacy system — A large software system that we don’t know how to cope with but that is vital to our organization.
Bennett (1995)
Eine Software-Erblast ist also dadurch gekennzeichnet, dass sie dringend gebraucht, aber von niemandem verstanden wird.
Folgende Merkmale sind für eine Software-Erblast typisch (siehe z. B. Kroha, 1997):
Sie ist sehr groß (106 LOC oder mehr) und alt (älter als 10 Jahre).
Die Entwickler und die Architekten der Software sind nicht mehr verfügbar, arbeiten nicht mehr im Unternehmen.
Die zur Entwicklung eingesetzten Methoden und Sprachen sind veraltet und den verfügbaren Mitarbeitern kaum bekannt. Viele Systeme wurden mit einfachen Werkzeugen und den von diesen unterstützten Methoden, vor allem der »Strukturierten Analyse« (Structured Analysis; DeMarco, 1978), entwickelt; sie sind in Assembler, COBOL, FORTRAN oder PL/I codiert, eventuell auch in
einer älteren 4GL (4th Generation Language), also für einen proprietären Vorübersetzer.
Die Software läuft und hat für ihren Besitzer strategische Bedeutung, da sie kritische Geschäftsprozesse realisiert oder unterstützt. Er kann seine Leistungen nur erbringen, wenn die Software weiterhin eingesetzt wird.
Die Dokumentation ist obsolet oder fehlt ganz. Darum ist nicht mehr bekannt, wie die Software aufgebaut ist und wie zentrale Funktionalitäten realisiert sind.
Die Software basiert auf veralteter Hardware und System-Software.
Auf Grund dieser Merkmale ist die Wartung einer Software-Erblast extrem schwierig und teuer. Neue Anforderungen können nur noch schwer oder gar nicht mehr umgesetzt werden.
Die Erneuerung oder Ablösung einer Software-Erblast ist schwierig und mit Risiken verbunden. Typisch müssen dabei folgende Anforderungen erfüllt werden:
Die neue Software muss die alte voll ersetzen, also abwärtskompatibel sein.
Die neue Software muss zusätzlich alle aktuellen Anforderungen implementieren. Dazu zählen neue funktionale, aber auch nichtfunktionale Anforderungen. In der Regel soll neue Technologie (z. B. im Bereich der Datenhaltung, der Kommunikation oder der Codierung) genutzt werden.
Die Daten, die die alte Software be- oder verarbeitet, müssen übernommen werden.
Eine längere Unterbrechung des Betriebs zur Umstellung ist ausgeschlossen.
Es gibt drei Ansätze, um eine Software-Erblast weiter zu nutzen oder abzulösen:
1. Verpacken (wrapping)
Wenn eine alte Software oder Komponente stabil arbeitet und voraussichtlich nicht mehr wesentlich verändert werden muss, kann sie so in eine Hülle (»Wrapper«) eingebettet werden, dass die übrige Software nur noch mit der Hülle kommuniziert. Die alte Software wird also als moderne »verkleidet«.
2. Migration
Die alte Software wird schrittweise erneuert und/oder ersetzt (siehe Abschnitt 23.4.3).
3. Neuentwicklung (re-development)
Die Funktionalität der alten Software wird neu implementiert, dann wird die alte Software durch die Neuentwicklung ersetzt (siehe Abschnitt 23.4.4).
Diese Ansätze schließen sich nicht gegenseitig aus, es sind auch Mischformen möglich. So kann ein Teil der Software verpackt und weiter genutzt werden, ein anderer Teil wird von Grund auf neu entwickelt, die übrigen Teile werden schrittweise durch revidierte oder verpackte Komponenten ersetzt.
Wenn man Software nicht auf einen Schlag ablösen kann, muss dies schrittweise geschehen. Dazu muss die Software in Komponenten unterteilt werden, die möglichst unabhängig voneinander migriert werden können. Ist das möglich, dann muss über die Reihenfolge entschieden werden. Für jede einzelne Komponente sind folgende Schritte durchzuführen:
Es wird ermittelt, was die alte Komponente leistet, wie sie implementiert ist und wie sie mit den anderen Komponenten interagiert (Reverse Engineering).
Die Architektur der neuen Komponente wird entworfen; dabei können unter Umständen Teile der alten Komponente wiederverwendet werden. Damit die neue Komponente in die bestehende Software integriert werden kann, müssen unter Umständen Adapter (auch Gateways genannt) entwickelt werden, die zwischen der neuen und den alten Komponenten vermitteln.
Die neue Komponente wird implementiert.
Die Datenbestände werden konvertiert, also dem neuen System angepasst. Auch hier kann es Übergangslösungen geben, damit die neue Komponente mit den existierenden Komponenten zusammenarbeiten kann.
Die fertig entwickelte Komponente wird getestet, in die bestehende Software integriert und in Betrieb genommen.
Diese Vorgehensweise wird auch als inkrementelle Migration bezeichnet (Brodie, Stonebraker, 1995).
Eine Ablösung der alten Software auf einen Schlag (auch als Big Bang bezeichnet) ist offensichtlich riskant: Man muss sich sehr sicher sein, dass das neue System zuverlässig arbeitet. Dafür hat man den Vorteil, nicht mit Mischsystemen und mehreren Umstellungen arbeiten zu müssen. Heydenreich (1991) hat eine erfolgreiche Umstellung dieser Art beschrieben und die Lehren aus diesem Projekt zusammengefasst.