Agile Modellierung mit
UML
Loading

9.2 Methodik des Refactoring

9.2.1 Technische und methodische Voraussetzungen für Refactoring

Wie viele andere Elemente im Portfolio einer agilen Methodik ist Refactoring besonders erfolgreich in Kombination mit weiteren Techniken und Konzepten:

  • Objektorientierung, speziell Vererbung und Polymorphie
  • Automatisierte Tests
  • Gemeinsamer Modell- und Codebesitz
  • Keine oder kaum zusätzliche Dokumentation
  • Modellierungs- beziehungsweise Codierungsstandards

Bereits in [Mey97] werden objektorientierte Konzepte als hilfreich für die Wiederverwendung von Softwarekomponenten angesehen. Insbesondere die Bildung von Unterklassen, die dynamische Konfigurierbarkeit von Objektstrukturen, die dynamische Bindung von Methoden und die daraus resultierende Möglichkeit zur partiellen Redefinition von Verhalten werden als Faktoren zur besseren Wiederverwendung erkannt. Die in Kapitel 8 vorgestellten Testmuster zeigen, dass objektorientierte Konzepte auch zur Definition von Tests hilfreich eingesetzt werden können.

Automatisierte Tests aber bilden einen wesentlichen Eckpfeiler für den Erfolg der Refactoring-Techniken. Automatisierte Tests erlauben die effiziente Überprüfung, ob bei der Durchführung eines Refactorings die nicht direkt betroffene Funktionalität noch ihre Aufgaben erfüllt. Fehlen Tests für Systemteile die einem Refactoring unterworfen werden sollen, so ist zu empfehlen, zunächst geeignete Tests zu entwickeln. Wie in Abschnitt 9.3.3 noch besprochen wird, legen außerdem die vom Anwender des zu entwickelnden Systems vorgegebenen Akzeptanztests einen Beobachtungsbegriff für Refactoring-Schritte fest.

Ist jedes Artefakt einem Besitzer zugeordnet, der dieses Artefakt kontrolliert und als einziger modifizieren darf, dann ist Refactoring zum Scheitern verurteilt. Der Abstimmungsaufwand, der zwischen den Besitzern notwendig ist, an mehreren Artefakten parallel (und aufgrund der Mikro-Iterationen nahezu zeitgleich) Änderungen vorzunehmen, ist praktisch nicht realisierbar. Hinzu kommt, dass Besitzer eines Artefakts die zusätzliche Arbeitsbelastung ablehnen, wenn ein Refactoring nicht für sie, sondern nur für das Nachbarsystem von Vorteil ist. In solchen Fällen wird oft nicht die für die Architektur beste Lösung, sondern die für den Diskussionssieger am wenigsten arbeitsintensive Lösung gewählt. Der gemeinsame Modellbesitz erlaubt einzelnen Entwicklern, Refactorings auch über Schnittstellen und Artefakte hinweg effizient durchzuführen und erst das modifizierte und wieder komplett lauffähige System in das Repository einzuchecken. Durch die Existenz der automatisierten Tests verlagert sich der Abstimmungsaufwand zwischen den Entwicklern zu einem „Abstimmungsaufwand“ zwischen dem Entwickler (oder Entwickler-Paar) und den automatisch durchführbaren Tests.

Refactoring verändert die Struktur eines Systems. Ist die Struktur nicht nur durch die für die Codegenerierung verwendeten Modelle, sondern zusätzlich durch weitere Dokumentation beschrieben, so entsteht der Aufwand, diese Dokumente ebenfalls zu aktualisieren. Dieser Aufwand kann aber leicht dieselbe Größenordnung wie das Refactoring selbst erreichen und übertreffen. Nicht zu unterschätzen ist dabei der Aufwand, dass in einem Dokument die zu ändernden Stellen zuerst zu identifizieren sind. Die Sicherung der Qualität eines Dokuments, also insbesondere der Übereinstimmung mit der Implementierung ist hier besonders kritisch. Werden dafür keine geeigneten Maßnahmen getroffen, so ist das Vertrauen der Entwickler in die Aktualität der Dokumente nicht gegeben und die Dokumente sind relativ wertlos. Refactoring kann also am effektivsten eingesetzt werden, wenn keine zusätzliche detaillierte Dokumentation existiert. Dafür sind aber präzise Modellierungsstandards einzuhalten, damit den Entwicklern der Zugang zu den Modellen erleichtert wird. Auch Ansätze wie „Literate Programming“ mit intensiver Verzahnung von Modell und Dokumentation sind dafür leider wenig geeignet, da die informelle Dokumentation nicht automatisch angepasst werden kann.

