Agile Modellierung mit
UML
Loading

9.1 Einführende Beispiele für Transformationen

Dieser Abschnitt demonstriert anhand einiger Beispiele welche Arten von Refactoring-Schritten es gibt. Wie bereits in [Fow99] diskutiert, kann anhand kleinster Beispiele zwar das Prinzip, nicht aber die Motivation für Refactoring demonstriert werden. [Fow99] nutzt deshalb ein größeres Beispiel von 50 Seiten, um zu zeigen, dass mit Refactoring-Techniken Komplexität in der Evolution beherrschbar wird. Mit den nachfolgenden kleinen Beispielen wird deshalb weniger die Notwendigkeit des Einsatzes von Refactoring motiviert, sondern die Einsatzmöglichkeiten beschrieben.

In Abbildung 9.1 ist eine für die nachfolgenden Beispiele ausreichende Begriffsdefinition aus der Literatur angegeben.1

Definitionen des Begriffs „Refactoring“ aus der Literatur:
  • Refactoring“ lässt sich als Operation zur Restrukturierung eines Programms charakterisieren, die den Entwurf, die Evolution und die Wiederverwendung objektorientierter Frameworks unterstützt. [Opd92, S. iii]

Das zum Thema Refactoring am meisten zitierte Werk [Fow99] bietet in der deutschen Übersetzung [Fow00] folgende Definitionen:

  • Refaktorisierung (Substantiv): Eine Änderung an der internen Struktur einer Software, um sie leichter verständlich zu machen und einfacher zu verändern, ohne ihr beobachtetes Verhalten zu ändern.“ [Fow00, S. 41]
  • Refaktorisieren (Verb): Eine Software umstrukturieren, ohne ihr beobachtbares Verhalten zu ändern, indem man eine Reihe von Refaktorisierungen anwendet.“ [Fow00, S. 41]
Abbildung 9.1: Begriffsdefinitionen für das „Refactoring“

In [Opd92] werden Refactoring-Schritte auch als Pläne zur Reorganisation von Software bezeichnet, die Veränderungen auf einem mittleren Level erlauben. Als „Low-Level“ werden dort Veränderungen einzelner Code-Zeilen und als „High-Level“ die Anpassung ganzer, für den Anwender sichtbarer Funktionalitäten bezeichnet. [Opd92] sieht Refactoring vor allem als Technik zur Weiterentwicklung von Frameworks. Ein Einsatz zur Evolution der Architektur innerhalb eines Projekts wird erst in [Fow99] vorgeschlagen und zum Beispiel im Extreme Programming-Ansatz (siehe Abschnitt 2.2) eingesetzt.

In Erweiterung der diskutierten Definitionen wird in diesem Buch eine Transformation als Refactoring bezeichnet, die ein gegebenes Modell und die darin enthaltenen Codeteile durch Anwendung eines oder mehrerer Schritte in ein neues, nach einem geeigneten Beobachtungsbegriff äquivalentes Modell transformiert.

Algebraische Umformungen

Die einfachste Form des Refactorings ist die algebraische Umformung eines Ausdrucks. Dabei werden die mathematischen Gleichungen herangezogen, die zum Beispiel zur Vereinfachung von Ausdrücken eingesetzt werden können. Eine solche Regel ist beispielsweise:

Diese Regel kann sowohl auf Java-Ausdrücke als auch auf die OCL angewandt werden. Die Regel nutzt die bereits in Kapitel 4 eingeführten Schemavariablen als Platzhalter für andere Ausdrücke. Wichtig ist, dass a für beliebige Ausdrücke der Basissprache und nicht nur für Variablen steht. So einfach diese Regel aussieht, so hat sie bei einem Einsatz in Java doch Kontextbedingungen. So darf der für a eingesetzte Ausdruck keine Seiteneffekte haben. Zum Beispiel würde nach der Anwendung der Transformation auf (i++)+(i++) die Variable i einen anderen Inhalt haben und ein anderes Ergebnis entstehen.

