Eclipse Refactoring Framework

Aus Eclipse
Wechseln zu: Navigation, Suche

◄== zurück zur Übersichtsseite


Bei der Entwicklung von Refactoring-Tools unterstützt Eclipse den Programmierer mit einem Framework. Der Großteil dieses Frameworks besteht aus abstrakten Klassen und folgt somit dem Entwurfsmuster Schablonenmethode (Template Method). Dabei muss der Entwickler lediglich einige anwendungsspezifische Lücken im Refactoring-Mechanismus ausfüllen, indem er die abstrakten Klassen des Frameworks erweitert und deren abstrakte Methoden implementiert.

JDT und LTK

Ab der Version 3.1 hat das JDT-Team die wiederverwendbaren Teile der Standard-Refactoring-Tools in eine sprach-neutrale API ausgelagert, das Refactoring Language Tool Kit (LTK). Das LTK besteht aus den Plugins org.eclipse.ltk.core.refactoring (Kern-Elemente) und org.eclipse.ltk.ui.refactoring (GUI-Elemente). Weiterhin bietet das LTK eine Infrastruktur, die es anderen Plugins erlaubt, Teilhaber von Refactorings zu werden (refactoring participants). Ein gutes Beispiel dafür ist das PDE (Plugin Development Environment). Dieses Plugin bietet ein Framework zur Entwicklung eigener Plugins in Eclipse (und somit auch eigener Refactoring-Tools). Jedes Plugin hat eine Manifest-Datei, welche die zentralen Komponenten eines Plugins beschreibt. Wird durch ein Rename-Refactoring der Name einer Plugin-Klasse geändert, ändert das PDE den Namen dieser Klasse auch im Manifest. Das LTK-Framework ermöglicht uns die Refactorings in der Refactoring History zu dokumentieren, daraus Refactoring-Scripts[1] zu erzeugen und unsere Änderungen an die Eclipse-Workbench weiterzugeben. Weiterhin wird eine Undo-Funktion angeboten. Ferner unterstützt das Framework beim Validieren der Vor- und Nachbedingungen sowie den eigentlichen Codeänderungen. Das Plugin org.eclipse.ltk.ui.refactoring bringt die für das Refactoring-Tool notwendigen User-Interface-Komponenten mit. Es definiert abstrakte Wizards und Eingabedialoge, visualisiert die Validierung der Vorbedingungen, zeigt Vorschauen für die Änderungen (Nachbedingungen) und meldet aufgetretene Fehler. ▲___zum Seitenanfang

Architektur des Frameworks

Pflichtkomponenten

Ein Refactoring-Tool braucht zwingend einen Kern und eine Hülle. Jedes Tool muss mindestens die Klasse Refactoring aus dem Core-Package des LTK erweitern. Es benötigt eine Oberfläche, über die der User die benötigten Informationen liefern und den Fluss des Refactorings (Validierungen, Vorschauen, etc.) kontrollieren kann. Letztlich muss das Refactoring durch einen Mechanismus angestoßen werden; dazu eignet sich am besten eine Action. ▲___zum Seitenanfang

Refactoring Action

Actions werden in Eclipse eingesetzt, um bestimmte Vorgänge anzustoßen. Hinter jedem Klick auf einen Button in der Toolbar oder auf einen Menüpunkt im Haupt- oder Kontextmenü steht meist eine Action, die auf das Klick-Ereignis reagiert und einen Prozess anstößt. Für den Start eines Refactoring-Tools wird eine Action benutzt (z.B. eine Implementierung von org.eclipse.ui.IWorkbenchWindowActionDelegate). Eine Action in Eclipse benutzt den workbench selection service. Über die Methode selectionChanged(IAction, ISelection) wird sie vom Service benachrichtigt, wenn eine Selection in der Eclipse Workbench erfolgt. In der Methode kann ausgewertet werden, was selektiert wurde und ob die Vorbedingungen für den Start des Refactorings erfüllt sind. Danach kann das eigentliche Refactoring gestartet oder ein Hinweis über die fehlenden Voraussetzungen ausgegeben werden. Die Methode selectionChanged(IAction, ISelection) sollte eine schnelle Auswertung durchführen, damit der User nicht lange auf das Ergebnis seiner Auswahl bzw. seines Klicks warten muss. ▲___zum Seitenanfang