9.2.2 Qualität des Designs

Wie in Abbildung 9.3 skizziert, ist Refactoring orthogonal zur Entwicklung neuer Funktionalität. Während in der normalen Weiterentwicklung neue Funktionalität hinzugefügt und dabei in Kauf genommen wird, die Qualität des Designs zu verschlechtern, wird beim Refactoring die Funktionalität beibehalten und die Qualität des Designs normalerweise verbessert.


Abbildung 9.3: Refactoring und Weiterentwicklung ergänzen sich

Für die Funktionalität eines Systems existieren Maße, wie beispielsweise die Function Point Methode [AG83] oder deren objektorientierte Anpassung [Sne96]. Im einfachsten Fall kann auch der Anteil der bereits umgesetzten Anforderungen als Maß für die implementierte Funktionalität verwendet werden.

Demgegenüber gibt es derzeit kein allgemein anerkanntes Verfahren, um die Qualität eines Designs objektiv zu messen. Kriterien, die für Programmiersprachen entwickelt wurden, um die „Optimalität“ eines Designs zu messen, sind abhängig von der Programmiersprache und der Erfahrenheit der Entwickler. Zum Beispiel werden für objektorientierte Sprachen andere Kriterien vorgeschlagen, als vor 10-20 Jahren für prozedurale Sprachen diskutiert wurden. Wesentlich sind zum Beispiel, die Kopplung von Klassen möglichst gering zu halten, einzelne Klassen nicht zu groß oder klein zu halten, die Tiefe einer Vererbungshierarchie zu beschränken, etc. Defizite werden unter anderem in [Fow99] als „Bad Smells“ bezeichnet und 22 davon aufgezählt.

Jedoch sind nicht nur Refactoring-Schritte sinnvoll, die das Design bezüglich einer gegebenen Metrik verbessern. Refactoring sollte vor allem auch eingesetzt werden, um in einem nachfolgenden Schritt neue Funktionalität besser hinzufügen zu können. Das zielgerichtete Refactoring kann daher zunächst ein Design verschlechtern, um neue Funktionalität hinzuzufügen oder um weitere Refactoring-Schritte vorzunehmen. Ein typisches Beispiel ist etwa die Teilung einer Klasse in zwei, durch eine 1-zu-1-Assoziation verbundene Klassen und die nachfolgende Verallgemeinerung der Assoziation in die Form 1-zu-⋆.

Hilfreich ist dabei zum Beispiel die Identifikation von Defiziten mit Metriken sowie der Vorschlag zu deren Behebung durch geeignete Refactoring-Schritte. Die Entscheidung, ob ein Refactoring-Vorschlag durchgeführt wird, muss allerdings beim Anwender liegen, der das Design und die zugrunde liegende Motivation kennt.

Einige durch Metriken auf Programmiersprachen typischerweise gemessenen Elemente, wie die Vererbungshierarchie oder die Kopplung von Klassen können weitgehend unverändert übernommen werden. Andererseits erlaubt die größere Kompaktheit der UML gegenüber Java eine größere Dichte von Funktionalität innerhalb einer Klasse und damit die Reduktion der Anzahl notwendiger Klassen. Immerhin wird ein Teil der Standardfunktionalität, wie get- und set-Methoden für Attribute, Factories, technische Methoden zur Speicherung, etc. durch den Codegenerator hinzugefügt und sind deshalb im Modell für den Entwickler nicht mehr sichtbar.

