Unter "Refactoring" versteht man in erster Linie eine Programmiertechnik, bei der die Qualität des Quelltextes (z.B. hinsichtlich der Wartbarkeit oder Erweiterbarkeit) verbessert wird. Dies geschieht durch die schrittweise Umformulierung des Codes, bei der die Semantik des bearbeiteten Programmteils unverändert belassen wird. Um sicherzustellen, dass nicht versehentlich Fehler eingebaut werden, wird das Refactoring mittels einer Sammlung (Suite) von Unit-Tests kontrolliert.
Größere Refactorings können eine erhebliche Menge an Programmcode betreffen und einen nicht geringen Arbeitsaufwand bedeuten. Solange allerdings das Prinzip der kleinen Schritte eingehalten wird, können sie oft einfach als eine Sequenz atomarer Änderungen angesehen werden. Solche Änderungen, wie etwa das Umbenennen einer Variablen oder das Extrahieren eines Programmblocks in eine Hilfsmethode, werden ebenfalls als Refactoring bezeichnet. Speziell für Java bietet [1] einen umfangreichen Katalog mit präziser Beschreibung der jeweils erforderlichen Schritte - und auch der potenziellen Gefahren - in einem bestimmten Refactoring.
Die Zuverlässigkeit ist am größten, wenn die atomaren Schritte des Refactorings automatisiert werden können - nicht nur, weil die Korrektheit der automatisierten Schritte durch ein Werkzeug gewährleistet ist, sondern auch, weil die Entwickler, die das Refactoring durchführen, ihr Augenmerk auf den größeren Zusammenhang richten können, statt die vielen kleinen Änderungen am Quellcode selbst durchführen zu müssen. Die Java IDE von Eclipse (JDT) bietet inzwischen eine Vielzahl solcher automatisierter Refactorings (die gebräuchlichsten werden in [2] im Detail beschrieben).
Einen Teil der immer wiederkehrenden Elemente in der Implementierung dieser Refactorings hat das Team von JDT in eine sprachunabhängige Schicht extrahiert, die nunmehr in der Eclipse-Plattform als eigenständiges API verfügbar ist und somit unabhängig von der Java-Unterstützung benutzt werden kann. Diese Schicht wird als Language Toolkit (LTK) bezeichnet und findet sich in den Plug-ins org.eclipse.ltk.core.refactoring und org.eclipse.ltk.ui.refactoring. Sie enthält auch eine Infrastruktur (die so genannten Refactoring Participants), die es anderen Plug-ins ermöglicht, an Refactorings via Extensions gezielt teilzunehmen (siehe Kasten "Refactoring Participants").
| Refactoring Participants |
|
Ein prominentes Beispiel für den Nutzen von Refactoring Participants liefert PDE (die Plug-in Development Environment von Eclipse). Sie kontrolliert die Manifest-Dateien für die Entwicklung von Plug-ins (in erster Linie die Manifest.mf und plugin.xml), in denen oft Java-Klassen benannt werden. Ändert sich der Name einer Klasse während eines Refactorings, das im Java-Quellcode gestartet wird, so sollte sich natürlich auch die Deklaration dieser Klasse in der Manifest-Datei ändern.
Ab Eclipse 3.2 (Meilenstein 2) partizipiert PDE an Java-Refactorings, um diese Anforderung zu erfüllen. Das Umbenennen von Klassen, die in Manifest-Dateien deklariert sind, wird nun von PDE erkannt und die entsprechenden Anpassungen werden vorgenommen.
Refactorings außerhalb der Java-IDE Eclipse ist vermutlich noch immer am besten als eine Java-IDE bekannt. Mehr und mehr wird jedoch auch das Potenzial deutlich, das Eclipse als Plattform zur Integration von Werkzeugen schafft, auch solchen, die aus verschiedenen Programmiersprachenwelten stammen. Dies lässt sich besonders gut anhand der Refactoring Participants verdeutlichen: Bereits seit einigen Jahren gibt es unter dem Dach von Eclipse.org ein Projekt, das auf der Basis von Eclipse eine IDE für C/C++ entwickelt: CDT. Dieses bietet seit der kürzlich erschienenen Version 3.0 seiner Software [3] einige automatisierte Refactorings an, die auf der Basis des LTK implementiert wurden. Bilaterale Refactorings Nachdem diese Refactorings nun von CDT-Anwendern benutzt werden können, ist es ein nahe liegender Schritt, Refactoring Participants in einem Bereich einzusetzen, der beide Programmiersprachen verbindet: JNI Bindings. Das JNI (Java Native Interface) ermöglicht es, in C implementierte Funktionen aus einer Bibliothek an eine als native deklarierte Methode in einer Java-Klasse zu binden, sodass sie aus Java-Code heraus aufgerufen werden kann. Dafür muss allerdings die C-Funktion einer bestimmten Namenskonvention folgen, die vorschreibt, wie der Name der Java-Methode (sowie der Klasse, zu der sie gehört) in ihren Namen einkodiert wird. So wird die native Implementierung dieser Methode package com.xyz; public class NativeImpl { private static native void someMethod(); } im C-Code folgendermaßen genannt: JNIEXPORT void JNICALL Java_com_xyz_NativeImpl_someMethod(JNIEnv* env, jclass cl) In einem Workspace, in dem Java- und C-Entwicklung kombiniert sind, wäre es natürlich wünschenswert, wenn Refactorings sowohl auf der Java-Seite (wie das Umbenennen oder Verschieben von nativen Methoden oder von Klassen, die solche Methoden enthalten) als auch auf der C-Seite (das Umbenennen von mit JNI gebundenen Funktionen) auf der jeweils korrespondierenden Seite erkannt würden und die daraus folgenden Änderungen automatisch erfolgen könnten. Die Implementierung der Refactoring Participants müsste in einem eigenständigen Plug-in erfolgen, damit JDT und CDT nicht wechselseitig voneinander abhängig würden. Ein solches Plug-in wäre also eine optionale Erweiterung beider Entwicklungsumgebungen. |
Refactoring von Properties
Das folgende Implementierungsbeispiel soll das Umbenennen von Schlüsseln in Properties-Dateien erlauben. Oft werden mehrere solcher Dateien (die in einem Projekt liegen können, aber möglicherweise auch über den gesamten Workspace verteilt sind) zu einem so genannten Bundle zusammengefasst, z.B. für die Lokalisierung von Oberflächentexten. Zusammengehörige Dateien werden mithilfe einer Namenskonvention erkannt. Die Datei texts_de.properties enthält dann Übersetzungen ins Deutsche, texts_fr.properties für französische Texte usw. Üblicherweise zieht das Umbenennen eines Schlüssels in einer solchen Datei Änderungen an allen anderen Dateien des Bundle nach sich. Ein solcher Vorgang bietet sich zur Automatisierung geradezu an - und die Beispielimplementierung soll genau dies ermöglichen.Hat der Benutzer in einem Texteditor eine Properties-Datei geöffnet und einen Property-Schlüssel selektiert, so soll er mittels eines Eintrags im Kontextmenü ein Refactoring starten können, das wahlweise in allen Dateien des zugehörigen Bundle oder sogar in allen Properties-Dateien im Workspace diesen Schlüssel umbenennt. Natürlich ist es wünschenswert, dass der Anwender die aus der Java-IDE vertraute Benutzeroberfläche für dieses Refactoring wiederfindet; er sollte auch die gleichen Möglichkeiten zur Vorschau geboten bekommen usf. Diese Aufgabe soll anhand des LTK gelöst werden.
Vorbedingungen
Zunächst muss jedoch betont werden, dass die Unterstützung von Refactorings in IDE-Plug-ins nicht ohne Voraussetzungen ist. Refactorings sind Transformationen des Programms, das entwickelt wird - in den meisten Fällen eine Manipulation von Quellcode. Um solche Transformationen (und die ihnen vorausgehenden Analysen) durchführen zu können, wird eine programmatisch manipulierbare Repräsentation des entwickelten Systems benötigt.JDT beispielsweise verfügt über ein Objektmodell aller Sprachelemente (Klassen, Fields, Methoden - sowohl im Quelltext als auch in eingebundenen Bibliotheken oder kompilierten class-Dateien) des gesamten Eclipse-Workspace (alle Projekte) - sowie über eine (on demand erzeugte) Repräsentation einzelner Quelldateien als abstrakter Syntaxgraph (Abstract Syntax Tree - AST). Dies ermöglicht die gezielte Suche nach Vorkommnissen von Sprachelementen. Selbst einfache Refactorings enthalten Teilschritte wie "Finde alle Stellen, an denen die Variable x benutzt wird". Ohne Hilfsmittel für solcherart Analysen wird kaum ein Plug-in automatisierte Refactorings anbieten können.
Ebenso notwendig (beinahe noch wichtiger) ist der theoretische Hintergrund: Erforderlich ist ein sehr gutes Verständnis der Stellen, an welchen sich der Quellcode ändern muss (und wie der Code nach der Änderung aussehen sollte), damit die Programmsemantik erhalten bleibt. Idealerweise liegt eine formale Beschreibung vor, die aus der Sprachspezifikation abgeleitet wurde. Mit einer solchen Beschreibung können sowohl die Codemanipulationen selbst als auch die entsprechenden Testfälle erstellt werden.
Der Lebenszyklus eines Refactorings
Refactorings in Eclipse folgen einem fest vorgegebenen Ablauf:- Das Refactoring wird vom Benutzer gestartet.
- Eine erste, schnelle Prüfung erfolgt, ob das Refactoring im vom Benutzer gewünschten Kontext überhaupt anwendbar ist (checkInitialConditions()).
- Gegebenenfalls werden weitere Informationen vom Benutzer erfragt.
- Nachdem alle zur Durchführung des Refactorings erforderlichen Informationen vorliegen, werden eine gründliche Prüfung ausgelöst (checkFinalConditions()) und die einzelnen Änderungen im Quelltext werden berechnet (createChange()).
- Der Preview-Dialog zeigt die Änderungen an; bestätigt der Benutzer sie, wendet das LTK sie auf den Workspace an.
Sowohl die API-Klassen des Core-Plug-ins als auch die Elemente der Benutzeroberfläche, die das LTK bereitstellt, sind auf diesen Ablauf abgestimmt. Im Folgenden werden die einzelnen Schritte am Code des Beispiel-Plug-ins genauer erläutert.
Core und UI
In jedem Fall muss eine Subklasse von org.eclipse.ltk.core.refactoring.Refactoring gepictureet werden. Diese Klasse ist der Kern eines Refactorings; sämtliche Validierungen der erforderlichen Operationen werden in ihr vorgenommen; auch die eigentlichen Änderungen am Quelltext werden hier berechnet. Die im Ablauf unter 2. und 4. genannten Methoden gehören zum Interface dieser Klasse.Falls Refactoring Participants unterstützt werden sollen, muss stattdessen die Klasse ProcessorBasedRefactoring erweitert werden; die eigentliche Funktionalität wird dann an eine Implementierung eines RefactoringProcessor delegiert. Dieser definiert neben den Lebenszyklus-Methoden des Refactorings auch einen Einsprungspunkt zum Laden der Participants. (Da das Beispiel zu diesem Artikel keine Participants vorsieht, bleibt die Implementierung leer.)
Auf der UI-Seite wird eine Subklasse von org.eclipse.ltk.ui.refactoring.RefactoringWizard benötigt. Diese ist vor allem dafür verantwortlich, die einzelnen Seiten des Wizards zu verwalten, durch welche der Benutzer mittels NEXT und BACK blättert, und beim Beenden des Vorgangs die abschließende Operation aufzurufen. Alle diese Aufgaben werden von der Oberklasse (RefactoringWizard) bereits übernommen. Es ist lediglich erforderlich, dem Wizard eine Referenz auf eine Refactoring-Subklasse zur Verfügung zu stellen und ggf. eigene Wizard-Seiten (abgeleitet von UserInputWizardPage) einzufügen. Diese Seiten werden während des Ablaufs des Refactorings im Wizard angezeigt.
Action!
Schritt 1. Der typische Auslöser für ein Refactoring ist eine Aktion des Benutzers während der Arbeit im Editor oder auch ein Kontext-Klick auf ein Sprachelement, das in der Outline View angezeigt wird. In beiden Fällen ist es also eine Implementierung einer Eclipse-Action, die den Einsprungspunkt für das Refactoring liefert. In der run()-Methode werden Instanzen von RenamePropertyRefactoring sowie von RenamePropertyWizard (den jeweiligen Subklassen von Refactoring bzw. RefactoringWizard in der Beispielimplementierung) gepictureet und der Refactoring-Lebenszyklus des LTKs mittels einer RefactoringWizardOpenOperation in Gang gesetzt (Listing 1).Zuvor wird noch die in der Action verfügbare Information darüber gesammelt, was der Benutzer ausgewählt hatte, als er das Refactoring auslöste. Mit der aktuellen Selektion lässt sich bestimmen, welches Sprachelement umbenannt, verschoben oder anderweitig manipuliert werden soll. Im Beispiel-Plug-in handelt es sich um eine Textselektion; der selektierte Text und seine Position im Textdokument werden in ein Info-Objekt gespeichert, das später im RenamePropertyRefactoring ausgewertet wird.
Listing 1
Klasse RenameProperty (ui.actions)
RefactoringProcessor processor = new RenamePropertyProcessor( info );RenamePropertyRefactoring ref = new RenamePropertyRefactoring( processor );RenamePropertyWizard wizard = new RenamePropertyWizard( ref, info );RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation( wizard );try {String titleForFailedChecks = ""; //$NON-NLS-1$op.run( getShell(), titleForFailedChecks );} catch( InterruptedException irex ) {// operation was cancelled}
Schritt 2. Das LTK übernimmt die Kontrolle über den weiteren Ablauf. Zunächst ruft es die Methode checkInitialConditions() auf dem Refactoring-Objekt auf. Diese Methode sollte eine schnelle Prüfung durchführen, ob die grundlegenden Voraussetzungen für das Refactoring erfüllt sind. Die Beispielimplementierung verifiziert lediglich, dass der Benutzer tatsächlich einen existierenden Schlüssel in einer Properties-Datei ausgewählt hat. Trifft sie auf eine fatale Situation (etwa eine schreibgeschützte Quelldatei), erzeugt sie ein Objekt vom Typ RefactoringStatus, das nähere Informationen über das Problem enthält (Listing 2). In einem solchen Fall führt das LTK das Refactoring nicht fort, sondern setzt den Benutzer mittels einer Dialog-Box von dem Abbruch in Kenntnis.
Listing 2
Klasse RenamePropertyDelegate (core)
RefactoringStatus checkInitialConditions() {RefactoringStatus result = new RefactoringStatus();IFile sourceFile = info.getSourceFile();if( sourceFile == null || !sourceFile.exists() ) {result.addFatalError( CoreTexts.renamePropertyDelegate_noSourceFile );} else if( info.getSourceFile().isReadOnly() ) {result.addFatalError( CoreTexts.renamePropertyDelegate_roFile );} else if( isEmpty( info.getOldName() )|| !isPropertyKey( info.getSourceFile(), info.getOldName() ) ) {result.addFatalError( CoreTexts.renamePropertyDelegate_noPropertyKey );}return result;}
Mehr Interaktion
Schritt 3. Nachdem sichergestellt ist, dass dem Umbenennen des Schlüssels keine grundlegenden Hindernisse im Weg stehen, hilft der nun erscheinende Wizard dabei, vom Benutzer zusätzliche Informationen zu erfragen: beispielsweise den neuen Namen, den er für das Sprachelement vergeben möchte; aber auch Angaben über den Bereich, in dem das Refactoring angewandt werden soll (nur in der aktuellen Quelldatei, im Projekt, oder im gesamten Workspace). Das dazu nötige UI muss in Form von Subklassen von UserInputWizardPage implementiert werden. Die hier eingegebenen Daten werden mithilfe des Info-Objekts (das die Rolle eines Presentation Model spielt) auch dem RenamePropertyRefactoring verfügbar gemacht.Detailarbeit
Schritt 4. Bevor der Wizard die letzte Seite anzeigt, die eine detaillierte Vorschau auf die durchzuführenden Änderungen bietet, geschehen zwei Dinge: Auf dem Refactoring-Objekt wird die Methode checkFinalConditions() aufgerufen und die eigentlichen Änderungen an allen betroffenen Dateien werden berechnet. Letzteres muss in der Methode createChange() implementiert werden.Die Beispielimplementierung demonstriert diese beiden Schritte, indem zunächst sämtliche betroffenen Properties-Dateien im Workspace gesucht und auf Editierbarkeit geprüft werden; dabei werden zugleich die Vorkommnisse des Property-Schlüssels ausfindig gemacht. Später wird für jedes dieser Vorkommnisse ein TextFileChange-Objekt erzeugt, welches das Ersetzen eines Textes an einer gegebenen Position in der Datei durch einen anderen Text beschreibt (Listing 3). Diese Change-Objekte werden in eine baumartige Struktur eingeordnet, deren Wurzel von der Methode createChange() zurückgegeben wird. (Das Manipulieren von Textdateien im Workspace kann im Rahmen dieses Artikels nicht behandelt werden, einen guten Einstieg in dieses Thema bietet [4]).
Listing 3
Klasse RenamePropertyDelegate (core)
private Change createRenameChange() {// create a change object for the file that contains the property// which the user has selected to renameIFile file = info.getSourceFile();TextFileChange result = new TextFileChange( file.getName(), file );// a file change contains a tree of edits, first add the root of themMultiTextEdit fileChangeRootEdit = new MultiTextEdit();result.setEdit( fileChangeRootEdit );// edit object for the text replacement in the file, this is the only childReplaceEdit edit = new ReplaceEdit( info.getOffset(),info.getOldName().length(),info.getNewName() );fileChangeRootEdit.addChild( edit );return result;}
Schritt 5. Falls der Benutzer dies nicht überspringt und sofort FINISH drückt, wird nun als letzte Seite des Wizard eine Vorschau angezeigt, in der er alle durchzuführenden Änderungen im Detail einsehen (und gegebenenfalls einige von ihnen deselektieren und damit unwirksam machen) kann. Bis zu diesem Punkt ist noch keine der Dateien im Workspace tatsächlich verändert worden. Erst nach der Bestätigung durch den Benutzer werden schließlich alle Änderungen auf die Dateien appliziert. (Insofern übrigens lediglich Change-Objekte aus dem LTK benutzt werden, wird die Undo-Funktionalität als kleines Geschenk noch mitgeliefert.)
Fazit
Das Language Toolkit erleichtert das Implementieren automatisierter Refactorings. Es definiert den Ablauf eines Refactorings und stellt abstrakte Klassen im Core (Refactoring, Change) sowie UI-Komponenten (RefactoringWizard) bereit, die den sprachneutralen Teil einer Implementierung weitgehend abdecken. Die sprachspezifische Berechnung der Änderungen im Quelltext kann in diesen Rahmen eingefügt werden, sodass Benutzer das Refactoring in der vertrauten Form der von JDT bekannten Java-Refactorings durchführen können.Leif Frenzel ist Senior Architect bei Innoopract. Schwerpunkt seiner Arbeit sind die Eclipse-Distribution Yoxos sowie Entwicklungsumgebungen auf der Basis der Eclipse-Plattform. Er ist außerdem Autor von Open-Source-Plug-ins zur Unterstützung funktionaler Programmiersprachen in Eclipse.
Links & Literatur
- [1] Martin Fowler: Refactoring. Improving the Design of Existing Code, Addison-Wesley, 2000
- [2] Martin Lippert, Andreas Havenstein: Elementares Handwerkszeug. Refactorings mit Eclipse schrittweise, effizient und sicher durchführen, in Eclipse Magazin Vol. 4
- [3] Markus Knauer, Elias Volanakis: Back to the Future. C- und C++-Entwicklung mit Eclipse und CDT 3.0, in Eclipse Magazin Vol. 4
- [4] Kai-Uwe Mätzel: Safely Manipulating the Contents of Files - How to Get it Right, Präsentation auf der EclipseCon 2005: www.eclipsecon.org/2005/presentations/EclipseCon2005_5.1Maetzel.pdf