RefactoringStatus Objekt

Das org.eclipse.ltk.core.refactoring.RefactoringStatus Objekt wird benutzt, um das Ergebnis der Validierung der Vor- und Nachbedingungen an das Refactoring-Framework zu kommunizieren. Deshalb wird dieses Objekt von den beiden Methoden checkInitialConditions und checkFinalConditions der Klasse Refactoring zurückgegeben. Aufgetretene Fehler fügt man je nach Schweregrad (severity) dem RefactoringStatus Objekt mit den folgenden Methoden hinzu:

  • addInfo(String)
  • addWarning(String)
  • addError(String)
  • addFatalError(String)

▲___zum Seitenanfang

Change Objekt

Im Change-Objekt werden alle vom Refactoring-Tool vorgesehenen Änderungen am Quelltext vorerst nur vermerkt und an das Framework zur Ausführung übergeben. Ob diese Änderungen ausgeführt werden, hängt vom Ergebnis der Validierung der Vor- und Nachbedingungen ab und von User selbst. Dieser kann das Refactoring nach der Sichtung der Vorschau im Refactoring-Wizard jederzeit abbrechen. Das Change-Objekt wird meist mit Refactoring.createChange() erzeugt. Dieser Aufruf erfolgt durch das Framework, nachdem der Entwickler diese abstrakte Methode implementiert hat. Ausgeführt werden die Änderungen, die im Change-Objekt vermerkt sind durch das Framework mit der Methode Change.perform(). Wenn der Entwickler das Refactoring-Tool aus seinem Programm heraus ansprechen will, kann er das Change-Objekt auch selbst erzeugen und ausführen. (s. Artikel Refactoring-Tools in Eclipse intern ansprechen). Die Klasse org.eclipse.ltk.core.refactoring.Change ist abstrakt und muss erweitert werden, wenn ein eigenes Change-Objekt implementiert werden soll. Oft reichen die bereits implementierten Subklassen aus, z.B. org.eclipse.ltk.core.refactoring.CompositeChange. ▲___zum Seitenanfang

Klasse Refactoring

Jedes neue Refactoring-Tool erweitert die Klasse org.eclipse.ltk.core.refactoring.Refactoring. Sollen auch andere Plugins am Refactoring teilhaben (refactoring participants), dann erweitert man die Klasse org.eclipse.ltk.core.refactoring.ProcessorBasedRefactoring - diese Klasse ist eine Erweiterung der Oberklasse org.eclipse.ltk.core.refactoring.Refactoring.