Außerdem muss a deterministisch sein. Eine Abfrage der Zeit mit der statischen Query Time.now() für a wäre zum Beispiel nicht geeignet. Dies würde insbesondere dann auffallen, wenn die Regel:

angewandt werden würde. Bei algebraischen Umformungen können weitere Effekte auftreten, die zu beachten sind. So kann der Tausch in der Reihenfolge einer Addition gemäß Regel

zu einem Überlauf führen, der nur in der neuen Berechnung auftritt, wenn a und c sehr groß und b negativ sind. Umgekehrt kann mit einer algebraischen Umformung das System robuster gegen arithmetische Exceptions gemacht werden. Durch geeignete Umformung numerischer Berechnungen kann das Ergebnis außerdem in seiner Genauigkeit beeinflusst werden.

Algebraische Umformungen sind jedoch keineswegs auf numerische Berechnungen beschränkt, sondern können auch auf andere Grunddatentypen und Container angewandt werden. Dazu zählen Vereinfachungen boolescher Berechnungen, Umformungen bei Strings, Ausnutzung von Kommutativitäts- und anderen Gesetzen bei Mengen, etc. Allerdings ist dabei die Seiteneffektfreiheit und die vorhandene Identität zum Beispiel bei Containern zu beachten.

Die ebenfalls aus der Mathematik bekannte Substitution von Gleichem kann wie folgt formuliert werden:

Diese Regel kann zum Beispiel angewandt werden, um einen Methodenaufruf durch einen anderen, allgemeineren zu ersetzen (0 kann dabei auch eine andere Konstante sein):

Damit lässt sich eine neue, allgemeinere Methode bar einführen und die alte Methode foo eliminieren. Diese Regel fordert in ihren Kontextbedingungen auch die Gültigkeit einer OCL-Invariante.

Algebraische Umformungen sind aus der Mathematik und den algebraischen Spezifikationstechniken [BFG+93EM85] bekannt, werden aber nicht als Kern der für objektorientierte Sprachen entwickelten Refactoring-Techniken angesehen. Sie sind aber eine Grundlage, um die oft notwendigen Umformungen von Coderümpfen und Invarianten durchzuführen.

Expansion einer Methode

Ein weiteres Beispiel ist die Expansion einer Methode:

Etwas komplexere Methodenrümpfe erfordern ggf. Umbauten des expandierten Methodenrumpfs:

Diese Regelanwendungen nutzen dasselbe Prinzip, das beim „Inlining“ von Methoden, zum Beispiel von optimierenden Compilern durchgeführt wird. Eine generalisierte Formulierung der Expansionsregeln ist aber mit vielen Randbedingungen versehen, weil z.B. Variablen in einen anderen Bindungsbereich expandiert werden und versehentliche Gleichbennenung nicht erlaubt ist. Bei der Verwendung einer solchen Regel für das Refactoring wird oft das Ziel verfolgt, durch die Expansion eine nachfolgende algebraische Vereinfachung vorzunehmen oder das entstandene Codestück durch einen anderen Methodenaufruf wieder neu zu faktorisieren.

Tatsächlich lassen sich die skizzierten Expansionsregeln auch in umgekehrter Richtung als Extraktionsregeln2

Die Anwendung der Expansionsregel hat ebenfalls Kontextbedingungen und erfordert meistens auch zusätzliche Maßnahmen zur Anpassung des expandierten Codes. Ist die expandierte Methode von einer anderen Klasse, so ist zum Beispiel als Kontextbedingung zu fordern, dass kein Attribut dieser Klasse verwendet wird. Dies kann durch eine vorbereitende Maßnahme erreicht werden, indem alle verwendeten Attribute durch Methodenaufrufe gekapselt werden.

Durch die Expansion geht die dynamische Bindung der Methode verloren. Deshalb darf die expandierte Methode in keiner Unterklasse redefiniert worden sein. Alternativ könnte durch Datenflussanalysen gesichert werden, dass das Objekt in a tatsächlich genau von Klasse A ist.