Parallel dazu können für die Notationen der UML/P weitere Kriterien guten Entwurfs angegeben werden. Ein Objektdiagramm, das sehr viele Objekte beinhaltet sollte zum Beispiel in mehrere Objektdiagramme geteilt und durch die in Abschnitt 4.3, Band 1 skizzierte Logik für Objektdiagramme kombiniert werden. Auch die Größe und Form von Statecharts, Sequenz- und Klassendiagrammen sowie von OCL-Bedingungen können geeigneten Metriken unterliegen, die aber erst auf Basis empirischer Untersuchungen erstellt werden müssen. Zum Beispiel kann dabei die aus anderen Bereichen bereits bekannte Regel eingesetzt werden, dass ein Betrachter nur bis zu 5 ± 2-Elemente gleichzeitig erfassen kann.

9.2.3 Refactoring, Evolution und Wiederverwendung

Das Refactoring der innneren Strukturen eines Systems führt zunächst zu keiner für den Anwender und Kunden sichtbaren Verbesserung der Systemfunktionalität. Entsprechend ist die Motivation ein Refactoring durchzuführen gut zu begründen.

Unter ökonomischen Gesichtspunkten betrachtet ist ein Refactoring nur dann sinnvoll, wenn es auf ein Ziel ausgerichtet ist, das den Arbeitsaufwand rechtfertigt. Wie in Abbildung 9.3 gezeigt, muss das entstehende System kein optimales Design besitzen. Das Design muss jedoch während des gesamten Entwicklungsprozesses gut genug sein, um Weiterentwicklungen zu erlauben. Um auch zukünftige Weiterentwicklungen zu ermöglichen, sollte das Design auch gegen Ende des Projekts eine gute Qualität besitzen.

Deshalb ist der Nutzen eines Refactorings immer gegen den notwendigen Aufwand abzuschätzen. Insbesondere für große Modifikationen, die die technische oder fachliche Architektur des Systems verändern, indem sie zum Beispiel die Kommunikationsinfrastruktur (Middleware) austauschen oder Kernelemente der Datenstruktur modifizieren, sollten vorher in ihren Aufwänden abgeschätzt werden. Es kann aufgrund bisheriger praktischer Erfahrungen festgehalten werden, dass bei konsequenter Anwendung von Refactoring-Schritten sogar durch die relativ einfache Unterstützung durch Such- und Ersetzungs-Funktionalität einer Entwicklungsumgebung und einer vorhandenen Testsammlung der Fortschritt deutlich schneller ist, als oft geschätzt.

Refactoring-Schritte können nicht nur innerhalb eines Softwareentwicklungsprojekts, sondern auch für den ursprünglich in [Opd92] vorgesehenen Zweck, der Evolution und Wiederverwendung von Frameworks in unterschiedlichen Projekten, eingesetzt werden.

Ein Framework wird normalerweise als eigenständiges Artefakt weiterentwickelt, indem in jeder Applikation neue Funktionalität zum Framework hinzugefügt wird und notwendige Restrukturierungen vorgenommen werden. Dabei wird aber besonders darauf geachtet, die Kompatibilität des Frameworks mit den ursprünglichen Applikationen zu wahren, um auch diese weiterentwickeln zu können. In einem agilen Projekt spielen bei Anwendung des Prinzips der Einfachheit diese Überlegungen keine Rolle. Deshalb ist Extreme Programming ohne Anpassungen nicht geeignet, um damit Frameworks zu entwickeln. Um die Wiederverwendung von Frameworks zu erhöhen, sind zusätzliche methodische Vorkehrungen zu treffen, die eine Abschirmung des Frameworks gegen beliebige Modifikationen erlauben. In diesem Fall sind agile und Framework-basierte Methoden zu kombinieren, wie dies zum Beispiel in [FPR01] skizziert ist.


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012