Die wichtigsten Methoden, die jedes Tool implementieren muss (diese sind in der Klasse Refactoring abstrakt definiert), sind:

  • checkInitialConditions(IProgressMonitor) : RefactoringStatus
    Diese Methode wird als erste nach dem Start des Refactorings aufgerufen. Ziel dieser Methode ist, den vorliegenden Workspace nach Fehlern zu sondieren. Eine mögliche Ursache wäre ein Workspace mit Compiler Fehlern. Der Rückgabewert vom Typ RefactoringStatus enthält die Information für das Framework, ob das Refactoring auf dem vorliegenden Workspace problemlos ausgeführt werden kann. Enthält der Rückgabewert mindestens einen Fehler mit dem Schweregrad (severety) RefactoringStatus.FATAL, dann wird das Refactoring abgebrochen. Die Validierungen in der Methode checkInitialConditions sollten nicht zu rechenintensiv sein, da das Ergebnis der Methode das Verhalten der Benutzeroberfläche des Refactoring-Tools bestimmt und deshalb auf kurze Antwortzeiten angewiesen ist.
    Diese Methode kann, je nach Implementierung, mehrmals durch die Refactoring-Tool-GUI aufgerufen werden.
  • checkFinalConditions(IProgressMonitor) : RefactoringStatus
    Diese Methode wird nach der Methode checkInitialConditions aufgerufen, nachdem der User alle benötigten Informationen über die Benutzeroberfläche des Refactoring-Tools (i.d.R. ein Refactoring-Wizard) eingegeben hat. Ziel der Methode ist, alle noch ausstehenden Validierungen durchzuführen, bevor die eigentlichen Codeänderungen erfolgen (z.B. bei Rename: Ist der neue Name bereits vorhanden?). Zusätzlich zu diesen Validierungen berechnet die Methode die eigentlichen Codeänderungen, die das Refactoring vorsieht. Je nach Vorgabe bedeutet das, den gesamten Workspace nach Vorkommnissen von zu ändernden Codepassagen zu durchsuchen und diese durch neuen Code zu ersetzen. Daher ist diese Methode wesentlich rechenintensiver als checkInitialConditions. Die Ergebnisse der Codeänderungen werden in einer Liste von change descriptions gespeichert, die später bei der Berechnung der tatsächlichen Änderungen benutzt wird.
    Enthält der Rückgabewert mindestens einen Fehler mit dem Schweregrad (severety) RefactoringStatus.FATAL, dann wird das Validieren abgebrochen. Dem User wird auf der GUI der Grund des Abbruchs mitgeteilt. Die Methode kann je nach Refactoring mehrmals aufgerufen werden. Grundsätzlich haben alle Aufrufe nach den Aufrufen von checkInitialConditions und vor dem Aufruf von createChange zu erfolgen!
  • createChange(IProgressMonitor) : Change
    Diese Methode wird aufgerufen, nachdem alle Vor- und Nachbedingungen positiv validiert wurden (obwohl noch keine einzige Codeänderung erfolgt ist). Ziel der Methode ist, die eigentlichen Codeänderungen zu berechnen. Dabei wird auf die Vorberechnungen von checkFinalConditions zurückgegriffen. Die Methode liefert als Rückgabewert ein Objekt vom Typ org.eclipse.ltk.core.refactoring.Change. Das Change-Objekt enthält eine Liste von Änderungsbeschreibungen (change descriptions) und wird von der Oberfläche benutzt, um eine Vorschau der Änderungen zu präsentieren. Die von den Änderungen betroffene Module werden in einem Tree präsentiert und können einzeln an- und abgewählt werden. Wenn der User die Änderungen mit OK bestätigt, wird das Change-Objekt an das Framework übergeben, welches erst jetzt die (bis jetzt nur berechneten) Änderungen im gesamten Workspace vornimmt. Nach den Änderungen werden die Dateien automatische gespeichert und (wie bei Eclipse üblich) inkrementell compiliert. Das Refactoring ist damit beendet.

▲___zum Seitenanfang

Refactoring Wizard

Wizards eigenen sich am besten für ein User Interface eines Refactoring-Tools, denn sie führen den Benutzer Schritt für Schritt durch die Abfrage der Parameter und können im letzten Schritt eine Vorschau präsentieren. Ferner kann der User dem Refactoring-Tool über die GUI Informationen für vorgesehene Änderungen liefern. Das Framework bietet auch hier abstrakte Klassen, die erweitert werden müssen. Um einen eigenen Wizard anzulegen, muss die Klasse org.eclipse.ltk.ui.refactoring.RefactoringWizard erweitert werden. Die erweiternde Klasse muss die abstrakte Methode addUserInputPages implementieren, um den Wizard mit eigenen Inhalten zu füllen. ▲___zum Seitenanfang

Optionale Komponenten

Diese Komponenten sind optional, d.h. sie werden nicht unbedingt benötigt, um ein Refactoring durchzuführen. Sie tragen aber dazu bei, das Refactoring-Tool beim Framework anzumelden und die Änderungen besser zu beschreiben. ▲___zum Seitenanfang

Refactoring Descriptor