Die Kontextbedingungen für das Faktorisieren eines Codestücks in eine Teilmethode sind typischerweise komplex. Allerdings sind die meisten dieser Kontextbedingungen durch syntaktische Analysen überprüfbar, so dass die Korrektheit einer solchen Regelanwendung wie auch im Fall der Expansion automatisch geprüft werden kann. Nicht entscheidbare Kontextbedingungen können oft durch relativ einfach prüfbare syntaktische Bedingungen so verschärft werden, dass Entscheidbarkeit gegeben ist. Beispiel ist etwa die Sicherstellung, dass die private Methode foo nur auf das eigene Objekt self angewendet wird: Während das für beliebige Ausdrücke a.foo() unentscheidbar ist, ist es durch Restriktion auf self.foo() trivial.

Restrukturierung der Klassenhierarchie

Weitere Refactoring-Techniken bearbeiten die durch Klassendiagramme vorgegebene Struktur des Systems. So können Abstraktionen als neue, gemeinsame Oberklassen faktorisiert werden oder Attribute und Methoden entlang der Klassenhierarchie verschoben werden. Die Abbildung 9.2 demonstriert die Einführung einer neuen Klasse inmitten der Klassenhierarchie. Diese Klasse erhält die aus der Unterklasse extrahierte gemeinsame Implementierung der Methode validateBid(), die an diese Stelle verschoben wird. Damit dies möglich wird, werden klassenspezifische Unterschiede in eigenständige Methoden faktorisiert. Zum Beispiel enthält die neue Methode setNewBestBid() den für die Unterklassen spezifischen Vergleich, ob sich ein Gebot als neues Bestgebot qualifiziert.


Abbildung 9.2: Einführung einer neuen Oberklasse

Das dargestellte Refactoring ist bereits auf die zu bearbeitende Applikation spezialisiert und zeigt mehrere Schritte gleichzeitig. Unter Benutzung der in Abschnitt 4.4.2 eingeführten Schemavariablen lässt sich die Einführung der neuen Klasse auch allgemeiner darstellen, indem statt konkreten Klassen- und Methodennamen ausfüllbare Platzhalter eingesetzt werden.3

Attribute und Methoden können entlang von Assoziationen verschoben werden, wenn die entsprechende Assoziation und die daran beteiligten Klassen bestimmte Bedingungen erfüllen. So darf sich ein etablierter Link einer solchen Assoziation normalerweise nicht mehr verändern, um den Zugriff auf ein verlagertes Attribut zu erlauben. Diese zeitlichen Kontextbedingungen können aber durch eine statische Analyse normalerweise nicht mehr erkannt werden sondern erfordern zusätzliche Maßnahmen, wie zum Beispiel den Einsatz geeigneter Invarianten und Tests.

Signaturveränderung

Refactoring-Schritte können interne Veränderungen bewirken oder Veränderungen der Signatur von Systemteilen hervorrufen. Die Entfernung einer nicht mehr benötigten lokalen Variable ist zum Beispiel unkritisch. Die Entfernung einer Methode ist dann problematisch, wenn diese in der Signatur der Klasse oder des Subsystems publiziert wurde und es möglich ist, dass andere Entwickler die für die Löschung vorgesehene Methode verwenden. Deshalb ist es oft notwendig, Refactoring nicht auf lokal begrenzte, offene Teilsysteme anzuwenden, sondern das System möglichst in seiner Gänze zu bearbeiten.

Die Veränderung von Signaturen zeigt auch, dass hier der Beobachtungsbegriff eine wesentliche Rolle spielt. Über die in Abbildung 9.1 geforderte Verhaltensäquivalenz des Gesamtsystems hinaus bieten Schnittstellen innerhalb des Systems und zu anderen Systemteilen zusätzliche Beobachtungspunkte, die bei einer Anwendung von Refactoring-Techniken zu beachten sind.


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012