Der Refactoring-Descriptor beschreibt eine bestimmte Refactoring-Instanz und enthält wichtige Informationen über das Refactoring, die vom Framework weiterverarbeitet werden können. Ein eigener Refactoring-Descriptor muss die Klasse org.eclipse.ltk.core.refactoring.RefactoringDescriptor erweitern. Der Refactoring-Descriptor enthält folgenden Informationen:

  • Refactoring ID Eine eindeutige ID (darf nicht leer und nicht null sein), die meist aus der ID des Plugins und der Plugin-weit eindeutigen ID des Refactoring-Tools zusammengesetzt wird (z.B. org.eclipse.ltk.core.refactoring.renameFile).
  • Project Das Projekt, auf welches sich das Refactoring bezieht. Bei null bezieht sich das Refactoring auf den gesamten Workspace
  • Timestamp Zeitpunkt der Ausführung des Refactorings in Millisekunden seit dem 1. Januar 1970 00:00:00 GMT (optional)
  • Description Eine kurze Beschreibung des Refactorings (darf nicht null sein)
  • Comment Ein ausführlicherer Kommentar des Refactorings (optional)
  • Flags Verschiedene Flags, u.a. ein Flag, welches angibt, ob alle Referenzen im Workspace geändert werden sollen (Checkbox im RefactoringWizard)

Die einzige zu implementierende abstrakte Methode dieser Klasse ist
createRefactoring(RefactoringStatus) : Refactoring
Diese Methode wird vom Framework aufgerufen, wenn alle Vor- und Nachbedingungen positiv validiert wurden und alle Eingaben des Benutzers aus dem RefactoringWizard vorliegen. createRefactoring erzeugt eine Refactoring-Instanz, die voll konfiguriert und bereit ist, vom Framework ausgeführt zu werden. Zugriff auf einen Refactoring-Descriptor erhält das Framework mit Change.getDescriptor(), nachdem die Änderungen des Change-Objekts auf das Workspace angewendet wurden. Refactoring-Tools, die die durchgeführten Änderungen in der Refactoring History persistieren sollen, müssen getDescriptor() überschreiben, um den richtigen Descriptor zu liefern. ▲___zum Seitenanfang

Refactoring Contribution

Refactoring-Contributions ermöglichen dem Entwickler, sein Refactoring-Tool am Framework durch eine dynamische Instantiierung des Refactoring Objekts anzumelden. Die Notwendigkeit, diesen Mechanismus zu benutzen, entsteht erst dann, wenn das Tool die Dienste der Refactoring-History und des Refactoring-Scripting benutzen soll. Das Benutzen dieses Mechanismus und somit auch der Einsatz der Refactoring-Contributions ist optional.
Durch die dynamische Instanziierung des Refactoring-Objekts wird es diesen Diensten ermöglicht, eine voll konfigurierte Instanz eines Refactorings aus einer Aufnahme, einem Refactroring-Script, zu erzeugen. Refactoring-Contributions müssen den Extension Point org.eclipse.ltk.core.refactoring.refactoringContributions erweitern, welcher die Erweiterung der abstrakten Klasse org.eclipse.ltk.core.refactoring.RefactoringContribution vorschreibt. Die abstrakte Oberklasse schreibt ihrerseits die Implementierung folgender Methoden vor:

  • createDescriptor(String, String, String, String, Map, int) : RefactoringDescriptor
    Diese Methode benutzt das Framework zum Erzeugen des Refactoring-Descriptors auf der Basis eines Refactoring-Scripts. Die ersten vier und der sechste Methodenparameter nehmen id, project, description, comment und flags auf (s. auch Abschnitt Refactoring Descriptor). Der fünfte Parameter enthält eine Map, eine Liste von Key-Value-Paaren (meist Map<String, String>), die den Zustand des Refactorings (Refactoring-Script s.o.) speichert.
  • retrieveArgumentsMap(RefactoringDescriptor) : Map
    Diese Methode ist zwar nicht astrakt, doch sollte ihre Default Implementierung in der Subklasse überschrieben werden, um ein anwendungsspezifisches Refactoring-Script zu liefern. Der Rückgabeparameter sollte daher eine Map vom selben Format liefern wie das fünfte Argument von createDescriptor.

Diese beiden Methoden bilden einen generischen erweiterbaren Mechanismus, um Refactoring-Descriptoren dynamisch zu instanziieren und zu persistieren. ▲___zum Seitenanfang

Zentrale Komponente RefactoringCore

Die Factory-Klasse org.eclipse.ltk.core.refactoring.RefactoringCore ist ein zentraler Zugriffspunkt eines Refactoring-Plugins, die Zugriff auf die Services des Frameworks bietet. Sie enthält folgende statische Factory Methoden:

  • getUndoManager() : IUndoManager
    Diese Methode liefert eine neue Instanz eines UndoManagers. Ein Undo-Manager kann von einem Refactoring-Tool benutzt werden, um die durchgeführten Änderungen mittels eines Stacks zu protokollieren.
  • getHistoryService() : IRefactoringHistoryService
    Diese Methode liefert eine Instanz auf den Historisierungsdienst von Refactoring-Tools. Dieser Dienst liefert die Refactoring History des ganzes Workspaces oder eines bzw. mehrerer Projekte.
  • getRefactoringContribution(String id) : RefactoringContribution
    Diese Methode liefert eine Refactoring-Contribution zu einer Id.

▲___zum Seitenanfang

Lebenszyklus eines Refactorings

Der Ablauf eines Refactorings ist ein komplexer Prozess, der größtenteils in den Klassen des LTK-Frameworks abläuft. Wesentlich ist, dass die Änderungen am Sourcecode zuerst nur berechnet werden. Erst mit der Freigabe durch den User (Klick auf den OK-Button im Refactoring-Wizard) kommen die Änderungen durch das Framework zur Ausführung. Die geänderten Dateien werden danach automatisch gespeichert und neu kompiliert. Wenn bei den Berechnungen der Vor- oder Nachbedingungen Fehler der Stufe RefactoringStatus.FATAL auftreten, wird das Refactoring abgebrochen und nicht durchgeführt. Jedes sauber programmierte Refactoring-Tool sollte auch sauber rückgängig gemacht werden können (Undo-Funktion).

Abb. 1: Der Lebenszyklus eines Refactorings

  1. Der User startet das Refactoring von einem Feld- oder einem Methodennamen, indem er im Refactoring-Menü z.B. Rename aufruft
  2. Mit der Methode checkInitialConditions() wird überprüft, ob das Refactoring im gegebenen Kontext durchführbar ist (z.B. könnte die Quelldatei schreibgeschützt sein). Wenn die Vorbedingungen nicht erfüllt sind, bricht das Refactoring ab und der User wird auf die Ursache hingewiesen
  3. Danach wird überprüft, ob der User noch zusätzliche Parameter eingeben soll (z.B. den neuen Namen). Es wird dem User ein Dialog oder ein Wizard präsentiert, in den er die fehlenden Parameter eingeben kann.
  4. Liegen alle Parameter vor, wird ein Check der Nachbedingungen mit checkFinalConditions() durchgeführt. Tritt ein Fehler der Stufe RefactoringStatus.FATAL auf, dann wird das Refactoring abgebrochen und der User darüber informiert.
  5. Danach werden die Änderungen mit createChange() berechnet. Diese Methode liefert ein Change-Objekt. Das Change-Objekt ist die Wurzel einer Baumstruktur aller Änderungen im gesamten Workspace.
  6. Die berechneten Änderungen werden dem User in der Vorschau des Refactoring-Wizards präsentiert (die Vorschau ist optional, kann aber z.B. bei gravierenden Änderungen erzwungen werden). Dabei wird links der Vorher- und rechts der Nachher-Zustand übersichtlich dargestellt. Oberhalb wird der Change-Tree dargestellt. Der User kann bei Bedarf einzelne Änderungszweige des Trees abwählen:
    Abb. 2: Vorschau der Änderungen eines Refactorings

  7. Bis jetzt wurden noch keine Änderungen durchgeführt. Erst wenn der User die Änderungen bestätigt, wird das Change-Objekt an das LTK weitergegeben, welches die tatsächlichen Änderungen im ganzen Workspace durchführt.

▲___zum Seitenanfang

Refactoring History

Es ist guter Stil, seine Refactoring-Tools so zu programmieren, dass die durchgeführten Änderungen am Sourcecode in der Refactoring-History dokumentiert werden. Später lässt sich sehr komfortabel nachvollziehen, wann und wo welche Änderungen durchgeführt wurden.

Abb. 3: Refactoring-History in Eclipse

▲___zum Seitenanfang


  1. Aufnahmen von Refactorings, die man weitergeben kann, um dieselben Änderungen auf einem anderen System durchzuführen

◄== zurück zur Übersichtsseite