Constraintbasiertes Refaktorisieren von Identifiern in Java

Werbung
FernUniversität in Hagen
Fakultät für Mathematik und Informatik
Lehrgebiet Programmiersysteme
Prof. Dr. Friedrich Steimann
Constraintbasiertes Refaktorisieren von
Identifiern in Java
Abschlussarbeit im Studiengang
Bachelor of Science in Wirtschaftsinformatik
Betreuer: Dipl.-Inf. Andreas Thies
10. Juli 2013
Hermine Spengler
Hütwiesstrasse 8
86510 Ried
Matrikelnummer: 7707118
Danksagung
Herrn Prof. Dr. Steimann danke ich für die Möglichkeit, meine Bachlorarbeit
am Lehrgebiet Programmiersysteme der FernUniversität in Hagen anfertigen
zu können.
Ganz besonders bedanken möchte ich mich bei meinem Betreuer
Dipl.-Inf. Andreas Thies für seine Unterstützung in allen Fragen und die vielen
hilfreichen Tipps und Anmerkungen.
Ebenso möchte ich mich an dieser Stelle ganz herzlich bei meiner Familie,
Freunden, Bekannten und Kollegen bedanken für deren Verständnis und die
Unterstützung der letzten Jahre.
Zusammenfassung
Bestehende Software wird in vielen Fällen weiterentwickelt. Damit Änderungen im
Laufe der Zeit nicht das ursprüngliche Design negativ beeinflussen, ist ein regelmäßiges Umstrukturieren des Quelltextes nötig. Gerade das Überarbeiten von Identifiern ist sinnvoll, da sprechende Bezeichnungen im Quelltext wichtig sind für ein
schnelles Programmverständnis. Dennoch ist es nicht leicht, sofort einen geeigneten
Namen zu finden. Daher ist häufig ein nachträgliches Umbenennen erforderlich.
Spezielle Refaktorisierungswerkzeuge können diese Tätigkeit wirkungsvoll unterstützen, wenn sie zuverlässig und fehlerfrei arbeiten. Die Erstellung eines solchen
Werkzeugs ist allerdings nicht trivial. Ein Refaktorisierunswerkzeug muss eine
Vielzahl an Bedingungen beachten, damit es keine Fehler in das zu ändernde Programm einbringt. Diese Arbeit untersucht die notwendigen Bedingungen für die
Refaktorisierung von Identifiern in der Programmiersprache Java. Bei der Umsetzung wird auf das Framework REFACOLA aufgesetzt, welches die Erstellung von
programmiersprachenunabhängigen Refaktorisierungswerkzeugen unterstützt.
Inhaltsverzeichnis
I
Inhaltsverzeichnis
Abbildungsverzeichnis .........................................................................................................III
Listingverzeichnis.................................................................................................................IV
Regelverzeichnis ..................................................................................................................VI
Tabellenverzeichnis ............................................................................................................VII
Abkürzungsverzeichnis.......................................................................................................VIII
Hinweise zur Benutzung.......................................................................................................IX
1
Einleitung .......................................................................................................................1
2
Problembeschreibung .....................................................................................................3
3
2.1
Beitrag der Arbeit............................................................................................................ 4
2.2
Aufbau der Arbeit ........................................................................................................... 5
Grundlagen.....................................................................................................................6
3.1
3.2
4
Die Java-Sprachspezifikation........................................................................................... 6
3.1.1
Allgemeines ....................................................................................................... 6
3.1.2
Namen in Java.................................................................................................... 7
3.1.3
Wichtige Sprachkonzepte in Java ...................................................................... 8
Constraintbasierte Refaktorisierung mit REFACOLA..................................................... 13
3.2.1
Definition Refaktorisierungsmodule................................................................ 14
3.2.2
Ablauf einer Refaktorisierung in REFACOLA.................................................... 17
3.2.3
Eclipse - Integration......................................................................................... 17
Implementierung.......................................................................................................... 19
4.1
Ausgangssituation ......................................................................................................... 19
4.2
Das Regelset im Überblick............................................................................................. 19
4.2.1
Allgemeines ..................................................................................................... 19
4.2.2
Eindeutige Namen für Typen, Felder und Methoden...................................... 19
4.2.3
Namensgleichheit von Typ und Konstruktor ................................................... 23
4.2.4
Namensgleichheit von Topleveltyp und Quelltextdatei .................................. 23
4.2.5
Referenzen....................................................................................................... 24
4.2.6
Überschreiben und Verbergen von Methoden ............................................... 25
4.2.7
Eager Interface ................................................................................................ 28
4.2.8
Eindeutige Namen für lokale Variablen und formale Parameter .................... 29
4.2.9
Pakete und Importanweisungen ..................................................................... 31
Inhaltsverzeichnis
4.3
5
Einschränkungen bei der Regeldefinition ..................................................................... 33
4.3.1
Ergänzung von Qualifiern ................................................................................ 33
4.3.2
Ergänzung Qualifizierte Namen....................................................................... 37
4.3.3
Erweiterte Prüffunktionen............................................................................... 37
4.3.4
Offene Themen................................................................................................ 40
4.4
Identifier in Fremdbibliotheken .................................................................................... 42
4.5
Tests .............................................................................................................................. 42
4.5.1
Unit-Tests......................................................................................................... 43
4.5.2
Mutationstests................................................................................................. 43
4.5.3
RTT-Tests ......................................................................................................... 45
4.5.4
Testgetriebene Entwicklung ............................................................................ 46
Diskussion .................................................................................................................... 47
5.1
5.2
5.3
6
II
Ergebnisse ..................................................................................................................... 47
5.1.1
Prototyp zur Ermittlung umschließender lokaler Variablen............................ 47
5.1.2
Einbindung Locked Bindings ............................................................................ 49
5.1.3
Auswertung der Tests...................................................................................... 50
Verwandte Arbeiten...................................................................................................... 53
5.2.1
Reflektive Aufrufe............................................................................................ 53
5.2.2
Sprachübergreifendes Refaktorisieren............................................................ 53
Ausblick ......................................................................................................................... 54
5.3.1
Unterstützung durch Benutzerschnittstelle .................................................... 54
5.3.2
Java-Versionen................................................................................................. 55
Schlußbetrachtung........................................................................................................ 56
Literaturverzeichnis ............................................................................................................ 57
Anhang .............................................................................................................................. 60
A) Referenz Constraintregeln .............................................................................................. 61
B) Refaktorisierungsdefinition RenameMember .................................................................. 64
C) JUnit-Testsuite................................................................................................................ 65
D) Inhalt der CD .................................................................................................................. 66
Eidesstattliche Versicherung ............................................................................................... 67
Abbildungsverzeichnis
III
Abbildungsverzeichnis
Abbildung 1.1:
modifizierter Software-Lebenszyklus.............................................................. 1
Abbildung 3.1:
Gültigkeitsbereiche am Beispiel von Variablen .............................................. 9
Abbildung 3.2:
Module eines REFACOLA-Refaktorisierungswerkzeugs................................ 14
Abbildung 4.1:
Eager Interface.............................................................................................. 28
Abbildung 4.2:
Refacola-Eingabedialog „Rename“ - Prüfung gültiger Identifier .................. 39
Abbildung 4.3:
Menü RTT-Tests für Rename in Eclipse Package Explorer ............................ 46
Abbildung 5.1:
gleichnamige lokale Variablen in verschiedenen Anweisungsblöcken......... 47
Abbildung 5.2:
lokale Klasse und lokale Variablen................................................................ 49
Listingverzeichnis
IV
Listingverzeichnis
Listing 2.1:
Refaktorisierungsbeschreibung „Rename Method“....................................... 3
Listing 2.2:
Umbenennen eines Klassennamens ............................................................... 4
Listing 3.1:
Varianten bei der Definition von Variablendeklaration und –initialiserung.. 7
Listing 3.2:
Deklaration und Referenzierung in Java ........................................................ 7
Listing 3.3:
Namensaufbau in Java ................................................................................... 8
Listing 3.4:
Vermeidung Namenskonflikt durch Verwendung qualifizierter Namen ........ 8
Listing 3.5:
Abschatten einer Typdeklaration.................................................................. 10
Listing 3.6:
Verdunkeln durch eine Typdeklaration ........................................................ 11
Listing 3.7:
Methodenbindung ........................................................................................ 12
Listing 3.8:
unvollständige Refaktorisierung ................................................................... 14
Listing 3.9:
Ausschnitt REFACOLA-Sprachdefinition für Java .......................................... 15
Listing 3.10:
Ausschnitt Regelset Names.ruleset.refacola ................................................ 16
Listing 3.11:
Ausschnitt Refaktorisierungsdefinition........................................................ 16
Listing 4.1:
gleichnamige Elemente in einem Typen....................................................... 20
Listing 4.2:
Verletzung Eindeutigkeit eines Typ-Identifiers............................................. 20
Listing 4.3:
eindeutige Namen bei verschachtelten Typen ............................................. 21
Listing 4.4:
gleichnamige Konstruktor- und Methodendeklaration in einer Klasse ........ 23
Listing 4.5:
Typreferenz in extends-Klausel..................................................................... 24
Listing 4.6:
Überschreiben einer Methode durch Umbenennen .................................... 26
Listing 4.7:
Methode mit final-Modifier.......................................................................... 28
Listing 4.8:
gleichnamiger Topleveltyp und Single-Type-Import..................................... 32
Listing 4.9:
Abschattung einer Instanzvariablen nach Refaktorisierung I ....................... 33
Listing 4.10:
Abschattung einer Instanzvariablen nach Refaktorisierung II ...................... 33
Listing 4.11:
Qualifizierter Referenzname in Zuweisung................................................... 35
Listing 4.12:
geschachtelte Expression in Zuweisung........................................................ 35
Listing 4.13:
Problematik von fehlendem Qualifier bei lokaler Variable .......................... 35
Listing 4.14:
Ergänzung Quelltext mit explizitem this-Qualifier........................................ 36
Listing 4.15:
diverse Aufrufe mit Qualifiern ...................................................................... 36
Listing 4.16:
Einfügen von qualifizierten Namen .............................................................. 37
Listing 4.17:
Methodenaufruf bei überladenen Methoden .............................................. 38
Listing 4.18:
Methodenaufruf mit Autoboxing.................................................................. 38
Listing 4.19:
Funktion für casesensitivem Stringvergleich ................................................ 40
Listing 4.20:
Kommentare in Java ..................................................................................... 42
Listingverzeichnis
V
Listing 5.1:
Ausschnitt REFACOLA-Sprachspezifikation für Java - lokale Variablen......... 48
Listing 5.2:
Klasse mit reflektivem Aufruf ....................................................................... 53
Listing 5.3:
Ausblick voraussichtliche Lambda-Syntax .................................................... 55
Regelverzeichnis
VI
Regelverzeichnis
Regel 1:
Names.UniqueTopLevelTypeIdentifier ......................................................... 20
Regel 2:
Names.UniqueMemberTypeNames2 ........................................................... 21
Regel 3:
Names.UniqueMemberTypeNames1* ......................................................... 21
Regel 4:
Names.UniqueFieldIdentifier........................................................................ 22
Regel 5:
Names.UniqueMethodIdentifier................................................................... 22
Regel 6:
Names.ConstructorNames............................................................................ 23
Regel 7 :
Names.TopLevelTypeNames......................................................................... 24
Regel 8:
Names.TypeReferenceIdentifier ................................................................... 25
Regel 9:
Names.ReferenceIdentifier*......................................................................... 25
Regel 10:
Names.OverridingMethodNames................................................................. 26
Regel 11:
Accessibility.AccidentalOverriding*.............................................................. 27
Regel 12:
Names.AccidentalHidingFinalMethod .......................................................... 28
Regel 13:
Names.EagerInterfaceMethodNames .......................................................... 29
Regel 14:
Names.UniqueLocalVariableNames1............................................................ 30
Regel 15:
Names.UniqueLocalVariableNames2............................................................ 30
Regel 16:
Names.UniqueLocalVariableNames3............................................................ 31
Regel 17:
Names.PackageDeclarationNames* ............................................................. 31
Regel 18:
Names.ImportDeclarationNames1 ............................................................... 32
Regel 19:
Names.ImportDeclarationNames2 ............................................................... 32
Regel 20:
Names.UniqueEnclosingMemberTypeNamesInAssignment1 .................... 34
Regel 21:
Names.UniqueEnclosingMemberTypeNamesInAssignment2 .................... 34
* Autor A. Thies
Tabellenverzeichnis
VII
Tabellenverzeichnis
Tabelle 4.1:
Mutationstest für das JUnit-Test RenameMember ...................................... 44
Tabelle 5.1:
JUnit-Testsuite AllRenameMember .............................................................. 50
Tabelle 5.2:
RTT-Test Rename Only Type ......................................................................... 51
Tabelle 5.3:
RTT-Test Rename Only Field ......................................................................... 51
Tabelle 5.4:
RTT-Test Rename Only Method.................................................................... 51
Tabelle 5.5:
RTT-Test Rename Only LocalVariable ........................................................... 52
Tabelle 5.6:
Parameter Testumgebung ............................................................................ 52
Abkürzungsverzeichnis
Abkürzungsverzeichnis
API
Application Programming Interface
AST
Abstract Syntax Tree
CCD
Clean Code Developer
DSL
Domain specific Language
IDE
Integrated Development Environment
JDT
Java Development Tools
JLS
Java Language Specification
JSP
Java Server Pages
JVM
Java Virtual Machine
REFACOLA
REFActoring COnstraint LAnguage
RTT
Refactoring Tool Tester
TDD
Test Driven Development
XML
Extensible Markup Language
XP
Extreme Programming
VIII
Hinweise zur Benutzung
IX
Hinweise zur Benutzung
Sprache
In der Informatik haben sich im täglichen Sprachgebrauch viele fachspezifische
Begriffe in englischer Sprache etabliert, wie zum Beispiel Interface oder Sourcecode.
Da diese sich teilweise nur unzureichend übersetzen lassen und zudem das Verständnis dadurch eher erschwert wird, verwendet diese Arbeit an vielen Stellen die
englischen Begriffe. Auf eine besondere Hervorhebung wird aus Gründen der Lesbarkeit verzichtet.
Darstellung Quelltext
Diese Arbeit verwendet zwei Varianten für die Darstellung von Quelltexten. Die
eine dient ausschließlich der Erläuterung einer bestimmten Ausgangssituation. Die
andere veranschaulicht die Transformation eines Programms und seines Refaktorisierungsergebnisses.
Quelltext zur Erläuterung
Quelltext, welcher der Erläuterung dient, stellt einen Programmausschnitt dar und
kann ein- oder zweispaltig angegeben sein.
package a;
package a;
class A {
class B {
public B (){}
void m() {
B b2 = new B();
}
}
}
Refaktorisierter Quelltext
Hier befindet sich das Originalprogamm jeweils auf der linken Seite, das refaktorisierte Ergebnis jeweils rechts. Ein Pfeil weist auf die Transformation hin.
package a;
package a;
public class A{
public class A{
int i;
int k;
int j=i;
}
int j=k;
}
Einleitung
1
1 Einleitung
Eine wichtige Eigenschaft von Software ist, dass sie relativ leicht änderbar ist.
Denn je erfolgreicher ein Produkt ist, desto eher wird es weiterentwickelt. So stehen, wenn die erste Version eines Programms fertig ist, häufig schon die nächsten
Themen auf der meist langen Wunschliste der Auftraggeber [We06, S. 5]. Aber auch
ohne Erweiterungen endet die Entwicklung in der Regel nicht mit der Inbetriebnahme der Software. Um die geforderten Aufgaben über viele Jahre erfüllen zu
können, müssen die meist fehlerhaften Systeme gewartet werden.
Abbildung 1.1: modifizierter Software-Lebenszyklus
Im Gegensatz zur initialen Entwicklung eines Produktes werden Änderungen oftmals ohne zeitaufwändige Analyse oder Konzeptionierung eingefügt (vgl. blauer
Kreislauf in Abbildung 1.1). So wächst eine Applikation und mit jeder Anpassung
steigt die Gefahr, dass das ursprüngliche Design verloren geht. Vergleichbar mit
einem Haus, bei dem Erweiterungen und Anbauten vorgenommen werden, die nicht
ohne weiteres ins Gesamtkonzept passen [MCo07, S. 17], fängt die bestehende Architektur an, mehr und mehr zu „verfallen“. Was bei einem Haus schnell offensichtlich wird, ist in der Softwareentwicklung oftmals ein schleichender Prozess, der erst
sehr spät wahrgenommen wird. Analysen, Fehlerbehebung und Erweiterungen
werden erschwert, weil der Quelltext immer unverständlicher und undurchsichtiger
wird und irgendwann einem „Flickenteppich“ gleicht. Dies hat unmittelbar Auswirkung auf die Qualität der Software. Werden keine Gegenmaßnahmen ergriffen,
muss das Produkt im Extremfall von Grund auf neu programmiert werden. Ist dies
nicht mehr möglich, wird die Entwicklung eingestellt [MCo07, S. 575 ff.]. Um diesem schleichenden Niedergang entgegenzuwirken, fordert die Initiative Clean Code
Developer1 ein „pro-aktives Handeln“. Quelltexte sollten rechtzeitig überprüft und,
falls nötig, überarbeitet werden, um deren Qualität zu gewährleisten oder gar zu
verbessern [CCD].
1
Informationen finden sich unter http://www.clean-code-developer.de/.
Die Initiative beruft sich dabei unter anderem auf [Ma11].
Einleitung
2
Auch Fowler beschreibt bereits 1999 in [Fo11, S.53 ff.] die Notwendigkeit zur regelmäßigen Verbesserung der bestehenden Quelltextstruktur. Mithilfe von rein strukturellen Änderungen sollen Quelltexte leichter les-, wart- und erweiterbar gemacht
werden, um damit die Qualität in der Programmierung zu erhöhen. Er empfiehlt
Refaktorisierung als grundlegende Technik. Das Ziel dabei ist, „ein Softwaresystem
so zu verändern, dass das externe Verhalten nicht verändert wird, es jedoch eine
bessere interne Struktur erhält“ [Fo11, S. XVi].
Westphal schreibt, ohne ständige Code-Pflege würde die Qualität von Quelltexten
mit zunehmender Entwicklung für gewöhnlich abnehmen, mit der Gefahr, dass jegliche Weiterentwicklung be- oder sogar verhindert wird. Als einzig existierende Gegenmaßnahme sieht er stetiges Refaktorisieren [We06, S. 3]. Es ist daher sinnvoll,
diese fest in den täglichen Arbeitsablauf zu integrieren, um so die Qualität dauerhaft zu erhalten oder sogar zu verbessern [CCD]. Damit ist Refaktorisierung nicht
an ein festes Vorgehensmodell gebunden, sondern ein Prozess, der in allen Phasen
der Implementierung unterstützen kann.
Die Entscheidung, welche Stellen nun verbesserungswürdig sind, hängt zunächst
von der subjektiven Einschätzung des jeweiligen Entwicklers ab. Häufig sind es
Stellen im Quelltext, die ohne zusätzliche Informationen oder Kommentare nur
schwer verständlich sind [Ma11, S. 54ff.]. In den letzten Jahren haben sich eine
Reihe von Beschreibungen etabliert, die helfen, solche Stellen zu identifizieren, sogenannte Codesmells. Beispiele sind „Duplication“ oder „Commented Out Code“
[Ma11, S. 285ff.].
Ein weiterer Codesmell „Choose Descriptive Names“ weist auf das Problem von
schlecht gewählten Benamungen hin. Diese und die inkonsistenter Handhabung
von Begriffen2 erschweren das Programmverständnis erheblich. Angesichts dessen,
dass Identifier ca. 70% des gesamten Quelltextes ausmachen [DP06], tragen sie wesentlich zur Lesbarkeit des Programmes bei, was Fowler [Fo11, S.15] wie folgt dokumentiert:
„Guter Code sollte klar kommunizieren was er tut… und Namen sind ein Schlüssel dazu …“.
Je treffender Bezeichner also sind, desto leichter fallen Einarbeitung und Wartung
[DP06, Ma11, S309 ff.]. Nur leider ist die spontane Suche nach einem geeigneten
Namen oftmals schwierig, weshalb nachträgliches Umbenennen eine häufige Refaktorisierung ist.
2
Der „Codesmell“ dazu ist „Same Name Different Meaning“
Online unter: http://c2.com/cgi/wiki?SameNameDifferentMeaning
Problembeschreibung
3
2 Problembeschreibung
Selbst wenn die Notwendigkeit von Strukturverbesserungen gesehen wird, ist die
Hemmschwelle, Änderungen an einem funktionierenden System vorzunehmen, oftmals groß, weil es mit jeder Änderung auch immer zu unerwünschten Nebeneffekten kommen kann. Inwieweit es sinnvoll ist, diese manuell durchzuführen, lässt
sich hinterfragen. Selbst eine vermeintlich einfache Refaktorisierung kann zu einer
aufwändigen Prozedur werden und ist nicht immer risikolos [Fo11, S.15]. Fowler
stellt in [Fo11] eine Reihe von Refaktorisierungsmustern für die manuelle Umstrukturierung von Quelltexten vor.3 Exemplarisch wird in Listing 2.1 anhand des Musters „Rename Method“ gezeigt, welche Einzelschritte von Hand für das Umbenennen
einer Methode durchzuführen sind.
Rename Method
1. Prüfen, ob eine gleichnamige Methode mit derselben Signatur bereits besteht
(inkl. Super-/Subklassen)
2. Eine zusätzliche Methode mit neuem Namen deklarieren und den Inhalt der
alten Methode hineinkopieren
3. Kompilieren
4. Anschließend die neue Methode im Rumpf der alten Methode aufrufen
(bei wenigen Referenzen kann der Punkt übersprungen werden)
5. Kompilieren und Testen
6. Jeweils eine Referenz der alten Methode suchen und auf den neuen Namen
ändern, kompilieren und testen (Schritt 6 für alle Referenzen wiederholen)
7. Alte Methode entfernen, wenn sie kein Teil einer Schnittstelle (interface) ist,
andernfalls kann sie nicht entfernt werden, sondern muss auf deprecated gesetzt werden
8. Kompilieren und Testen
Listing 2.1: Refaktorisierungsbeschreibung „Rename Method“ [Fo11, S. 273 ff.] 4
Das Beispiel zeigt, wie der Umfang dieser vergleichsweise einfachen Refaktorisierung davon abhängt, wie viele Stellen im Programm angepasst werden müssen (vgl.
Listing 2.1, Schritt 6). Dadurch kann diese Refaktorisierung unter Umständen sehr
zeitaufwändig werden. Wird erst zu einem fortgeschrittenen Zeitpunkt bemerkt,
dass die Umstrukturierung doch nicht durchgeführt werden kann, weil es beispielsweise zu einem Namenskonflikt von Methoden kommt, der übersehen wurde,
müssen die bereits geänderten Quelltextfragmente zurückgesetzt werden. Insgesamt ist dies nicht nur fehleranfällig, sondern auch ineffizient.
3
Viele dieser Refaktorisierungsmuster und Zusatzinformationen finden sich auch auf der Internetpräsenz von Martin Fowler http://refactoring.com/catalog/index.html
4
Anmerkung: Der englische Text ist frei übersetzt
Problembeschreibung
4
Für die automatisierte Durchführung einer Refaktorisierung bieten sich spezielle
Werkzeuge an. Diese werden von Entwicklungsumgebungen5, wie Eclipse [Ecl], IntelliJ
IDEA [InJ] oder NetBeans IDE [NeB] bereits seit Jahren integriert [SKP11, Sc10, Wa12].
Allerdings zeigte 2009 eine Umfrage [MPB09], dass Refaktorisierungswerkzeuge
noch nicht in dem Maße genutzt werden, wie es Entwicklern möglich wäre. Laut
den Autoren werden trotz einer Vielzahl verfügbarer Werkzeuge immer noch erstaunlich viele Refaktorisierungen von Hand durchgeführt. Einen Grund, warum
Entwickler den manuellen Weg wählen oder eine „Suchen und Ersetzen“-Funktion
bevorzugen, sehen einige Autoren darin, dass die Werkzeuge zum Teil immer noch
fehlerhaft arbeiten [SKP11, SEM08].
Zu den am häufigsten genutzten Refaktorisierungswerkzeugen gehören solche, die
Programmelemente umbenennen [MPB09, Sc10].
Diese Umstrukturierung ist jedoch nicht immer so einfach, wie es auf den ersten
Blick erscheinen mag. Einen ersten Eindruck soll Listing 2.2 vermitteln. Es zeigt
ein Javaprogramm, in welchem der Name der Klasse B in C umbenannt werden soll.
package a;
import b.B;
class A {
B b1;
B n() {
B b2 = new B();
return b;
}
}
package b;
//umbenennen B -> C
class B {
public B (){
}
void m(B b){
}
}
Listing 2.2 Umbenennen eines Klassennamens
Im Beispiel wird deutlich, dass viele Stellen im Quelltext die Klasse B referenzieren.
Diese müssen beim Umbenennen der Klasse ebenfalls geändert werden. Eine nicht
offensichtliche Voraussetzung ist, dass die Änderung nur durchgeführt werden
kann, wenn es im Paket b nicht bereits eine gleichnamige Klasse C gibt. Dies würde
zu einem Namenskonflikt führen (vgl. Kapitel 4.2).
2.1 Beitrag der Arbeit
Abhängig von der Programmiersprache gibt es für ein Refaktorisierungswerkzeug
eine Vielzahl an Vorbedingungen zu beachten, um sicherzustellen, dass sich das
Verhalten des Programms durch die Umstrukturierung nicht ändert [SKP11, St10].
Sind diese zu schwach, kann es nach der Refaktorisierung zu Übersetzungsfehlern
oder einer Veränderung im Programmverhalten kommen [SMG11]. Lehnt ein
5
Die Entwicklungsumgebungen finden sich unter:
http://www.eclipse.org/,
http://netbeans.org/,
http://www.jetbrains.com/idea/
Problembeschreibung
5
Werkzeug eine Änderung aufgrund zu strenger Prüfungen ab, ist dies zwar weniger
fatal, aber für den Benutzer sehr ärgerlich [SKP11]. Beides ist für die Akzeptanz
eines solchen Werkzeuges nicht hilfreich.
Am Lehrgebiet Programmiersysteme der FernUniversität in Hagen wird aktuell ein
Framework namens REFACOLA (REFActoring COnstraint LAnguage)6 für die Erstellung
von Refaktorisierungswerkzeugen entwickelt, mit dem Ziel korrekte Refaktorisierungen zu erreichen, indem deren Bedingungen korrekt und vollständig beschrieben
werden.
Zielsetzung
Ziel der Arbeit ist es, Vorbedingungen für die Refaktorisierung von Identifiern in
Java-Quelltexten zu identifizieren und in einem Regelwerk in REFACOLA abzubilden.
Im Fokus stehen Möglichkeiten, aber auch Einschränkungen, die sich ausschließlich
auf das Umbenennen von Identifiern im Java-Quelltext beziehen. Ein Problem in
diesem Zusammenhang ist das Umbenennen von Identifiern in reflektiven Ausdrücken (vgl. [TB12]). Eine weitere, spezielle Problematik ergibt sich daraus, dass ein
Identifier nicht nur im Quelltext, sondern auch in anderen Dateien außerhalb des
Quelltextes verwendet werden kann (vgl. [Kl10]). Auf Probleme, die sich bei der
Refaktorisierung aufgrund dieser beider Sachverhalte ergeben, kann im Rahmen
dieser Arbeit nur kurz theoretisch eingegangen werden.
Da sich das REFACOLA-Framework noch in der Entwicklung befindet, sind bei Bedarf
Erweiterungen vorzunehmen. Die Entwicklung setzt auf ein vorhandenes Refaktorisierungswerkzeug für das Umbenennen von Javaelementen auf, welches bereits
über eine Benutzerschnittstelle „Rename“ verfügt. Zum Zeitpunkt der Arbeit basiert
das REFACOLA-Framework auf der Java-Version 1.6 [GJSB05]. Neuere Versionen sind
für diese Arbeit nicht nicht vorgesehen.
Refaktorisierungswerkzeuge zählen, wie auch Compiler und Debugger, zu den Metaprogrammen, weil sie ihre Funktionen direkt auf dem Quelltext eines Programms
ausführen und diesen ändern. Dementsprechend groß ist der Anspruch für den Bau
und Test an ein solches Programm [St10]. Zur Überprüfung der Refaktorisierungsergebnisse beim Umbenennen von Identifiern ist eine Möglichkeit zu schaffen, mit
der die Funktionsweise des Regelwerks getestet werden kann.
2.2 Aufbau der Arbeit
Kapitel 3 fasst wichtige Eigenschaften und Konzepte der Sprache Java zusammen
und führt in die Grundlagen der constraintbasierten Refaktorisierung auf Basis des
REFACOLA-Frameworks ein.
Kapitel 4 erläutert Details der Entwicklung des Regelwerks für das Refaktorisieren
von Identifiern. Ebenso werden Problematiken bei der Definition der Bedingungen
aufgezeigt. Abschließend wird eine Testmöglichkeit für das entwickelte Regelset
vorgestellt.
Kapitel 5 reflektiert Ergebnisse der Arbeit und greift ergänzende Themen auf.
Kapitel 6 fasst abschließend die wichtigsten Punkte zusammen.
6
Siehe auch Webpräsenz: http://www.fernuni-hagen.de/ps/prjs/refacola/
Grundlagen
6
3 Grundlagen
3.1 Die Java-Sprachspezifikation
Damit sich das Verhalten eines Programms bei einer Refaktorisierung nicht ungewollt verändert müssen die syntaktischen und semantischen Vorgaben einer Sprache beachtet werden [SKP11, St10]. Diese geben vor, wie ein Java-Programm strukturiert sein muss, damit es vom Compiler übersetzbar ist. Für Java sind diese in der
Java Language Specification7 [GJSB05] beschrieben, nachfolgend mit JLS abgekürzt.
Wichtige Aspekte der Sprache Java, die für die weiteren Ausführungen als wichtig
erachtet werden, sind in den anschließenden Kapiteln kurz zusammengefasst. Verweise auf die Sprachspezifikation werden jeweils durch ein §-Zeichen gekennzeichnet, gefolgt von der Angabe des zugehörigen Kapitels. So bezieht sich die Angabe
JLS §4.1 auf die Java-Sprachspezifikation Kapitel 4.1.
3.1.1
Allgemeines
Die Deklaration von Javaelementen findet in der Quelltextdatei statt, hier auch als
Compilationunit bezeichnet. Javaelemente können hierarchisch geordnet bzw. ineinander geschachtelt werden, beispielsweise Pakete oder Typen.
Java unterscheidet zwischen primitiven Datentypen, wie boolean oder int, und
Referenztypen (JLS §4). Zu letzteren zählen Klassen, Interfaces und Arrays (JLS
§4.3). Enumerationen sind spezielle Klassen (JLS §8.1). Im Folgenden sollen Klassen, Interfaces und Enumerationen auch unter dem Begriff Typen zusammengefasst
werden.8 Anhand ihrer hierarchischen Ordnung lassen sie sich weiter unterscheiden
in (vgl. [Wa12]):
Ein Topleveltyp wird selbst von keinem anderen Typen umschlossen. In einer Compilationunit kann es mehrere geben (JLS §7.6).
Nestedtypes sind Typen, die innerhalb eines anderen Typen deklariert sind.
Dieser Begriff fasst eine Reihe von spezielleren Typen zusammen, nämlich
Membertypen bzw. innere, lokale oder anonyme Klassen (JLS §8, §9, §14.3,
§15.9.5). Während ein Membertyp auch statisch sein kann, gilt dies für die
anderen nicht.
Innere Klassen sind wie der Name es bereits andeutet, kein Interface und
keine Enumeration (§8.1.3).
Lokale und anonyme Klassen sind spezielle innere Klassen und werden innerhalb von Methoden, Konstruktoren oder Initialisierungsblöcken definiert
(JLS §14.3, §15.9.5). Anonyme Klassen haben selbst keinen Namen. Sie erweitern bei der Erzeugung implizit die Klasse oder das Interface, welches im
Instanziierungsausdruck angegeben ist.
Konstruktoren gleichen vom Aufbau her stark Methoden und können daher an vielen Stellen gleich behandelt werden. Im Wesentlichen unterscheiden sich sich darin,
dass sie den gleichen Namen haben wie der sie deklarierende Typ und über keinen
Rückgabetyp verfügen (JLS §8.8).
7
8
Hier in der 3. Version der Java-Sprachspezifikation (vgl. [GJSB05])
Arrays werden für diese Arbeit in diesem Zusammenhang vernachlässigt
Grundlagen
7
Des Weiteren werden diverse Arten von Variablen unterschieden, nämlich Klassenoder Instanzvariablen, formale Parameter von Methoden oder Konstruktoren und
lokale Variablen (JLS §4.12.3).9
Neben den vielfältigen Möglichkeiten, welche der Sprachumfang von Java an sich
bietet, sind für die Formulierung eines bestimmten Sachverhalts häufig mehrere
Varianten denkbar. Es ist beispielsweise zulässig Deklaration und Initialisierung
getrennt oder in einer Anweisung vorzunehmen. Listing 3.1 zeigt drei Varianten für
die Deklaration und Initialisierung einer Instanzvariablen. Für ein Refaktorisierungswerkzeug müssen alle Varianten berücksichtigt werden.
package a;
public class A {
int x;
x = 0;
int y = 0;
int a=0, b=0;
}
Listing 3.1: Varianten bei der Definition von Variablendeklaration und –initialiserung
3.1.2
Namen in Java
In Java besitzen Programmelemente, wie Pakete, Typen, Variablen, Methoden oder
Typparameter einen Namen.10 Dieser wird bei der Deklaration an das jeweilige
Objekt gebunden, um anhand dessen auf das Programmelement zugreifen zu können (JLS §6). Diese Namen können bei einer Refaktorisierung geändert werden.
Listing 3.2 zeigt die Deklaration für den Typen B. Über den Namen b kann das
Objekt in der Methode m() referenziert werden. Aber auch der Typ der Deklaration,
eine sogenannte Typreferenz, hat einen eigenen Namen, nämlich B.
package a;
public class A {
B b;
void m(){
B newB = b;
}
//deklaration
//referenz auf b
}
Listing 3.2: Deklaration und Referenzierung in Java
In dieser Arbeit werden die Begriffe Identifier, Name und Bezeichner synonym gebraucht.
9
Weitere Variablenarten wie einzelne Array Komponenten und die Parameter der Exception-Handler
können für diese Arbeit vernachlässigt werden.
10
Der Auszug aus der JLS (§6) lautet dazu: „NAMES are used to refer to entities declared in a program. A declared entity (§6.1) is a, class type (normal or enum), interface type (normal or annotation type), member (class, interface, field, or method) of a reference type, type parameter (of a
class, interface, method or constructor) (§4.4), parameter (to a method, constructor, or exception handler), or local variable.“
Grundlagen
8
Aufbau von Namen in Java
Java unterscheidet zwischen einfachen und qualifizierten Namen. Erstere bestehen
lediglich aus einem einzelnen Identifier, qualifizierte Namen hingegen setzen sich
aus mehreren durch „.“ getrennte Identifier zusammen (vgl. Listing 3.3). Ein qualifizierter Name setzt sich aus Paket- und Typnamen zusammen und spiegelt damit
auch die hierarchische Organisation eines Javaelements wieder. Man spricht auch
von vollqualifizierten Namen.
Einfacher Name:
Identifier
Qualifizierter Name:
Identifier { . Identifier }
Listing 3.3: Namensaufbau in Java (JLS §18.1)
Die Angabe eines qualifizierten Namens ist nötig, wenn der einfache Name des
Elements nicht eindeutig ist. Dies ist zum Beispiel der Fall, wenn, wie in Listing 3.4
dargestellt, mehrere gleichnamige Typen in einer Klasse verwendet werden sollen.
package examples;
public class NeedQualifiedName {
a.I i1;
b.I i2;
}
Listing 3.4: Vermeidung Namenskonflikt durch Verwendung qualifizierter Namen
Gültige Identifier und Konventionen
Ein Identifier muss in Java einem bestimmten Aufbau genügen, um gültig zu sein.
Er kann beliebig lang sein und aus Unterstrichen, diversen Zeichen11 und Zahlen
bestehen. Allerdings darf er nicht mit einer Zahl beginnen. Schlüsselwörter12 und
Literale wie true, false oder null sind als Identifier ebenfalls nicht erlaubt (JLS
§3.8). Java unterscheidet Groß- und Kleinschreibung, damit sind aAa und AaA zwei
unterschiedliche Bezeichner.
Zusätzlich zu den syntaktischen und semantischen Vorgaben schlägt die JLS auch
einige Konventionen für die Namensgebung vor (JLS §6.8). Beispielsweise soll der
Name einer Klasse mit einem Großbuchstaben beginnen, während Pakete kleingeschrieben werden sollten. Die Einhaltung ist jedoch nicht verpflichtend.
3.1.3
Wichtige Sprachkonzepte in Java
Weiterhin gibt es eine Reihe von Sprachkonzepten, die Einfluss auf die NamensBindung von Referenzen an eine deklariertes Javaelement hat, oder darauf ob ein
Identifier in einem bestimmten Kontext eindeutig sein muss. Die einzelnen Konzepte stehen zum Teil in engen Abhängigkeiten zueinander und können sich daher beeinflussen.
11
Es gibt einige Ausnahmen für Zeichen mit besonderer Bedeutung, wie Prozent, Gleichheits- oder
Ausrufezeichen, sowie Leerzeichen. Diese sind nicht erlaubt, Umlaute dagegen sehr wohl.
12
Die vollständige Liste findet sich in JLS §3.9, Beispiele sind class, enum oder void
Grundlagen
9
Zugriffsmodifier (JLS §6.6)
Java bietet durch sogenannte Zugriffsmodifier einen Mechanismus, über den der
Zugriff auf Umsetzungsdetails eines Pakets oder eines Typs geregelt werden kann.
Anstatt von Zugreifbarkeit wird auch von Sichtbarkeit gesprochen.13
Die vier Modifier können geordnet werden, dabei ist public weniger restriktiv als
protected.
public > protected > package private14 > private
Javaelemente, welche die Zugreifbarkeit private haben sind nur innerhalb des
Typs, in dem sie deklariert wurden, einschließlich der Nestedtypes zugreifbar. Ist
kein Modifier angegeben wird dies auch als default access oder package private bezeichnet. Javaelemente sind dann nur innerhalb des gleichen Pakets sichtbar.
Haben diese dagegen den Modifier protected, können zusätzlich auch erbende
Elemente darauf zugreifen. Für public gibt es keine Einschränkung.
Die Zugriffskontrolle hat zum Beispiel Auswirkungen auf Vererben, Verbergen oder
Überschreiben von Methoden (JLS §6.6). Die Eindeutigkeit eines Identifiers hängt
unter anderem von dessen Sichtbarkeit ab.
Gültigkeitsbereiche (JLS § 6.3)
Der Gültigkeitsbereich (scope) eines Javaelements ist davon abhängig, wo es deklariert wurde. Innerhalb von diesem Bereich kann es über seinen einfachen Namen
referenziert werden. Abbildung 3.1 illustriert das Prinzip. Der innerste Bereich, hier
eine For-Schleife, beinhaltet eine Variable y, die nur innerhalb des Blocks gültig ist.
Für außerhalb liegende Javaelemente ist sie nicht referenzierbar. Allerdings können Elemente eines innenliegenden Block auf die Elemente in den sie umschließenden Blöcken zugreifen. Daher kann beispielsweise auf die Membervariable x in der
ganzen Klasse zugegriffen werden.
class A{
private int x=0;
void m(){
int y = x;
for(int i=0; i<5; i++){
int x; //abschatten der instanzvariable x
}
}
}
Abbildung 3.1: Gültigkeitsbereiche am Beispiel von Variablen
13
TM
Vgl. Java Tutorial (http://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html)
14
In der Sprachspezifikation für Java wird von default access anstatt von package private
gesprochen (JLS §6.6.1). Zur besseren Unterscheidung der einzelnen Modifier wird in dieser ArTM
beit jedoch der Begriff package private verwendet, wie er auch im Java Tutorial von Oracle
zu finden ist (vgl. http://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html)
Grundlagen
10
Für Identifier hat der Gültigkeitsbereich eines Javaelements zwei Konsequenzen.
Zum einen müssen die Bezeichner innerhalb bestimmter Bereiche eindeutig sein.
Wird diese Eindeutigkeit verletzt, kommt es zu Übersetzungsfehlern im Programm.
Zum anderen können sich gleichnamige Identifier „überdecken“.
Abschatten (Shadowing, JLS §6.3.1)
Gleichnamige Deklarationen können sich abschatten, wenn die eine innerhalb des
Gültigkeitsbereichs der anderen liegt und beide vom gleichen Elementtyp sind. Eine
Methode kann folglich nicht durch eine gleichnamige lokale Variable abgeschattet
werden, eine Instanzvariable von einer lokalen Variable aber sehr wohl (vgl. Abbildung 3.1). Über den einfachen Bezeichner kann nicht mehr auf das abgeschattete
Javaelement zugegriffen werden. Auch Methoden oder Typen können abgeschattet
werden.
package a;
import b.*;
class A {
class B{
void n(){}
}
package b;
public class B{
void xy(){}
}
void m(){
B b = new B();
b.n();
}
}
Listing 3.5: Abschatten einer Typdeklaration
Listing 3.5 zeigt die Abschattung einer Typdeklaration. Im Beispiel gibt es zwei
Klassen, die denselben einfachen Namen haben, aber in unterschiedlichen Paketen
liegen. Die Klasse b.B wird zwar in die Klasse a.A importiert, jedoch von der Deklaration der inneren Klasse a.B abgeschattet.
Abschatten unterscheidet sich außerdem von den Konzepten Verdunkeln und Verbergen. Diese werden in den folgenden Absätzen beschrieben.
Verdunkeln (Obscuring, JLS §6.3.2)
Es ist zulässig, dass eine Variable, ein Typ oder ein Paket denselben einfachen Namen im gleichen Kontext hat. Damit ähnelt Verdunkeln dem Abschatten, nur dass
hier die Javaelemente von unterschiedlichem Elementtyp sind. Für solche Situationen definiert die Java-Sprachspezifikation eine Reihenfolge, wie die Javaelemente
ausgewählt werden. Beispielsweise werden Variablen gegenüber Typen bevorzugt.
Namenskonventionen (JLS §6.8) können helfen, Verdunklung zu vermeiden.
Listing 3.6 zeigt ein Beispiel, bei dem anhand des einfachen Namens nicht mehr auf
das statische Element PI der Klasse java.lang.Math zugegriffen werden kann,
weil es in der Klasse A eine gleichnamige Typdeklaration gibt. Deshalb muss der
Zugriff über den qualifizierten Namen Math.PI erfolgen.
Grundlagen
11
package a;
import static java.lang.Math.PI;
class A {
PI PI = new PI();
double x = PI;
//Typkonflikt, qualifizierter Aufruf nötig
double y = Math.PI;
class PI{}
}
Listing 3.6: Verdunkeln durch eine Typdeklaration
Verbergen (Hiding, JLS §8, §9).
Verbergen tritt in Kombination mit Vererbung auf. Javaelemente eines Supertyps
können durch eine gleichnamige Deklaration aus dem Subtyp verborgen werden,
vorausgesetzt, beide haben den gleichen Elementtyp und eine entsprechende Zugreifbarkeit.15 Verbergen ist möglich bei Klassenmethoden, Instanz- und Klassenvariablen. Bei Instanzmethoden wird nicht von Verbergen, sondern von Überschreiben
gesprochen. Statische Methoden können keine Instanzmethoden verbergen.
Überschreiben (Overriding, JLS §8.4.8)
Eine Instanzmethode überschreibt eine Instanzmethode des Supertyps, wenn diese
die gleiche Signatur besitzt und für den Subtyp sichtbar ist. Sie kann keine statische Methode überschreiben, dies würde zu einem Kompilierungsfehler führen.
Bei Rückgabetypen von überschriebenen Methoden ist eine Besonderheit zu beachten. Bis Java 1.4 musste der Rückgabetyp von Methoden beim Überschreiben genau
übereinstimmen. Seit Java 1.5 sind auch Subtypen des in der Supermethode angegebenen Rückgabetyps erlaubt, sogenannte kovariante Rückgabetypen (JLS § 8.4.5).
Mit dem Schlüsselwort final gekennzeichnete Methoden können ebenso nicht
überschrieben werden, wie Instanzmethoden mit dem Zugriffsmodifier private. Im
zweiten Fall kann aber im Subtyp eine Methode mit gleicher Signatur definiert
werden. Dies gilt gleichermaßen für statische Methoden im Zusammenhang mit
Verbergen.
Überladen (Overload, JLS §8.4.9)
Gibt es mehrere gleichnamige Methoden innerhalb eines Typs oder seines Supertyps, die sich anhand ihrer Signatur unterscheiden, spricht man von Überladen.
Qualifizierung von Namen (JLS §15.8.3, §15.8.4, §15.11.2)
Der einfache Name eines Programmelements kann über die Qualifier this oder
super qualifiziert werden. Um zwischen gleichnamigen überschriebenen oder versteckten Programmelementen in Sub- und Supertyp zu unterscheiden, wird super
verwendet. Eine Unterscheidung innerhalb eines Typs, zum Beispiel von Instanzva-
15
Beispielsweise können Programmelemente im Supertyp mit dem Modifier private nicht verborgen werden, da sie im Subtyp nicht sichtbar sind.
Grundlagen
12
riablen und lokalen Variablen ist mit dem Qualifier this möglich. Bei verschachtelten Klassen kann zur Unterscheidung der verschiedenen Klassen ein qualifiziertes
this verwendet werden. Dies hat die Form ClassName.this.
Statische Javaelemente können über den Namen des Typs, in dem sie deklariert
sind, qualifiziert werden.
Methodenbindung (JLS §15.12)
Konzepte wie Überschrieben bzw. Überladen machen die Methodenbindung in Java
nicht ganz einfach. Zum einen erfolgt die Bindung einer Methode statisch zur Compilezeit. Gibt es in einer Klasse mehrere gleichnamige Methoden, die anhand der
Parameterliste zu einem Aufruf passen könnten, wird die speziellere verwendet
(JLS §15.12.2.5), wie es in Listing 3.7 dargestellt wird. In dem Beispiel wird eine
überladene Methode mit einem Argument i vom Typ int aufgerufen. Die aufzurufende Methode ist daher m(int i), da diese den spezielleren Parametertyp hat.
package a;
public class A {
void m(int i) {} //aufgerufene Methode
void m(float f) {}
public static void main(String [] args){
A a = new A();
int i = 1;
a.m(i);
//methodenaufruf
}
}
Listing 3.7: Methodenbindung
Bei überschriebenen Methoden hängt der Methodenaufruf zusätzlich vom aktuell
geladenen Objekt ab und wird daher dynamisch zur Laufzeit ermittelt. Ausgehend
von diesem wird solange die Vererbungshierarchie aufsteigend jedes Objekt nach
einer passenden Methode durchsucht, bis eine Methode gefunden ist, welche anhand ihrer Signatur für den Aufruf passt (JLS 15.12.4).
Typumwandlung (JLS §5)
Typumwandlungen können in Java explizit oder implizit durchgeführt werden. Erstere können durch eine Cast-Anweisung erzwungen werden. Letztere werden vom
Java-Compiler selbstständig durchgeführt, wenn sie zu keinem Informationsverlust
führen. Ein primitiver Datentyp short kann beispielsweise ohne Probleme in die
Datentypen int, long, float oder double konvertiert werden (JLS §5.1.2).
Seit Java 1.5 gibt es eine spezielle Form der Typwandlung, das Autoboxing (JLS
§5.1.7). Hierbei werden primitive Typen automatisch in deren Wrappertypen gewandelt. Der umgekehrte Weg ist Autounboxing (JLS §5.1.8).
Grundlagen
13
3.2 Constraintbasierte Refaktorisierung mit REFACOLA
Für den Bau von Refaktorisierungswerkzeugen gibt es verschiedene Ansätze. Steimann nennt in [St10] den Ansatz der constraintbasierten Refaktorisierung, auf dem
bereits einige Refaktorisierungswerkzeuge beruhen, als vielversprechend.16 Ein
Framework, welches die Erstellung von constraintbasierten Refaktorisierungswerkzeugen unterstützt, ist REFACOLA (REFActoring COnstraint LAnguage)17. Dieses
wird am Lehrgebiet Programmiersysteme der FernUniversität in Hagen entwickelt.
Damit eine Refaktorisierung zu einem korrekten Ergebnis führt, muss ein Refaktorisierungswerkzeug die „Einschränkungen und spezifischen Regeln“ einer Programmiersprache, wie sie zum Beispiel in der Java-Sprachspezifikation beschrieben
sind, beachten [St10]. Sie bilden die Vorbedingungen für eine Refaktorisierung.
Diese Arbeit soll nun die Definition der Vorbedingungen für das Umbenennen von
Identifiern auf Basis des constraintbasierten Ansatzes diskutieren. Obwohl das REFACOLA-Framework unterschiedliche Programmiersprachen18 unterstützt, wird für
diese Arbeit ausschließlich der Java-spezifische Teil betrachtet.
Die Idee bei der constraintbasierte Programmierung ist, ein Problem deklarativ zu
beschreiben, um die Lösung dann von einem speziellen Programm, dem Constraintlöser, berechnen zu lassen [HW07, S. 53].
In REFACOLA wird dies genutzt, um Bedingungen für eine Refaktorisierung auf einfache Art in einer Constraintregel zu beschreiben. Dafür wird die zum REFACOLAFramework gehörende, gleichnamige, domänenspezifische Sprache verwendet.
Ein Constraint legt „Bedingungen oder Einschränkungen auf Objekten oder Beziehungen zwischen diesen fest“ [HW07, S. 53]. Eine Constraintregel könnte dann
sinngemäß folgendes aussagen: „der deklarierte Name einer Variablen muss mit
dem Referenznamen für die Variablen übereinstimmen“, oder kurz
Variablendeklaration.identifier = Variablenreferenz.identifier.
Durch Anwendung der Constraintregeln auf das zu ändernde Programm können
Constraints erzeugt werden, die im Gesamten ein sogenanntes Constraintsystem
bilden [SKP11]. Dieses kann vom Constraintlöser auf Erfüllbarkeit geprüft werden.
Kann der Constraintlöser für ein Problem keine Lösung finden, bedeutet dies, dass
eine Refaktorisierung nicht durchgeführt werden kann. Es kann aber auch mehrere
Lösungen geben, die dann alle gleichermaßen richtig sind und von denen eine beliebige ausgewählt werden kann.
Aufgrund der oben dargestellten Regel ergibt sich für eine korrekte Änderung, dass
alle Referenznamen geändert werden müssen, wenn der deklarierte Name geändert
wird. Ohne die Regel kommt es zu einem Übersetzungsfehler, wie es in Listing 3.8
dargestellt wird, da nur der Name der Deklaration x geändert wird, nicht aber der
Name der Referenz auf die Variable.
16
Ein anderer Ansatz fasst „Programme als Graphen“ auf. Die notwendigen Bedingungen einer Programmiersprache werden ebenfalls deklarativ als Graph-Transformationsregeln beschrieben
17
Eine detaillierte Darstellung findet sich unter anderem in [SKP11].
18
Beispielsweise werden die Programmiersprachen Java und Eiffel unterstützt vgl. [SKP11]
Grundlagen
14
package a;
public class A {
int x;
int y = x;
}
package a;
public class A {
int xNew;
//ok
int y = x;
//compile fehler
}
Listing 3.8 unvollständige Refaktorisierung
3.2.1
Definition Refaktorisierungsmodule
In REFACOLA benötigt man drei verschiedene Module für die Erstellung eines konkreten Werkzeugs [SKP11]:
eine Sprachdefinition (language specification)
ein Regelset (ruleset) und
eine Refaktorisierungsdefinition (refactoring definition)
Abbildung 3.2: Module eines REFACOLA-Refaktorisierungswerkzeugs
Die Module müssen, damit sie genutzt werden können, vom REFACOLA eigenen
Compiler in Quelltext übersetzt werden. Nachfolgend werden zunächst die einzelnen Komponenten erläutert. Die weiteren Beschreibungen der einzelnen Module
beruhen auf [SKP11].
Die REFACOLA-Sprachdefinition - Java.language.refacola
Um ein korrektes Ergebnis zu erzielen, benötigt ein Refaktorisierungswerkzeug
Informationen über die Programmelemente und ihre spezifischen Gegebenheiten.
Diese werden in der REFACOLA-Sprachdefinition abgebildet und von den Regelsets
und Refaktorisierungsdefinitionen eingebunden. Damit stellt sie sozusagen einen
Baukasten für diese dar. Da sich die aktuelle Sprachdefinition für Java noch in der
Entwicklung befindet, sind noch nicht alle benötigten Elemente vollständig abgebildet. Ein Ausschnitt findet sich in Listing 3.9 und wird kurz beschrieben.
Nach Festlegung der Programmiersprache werden die einzelnen Programmelemente kinds mit ihren Eigenschaften properties beschrieben, wie beispielsweise der
Identifier identifier oder die Zugreifbarkeit accessibility. Beim Refaktorisieren werden eben diese Eigenschaften geändert. Für diese können weiterhin Wertebereiche festgelegt werden. Für Zugriffsmodifier sind dies zum Beispiel die Werte
private, package, protected und public.
Grundlagen
15
language Java
kinds
abstract Entity <: ENTITY
NamedEntity <: Entity{identifier}
LocalVariable <: NamedEntity,TypedEntity
properties
identifier: Identifier
hostPackage: Packages
accessibility: AccessModifierDomain
//Element mit Eigenschaft
//Mehrfachvererbung
// Eigenschaften
domains
//Wertebereiche
AccessModifierDomain = {private, package, protected, public}
queries
mainMethod(mainMethod: StaticMethod)
//Query
Listing 3.9: Ausschnitt REFACOLA-Sprachdefinition für Java (Java.language.refacola)19
kinds können voneinander erben, auch mehrfach. Interessant für die Refaktorisierung von Identifiern sind vor allem die kinds NamedEntity bzw. NamedReference
und alle die von ihnen erben.
Abschließend können noch queries definiert werden, auf diese wird im Folgeabschnitt eingegangen.
Das Regelset - Names.ruleset.refacola
Im Regelset werden die einzelnen Constraintregeln beschrieben. Ziel ist es die
synthaktischen und semantischen Vorgaben einer Programmiersprache vollständig
in Constraintregeln abzubilden. Damit wären korrekte Refaktorisierungen ohne
zusätzliche Sonderfallbehandlungen, die häufig die Ursache für Fehler sind, möglich [St10]. Die Constraintregeln bilden die Basis für die Constraintgenerierung und
haben nach [SKP11] den allgemeinen Aufbau:
programm queries
constraints
Vor der Constraintregel werden zunächst im for all–Block die Regelvariablen
definiert. Diese entsprechen den Programmelementtypen, für welche eine
Constraintregel gelten soll. In dem Beispiel in Listing 3.10 sind dies alle Typen mit
einem Namen (NamedType) und alle Konstruktoren (Constructor) eines Programms.
Anhand einer Query kann die Anzahl der Programmelemente eingegrenzt werden,
welche bei der Constraintprüfung und -generierung berücksichtigt werden müssen.
Queries werden in der Sprachdefinition festgelegt. Ist keine Query vorhanden, werden alle Programmelemente selektiert, die dem Typ der Regelvariablen entsprechen. Der constraint-Teil beschreibt die Bedingungen, welche bei einer Refaktorisie-
19
Einträge im Listing 3.9 wurden aus Platzgründen teilweise modifiziert
Grundlagen
16
rung für die zu ändernde Eigenschaft einzuhalten sind und dient als Vorlage zur
Generierung der Constraints. Um die einzelnen Constraintregeln besser zu strukturieren, sind sie auf mehrere Regelsets aufgeteilt.
import "Java.language.refacola"
ruleset Names
//eindeutiger Name
language Java
rules
ConstructorNames
for all
type: Java.NamedType
//Regelvariablen
constructor: Java.Constructor
do
if
Java.instantiates(constructor, type)
//Query
then
type.identifier = constructor.identifier //Constraint
end
Listing 3.10: Ausschnitt Regelset Names.ruleset.refacola
Die Refaktorisierungsdefinition - RenameMember.refactoring.refacola
Abschließend wird die Refaktorisierungsdefinition beschrieben. In diesem Modul
erfolgt die Festlegung, welche Eigenschaft bei einer Refaktorisierung geändert werden soll. Diese wird unter forced changes angegeben. Beim Umbenennen eines
Programmelements ist dies die Eigenschaft identifier. Allerdings reicht es häufig nicht aus, nur den Bezeichner eines Programmelements zu ändern. Damit das
Programm korrekt bleibt, müssen auch Referenzen auf das Element, beispielsweise
in Importen, angepasst werden [St10]. Diese zusätzlichen Änderungen müssen unter allowed changes explizit für die einzelnen kinds erlaubt werden [SKP11].
Nicht zuletzt müssen die Regelsets angegeben werden, welche für die Refaktorisierung verwendet werden sollen.
import
import
import
import
"Java.language.refacola"
"Accessibility.ruleset.refacola"
"Types.ruleset.refacola"
"Names.ruleset.refacola"
refactoring RenameMember
languages Java
uses Accessibility, Types, Names
forced changes
identifier of Java.NamedEntity as NewName
allowed changes
identifier of Java.NamedReference {initial, NewName @ forced}
identifier of Java.Constructor {initial, NewName @ forced}
Listing 3.11: Ausschnitt Refaktorisierungsdefinition (RenameMember.refactoring.refacola)
Grundlagen
17
Abhängig von der Komplexität einer Refaktorisierung leisten Refaktorisierungswerkzeuge unterschiedlich viele Einzelschritte. Je komplizierter die Umstrukturierung ist, desto mehr Einzelaktionen sind nötig. Vergleicht man Werkzeuge
miteinander, lassen sich einige Parallelen feststellen. Beispielsweise müssen beim
Umbenennen einer Klasse alle Deklarationen oder Importe für den Typ mit angepasst werden. Beim Verschieben eines Typs in ein anderes Paket müssen Paketdeklaration und ebenfalls Importe angepasst werden. Für beide Aktionen ist die Eindeutigkeit von Namen zu prüfen.
REFACOLA löst dies, indem in die einzelnen Refaktorisierungsdefinitionen mehrere
Regelsets eingebunden werden können. Ein Regelset wird folglich nicht speziell für
ein einzelnes Werkzeug definiert, sondern kann von mehreren Werkzeugen genutzt
werden. Die einzelnen Regeldefinitionen ergänzen sich.
3.2.2
Ablauf einer Refaktorisierung in REFACOLA
Um eine Refaktorisierung mit REFACOLA durchzuführen, wird zunächst der komplette Quelltext eines Programms eingelesen. Hierbei werden die einzelnen Programmelemente und deren Eigenschaften, Beziehungen und Abhängigkeiten identifiziert.
REFACOLAAlle
relevanten
Informationen
werden,
entsprechend
der
Sprachdefinition, als Fakten gespeichert.
Anschließend wird geprüft, ob eine vordefinierte Constraintregel die für die zu ändernde Eigenschaft angewendet werden kann. Wenn dem so ist, werden mittels
Queries, diejenigen Fakten ermittelt, die für die Refaktorisierung in Frage kommen.
Sind die Bedingungen erfüllt werden entsprechende Constraints generiert. Ein
Constraintlöser versucht, eine Lösung für das Problem zu berechnen.20 Wird keine
Lösung gefunden, wird der Vorgang abgebrochen. Anderenfalls werden die Änderungsanweisungen erstellt. Anschließend führt eine Rückschreibekomponente die
Änderungen, wie Einfügen, Verschieben, Löschen oder Ändern am bestehenden
Quelltext durch.
3.2.3
Eclipse - Integration
Die aktuelle Version von REFACOLA für Java ist in die Entwicklungsumgebung Eclipse21 integriert [SKP11]. Unter dem Namen Eclipse wird nicht nur eine Entwicklungsumgebung (IDE), sondern ein ganzes „Framework für die Anwendungsentwicklung“ [TH12] bereitgestellt. Zu diesem gehören mehrere Projekte. Eines
davon sind die Java Development Tools (JDT).22 Diese stellen Bibliotheken zu Verfügung, mit denen es möglich ist, bestehenden Java-Quellcode einzulesen und dessen
Elemente in einem Baum darzustellen. Zum einen ist dies das Java Model, zum
anderen der Abstrakte Syntaxbaum (AST). Darüber kann auf einzelne Programmelemente und deren Eigenschaften zugegriffen werden. Dies wiederum nutzt das
Refaktorisierungswerkzeug, um die Elemente zu analysieren oder ändern. Das Java
Model entspricht in etwa der Ansicht im Eclipse Package Explorer bzw. der Outline
und ist nicht so detailliert wie der AST. Um auf Elemente innerhalb von Methoden
zuzugreifen, wie beispielsweise lokale Variablen oder innere Klassen, benötigt man
20
Der Algorithmus zur Ermittlung der Constaints wird in [SKP11] vorgestellt (vgl. Kapitel 4)
Hier Eclipse Juno in der Version 4.2 , unter http://www.eclipse.org/juno/
22
http://www.eclipse.org/jdt/
21
Grundlagen
18
den AST. Die JDT-Bibliotheken werden für die Implementierung von Faktengenerierung und Rückschreibekomponente ebenso eingesetzt wie für die Testerstellung.
Des Weiteren erlaubt es Eclipse dem Entwickler, über seinen Plugin-Mechanismus
die IDE individuell zu erweitern. Dies wird in REFACOLA durch eine Reihe von eigenen Plugins genutzt, unter anderem für Tests.
Implementierung
19
4 Implementierung
4.1
Ausgangssituation
REFACOLA wird seit geraumer Zeit am Lehrstuhl Programmiersysteme der FernUniversität in Hagen entwickelt, deshalb sind eine Vielzahl von Komponenten bereits
zu einem großen Teil vorhanden. Dazu gehören die REFACOLA-Sprachdefinition für
Java, die Faktengenerierung und eine Rückschreibekomponente.
Dagegen waren Regelset und Refaktorisierungsdefinition für das Ändern von Identifiern jeweils nur mit einem Eintrag vorhanden und bildeten damit den Ausgangspunkt für diese Arbeit. Ein Dialog für das Umbenennen von Programmelementen
existierte ebenfalls. Zudem sind in REFACOLA eine Reihe von Test-Plugins vorhanden, die individuell erweitert werden können.
4.2 Das Regelset im Überblick
Die im Laufe dieser Arbeit entstandenen Regeln lassen sich grob in zwei Gruppen
einteilen. Ein Teil stellt die Eindeutigkeit eines Identifiers in dessen Gültigkeitsbereich sicher. Der andere Teil gewährleistet den Erhalt der Bindung von Referenzen an das richtige Programmelement. Das gilt beispielsweise für Methodenaufrufe
oder Referenzen auf Variablen.
In den Constraintregeln werden Bedingungen formuliert, die verhindern sollen,
dass nach einer Refaktorisierung die semantische und syntaktische Vorgaben der
Programmiersprache Java verletzt werden. Darüber hinaus sollen sie für die Erhaltung des ursprünglichen Programmverhaltens sorgen [SKP11].
Einige der im Anschluss vorgestellten Regeln wurden nicht im Rahmen dieser Arbeit entwickelt. Sie werden aber zur Ergänzung mit aufgeführt. Um sie zu unterscheiden, sind sie mit dem Kommentar „Autor: Name“ gekennzeichnet
4.2.1
Allgemeines
Ein wesentlicher Aspekt bei der Entwicklung der Regeln ist, inwieweit die benötigten Komponenten in der REFACOLA-Sprachdefinition vorhanden sind. Gleiches gilt
für Faktengenerierung und Rückschreibekomponente. Nur wenn die Fakten richtig
ermittelt und die Änderungen korrekt in das zu refaktorisierende Programm übertragen werden, kann eine Refaktorisierung erfolgreich durchgeführt werden. Falls
Erweiterungen in diesen Komponenten nötig waren, werden diese mit der jeweiligen Regelbeschreibung vorgestellt.
4.2.2
Eindeutige Namen für Typen, Felder und Methoden
Es gibt in der Java-Sprachspezifikation einige Vorgaben die bestimmen, wann der
einfache Name eines Programmelements in einem bestimmten Kontext eindeutig
sein muss. Ist diese nach einer Refaktorisierung nicht mehr gegeben, führt dies zu
Kompilierungsfehlern.
Da Typen, Felder und Methoden in unterschiedlichen Kontexten verwendet werden
und damit eindeutig zu unterscheiden sind, erlaubt Java hier die Deklaration
gleichnamiger Programmelemente in einem Typen (JLS §8.3). So ist es möglich, in
einer Klasse sowohl eine Methode und eine Variable zu deklarieren, die den gleichen Namen wie sie selbst haben (vgl. Listing 4.1).
Implementierung
20
package a;
public class A{
String A;
void A(){}
}
Listing 4.1: gleichnamige Elemente in einem Typen
Handelt es sich dagegen um gleiche Programmelementtypen, müssen bei der Refaktorisierung Bedingungen für die Eindeutigkeit von Identifiern beachtet werden.
Zunächst soll dies bei Klassen, Enumerationen und Interfaces betrachtet werden,
anschließend werden Felder und Methoden diesbezüglich untersucht.
Eindeutigkeit für Typnamen
Innerhalb desselben Paketes darf es keine zwei gleichnamigen Topleveltypen geben
(JLS §7.6). Dieses wird über die Regel Names.UniqueTopLevelTypeIdentifier23
sichergestellt. Dabei ist es unerheblich, ob die Typen innerhalb der gleichen Compilationunit definiert werden oder nicht. Auch deren Zugreifbarkeit hat keinen Einfluss auf diese Bedingung.
UniqueTopLevelTypeIdentifier
for all
tlt1: Java.TopLevelType
tlt2: Java.TopLevelType
do
if
tlt1 != tlt2
then
(tlt1.hostPackage = tlt2.hostPackage)
-> (tlt1.identifier != tlt2.identifier)
end
Regel 1: Names.UniqueTopLevelTypeIdentifier
Membertypen müssen eindeutige Identifier haben, wenn sie in einem Typen auf
gleicher Ebene deklariert werden. Listing 4.2 zeigt ein Beispiel, in dem in einer
Klasse A zwei gleichnamige Membertypen deklariert sind. Auch wenn es sich hierbei um verschiedene Arten von Typen handelt, ist dies nicht erlaubt (JLS §8.5). Das
Problem wird durch die Regel Names.UniqueMemberTypeNames2 vermieden.
package a;
public class A{
class B{}
interface B{} //compile fehler, Namenskonflikt mit Klasse B
}
Listing 4.2: Verletzung Eindeutigkeit eines Typ-Identifiers
23
Verweise auf Regeln werden immer in der Form Regelsetname.Regelname vorgenommen.
Für Names.UniqueTopLevelTypeIdentifier ist Names der Name des Regelsets in dem die
Regel definiert ist und UniqueTopLevelTypeIdentifier ist der eigentliche Regelname.
Implementierung
21
UniqueMemberTypeNames2
for all
memberType1: Java.MemberType
memberType2: Java.MemberType
do
if
memberType1 != memberType2
then
(memberType1.owner = memberType2.owner)
-> (memberType1.identifier != memberType2.identifier)
end
Regel 2: Names.UniqueMemberTypeNames2
Weiterhin können Typen innerhalb einer Compilationunit beliebig ineinander geschachtelt werden. Dabei darf ein eingeschlossener Typ nicht den gleichen Namen
haben wie ein ihn umschließender Typ, einschließlich des Topleveltyps (JLS §8.1,
§9.1, §8.5). Gleichnamige Membertypen innerhalb eines Typen sind erlaubt, wenn
sie nicht auf gleicher Ebene deklariert und nicht ineinander verschachtelt sind.
Listing 4.3 illustriert die Problematik. In Klasse A gibt es zwei Namenskonflikte die
jeweils zu Übersetzungsproblemen führen. Zum einen haben zwei Membertypen
denselben Namen B, zum anderen hat das Interface A den gleichen Namen wie der
Topleveltyp. Beides ist nicht erlaubt.
package a;
public class A {
class B {
class B {}
//namenskonflikt mit Membertyp B
}
class C {
interface A {} //namenskonflikt mit Topleveltyp A
}
}
Listing 4.3: eindeutige Namen bei verschachtelten Typen
Damit die Überprüfung der Namen von umschließenden Typen möglich ist, müssen
diese während der Faktengenerierung von REFACOLA für jeden Membertyp ermittelt
werden. Gespeichert werden sie dann in der Sequenz enclosingNamedTypes.
UniqueMemberTypeNames1 /* Autor: A.Thies */
for all
t: Java.MemberType
do
if
all(t)
then
t.identifier != t.enclosingNamedTypes.identifier, //1.Constraint
t.identifier != t.tlowner.identifier
//2.Constraint
end
Regel 3: Names.UniqueMemberTypeNames1
Implementierung
22
Bei der Regel Names.UniqueMemberTypeNames1 fällt auf, dass diese zwei Constraints abbildet, nämlich zum einen die Prüfung der eindeutigen Namen für die
umschließenden Typen und zum anderen die Prüfung für den Topleveltypnamen, da
enclosingNamedTypes den Namen des Topleveltypen nicht enthält. Wenn Regeln
über dieselben Regelvariablen verfügen und auch die Queries gleich sind, können
diese in einer Regel mit mehreren Constaints zusammengefasst werden. Alternativ
könnten aber auch zwei einzelne Regeln erstellt werden.
Obwohl Java eine sogenannte casesensitive Sprache ist, darf bei Namen von Topleveltypen eines Paketes ausnahmsweise nicht zwischen Groß- und Kleinschreibung
unterschieden werden. Dies aber nur indirekt ein Problem gleichnamiger Typnamen. Das eigentliche Problem ist der Name der Compilationunit. Die Problematik
wird in Kapitel 4.3.3 näher erläutert.
Eindeutigkeit für Felder und Methoden
Innerhalb eines Typen dürfen keine gleichnamigen Felder definiert sein, der Typ
der Deklaration spielt dabei keine Rolle (JLS §8.3). Dies wird über die Regel Names.UniqueFieldIdentifier sichergestellt. Methoden können nur dann denselben Namen haben, wenn sich ihre Signaturen unterscheiden (JLS §8.4.2). Dafür
sorgt die Regel Names.UniqueMethodIdentifier.
UniqueFieldIdentifier
for all
field1: Java.Field
field2: Java.Field
do
if
field1 != field2
then
field1.owner = field2.owner
->(field1.identifier != field2.identifier)
end
Regel 4: Names.UniqueFieldIdentifier
UniqueMethodIdentifier
for all
method1: Java.RegularTypedMethod
method2: Java.RegularTypedMethod
do
if
method1 != method2
then
(method1.owner = method2.owner)
and (method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType)
-> (method1.identifier != method2.identifier)
end
Regel 5: Names.UniqueMethodIdentifier
Implementierung
4.2.3
23
Namensgleichheit von Typ und Konstruktor
Wird in einer Klasse oder einer Enumeration ein Konstruktor deklariert, muss der
einfache Name eines Konstruktors mit dem einfachen Namen des ihn definierenden
Typen übereinstimmen (JLS §8.8, §8.9). Interfaces besitzen keine Konstruktoren.
Ein Konstruktor wird bei der Instanzierung eines Objekts aufgerufen. Die hier verwendete Query Java.instantiates ermittelt hierfür alle Fakten aus dem eingelesenen Programm.
ConstructorNames
for all
type: Java.NamedType
constructor: Java.Constructor
do
if
Java.instantiates(constructor, type)
then
type.identifier = constructor.identifier
end
Regel 6: Names.ConstructorNames
Bei der Entwicklung der Regeln fiel das in Listing 4.4 dargestellte Problem auf.
Konstruktor und Methode haben hier den gleichen Namen. Der Versuch, die Methode A umzubenennen, führt aufgrund eines Fehlers in den JDT-Bibliotheken zu
Problemen, da beim Ermitteln der Methode fälschlicherweise der Konstruktor zurückgeliefert wird.24
package a;
public class A{
A(){}
void A(){}
}
Listing 4.4: gleichnamige Konstruktor- und Methodendeklaration in einer Klasse
4.2.4
Namensgleichheit von Topleveltyp und Quelltextdatei
Ein Topleveltyp kann die Zugreifbarkeit public oder package private haben,
andere Zugreifbarkeiten sind für Topleveltypen nicht erlaubt (JLS §7.6). Hat ein
Topleveltyp die Zugreifbarkeit public, muss der Typname mit dem der Quelltextdatei übereinstimmen. Dies wird über die Regel Names.TopLevelTypeNames sichergestellt. Innerhalb einer Compilationunit darf es nur einen Typen mit dieser
Zugreifbarkeit geben. Ist diese mit package private angegeben, darf sich der Name der Datei unterscheiden. (vgl. JLS §7.6).
24
Da die Ursache in den JDT-Bibliotheken liegt, hat das Rename-Werkzeug von Eclipse dasselbe
Problem. Ein entsprechender Fehler wurde erfasst (vgl. EclipseBug 399476).
Implementierung
24
TopLevelTypeNames
for all
tlt: Java.TopLevelType
tr: Java.TypeRoot
do
if
all(tlt), all(tr)
then
(tlt.typeRoot = tr and tlt.accessibility = #public)
-> (tlt.identifier = tr.identifier)
End
Regel 7: Names.TopLevelTypeNames
4.2.5
Referenzen
Bei einem Großteil der Stellen, die beim Umbenennen eines Identifiers zu prüfen
sind, handelt es sich um Referenzen. Zu unterscheiden sind zum einen Referenzen,
die dem Zugriff auf ein Element dienen und deren Name bei der Deklaration festgelegt wurde. Zum anderen sind es Referenzen auf einen Typ selbst.
Typreferenz
Bei einer Typreferenz muss der Referenzname mit dem Namen des Typs übereinstimmen, dies wird über die Regel Names.TypeReferenceIdentifier sichergestellt.
Zu den Typreferenzen gehören:
Typangaben einer Deklaration
Typangaben in implements-, extends-, throws-Klauseln
Typangaben in Cast- oder instanceOf-Expressions
Typangaben für den Returntyp einer Methode
Typangaben für formale Parameter in Methoden und Konstruktoren
Listing 4.5 zeigt eine Typreferenz der Klasse B in der extends-Klausel der Klasse A.
Wird nun beispielsweise die Klasse B umbenannt, muss auch diese Typreferenz
umbenannt werden.
package a;
public class A extends B {
package a;
public class B{
}
}
Listing 4.5: Typreferenz in extends-Klausel
Für die Auswahl der Programmelemente gibt es hier keine spezielle Query, daher
werden hier alle Typen und allen Referenzen des eingelesenen Programms ausgewählt. Im Rahmen dieser Arbeit wurde die Ermittlung von einigen Typreferenzen in
der Faktengenerierung ergänzt, beispielsweise für Parametertypen oder throwsKlauseln.
Implementierung
25
TypeReferenceIdentifier
for all
type: Java.NamedType
ref: Java.TypeReference
do
if
all(type),
all(ref)
then
(type = ref.typeBinding)
-> (type.identifier = ref.identifier)
end
Regel 8: Names.TypeReferenceIdentifier
Referenz einer Deklaration
Auch für den Bezeichner einer Deklaration muss sichergestellt werden, dass nach
einer Refaktorisierung dessen Referenzen noch namensgleich sind. Ansonsten geht
die Bindung an das deklarierte Programmelement verloren. Dies gewährleistet die
Regel Names.ReferenceIdentifier. Betroffen sind neben Deklarationen von Variablen und formalen Parametern auch Methoden und Konstruktoren.
ReferenceIdentifier /* Autor: A.Thies */
for all
reference: Java.NamedTypedReference
entity: Java.NamedEntity
do
if
Java.binds(reference, entity)
then
reference.identifier = entity.identifier
end
Regel 9: Names.ReferenceIdentifier
4.2.6
Überschreiben und Verbergen von Methoden
Die Regel Names.OverridingMethodNames sorgt dafür, dass der Name einer
überschiebenen Methode im Supertyp mit der des Subtyps übereinstimmt. Sie kann
sehr einfach gehalten werden, weil die Ermittlung überschriebener Methoden über
die Query Java.overrides erfolgt. Daher ist es unnötig, beispielsweise explizit zu
prüfen, ob die Parameterlisten der beiden Methoden übereinstimmen oder der
Rückgabewert zulässig ist.
Implementierung
26
OverridingMethodNames
for all
superMethod: Java.InstanceMethod
subMethod: Java.InstanceMethod
do
if
Java.overrides(subMethod, superMethod)
then
subMethod.identifier = superMethod.identifier
end
Regel 10: Names.OverridingMethodNames
Für ein Refaktorisierungswerkzeug ist es wichtig, dass sich bei der Änderung eines
Methodennamens die Methodenbindungen nicht verändern, ansonsten kann dies zu
einem veränderten Programmverhalten führen. In Listing 4.6 darf daher die Methode n() nicht in m() umbenannt werden, weil ansonsten die Methode der Superklasse überschrieben wird und somit die Methode m() der Klasse C aufgerufen wird
anstatt der Methode in Klasse A.
package a;
public class A{
class B{
int m(){return 0;}
}
class C extends B{
int n(){
//umbennenen -> m nicht erlaubt ...
return 1;
}
void getM(){
int myM = m(); //...sonst Änderung der Methodenbindung
}
}
}
Listing 4.6: Überschreiben einer Methode durch Umbenennen
Daher ist vor der Umstrukturierung ist zu prüfen, ob eine Methode im Vorfeld bereits überschrieben wurde oder nicht. Die Refaktorisierung darf an diesem Umstand
nichts ändern.
Für das Überschreiben von Methoden sind lt. JLS folgende Vorgaben zu beachten
Ein Überschreiben von Methoden ist nur für Instanzmethoden zulässig
(JLS §8.4.8.1).
Eine Instanzmethode darf keine statische Methode überschreiben und umgekehrt (JLS §8.4.8.1, §8.4.8.2).
Implementierung
27
Das Regelset Accessibility25 enthält hierfür bereits entsprechende Regeln. Die
erste Vorgabe wird durch die Regel Accessibility.AccidentalOverriding
umgesetzt. Des Weiteren gibt es für die zweite Vorgabe die Regeln Accessibility.InstanceMethodOverridingStaticMethod und Accessibility.StaticMethodHidesInstanceMethod.26
Exemplarisch wird Accessibility.AccidentalOverriding vorgestellt, auf
eine detaillierte Darstellung der anderen Regeln wird jedoch verzichtet.
Die Regel Accessibility.AccidentalOverriding gibt einen guten Eindruck,
dass die Formulierung eines Constraints durchaus kompliziert werden kann. Hier
wird eine ganze Reihe an Bedingungen in dem Constraint formuliert, die alle erfüllt
sein müssen. So ist zum Beispiel eine Bedingung, dass die beiden Methoden für die
die Regel gelten soll, sich bereits überschreiben, oder dass es keine Vererbungsbeziehung zwischen den Methodenbesitzern (beispielsweise Klassen) gibt, oder die
Bezeichner der Methoden unterschiedlich sind, usw.
AccidentalOverriding /* Autor: A.Thies */
for all
superMethod: Java.InstanceMethod
subMethod: Java.InstanceMethod
do
if
all (subMethod), all(superMethod)
then
Java.overrides(subMethod, superMethod)
or (not Java.sub(subMethod.owner, superMethod.owner))
or (superMethod.identifier != subMethod.identifier)
or (not (subMethod.parameters.declaredParameterType
= superMethod.parameters.declaredParameterType))
or (superMethod.accessibility = #private)
or ((superMethod.accessibility = #package)
and (superMethod.hostPackage != subMethod.hostPackage))
end
Regel 11: Accessibility.AccidentalOverriding
Alle vorgestellten Regeln behandeln allerdings noch nicht den Fall, dass es sich bei
einer Methode im Supertyp um eine finale Methode handelt. Daher wird zusätzlich
die Regel Names.AccidentalHidingFinalMethod benötigt. Die Regel gilt sowohl
für statische als auch für Instanzmethoden, weil eine finale Methode weder überschrieben noch versteckt werden darf (JLS §8.4.3.3).
Soll die Methode n() der Klasse B im Listing 4.7 nach m() umbenannt werden,
kommt es zu einem Kompilierungsfehler, weil dies eine finale Methode der Superklasse verbergen würde.27
25
Regeln aus diesem Regelset wurden bereits vor dieser Arbeit erstellt (Autor A.Thies).
26
Es gibt weitere Constraintregeln im Regelset Accessibility welche das unerwünschte Überladen von Methoden verhindern. Auf eine detaillierte Darstellung wird hier verzichtet.
27
Interessanterweise lautet die Kompilermeldung von Sun „cannot override m“ obwohl es sich um
eine statische Methode handelt lt. JLS wird eigentlich von Hiding gesprochen.
Implementierung
28
package a;
public class A {
static final void m(){}
}
package a;
public class B extends A {
static void m(){} //compilefehler
}
Listing 4.7: Methode mit final-Modifier
Für die Ermittlung von Methoden mit dem Modifier final wurde zusätzlich die
Sprachdefinition in REFACOLA um die Query Java.finalMethod erweitert.
AccidentalHidingFinalMethod
for all
method1: Java.Method
method2: Java.Method
do
if
Java.finalMethod(method2), all(method1)
then
(method1.identifier != method2.identifier)
or (not Java.sub(method1.owner, method2.owner))
or (not(method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType))
or ((method1.tlowner != method2.tlowner)
and (method2.accessibility = #private))
or ((method1.hostPackage != method2.hostPackage)
and (method2.accessibility <= #package))
end
Regel 12: Names.AccidentalHidingFinalMethod
Konstruktoren sind von den vorgestellten Problematiken nicht betroffen, weil sie
weder überschrieben noch versteckt werden können (JLS §8.8).
4.2.7
Eager Interface
Supertyp
m()
<<interface>>
Subtyp
<<realize>>
EagerIF
m()
Abbildung 4.1: Eager Interface
Die Einbindung eines Interfaces in eine nicht abstrakte Klasse verlangt, dass von
dieser alle in dem Interface deklarierten Methoden implementiert (JLS §8.1.5). Es
wird jedoch nicht vorgeschrieben, ob sie dieses selbst tut. Ebenso kann eine Superklasse diese Aufgabe übernehmen, unabhängig davon, ob sie selbst das Interface
Implementierung
29
einbindet (vgl. Abb. 4.1). Zudem muss die implementierende Methode die Zugreifbarkeit public haben, weil alle Methoden eines Interface implizit public sind.
Soll nun der Name der Methode im Interface geändert werden, muss sichergestellt
werden, dass auch der Name der implementierenden Methode angepasst wird. Ansonsten käme es nach der Refaktorisierung zu einem Kompilierungsfehler. Dies gilt
auch im umgekehrten Fall, wenn der Name der Methode im Supertyp geändert
wird.
EagerInterfaceMethodNames
for all
type: Java.Class
typeMethod: Java.InstanceMethod
interfaceMethod: Java.InstanceMethod
do
if
Java.eagerInterfaceImplementation(type,
typeMethod, interfaceMethod)
then
typeMethod.identifier = interfaceMethod.identifier
end
Regel 13: Names.EagerInterfaceMethodNames
Für die Regel Names.EagerInterfaceMethodNames, welche die oben beschriebenen Bedingungen umsetzt, werden die Fakten anhand der Query Java.eagerInterfaceImplementation nach solchen Programmkonstellationen
durchsucht. Diese war bereits vorhanden, musste aber um den Parameter interfaceMethod erweitert.
4.2.8
Eindeutige Namen für lokale Variablen und formale Parameter
Aus Gründen der Lesbarkeit gelten die folgenden Ausführungen für Methoden
ebenso für Konstruktoren. Auf Besonderheiten oder Unterschiede wird explizit hingewiesen.
Innerhalb des Deklarationsbereichs einer Methode müssen die darin definierten
lokalen Variablen eindeutige Namen haben.
Formale Parameter und lokale Variablen
Obwohl formale Parameter auch als spezielle lokale Variablen gelten können, wird
hier für die Regeldefinition zwischen beiden unterschieden. Die formalen Parameter
einer Methode müssen innerhalb ihrer Parameterliste eindeutige Bezeichner haben
(JLS § 8.4.1).
Dies drückt sich in der Constraintregel Names.UniqueLocalVariableNames1
aus. UniqueLocalVariableNames2 stellt dagegen sicher, dass keine lokale Variable namensgleich mit einem der formalen Parameter einer Methode ist.
Um abfragen zu können, ob eine lokale Variable oder ein formaler Parameter zur
gleichen Methode gehört, wurde eine neue Query in die Sprachdefinition von REFACOLA aufgenommen nämlich Java.declaresLocalVariableOrParameter.
Implementierung
30
UniqueLocalVariableNames1
for all
parameter1: Java.FormalParameter
parameter2: Java.FormalParameter
method:
Java.MethodOrConstructor
do
if
Java.declaresLocalVariableOrParameter(method, parameter1),
Java.declaresLocalVariableOrParameter(method, parameter2),
(parameter1 != parameter2)
then
(parameter1.identifier != parameter2.identifier)
end
Regel 14: Names.UniqueLocalVariableNames1
UniqueLocalVariableNames2
for all
parameter:
Java.FormalParameter
localVariable: Java.LocalVariable
methodOrConstructor: Java.MethodOrConstructor
do
if
Java.declaresLocalVariableOrParameter(methodOrConstructor,
parameter),
Java.declaresLocalVariableOrParameter(methodOrConstructor,
localVariable)
then
(parameter.identifier != localVariable.identifier)
end
Regel 15: Names.UniqueLocalVariableNames2
Locale Variablen in Anweisungsblöcken
Initialisierungsblöcke besitzen keine formalen Parameter, daher finden sie für die
ersten beiden Regeln keine Berücksichtigung. Anders ist dies für die nächste Regel
Names.UniqueLocalVariableNames3. Weil innerhalb einer Methode verschiedene
Anweisungsblöcke deklariert werden können, muss die hierarchische Ordnung der
einzelnen Variablen beachtet werden es, wie es bei verschachtelten Typen der Fall
war (vgl. Kapitel 4.2.2). Auch in verschachtelten Anweisungsblöcken dürfen lokale
Variablen nicht den gleichen Identifier besitzen.
Implementierung
31
UniqueLocalVariableNames3
for all
localVariable: Java.LocalVariable
do
if
Java.hasEnclosingVariables(localVariable)
then
localVariable.identifier !=
localVariable.enclosingLocalVariables.identifier
end
Regel 16: Names.UniqueLocalVariableNames3
Allerdings ist die Ermittlung von umschließenden Blöcken nicht einfach, da es eine
Vielzahl von verschiedenen Anweisungsblöcken gibt, in denen lokale Variablen deklariert werden können. Da diese bei der Ermittlung jeweils eigens behandelt werden müssen, ist dies komplizierter als für Typen. Eine entsprechende Funktion fehlte in REFACOLA. Es wurde zunächst eine prototypische Implementierung vorgenommen, die in Kapitel 5.1.1 zur Diskussion gestellt wird. Außerdem wurde eine Query
Java.hasEnclosingVariables eingeführt. Sie prüft, obe es für eine lokale Variable überhaupt Variablen aus umschließenden Blöcken gibt. Lokale Klassen wurden
noch nicht berücksichtigt.
4.2.9
Pakete und Importanweisungen
Die Paketdeklaration einer Compilationunit muss mit dem Paketnamen übereinstimmen, in welchem sie abgelegt ist (JLS §7.4.1). Liegt sie im default-Paket gibt
es keine Paketdeklaration. Wird ein Typ von einem Paket a in ein Paket b verschoben, muss auch die Paketdeklaration angepasst werden. Dieses wird durch die Regel Names.PackageDeclarationNames sichergestellt.
PackageDeclarationNames
/* Autor: A.Thies */
for all
packageDeclaration: Java.PackageDeclaration
typeRoot: Java.TypeRoot
do
if
Java.packageDeclarationInTypeRoot(
packageDeclaration, typeRoot)
then
packageDeclaration.identifier =
typeRoot.hostPackage.identifier
end
Regel 17: Names.PackageDeclarationNames
Importe wurden in REFACOLA bisher weder in der Sprachspezifikation noch in Faktengenerierung oder Rückschreibekomponente unterstützt. Für einen ersten Schritt
wurden diese Komponenten insoweit erweitert, dass damit das Ändern für einen
konkreten Typ in einem Single-Type-Import (JLS §7.5.1) möglich ist.
import examples.refacola.BeispielTyp;
Implementierung
32
Über die Regel Names.ImportDeclarationNames1 wird sicherstellt, dass der Name eines verwendeten Typs mit dessen Bezeichnung in der Importanweisung übereinstimmt.
ImportDeclarationNames1
for all
importDeclaration: Java.ImportDeclaration
clazz: Java.NamedType
do
if
Java.importDeclarationInTypeRoot(importDeclaration, clazz)
then
(importDeclaration.typeBinding = clazz)
-> (importDeclaration.identifier = clazz.identifier)
end
Regel 18: Names.ImportDeclarationNames1
Des Weiteren darf der Name eines Topleveltypen nicht mit dem Typnamen in einem
Single-Type-Import übereinstimmen, wenn der Topleveltypen in einer anderen
Compilationunit deklariert wurde (JLS §6). Listing 4.8 zeigt eine Klasse, die einen
Kompilefehler aufweist, da sie einen gleichnamigen Typ aus einem anderen Pakt
importiert
package a;
import b.A; //compilefehler
public class A {
}
package b;
public class A {
}
Listing 4.8: gleichnamiger Topleveltyp und Single-Type-Import
Die Regel Names.ImportDeclarationNames2 setzt diese Vorbedingung um.
ImportDeclarationNames2
for all
importDeclaration: Java.ImportDeclaration
tlt: Java.TopLevelType
do
if
all(importDeclaration), all(tlt)
then
(importDeclaration.typeBinding != tlt)
-> (importDeclaration.identifier != tlt.identifier)
end
Regel 19: Names.ImportDeclarationNames2
Aufgrund der zeitlichen Begrenzung und fehlender Komponenten konnte die Thematik rund um Pakete und Importe an dieser Stelle jedoch nicht weiterverfolgt
werden. Die Unterstützung für statische Importe fehlt daher genauso wie das Ändern einzelner Fragmente einer Import- oder Paketanweisung.
Implementierung
4.3
33
Einschränkungen bei der Regeldefinition
Nicht alle Bedingungen für eine korrekte Refaktorisierung von Identifiern konnten
über das Regelwerk vollständig sichergestellt werden. Die Problematiken werden in
diesem Kapitel erläutert. Zudem werden Themen aufgeführt, die aus zeitlichen
Gründen nicht umgesetzt werden konnten.
4.3.1
Ergänzung von Qualifiern
Konzepte wie Abschatten, Verbergen oder Überschreiben machen es möglich, dass
namensgleiche Deklarationen sich „überlagern“. Probleme, die dabei entstehen,
werden exemplarisch für die Verschattung einer Instanzvariablen gezeigt.
In Listing 4.9 wird die Instanzvariable i der Toplevelklasse A der Instanzvariablen
j in der inneren Klasse B zugewiesen. Wird nun die Instanzvariable i nach j umbenannt, wird sie durch die gleichnamige Variable der inneren Klasse abgeschattet.
Dadurch es kommt zu einem Kompilierungsfehler, da nun die Variable j sich selbst
zugewiesen wird, bevor sie initialisiert ist.
package a;
public class A{
private int i;
class B{
int j=i;
}
}
package a;
public class A{
private int j;
class B{
int j=j; //compile fehler
}
}
Listing 4.9: Abschattung einer Instanzvariablen nach Refaktorisierung I
Im Gegensatz zu Listing 4.9, wo es sich um eine initiale Zuweisung handelt, wurde
in Listing 4.10 die lokale Variable j der Klasse B bereits im Vorfeld initialisiert.
Hier kommt es zu keinem Kompilierungsfehler. Jedoch hat sich unerwünschterweise der Ablauf in dem Programm verändert, was sich unter Umständen erst zur
Laufzeit bemerkbar macht.
package a;
public class A{
private int i;
class B{
int j=0;
j = i;
}
}
package a;
public class A{
private int j;
class B{
int j=0;
j = j; //geändertes Laufzeit//verhalten möglich
}
}
Listing 4.10: Abschattung einer Instanzvariablen nach Refaktorisierung II
Um die oben beschriebenen Fehler zu vermeiden, wurden die beiden Regeln Names.UniqueEnclosingMemberTypeNamesInAssignment1 und Names.UniqueEnclosingMemberTypeNamesInAssignment2 erstellt. Sie stellen für verschachtelte Typen sicher, dass Instanzvariablen, welche in verschiedenen Typen deklariert
sind, in Zuweisungen nicht namensgleich sein dürfen. Um differenzieren zu können,
ob es sich, wie in Listing 4.9, um eine initiale Zuweisung handelt, kann anhand der
Implementierung
34
Query Java.initialAssignment abgefragt werden. Im anderen Fall wird
Java.assignment verwendet.
UniqueEnclosingMemberTypeNamesInAssignment1
for all
field1: Java.InstanceField
field2: Java.RegularTypedInstanceField
fieldReference: Java.FieldReference
expression: Java.Expression
do
if
field1 != field2,
Java.binds(fieldReference, field1),
Java.isExpression(fieldReference, expression),
Java.initialAssignment(field2, expression)
then
field1.tlowner = field2.tlowner
and field1.owner != field2.owner
and Java.sub!(field2.owner,field1.owner)
->fieldReference.identifier != field2.identifier
end
Regel 20: Names.UniqueEnclosingMemberTypeNamesInAssignment1
UniqueEnclosingMemberTypeNamesInAssignment2
for all
field1: Java.InstanceField
field2: Java.InstanceField
fieldReference1: Java.FieldReference
fieldReference2: Java.FieldReference
expression1: Java.Expression
expression2: Java.Expression
do
if
field1 != field2,
Java.binds(fieldReference1, field1),
Java.binds(fieldReference2, field2),
Java.isExpression(fieldReference1, expression1),
Java.isExpression(fieldReference2, expression2),
Java.assignment(expression2, expression1)
then
field1.tlowner = field2.tlowner
and field1.owner != field2.owner
and Java.sub!(field2.owner,field1.owner)
->fieldReference2.identifier != fieldReference1.identifier
end
Regel 21: Names.UniqueEnclosingMemberTypeNamesInAssignment2
Implementierung
35
Problematik
Leider verhindern die Regeln auch ein Umbenennen, wenn es sich im Zuweisungsausdruck um eine qualifizierte Namensangabe handelt (vgl. Listing 4.11). Dies ist
unnötig und bedeutet eine stärkere Restriktion als gewollt, da hier eine eindeutige
Unterscheidung der verschiedenen Variablen eigentlich problemlos möglich ist.
package a;
public class A{
private int j;
class B{
A a = new A();
int j=a.j;
}
}
Listing 4.11: Qualifizierter Referenzname in Zuweisung
Ein Problem ist, dass die beiden Regeln nur einen speziellen Fall behandeln. Es gibt
allerdings eine Vielzahl ähnlicher Probleme. Um alle abzubilden, wären zusätzliche
Regeln nötig. Handelt es sich beispielsweise statt einer einfachen Variablenzuweisung um einen geschachtelten Ausdruck, wird dies durch die Regeln nicht mit abgedeckt (vgl. Listing 4.12).
package a;
public class A{
private int i;
class B{
int j=i;
}
}
package a;
public class A{
private int j;
class B{
int j= new Integer(j); //compile
//fehler
}
}
Listing 4.12: geschachtelte Expression in Zuweisung
Auch in dem Beispiel in Listing 4.13 kommt es zu einem Kompilierungsfehler nachdem die lokale Variable y und Instanzvariablen x nach dem Umbenennen des Feldes denselben Namen haben.
package a;
package a;
public class A {
int x = 0;
public class A {
int y = 0;
void m() {
int y = x;
}
}
void m() {
int y = y; //compile fehler
}
}
Listing 4.13. Problematik von fehlendem Qualifier bei lokaler Variable
Obwohl es sich in allen Fällen um die Verschattung einer Instanzvariable handelt,
die durch eine andere Variable in einer Zuweisung abgeschattet wird, hat jeder Fall
eigene Bedingungen, die letztlich in eigenen Regeln abgebildet werden müssten. Im
ersten Fall sind die Bedingungen der vorhandenen Regeln sogar zu restriktiv.
Implementierung
36
Ergänzung Qualifier im Quelltext
Eine einfache Lösung für die Problematik bietet Java durch das Einfügen des Qualifiers this an, wie es in Listing 4.14 für das Beispiel aus Listing 4.13 dargestellt ist.
Trotz identischer Namen ist jetzt eine Unterscheidung zwischen lokaler und Instanzvariable möglich und das Programm funktioniert auch nach wie vor korrekt.28
Der Qualifier müsste jedoch aktiv beim Refaktorisierungsvorgang ergänzt werden.
Eine Möglichkeit für das automatische Einsetzen eines Qualifiers gibt es in REFACOLA aktuell nicht.
package a;
package a;
public class A {
int x = 0;
public class A {
int y = 0;
void m() {
int y = x;
}
}
void m() {
int y = this.y;
}
}
Listing 4.14: Ergänzung Quelltext mit explizitem this-Qualifier
Schäfer, Ekman und de Moor stellen in [SEM08] den Ansatz locked names vor, bei
dem sie solche Ersetzungen vornehmen. In ihrem Konzept werden Referenzen über
symbolische Namen fest an ihre Deklarationen gebunden. Treten Namenskonflikte
auf, werden Qualifizierungen eingefügt und nur wenn die Refaktorisierung ein korrektes Ergebnis liefern kann, wird sie auch durchgeführt.
Listing 4.15 zeigt Beispiele für den Einsatz von Qualifiern.29 Über den Qualifier
super kann beispielsweise auf versteckte oder überschriebene Elemente des Supertyps zugegriffen werden.
package a;
class SuperA{
int x;
void m(){}
package a;
class B{
int x;
static void m(){}
}
class C {
void n(){
B.m();
}
package a;
class SupA extends SuperA{
void m(){
super.m();
this.x;
}
void o(){
int x = B.this.x;
}
}
}
Listing 4.15 diverse Aufrufe mit Qualifiern
28
Um das Problem bei verschachtelten Typen zu lösen, müsste ein qualifiziertes this ergänzt werden
(vgl. Listing 4.9 und Listing 4.10).
29
Ein ausführliches Beispiel findet sich in [STST12] Figure 7: Example for name lookup in Java
Implementierung
37
Namen von statischen Programmelementen können grundsätzlich mit dem Typnamen in dem das Element deklariert ist, qualifiziert werden.
Das Verfahren eignet sich nach Steimann, Kollee und von Pilgrim [SKP11] gut für
Java mit seinen komplizierten Konzepten Verbergen, Abschatten und Verdunkeln,
die ansonsten schwer über Constraintregeln zu behandeln sind. Um für REFACOLA
die beschriebene Problematik speziell zu untersuchen, wurde vom Lehrstuhl parallel eine eigene Abschlussarbeit [Ni13] vergeben. Daher wurden Probleme, die sich
durch die Einführung von Qualifiern lösen lassen, nicht weiter bei der Regeldefinition berücksichtigt.
4.3.2
Ergänzung Qualifizierte Namen
In einem Programmteil kann für ein Element solange der einfache Name verwendet
werden, wie er in diesem Kontext eindeutig ist. Geht die Eindeutigkeit bei Umbenennung eines Elements verloren, kommt es zu einem Kompilierungsproblem.
Durch Verwendung des qualifizierten Namens kann der Fehler vermieden werden.
Das Beispiel in Listing 4.16 zeigt eine Compilationunit, welche ein Interface mit
Namen I importiert. Außerdem deklariert sie selbst mehrere Schnittstellen. Wird
nun eines dieser Interfaces ebenfalls in I umbenannt, führt dies zu einem Namenskonflikt. Das Programm ist nicht mehr übersetzbar, wenn nicht der qualifizierte
Name ergänzt wird. Allerdings reicht dies für dieses Beispiel nicht, hier muss zusätzlich der Import entfernt werden.
package a;
public interface I{}
package a;
public interface I{}
package b;
import a.I;
package b;
public interface J{}
interface JJ{}
interface K extends I,JJ {}
public interface J{}
interface I{}
interface K extends a.I,b.I {}
Listing 4.16 Einfügen von qualifizierten Namen
Eine Möglichkeit, dies über eine Regel zu lösen, besteht nur darin, die Refaktorisierung zu verbieten, da es in REFACOLA keine Anweisung gibt, die bedingt, dass ein
einfacher Name durch einen qualifizierten ersetzt wird. Dies bedeutet wiederum
eine unnötige Restriktion für den Benutzer. Auch hier wäre eine automatische Anpassung wünschenswert, wie es für Qualifier vorgesehen ist. Das Thema ist ebenso
Teil der vom Lehrstuhl vergebenen Abschlussarbeit [Ni13] und wurde für die Regeldefinition daher ebenfalls nicht weiter berücksichtigt.
4.3.3
Erweiterte Prüffunktionen
Bei der Umsetzung des Regelsets wurden einige Punkte identifiziert, die eine erweiterte Prüffunktionalität benötigen, um eine Constraintregel zu formulieren.
Ungewolltes Überladen von Methoden
Handelt es sich bei den formalen Parametern einer Methode um primitive Typen,
gibt es Konstellationen, in denen das Umbenennen zum ungewollten Überladenen
einer Methoden führt. Dadurch kann es zu einer veränderten Methodenbindung
kommen. Listing 4.17 zeigt ein Beispiel. Das Umbenennen der Methode n(short
s) nach m(short s) führt zum Überladen der bereits vorhandenen gleichnamigen
Implementierung
38
Methode. Da im Methodenaufruf a.m(s) das Argument vom Typ short ist ändert
sich hier die Methodenbindung, da der Argumenttyp nun besser zur Methode
m(short s) passt als zu m(int i).
package a;
public class A {
package a;
public class A {
void m(int i) { } //aufruf
void n(short s) {}
void m(long l) {)
void m(int i) { }
void m(short s) {} //aufruf
void m(long l) {)
void n() {
short s = 1;
a.m(s);
}
void n() {
short s = 1;
a.m(s);
}
}
}
Listing 4.17: Methodenaufruf bei überladenen Methoden
REFACOLA stellt derzeit keine Möglichkeit bereit, das Problem mittels einer Regel zu
lösen.
Primitive Datentypen können anhand ihres Wertebereichs in eine Reihenfolge gebracht werden, beispielsweise byte < short < int < long. Dabei kann ein
short sowohl in einen int, wie auch in einen long konvertiert werden, ohne dass Informationen verloren gehen. Ob dieses Wissen für eine Prüfung ausreicht, wäre zu
untersuchen. Verkompliziert wird das Problem noch durch das sogenannte Autoboxing wie es in Listing 4.18 gezeigt wird. Auch hier ändert sich die Methodenbindung
nach Umbenennen der Methode n(int i). Auch hier wird nach dem Refaktorisieren die Methode gebunden, die für den Aufruf die am besten passende Parameterliste bietet (vgl. 3.1.3).
package a;
public class A {
void m(Integer i) {}
package a;
public class A {
void m(Integer i) {}
void p() {
m(1); //aufruf m(Integer)
}
void p() {
m(1); //aufruf m(int)
}
void n(int i) {}
}
void m(int i) {}
}
Listing 4.18: Methodenaufruf mit Autoboxing
Für das Vermeiden von ungewolltem Überladen von Methoden, welche zum Beispiel
Referenztypen als formale Parameter haben, gibt es im Regelset Accessibility einige Regeln, welche das unerwünschte Überladen von Methoden verhindern. Auf
eine detaillierte Darstellung wird hier verzichtet.
Gültige Bezeichner
Um ein korrektes Umbenennen durchführen zu können, müssen die Bezeichner
gültig sein. Der Aufbau eines gültigen Bezeichners wurde bereits in Kapitel 3.1.2
kurz erläutert. Diese Aussagen werden hier etwas Stelle präzisiert.
Die Java-Sprachspezifikation definiert einen gültigen Identifier als eine unbegrenzt
lange Sequenz von Java letters und Java digits. Des Weiteren unterstützt
Implementierung
39
Java den Unicode-Zeichensatz (JLS §3.8). Dies erlaubt es Entwicklern, Identifier
selbst in Sprachen wie beispielsweise Chinesisch zu schreiben.
REFACOLA selbst stellt aktuell keine Funktion für die Überprüfung von gültigen
Identifiern innerhalb der Regeldefinition zur Verfügung. Anstatt eine explizite Prüfung für alle Regeldefinitionen eigens zu formulieren, scheint eine zentrale Prüfung
für gültige Identifier auch sinnvoller.
Um nun prüfen zu können, ob es sich auch um zulässige Java letters handelt,
gibt es in der Klasse java.lang.Character spezielle Prüfmethoden30 (JLS §3.8).
Allerdings reichen diese für eine Überprüfung nicht immer aus. Da REFACOLA in
Eclipse integriert ist, muss hier zusätzlich das eingestellte Encoding der IDE beachtet werden.
REFACOLA führt diese Prüfung aktuell direkt in der Benutzerschnittstelle durch
(vgl. Abbildung 4.2). Es werden dazu Validierungsmethoden aus den JDTBibliotheken der Klasse org.eclipse.jdt.core.JavaConventions31 verwendet,
mit dem Vorteil, dass diese das Encoding der IDE bereits berücksichtigen. Da die
anderen REFACOLA-Refaktorisierungswerkzeuge im Benutzerdialog keine Eingabemöglichkeit für Identifier zur Verfügung stellen, wurde die Lösung zunächst als
ausreichend erachtet.
Abbildung 4.2: REFACOLA-Eingabedialog „Rename“ - Prüfung gültiger Identifier
Casesensitve Compilationunitnamen
Java beachtet für die Unterscheidung von Namen Groß- und Kleinschreibung bei
der Schreibweise (vgl. Kapitel 3.1.2). Ein Sonderfall, auf den bereits im Rahmen der
Regelvorstellung hingewiesen wurde (vgl. Kapitel 4.2.2), betrifft Compilationunits.
Wenn sie innerhalb des gleichen Pakets definiert werden, muss deren Name eindeutig sein. Allerdings wird darf dabei nicht casesensitiv unterschieden werden, obwohl
Java eine sogenannte casesensitive Sprache ist 32.
30
Die Prüfmethoden heißen Character.isJavaIdentifierStart(int) , Character.isJavaIdentifierPart(int);
Javadoc zu Klasse unter http://help.eclipse.org/juno/
topic/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/core/JavaConventions.html
32
In einem Windows XP–System könnten zwei Javadateien a.java und A.java auch nicht in einem
Ordner gespeichert werden.
31
Implementierung
40
Das Problem kann über die Regeldefinition aktuell nicht entsprechend behandelt
werden. Eine Erweiterung der Eingabeprüfung in der Benutzerschnittstelle wäre
möglich, erscheint auf den zweiten Blick jedoch wenig praktikabel, da nicht nur der
oben dargestellte Fall zu beachten ist, sondern auch welche, bei denen ein Typ „indirekt“ umbenannt wird, beispielsweise durch ein „Move Type“. Idealerweise sollte
die Prüfung in einer Regel formuliert werden, damit sie von allen Werkzeugen gleichermaßen eingebunden kann.
REFACOLA stellt derzeit keine Funktion für einen nicht-casesensitiven Vergleich von
Bezeichnern zur Verfügung, wie es beispielsweise Java mit der Methode equalsIgnoreCase33 tut. Mit einer solchen Funktion könnte eine Regel wie folgt formuliert werden.
UniqueCasesensitiveTyperootIdentifier
for all
tr1: Java.TypeRoot
tr2: Java.TypeRoot
do
if
tr1 != tr2
then
(tr1.hostPackage = tr2.hostPackage)
->(not equalsIgnoreCase(tr1.identifier,tr2.identifier))
end
Listing 4.19: Funktion für casesensitivem Stringvergleich
4.3.4
Offene Themen
Aufgrund der zeitlichen Begrenzung und fehlender Komponenten konnten einige
Themen nicht oder nur teilweise berücksichtigt werden. Paketen und Importen
wurden bereits in Kapitel 4,2.9 angesprochen. Im engen Zusammenhang mit Paketen und Importen steht der Umgang mit Qualifizierten Namen.
Einzelne Identifer eines Qualifizierten Namens
Im Rahmen dieser Arbeit wurden einfache Erweiterungen für das Umbenennen von
vollqualifizierten Klasseninstanziierungen und Importen eingefügt, die es bedingt
ermöglichen, qualifizierte Namen zu schreiben34. Hierbei kann allerdings jeweils
nur das letzte Fragment eines qualifizierten Namens geändert werden. Diese Erweiterung wurde vor allem aus Gründen der Testbarkeit beim Umbenennen von
Typen vorgenommen, um auch paketübergreifend testen zu können.
Bei einem qualifizierten Namen stellt jedes einzelne Fragment einen eigenen Identifier dar (vgl. Kapitel 3.1.2). Daraus resultiert die Anforderung, sie auch einzeln umbenennen zu können. Dies ist zum Beispiel für Pakete nötig, weil jedes Fragment
einer Paketdeklaration in einer Compilationunit einem einzelnen Verzeichnis entspricht. Wird ein solches umbenannt, müssen alle Paketdeklarationen der Compila-
33
34
Methode der Klasse java.lang.String
Die Änderungen betreffen in de.feu.ps.refacola.lang.java.jdt.manipulation.NodeChangeRequest
die Methoden rename(ClassInstanceCreation creation) und rename(QualifiedName qualifiedName). Die Stellen wurden entsprechend dokumentiert.
Implementierung
41
tionunits, welche in dem Paket liegen, angepasst werden. Ebenso müssen die Fragmente in Importanweisung und qualifizierten Namen geändert werden können.
Annotationen (JLS §9.6, §9.7)
Ein Annotationstyp ist eine spezielle Interface-Form (JLS §9.6). Mithilfe einer Annotation kann der Quelltext um Metainformationen erweitert werden.35 Es gibt bereits vordefinierte Annotationstypen in Java, wie beispielsweise @Override. Ist
eine Methode damit annotiert, kommt es zu einem Kompilierfehler, wenn diese
nicht tatsächlich eine Methode ihres Supertyps überschreibt (JLS §9.6.1). Es ist
zudem möglich, Annotationen selbst zu definieren.
Auch für Annotationen gelten eine Reihe von Bedingungen, die bei der Refaktorisierung von Identifiern zu beachten sind. Beispielsweise muss der Name einer Annotationsdeklaration mit dem Namen des Annotationstyps übereinstimmen. Außerdem
dürfen Annotationen nicht den gleichen Namen besitzen wie der sie umgebende
Typ.
Weil REFACOLA Annotationen derzeit an vielen Stellen nur sporadisch bis gar nicht
unterstützt, konnte die Thematik nicht mehr berücksichtigt werden.
Generische Typen
Obwohl REFACOLA generische Typen bereits in weiten Teilen unterstützt, gibt es für
Typparamter noch keine Eigenschaft identifier in der REFACOLA-Sprachdefinition. Daher können diese noch nicht umbenannt werden. Eine Erweiterung
war zeitlich nicht mehr möglich. Außerdem gibt es für die Vielzahl an Möglichkeiten, die generische Typen bieten, ebenso viele Bedingungen. Diese müssen noch
weiter analysiert werden.
Typparameter in extends-Klauseln werden beispielsweise bereits richtig refaktorisiert.
class A <T extends B> {}
Anonyme Klassen (JLS 15.9.5.1)
Anonyme Klassen erweitern implizit die Klasse oder das Interface, welches im Instanziierungsausdruck angegeben ist. Sie selbst haben keinen eigenen Namen. Wird
nun der Supertyp umbenannt, muss auch dieser Instanziierungsausdruck angepasst werden. In der Faktengenerierung werden bisher nur Fakten für die Anonyme
Klasse und deren ClassInstanceCreation berücksichtigt. Benötigt wird allerdings ein Fakt, welches die Referenz der anonymen Klasse auf die Superklasse repräsentiert. Auch diese Erweiterung war zeitlich nicht mehr möglich, daher fehlt
eine entsprechende Constraintregel.
Dokumentation im Quelltext
Mens und Tourwé weisen in [MT04, S. 5] darauf hin, dass zu einer korrekten Refaktorisierung nicht nur die Sicherstellung des Programmverhaltens gehört, sondern,
auch die „Konsistenz zu anderen Softwareartefakten, wie beispielsweise Pflichtenheft oder Entwurfsdokumente“. Dies gilt insbesondere für Dokumentation, die di-
35
Vgl. http://docs.oracle.com/javase/tutorial/java/annotations/
Implementierung
42
rekt im Quelltext hinterlegt ist. In Java ist es möglich, aus speziellen Kommentaren, der Javadoc, eine Dokumentation generieren zu lassen, die häufig mit öffentlichen Bibliotheken ausgeliefert wird. Wenn in Kommentaren nun beispielsweise
Verweise zu Programmelementen enthalten sind, ist es umso ärgerlicher, wenn
nach einer Refaktorisierung zwar alle Codestellen richtig angepasst wurden, die
Dokumentation jedoch von Hand geändert werden muss. Dies erfordert letztendlich
wieder ein „Suchen und Ersetzen“.
Bisher ist die Refaktorisierung von Kommentaren in REFACOLA noch gar nicht vorgesehen. Um das Regelwerk entsprechend erweitern zu können, müssen zuvor sowohl REFACOLA-Sprachdefinition, Faktengenerierung wie auch Rückschreibekomponente erweitert werden. Listing 4.20 zeigt die verschiedenen Arten von Kommentaren in einem Java-Quelltext.
package examples;
public class JavadocExample {
class A {}
/*Blockkommentar*/
/** Javadoc
* @param a
* @see examples.A
*/
void m(A a) {}
/** Javadoc
* @return A
*/
A n() {
return new A(); //zeilenkommentar
}
}
Listing 4.20: Kommentare in Java
4.4
Identifier in Fremdbibliotheken
Neben den vielen Programmelementen, die explizit umbenannt werden sollen, gibt
es in Projekten auch Stellen, an denen dieses unerwünscht ist. Dazu zählen Fremdbibliotheken, beispielsweise die Java-Bibliotheken. REFACOLA verhindert dies, indem
die Programmelemente bereits in der Faktengenerierung als nicht änderbar (readonly) gekennzeichnet werden. Eigene Regeln sind daher für diese Fälle nicht nötig.
4.5 Tests
Nach der Refaktorisierung eines Programmes erfolgt zunächst die syntaktische und
semantische Überprüfung des Quelltextes durch den Compiler. Treten hier Fehler
auf, werden diese sofort erkannt und können behoben werden. Steimann nennt sie
daher auch „gutartige Fehler“ (vgl. [St10]). Weiterhin muss aber auch überprüft
werden, ob sich das beobachtbare Verhalten des refaktorisierten Programmes geändert hat. Ein Nachweis der Korrektheit durch eine formale Verifikation ist zwar
theoretisch denkbar, jedoch meist aufgrund des hohen Aufwandes nicht durchführbar. Stattdessen wird Software in der Regel mithilfe von Tests auf korrektes Verhalten geprüft (vgl. [St10]).
Diese helfen, die vorhandene Funktionalität vor und nach der Änderung zu prüfen,
um Veraltensänderungen aufzudecken [We06]. Allerdings können sie keine Fehlerfreiheit garantieren, da jeweils nur geprüft wird, ob für eine vorgegebene Eingabe
Implementierung
43
auch das gewünschte Ergebnis geliefert wird. Außerdem besteht bei der Entwicklung von Testfällen häufig das Dilemma, dass nicht alle Konstellationen in einer
vorgegebenen Zeit erschöpfend geprüft werden können. Allein für das Umbenennen
eines Identifiers alle Möglichkeiten zu prüfen ist mit vertretbarem Aufwand kaum
möglich (vgl. [St10]).
Eine wichtige Anforderung an Testverfahren ist die effiziente Ausführung von
Tests, schnelles Feedback und reproduzierbare Ergebnisse. Manuelles Testen kann
dieses nur selten leisten, deshalb sind automatisierte Tests von Vorteil [We06, S.2].
4.5.1
Unit-Tests
In der Softwareentwicklung haben sich sogenannte Unit-Tests etabliert, die gezielt
begrenzte Funktionalitäten eines Programms in einzelnen Testmethoden überprüfen. Einmal geschrieben, lassen sie sich jederzeit automatisch ausführen. Es gibt
diverse Frameworks, die die Erstellung von Entwicklertests unterstützen. Der „Defacto-Standard“ für Java ist das JUnit-Framework36 (vgl. [We06, S. 21]). Vorteilhaft
daran ist die einfache Handhabung und eine gute Integration in die Entwicklungsumgebung Eclipse (vgl. [We06, S. 21ff.]). Dieses Framework wird bereits in der REFACOLA-Entwicklung eingesetzt.
Mit Hilfe eines speziellen Hilfsplugins in REFACOLA ist es möglich die Refaktorisierungswerkzeuge zu testen. Dazu wird für den Test eine weitere Eclipse-Instanz gestartet, welche dann bereits die REFACOLA-Refaktorisierungswerkzeuge integriert.
In dieser Umgebung lassen sich außerdem JUnit-Tests starten (vgl. [Wa12, S. 47]).
Dies wird für Tests des Regelwerks genutzt.
Jeder einzelne Testfall37 simuliert ein kleines Programm, auf dem eine Refaktorisierung durchgeführt wird. Dies hat zudem den positiven Nebeneffekt, dass hierbei der
komplette Refaktorisierungszyklus mit überprüft wird. Ziel ist es, möglichst Fehler
aufzudecken, um daraus neue Constraintregeln abzuleiten, oder bestehende abzuändern.
Neben den Funktionstests, die das erstellte Regelset testen, gibt es eine Reihe von
Tests, welche offene Punkte dokumentieren. Außerdem gibt es für Erweiterungen
an Faktengenerierung und Rückschreibekomponente zur besseren Nachvollziehbarkeit „Referenztestfälle“. Eine Übersicht findet sich in Anhang C.
4.5.2
Mutationstests
Testergebnisse können nur so gut sein wie der Test selbst. Dabei ist es denkbar,
dass nicht der zu testende Gegenstand fehlerhaft ist, sondern der Test selbst
[We06, S. 154ff.], weshalb es sinnvoll ist, auch die Tests einer Prüfung zu unterziehen, beispielsweise durch Mutationstesten. Dabei werden gezielt Fehler in ein Programm eingebracht und anschließend kontrolliert, ob die Fehler beim Testlauf entdeckt werden. Ist dies nicht der Fall, lässt dies Rückschlüsse auf fehlerhafte Tests
oder eine unzureichende Testabdeckung zu.
Für die Überprüfung der JUnit-Tests wurde daher schrittweise jede einzelne
Constraintregel auskommentiert und jeweils anschließend alle Tests ausgeführt.
Werden keine Fehler entdeckt, kann dies zwei Ursachen haben. Entweder liegen für
die Regel keine aussagekräftigen bzw. fehlerhaften Testfälle vor, oder eine Problem-
36
37
http://junit.sourceforge.net/
Die Testfälle befinden sich im Projekt de.feu.ps.refacola.lang.java.jdt.ui.tests.
Implementierung
44
stellung wird bereits mittels einer anderen Regel behandelt. In letzterem Fall kann
eine der Regeln entfernt werden.
Über die Ergebnisse der zuletzt durchgeführten Mutationstests gibt Tabelle 4.1
Auskunft. In Spalte „Test Rename“ wurden JUnit-Tests bewertet, die im Rahmen
dieser Arbeit entstanden. Für jede Regel aus Names.ruleset.refacola wurden
Fehler erkannt, mit Ausnahme von Names.UniqueMethodIdentifier. Hier wurde festgestellt, dass der Constraint für kovariante Rückgabetypen nicht benötigt
wird. Für die Änderung eines kovarianten Rückgabetypen gibt es bereits die Regel
Types.OverridingCovariantReturnType. Ein Umbenennen des Typen ist unproblematisch, wenn die Subtypbeziehung bestehen bleibt. Der Constraint wurde
daraufhin aus der Regel entfernt.
Die zweite Spalte gibt die Auswertung von JUnit-Tests wider, die für andere Refaktorisierungswerkzeuge in REFACOLA erstellt wurden. Beim Vergleich fällt auf, dass
hier kaum Fehler aufgetreten sind. Dies lässt auf eine mangelnde Testabdeckung
schließen. Hier wurde begonnen weitere Testfälle zu erfassen.
Status
Anzahl
Fehler
Tests
Andere38
Anzahl
Fehler
F: 22
F: 26
F:0
F:0
OK
F: 19
F:0
UniqueMemberTypeNames2
F: 11
F: 0
OK
UniqueEnclosingMemberTypeNamesInAssignment1
F: 4
F: 0
OK
UniqueEnclosingMemberTypeNamesInAssignment2
F: 1
F: 0
OK
TopLevelTypeNames
F: 78
F: 3
OK
ConstructorNames
F: 73
F: 0
OK
UniqueTopLevelTypeIdentifier
F: 1 E:7
F: 0
OK
TypeReferenceIdentifier
F: 105
F: 0
OK
ReferenceIdentifier
F: 256
F: 0
OK
UniqueFieldIdentifier
F: 17
F: 0
OK
UniqueMethodIdentifier
Teil 1 -> ok
F: 15
F: 0
Teil2
Prüfen
Regelname
UniqueMemberTypeNames1
Teil 1 ->ok
Test
Rename
(t.identifier != t.enclosingNamedTypes.identifier)
Teil 2-> ok
( t.identifier != t.tlowner.identifier)
((method1.owner = method2.owner)
and(method1.parameters.declaredParameterType
=method2.parameters.declaredParameterType)
->(method1.identifier != method2.identifier))
Teil 2 -> kovarianter Rückgabetyp prüfen
38
„Tests Andere“ bezieht sich auf die JUnit-Testsuiten: AllChangeAccessibilityTests,
AllPullUpTests, AllChangeDeclaredTypeTests, AllMoveCompilationUnitTests
Implementierung
45
OverridingMethodNames
F: 51
F: 0
OK
PackageDeclarationNames
F: 15
F: 5
OK
ImportDeclarationNames1
F: 35
F: 0
OK
AccidentalHidingFinalMethod
F: 19
F: 0
OK
UniqueLocalVariableNames1
F: 17
F: 0
OK
UniqueLocalVariableNames2
F: 16
F: 0
OK
UniqueLocalVariableNames3
F: 20
F: 0
OK
EagerInterfaceMethodNames
F: 27
F: 0
OK
(Stand 01.06.2013)
Tabelle 4.1: Mutationstest für das JUnit-Test RenameMember
4.5.3
RTT-Tests
JUnit-Tests können nur einfache Beispielprogramme simulieren. Darüber hinaus
ist es aber nötig, die Auswirkungen einer Refaktorisierung anhand einer umfangreicheren Quelltextbasis prüfen zu können.
Das Test-Framework Refactoring Tool Tester (RTT) bietet die Möglichkeit, ein zu testendes Refaktorisierungswerkzeug auf einer beliebigen Codebasis anzuwenden. Es
wurde ebenfalls am Lehrgebiet Programmiersysteme der FernUniversität in Hagen
entwickelt [Ho10].39
Vorteilhaft daran ist, dass für den Test von Refaktorisierungswerkzeugen jegliche
Codebasis verwendet werden kann, solange sie vor der Umstrukturierung ein ausführbares Verhalten zeigt [St10]. Hier bietet sich die Verwendung von OpensourceProjekten an, die in einer Vielzahl frei zur Verfügung stehen. Ein weiterer Vorteil
von Opensource-Projekten ist, dass diese häufig über eine sehr gute Testabdeckung
verfügen. Deren Tests können nach der Refaktorisierung genutzt werden, um die
Refaktorisierungsergebnisse zu überprüfen. Gerade Fehler die zu verändertem Verhalten führen können, sind so leichter zu entdecken.
Für diese Arbeit wurde der RTT um neue Funktionalitäten für das Umbenennen
von Typen, Methoden, Feldern und lokalen Variablen ergänzt (vgl. Abbildung. 4.4).
39
Siehe auch Webpräsenz: http://www.fernuni-hagen.de/ps/prjs/rtt/
Für diese Arbeit wurde eine Neuentwicklung des RTT verwendet.
Implementierung
46
Abbildung 4.3: Menü RTT-Tests für Rename in Eclipse Package Explorer
4.5.4
Testgetriebene Entwicklung
Als Vorgehensweise bei der Entwicklung wurde ein testgetriebener Ansatz gewählt.
Die Idee der sogenannten Test-First-Programmierung, auch bekannt als Test
Driven Development (TDD), ist, dass die Tests vor der eigentlichen Entwicklung
spezifiziert und bereitgestellt werden. Das geforderte Ergebnis muss folglich im
Vorfeld bekannt sein, um es in einem Testfall zu „dokumentieren“. Ein Vorteil dabei
ist, dass der Testfall nicht ungewollt oder unterbewusst an das Programm angepasst wird, sondern die Funktionalität sich an dem geforderten Ergebnis orientiert
(vgl. [We06, 2ff.]).
Für die Erstellung des Regelsets wurden daher zunächst für bestimmte Schwerpunkte diverse Testfälle identifiziert. Dadurch wurde schnell offenbar, ob es bereits
Constraintregeln gibt, oder ob eine neue Regel nötig ist. Das weitere iterative Vorgehen unterstützte die Erweiterung und Verfeinerung der Testfälle und Regeln.
Entsprechend des TDD-Ansatzes nehmen die Tests einen hohen Stellenwert für
diese Arbeit ein.
Diskussion
47
5 Diskussion
5.1
Ergebnisse
Im Zuge der Definition des Regelsets wurden einige Erweiterungen in der REFACOLASprachdefinition, der Faktengenerierung und der Rückschreibekomponente benötigt. Diese wurden größtenteils bei der Vorstellung des Regelsets angesprochen (vgl.
Kapitel 4.2). Exemplarisch soll hier der Prototyp für die Ermittlung umschließender
lokaler Variablen zur Diskussion gestellt werden.
5.1.1
Prototyp zur Ermittlung umschließender lokaler Variablen
In Kapitel 4.2.8 wurden Constraintregeln40 vorgestellt, welche die Eindeutigkeit von
Namen für lokale Variablen und formale Parameter in Methoden und Konstruktoren sichern sollen. Dieses Kaptitel stellt eine prototypische Implementierung zur
Diskussion. Formale Parameter werden nicht berücksichtigt, da über die Regel Names.UniqueLocalVariableNames2 bereits die Namenseindeutigkeit zu lokalen
Variablen sichergestellt wird.
Für die weiteren Ausführungen in diesem Kapitel gelten Aussagen, die für Methoden gemacht werden ebenso wie für Konstruktoren und Initialisierungsblöcke. Auf
Unterschiede wird explizit hingewiesen.
Lokale Variablen können in unterschiedlichen Anweisungsblöcken deklariert sein
(vgl. Kapitel 3.1.3). Dabei gilt, dass ein Identifier innerhalb der ihn umschließenden
Blöcke eindeutig sein muss. Liegt ein Anweisungsblock außerhalb der Hierarchie,
kann es auch innerhalb einer Methode mehrere gleichnamige Identifier geben, wie
es in Abbildung 5.1 dargestellt ist. Das Beispiel zeigt eine Methode, in der zwei
gleichnamige Variablen i deklariert werden. Dies ist möglich, weil die beiden ForSchleifen zwei voneinander getrennte Anweisungsblöcke sind.
class A{
private int y = 0;
void m(int x){
for(int i=0; i<5; i++){
int x; //fehler konflikt mit Parametername
}
for(int i=0; i<5; i++){
int y; //abschatten instanzvariable y
}
}
}
Abbildung 5.1: gleichnamige lokale Variablen in verschiedenen Anweisungsblöcken
40
Es handelt sich hier um die Regeln Names.UniqueLocalVariableNames1,
Names.UniqueLocalVariableNames2 und Names.UniqueLocalVariableNames3
Diskussion
48
REFACOLA bietet die Möglichkeit, einem Element eine Liste von Entitäten mitzugeben, sogenannte Sequences. Befüllt werden diese während der Faktengenerierung. In der REFACOLA-Sprachdefinition wurde LocalVariable um die Eigenschaft
enclosingLocalVariables erweitert (vgl. Listing 5.1). Names.UniqueLocalVariableNames3 verwendet diese für den Vergleich der Variablennamen.
language Java
kinds
abstract Entity <: ENTITY
abstract LocalVariable <: TypedEntity,
LocalVariableOrParameter {enclosingLocalVariables}
properties
identifier: Identifier
enclosingLocalVariables : Sequence(LocalVariables)
domains
AccessModifierDomain = {private, package, protected, public}
LocalVariables = [ LocalVariable ]
queries
declaresLocalVariableOrParameter(method: MethodOrConstructor,
localVariableOrParameter: LocalVariableOrParameter)
hasEnclosingVariables(localVariable : LocalVariable)
Listing 5.1: Ausschnitt REFACOLA-Sprachspezifikation für Java - lokale Variablen
Es gibt eine Vielzahl unterschiedlicher Anweisungsblöcke. Dazu zählen die unterschiedlichen Schleifen ebenso wie break- oder return-Anweisungen. Für die korrekte Ermittlung der lokalen Variablen müssen diese unterschieden werden. Die
prototypische Implementierung löst dieses in einer eigenen Klasse41, welche in die
Faktengenerierung eingebunden ist. Hier werden für jede lokale Variable alle umschließenden Variablen ermittelt und jeweils in der Sequenz enclosingLocalVariables der Entität gespeichert.
Die vorgeschlagene Lösung stellt einen einfachen Weg zur Behandlung von Namenseindeutigkeit für lokale Variablen vor. Ein Vorteil der Lösung ist die einfache
Integration in die bisherige Faktengenerierung und die zentrale Behandlung aller
Fälle. Da es sich um einen Prototypen handelt, werden bislang nur wenige Anweisungsblock-Arten unterstützt und daher noch nicht alle Variablen gefunden. Ein
Nachteil daran ist, dass in der Faktengenerierung die Anweisungsblöcke mehrmals
durchlaufen werden, da für jede lokale Variable alle umgebenden Variablen ermittelt und gespeichert werden. Alternativ könnten die Variablen bei der Ermittlung
der Fakten für die Methoden einmalig gespeichert werden, die dafür nötigen Mechanismen zur Speicherung und Abfrage wären dann aber komplizierter. Daher ist
mehr Speicherkapazität und Laufzeit notwendig. Wenn die Ermittlung aller Anwei-
41
Es handelt sich um die Klasse
de.feu.ps.refacola.lang.java.jdt.util.LocalVariableParseTool
Diskussion
49
sungsblöcke vollständig implementiert ist und folglich alle lokalen Variablen verfügbar sind, sollte dieser Aspekt kritisch betrachtet werden.
Die Lösung berücksichtigt noch nicht, dass innerhalb von Methoden lokale Klassen
deklariert werden können. Dies verkompliziert die Ermittlung nochmals, da die
Eindeutigkeit der Namen lokaler Variabeln wiederum für alle Variablen der der
umschließenden Anweisungsblöcke gelten muss (JLS §8.1.3). Allerdings besitzen
auch lokale Klassen Instanzvariablen, welche abgeschattet werden können. Das
Beispiel in Abbildung 5.2. zeigt dies für die Instanzvariable z der Klasse LocalA. In
der Methode n() in der Klasse LocalA werden sowohl Variablen der eigenen Klasse
wie auch der äußeren Klasse A verwendet.
class A{
private int x = 0;
void m(int x){
final int y = x;
class LocalA{
private int z;
void n(int x){
int localInnerVar = y+x+z;
}
}
}
}
Abbildung 5.2: lokale Klasse und lokale Variablen
5.1.2
Einbindung Locked Bindings
Viele Refaktorisierungsprobleme können aufgrund fehlender Qualifier nicht vollständig gelöst werden. Erstellte Constraintregeln sind daher teilweise zu restriktiv
(vgl. Kapitel 4.3.1). Die Einbindung der Locked Bindings [Ni13] verspricht eine
wichtige Ergänzung zum Regelset zu werden. Gegen Ende dieser Arbeit wurden
bereits erste Tests erfolgreich durchgeführt. Da hiermit auch die Limitierungen in
den Regeln obsolet werden, sollte das Regelset noch einmal kritisch daraufhin geprüft werden, ob und welche Regeln vereinfacht oder entfernt werden können. Die
Regel UniqueEnclosingMemberTypeNamesInAssignment142 zum Beispiel sollte
komplett entfallen können.
Die JUnit-Tests aus Kapitel 4.5.1 verfügen über eine Reihe an Tests, die speziell das
Einfügen von Qualifiern prüfen. Sie wurden entsprechend gekennzeichnet und können die Integration der Locked Bindings unterstützen. Dies gilt ebenso für die Ergänzung qualifizierter Namen.
42
Dies gilt ebenso für die Regel UniqueEnclosingMemberTypeNamesInAssignment2.
Diskussion
5.1.3
50
Auswertung der Tests
In diesem Kapitel werden exemplarisch Beispiele für Testergebnisse vorgestellt. Bei
den vorgestellten Statistiken handelt es sich um tagesaktuelle Ergebnisse, die sich
durch die laufende Weiterentwicklung von REFACOLA ändern können.
JUnit - Testsuite
Die Testsuite AllRenameMember enthält derzeit noch viele deaktivierte Tests
(Ignore). Dies liegt zum einen daran, dass einige Tests speziell für die Erweiterung
Locked Bindings vorbereitet wurden und daher erst mit deren Einbindung aktiviert
werden können. Die anderen sind Problemen zuzuordnen, die über das Regelwerk
noch nicht gelöst werden konnten, wie zum Beispiel das Umbenennen generischer
Typen. Ein weiterer Teil dient zur Dokumentation der Referenztestfälle oder offener
Fehler. Letztere sind bei der Zählung in Tabelle 5.2 nicht enthalten.
Testsuite
Gesamt
Failure/Errors
Ignore
Locked Bindings
Ignore
Sonstige
896
0
48
59
AllRenameMember
(Stand 07.06.2013 , ohne Tests für Dokumentation, vgl. Anhang C)
Tabelle 5.1: JUnit-Testsuite AllRenameMember
RTT-Tests
Bei der Auswahl der RTT-Testprojekte zeigen sich zum Teil deutliche Einschränkungen, die auf noch fehlende Funktionalitäten in REFACOLA zurückzuführen sind.
So können viele Opensource-Projekte noch nicht verwendet werden, da beispielsweise Vararg-Argumente oder Generics noch nicht vollständig unterstützt werden.
Ein Vorteil der RTT-Tests ist deren Flexibilität. Mit wenigen Änderungen werden
diverse Testkonstellationen geschaffen. Jedes Testwerkzeug kann sehr individuell
gestaltet werden. Beispielsweise gibt es für RenameOnlyType eine Konfigurationsmöglichkeit, nur diejenigen Fälle zu testen, bei denen eine Refaktorisierung nicht
möglich sein darf (Unsolved).
Tabelle 5.3 zeigt exemplarisch Ergebnisse für RenameOnlyType. Bei 1270 Einzelrefaktorisierungen für das Projekt jaxen1.1.1 wurden insgeamt 27 Kompilierungsprobleme entdeckt. Bei näherer Betrachtet konnte festgestellt werden, dass es sich
immer um das selbe Problem handelte. Außerdem schwanken die Ergebnisse für die
Einzelnen Projekte zum Teil deutlich, wie der Vergleich zwischen junit3.8 und
regexp zeigt. Um eine gute Testbasis zu bekommen ist es daher sinnvoll, verschiedene Projekte mit unterschiedlichem Umfang oder Einsatzzweck zu verwenden. Des
Weiteren ist es sinnvoll, gezielt zwei gleiche Projekte in verschiedenen Versionen zu
vergleichen, um beispielsweise das Verhalten für verschiedene Java-Versionen testen zu können. 43
43
Zum Beispiel JUnit3 und JUnit4 für den Wechsel der Javaversionen von 1.4 auf 1.5. Dies konnte
jedoch wegen der fehlenden Unterstützung von Annotationen, Varargs und Generics noch nicht
durchgeführt werden.
Diskussion
51
Rename Only Type
Anzahl
Anzahl
OK
Exception
Unsolved
Compile
Dauer ∅
jaxen 1.1.1
1270
98%
0
0
2,%*
1860
junit3.8
232
93%
0
0
6%
943
regexp
54
100%
0
0
0
439
(Stand 16.06.2013)
* 27 -mal gleiche Fehlermeldung
Tabelle 5.2: RTT-Test Rename Only Type
Tests für die Refaktorisierung von Feldern liefern bereits sehr gute Ergebnisse, wie
Tabelle 5.4 zeigt.
Rename Only Field
Anzahl
Anzahl
OK
Exception
Unsolved
Compile
Dauer
∅
jaxen 1.1.1
315
100%
0
0
0
880
junit3.8
119
100%
0
0
0
842
regexp
134
100%
0
0
0
802
(Stand 03.07.2013)
Tabelle 5.3: RTT-Test Rename Only Field
Sowohl in den Tests für Methoden, wie auch für loklae Variablen (vgl. Tablelle 5.5
und 5.6) fällt auf, dass hier eine größere Zahl von Refaktorisierungen abgeleht
wurden (Unsolved). Dies liegt daran, dass es für diese Tests noch keine Konfigurationsmöglichkeit gibt, diejenigen Fälle isoliert zu testen, bei denen eine Refaktorisierung nicht möglich sein darf. Daher fließen sie hier in die Gesamtstatistik ein.
Rename Only Method
Anzahl
Anzahl
OK
Exception
Unsolved
Compile
Dauer
∅
jaxen 1.1.1
1320
65%
0
25%
10%
1315
junit3.8
462
92%
0
6%
2%
403
regexp
132
94%
0
6%
0
179
(Stand 13.06.2013)
Tabelle 5.4: RTT-Test Rename Only Method
Diskussion
52
Rename Only LocalVariable
Anzahl
Anzahl
OK
Exception
Unsolved
Compile
Dauer
∅
jaxen 1.1.1
649
90%
5%
5%
0
161
junit 3.8
336
90%
8%
2%
0
105
regexp
171
81%
12%
7%
0
83
(Stand 21.06.2013)
Tabelle 5.5: RTT-Test Rename Only LocalVariable
Performance und Speicherverhalten
Neben der reinen Funktionalität sind Performance und Speicherverhalten wichtige
Aspekte für die Entwicklung eines Refaktorisierungswerkzeugs. Für diese Arbeit
stand die Entwicklung der Constraintregeln vor allem unter funktionalen Gesichtspunkten im Vordergrund. Daher wurden keine expliziten Performancemessungen
vorgenommen, die idealerweise auf einem vorgegebenen Standardsystem durchgeführt werden sollten. Tabelle 5.7 stellt die Daten der Testumgebung dar.
Eclipse-Version
Eclipse Juno 4.2
Betriebssystem
Windows XP 32 Bit
RAM
4 GB
Tabelle 5.6: Parameter Testumgebung
Diskussion
5.2
5.2.1
53
Verwandte Arbeiten
Reflektive Aufrufe
Java bietet mit dem Reflection-API (Applicaton Programming Interface) die Möglichkeit, Klassen und deren Bestandteile zur Laufzeit programmatisch zu analysieren. Damit ist es beispielsweise möglich, Klassen dynamisch zu laden. In reflektiven
Ausdrücken wird häufig der Name einer benötigten Ressource als Text übergeben.
Allerdings garantieren nicht alle Refaktorisierungswerkzeuge, dass bei einer Umstrukturierung das ursprüngliche Programmverhalten für diese Aufrufe bewahrt
wird [TB12].
package a;
package a;
public class A {
public int i = 0;
}
public class NewA {
public int i = 0;
}
package a;
package a;
public class B {
void m() throws Exception {
Class c = Class.forName("A");
String a = "A";
}
}
public class B {
void m() throws Exception {
Class c = Class.forName("A");
String a = "A";
}
}
Listing 5.2: Klasse mit reflektivem Aufruf
In Listing 5.3 besitzt die Klasse B einen solchen Aufruf (Class.forName("A")).
Nach der Transformation ist die Klasse A umbenannt, jedoch der reflektive Ausdruck nicht mit angepasst. Dieser Fehler wird vom Compiler nicht entdeckt, sondern erst zur Laufzeit bemerkt. Das Problem dabei ist, dass sich diese Stellen nicht
ohne weiteres von anderen Zeichenketten unterscheiden und damit eine herkömmliche statische Quelltextanalyse in der Regel nicht anwendbar ist.
In [TB12] beschreiben Thies und Bodden einen Ansatz, wie auf Basis des constraintbasierten Refaktorisierens mit reflekiven Programmaufrufen umgegangen werden
kann.44 Um Information über betroffene Programmstellen zu bekommen, registrieren sie diese Aufrufe während eines Testlaufs dynamisch. Auf dieser Basis können
dann die Constraints generiert werden. Voraussetzung für die erfolgreiche Ermittlung ist hier, dass für die Programmstellen geeignete Tests vorliegen.
5.2.2
Sprachübergreifendes Refaktorisieren
Eine Herausforderung bei der Refaktorisierung von Identifiern besteht darin, dass
Informationen über Programmelemente nicht ausschließlich im Quelltext hinterlegt
sein müssen. Viele Applikationen nutzen spezielle Dateien, wie beispielsweise
XML-, Property- oder JSP-Dateien, für Konfigurationen oder Oberflächenbeschreibungen. Wenn sich in diesen nun Referenzen auf Programmelemente des
44
Die Umsetzung basiert ebenfalls auf dem REFACOLA-Framework
Diskussion
54
ursprünglichen Javaprogramms befinden, wäre es für eine vollständige Refaktorisierung nötig, diese zu berücksichtigen und Änderungen auch an den betroffenen
Dateien vorzunehmen. Das ist problematisch, weil die Informationen nicht in der
Programmiersprache vorliegen und somit auch nicht den syntaktischen und semantischen Vorgaben der Java-Sprachspezifikation entsprechen [Kl10, Wa12].
Weiter verkompliziert wird dies, wenn die Refaktorisierung über mehrere Programmiersprachen hinweg stattfinden soll. Viele Projekte nutzen zunehmend mehrere Sprachen parallel. Unterstützt wird dies dadurch, dass die Java Virtual Machine (JVM) mittlerweile diverse andere Sprachen, wie Scala, Groovy oder JRuby,
unterstützt. Hier stehen Refaktorisierungswerkzeuge noch am Anfang. Es gibt aber
bereits erste Versionen, die zumindest für Sprachen, die innerhalb einer Laufzeitumgebung genutzt werden können, Unterstützung bieten [Kl10].
Auch im Rahmen der REFACOLA-Entwicklung wurden bereits Untersuchungen für
einen sprachübergreifenden Ansatz durchgeführt. An dieser Stelle soll auf zwei Abschlussarbeiten verwiesen werden. Zum einen betrachtet Wagner die sprachübergreifende Refaktorisierung in Verbindung mit XML-Dateien für Topleveltypen
[Wa12]. Osten stellt in [Os12] einen Lösungsansatz für das Refaktorisieren zwischen Java und JRuby vor. Hier ist zu beachten, dass die speziellen Bedingungen
der Sprachen nicht isoliert voneinander betrachtet werden dürfen, sondern durchaus Auswirkungen aufeinander haben können (vgl. [Os12, S. 32]).
5.3
5.3.1
Ausblick
Unterstützung durch Benutzerschnittstelle
Bei der Entscheidung, ob ein Benutzer eine Refaktorisierung manuell oder mithilfe
eines Werkzeugs ausführt, ist laut [MPB09] eine komfortable Benutzerschnittstelle
ein wesentlicher Aspekt. Über die Benutzerschnittstelle bieten sich zusätzliche
Möglichkeiten, ein Refaktorisierungswerkzeug für den Benutzer komfortabel zu gestalten und zusätzliche Hilfestellungen zu geben, wie zum Beispiel eine Vorschau.
Eine Benutzerschnittstelle für das Umbenennen von Elementen für Eclipse Package-Explorer bzw. Outline ist bereits vorhanden. Es fehlt jedoch noch die Möglichkeit, Programmelemente direkt im Quelltext für die Refaktorisierung auswählen zu
können. Dies wird für die Elemente benötigt, welche nicht in den Ansichten Outline
oder Package-Explorer zur Verfügung stehen, zum Beispiel lokale Variablen.
Ein Dialog eröffnet auch die Möglichkeit den Benutzer in gewissen Grenzen mit
entscheiden zu lassen, wie eine Refaktorisierung durchzuführen ist. Überladene
Methoden beispielsweise zeichnen sich dadurch aus, dass sie häufig einen gemeinsamen Einsatzzweck und daher alle den gleichen Namen haben. In REFACOLA werden diese beim Umbenennen einer Methode nicht mit umbenannt. Dies ist sinnvoll,
da die Entscheidung dem Benutzer überlassen werden sollte, ob er dies möchte oder
nicht. Perspektivisch wäre es aber denkbar, ihm über den Benutzerdialog hierfür
eine Konfigurationsmöglichkeit zur Verfügung zu stellen. Da es in REFACOLA nicht
möglich ist, einzelne Regeln aus einem Regelset aufzurufen, müssten hier die Regeln auf mehrere Regelsets aufgeteilt und eine eigene Refaktorisierungsdefinition
geschrieben werden.
Diskussion
5.3.2
55
Java-Versionen
Die Entwicklung für REFACOLA basiert aktuell auf Java 1.6. Neben den noch offenen
Themen (vgl. Kapitel 4.3.4) sind ebenfalls Erweiterungen für die aktuelle Version
Java 1.7 vorzusehen45. Eine der wenigen Änderungen ist, dass nun Strings in
switch-Anweisungen unterstützt werden. Außerdem bietet diese Version einen besseren Support für dynamische Sprachen, was den Einsatz von diesen zusätzlich
fördern dürfte (vgl. Kapitel 5.2.2).
Umfangreiche Neuerungen sind jedoch für die angekündigte Version 1.8 zu erwarten46. Mit Lambdas47 hält ein neues Konzept Einzug in die Sprache, welches ähnlich
einschneidende Änderungen mit sich bringen könnte wie Generics oder Annotationen in Java 1.5. Mithilfe von ihnen sollen funktionale Sprachmittel in Java integriert werden. Lambdas sind als anonyme Methoden vorstellbar [Go11]. Einen ersten
Eindruck auf die zu erwartende Syntax vermittelt Listing 5.3.
//übernimmt die übergebenen integer-argumente, Rückgabe (x+y)
(int x, int y) -> x + y
// keine Argumente, Rückgabe 42 (integer)
() -> 42
//druckt den übergebenen String (Console), keine Rückgabe
(String s) -> { System.out.println(s);}
Listing 5.3: Ausblick voraussichtliche Lambda-Syntax (vgl. [Go11])
45
Eine Liste findet sich unter http://openjdk.java.net/projects/jdk7/features/#f618
Eine Übersicht der Java 1.8- Meilensteine unter: http://openjdk.java.net/projects/jdk8/milestones
47
http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
46
Schlußbetrachtung
56
6 Schlußbetrachtung
Refaktorisierung gewinnt zunehmend an Bedeutung. Dies mag unter anderem daran liegen, dass es in den immer beliebter werdenden agilen Vorgehensmodellen ein
fester Bestandteil ist.48 Aber auch Initiativen wie die Clean Code DeveloperBewegung helfen, die Vorteile von regelmäßiger Quelltextpflege bewusst zu machen.
Daher ist es wichtig, dass Entwickler bei dieser Arbeit von Werkzeugen zuverlässig
unterstützt werden, gerade für eine so häufig genutzte Refaktorisierung wie das
Umbenennen von Programmelementen (vgl. [MPB09, Sc10]). Bisher bestehende
Werkzeuge sind zwar nicht fehlerfrei, allerdings schon sehr mächtig. Einen vergleichbaren Stand zu erreichen ist nicht einfach.
Diese Arbeit zeigt, dass die Erreichung einer korrekten Refaktorisierung von Identifiern durchaus eine Herausforderung darstellt. Die Analyse aller Möglichkeiten,
welche die Sprache Java bietet, erfordert zum Teil viel Recherchearbeit. Informationen aus der Java-Sprachspezifikation lassen sich zum Teil nur schwer herauslesen. Auch in der gängigen Literatur finden sich meist Beispiele, welche sich sehr an
Konventionen orientieren, Spezialfälle werden dagegen häufig nicht berücksichtigt.
Neben den spezifischen Eigenschaften der Sprache selbst unterstreichen vor allem
die sprachübergreifenden Ansätze die Vielschichtigkeit des Themas.
Fazit
Dennoch konnte diese Arbeit einige Lücken für die Refaktorisierung von Identifiern
in REFACOLA schließen. Das Regelset ist bereits fest in das Framework integriert
und wird von allen bestehenden Refaktorisierungswerkzeugen eingebunden. Gerade
aber im Zusammenhang mit fehlenden Qualifiern und qualifizierten Namen stößt
allein der constraintbasierte Ansatz an Grenzen. Constraintregeln zu definieren, die
das Abschatten einer Instanzvariablen korrekt behandeln, ist nur mit einer Vielzahl
von Regeln denkbar. Außerdem wären Restriktionen nötig, wie es in Kapitel 4.3.3
erläutert wurde. Die Einbindung der Locked Bindings sollte daher eine wertvolle
Ergänzung werden. Eine solche Kombination von Konzepten wurde auch bereits in
[STST12] diskutiert.
Erweiterungen am REFACOLA-Framework für Faktengenerierung und Rückschreibekomponente, welche während dieser Arbeit durchgeführt wurden, dienen allen
Werkzeugen. Zudem wurde für die Ermittlung umschließender lokaler Variablen
ein Prototyp vorgestellt.
Die JUnit-Testsuite und die Erweiterung des RTT-Testframeworks unterstützen die
Weiterentwicklung für das hier erstellte Refaktorisierungswerkzeug auch in Zukunft. Weil die Unittests den ganzen Refaktorisierungsablauf testen, werden darüber hinaus auch Faktengenerierung und Rückschreibekomponente geprüft.
Schließlich wurde für die Einbindung der Locked Bindings eine Reihe von Testfällen
zur Verfügung gestellt, die deren Einbindung ins Framework unterstützen können.
Der Ansatz der constraintbasierten Refaktorisierung hat viele Stärken gezeigt. Mit
vergleichsweise wenigen und einfachen Regeln konnten viele Probleme der Identifier-Refaktorisierungen in Java gelöst werden. Die Erstellung der Regeln wird
durch den deklarativen Ansatz erleichtert. Zudem können die vorhandenen
Constraintregeln von allen Werkzeugen genutzt werden. Die verbleibenden offenen
Punkte erfordern jedoch noch einigen Aufwand.
48
Extreme Programming (XP) zum Beispiel definiert Refaktorisierung als eigene Praktik [WR11].
Literaturverzeichnis
57
Literaturverzeichnis
CCD
Clean Code Developer
Unter: http://www.clean-code-developer.de/
Letzter Zugriff: 24.06.2013
DP06
Florian Deissenboeck , Markus Pizka: Concise and consistent naming;
In: Software Quality Control, Volume 14 Issue 3, September 2006 , S. 261 – 282,
Kluwer Academic Publishers Hingham, MA, USA
Unter http://www.itestra.de/uploads/media/
06_itestra_concise_and_consistent_naming.pdf
Letzter Zugriff: 26.06.2013
EAPOGA11
Laleh M. Eshkevari, Venera Arnaoudova, Massimiliano Di Penta, Rocco Oliveto,
Yann-Gaël Guéhéneuc, Giuliano Antoniol: An Exploratory Study of Identifier Renamings ; In: MSR '11 Proceedings of the 8th Working Conference on Mining
Software Repositories, 2011, S. 33-42, ACM New York, NY, USA
Unter: http://www.ptidej.net/publications/documents/MSR11.doc.pdf
Letzter Zugriff: 24.06.2013
FU10
FernUniversität in Hagen: Prüfungsordnung für den Bachelorstudiengang Wirtschaftsinformatik an der FernUniversität in Hagen (Stand vom 14.07.2010); 2012
Unter: http://www.fernuni-hagen.de/
wirtschaftswissenschaft/studium/download/ordnungen/ba_winf_po.pdf
Letzter Zugriff: 24.06.2013
Fo11
Martin Fowler (With contributions by Kent Beck, John Brant, William Opdyke,
Don Roberts): Refactoring Improving the Design of Existing Code; Addison-Wesley
Longman, Inc., 25.Auflage 2011 (Erstausgabe 1999)
GJSB05
James Gosling, Bill Joy, Guy Steele, Gilad Bracha: The Java TM Language Specification – The (3rd Edition); Addison-Wesley, 3. Auflage 2005
Unter http://docs.oracle.com/javase/specs/jls/se5.0/jls3.pdf
Letzter Zugriff: 10.05.2013
Go11
Brian Goetz: State of the Lambda; 4th edition, Dezember 2011
Unter: http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-4.html
Letzter Zugriff: 24.06.2013
Ho10
Osama El Hosami: Implementierung eines Eclipse-Plugins zum automatisierten
Testen von Refaktorisierungswerkzeugen; Masterarbeit, FernUniversität in Hagen,
August 2010
Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/masterarbeit-elhosami.pdf
Letzter Zugriff: 24.06.2013
HW07
Hofstedt Petra, Wolf Armin: Einführung in die Constraint-Programmierung
Grundlagen,Methoden, Sprachen, Anwendungen; Springer-Verlag Berlin Heidelberg 2007
Unter: http://link.springer.com/book/10.1007/978-3-540-68194-6/page/1
Letzter Aufruf: 25.05.2013
Literaturverzeichnis
58
Kl10
Michael Klenk: Sprachübergreifendes Refaktorisieren: Code über Sprachgrenzen
hinweg verbessern; OBJEKTspektrum Ausgabe 04/2010
unter: http://www.sigs-datacom.de/fileadmin/user_upload/
zeitschriften/os/2010/04/klenk_OS_04_10.pdf
Letzter Zugriff: 24.06.2013
Ma11
Robert C. Martin: Clean Code A Handbook of Agile Software Craftsmanship
Prentice Hall Pearson Education, Inc., 9. Auflage 2011
MCo07
Steve Mc Connell: Code Complete (Deutsche Ausgabe der Second Edition)
Übersetzung Detlef Johannis , Microsoft Press Deutschland 2007,
dt. Ausg. der 2nd ed. [Nachdruck]
MPB09
Emerson Murphy-Hill, Chris Parnin, Andrew P. Black: How we refactor, and how
we know it; In: ICSE '09 Proceedings of the 31st International Conference on
Software Engineering, 2009, S. 287-297
Unter: http://people.engr.ncsu.edu/ermurph3/papers/icse09.pdf
Letzter Zugriff: 23.06.2013
MT04
Tom Mens, Tom Tourwé: A Survey of Software Refactoring; In: IEEE Transactions
on software engineering, Volume 30 Issue 2, February 2004, S. 126–139, IEEE
Press Piscataway, NJ, USA
Unter: http://www.cs.bgu.ac.il/~se112/wiki.files/class-6-Refactoring-surveysoftware-refactorings-04.pdf
Letzter Zugriff: 28.06.2013
Ni13
Sven Nicolai: Locked Bindings als Ergänzung des constraintbasierten
Refaktorisierungsframeworks Refacola für die Programmiersprache Java
Diplomarbeit, FernUniversität in Hagen
Bisher unveröffentlicht, geplant für 2013
Os12
Antje Osten: Sprachübergreifendes Refaktorisieren zwischen Ruby und Java
Bachelorarbeit, FernUniversität in Hagen, April 2012
Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/bachelorarbeitosten.pdf
Letzter Zugriff: 24.06.2013
Sc10
Max Schäfer, Specification, Implementation and Verification of Refactorings;
PhD thesis, Oxford University Computing Laboratory, 2010
Unter: http://www.cs.ox.ac.uk/people/max.schaefer/
Letzter Zugriff: 01.06.2013
SEM08
Max Schäfer, Torbjörn Ekmann, Oege de Moor: Sound and Extensible Renaming
for Java; In: Proceedings of the 23rd Annual ACM SIGPLAN Conference on ObjectOriented Programming, Systems, Languages, and Applications, OOPSLA 2008,
October 2008, Nashville, TN, USA; S. 277–294, ACM; 2008
Unter: http://staff.cs.utu.fi/kurssit/doos/JavaRenaming.pdf
Letzter Zugriff: 01.01.2013
SKP11
Friedrich Steimann, Christian Kollee, Jens von Pilgrim: A Refactoring Constraint
Language and its Application to Eiffel; In Proceeding ECOOP'11 Proceedings of
the 25th European conference on Object-oriented programming, S. 255-280,
Springer-Verlag Berlin, Heidelberg, 2011
Unter: http://www.feu.de/ps/docs/Refacola.pdf
Letzter Zugriff: 15.06.2013
Literaturverzeichnis
ST12
59
Friedrich Steimann, Andreas Thies: From Public to Private to Absent: Refactoring
Java Programms under Constrained Accessibility; In: Genoa Proceedings of the
23rd European Conference on ECOOP 2009 Object-Oriented Programming,
S. 419 – 443, Springer-Verlag Berlin, Heidelberg, 2009
Unter: http://www.fernuni-hagen.de/ps/pubs/ECOOP2009.pdf
Letzter Zugriff: 11.06.2013
St10
Friedrich Steimann: Korrekte Refaktorisierungen: Der Bau von Refaktorisierungswerkzeugen als eigenständige Disziplin; OBJEKTspektrum April 2010, S. 24–29
Unter: http://www.sigs-datacom.de/fileadmin/user_upload/zeitschriften/
os/2010/04/steimann_OS_04_10.pdf
Letzter Zugriff: 24.06.2013
STST12
Max Schäfer, Andreas Thies, Friedrich Steimann, Frank Tip: A Comprehensive
Approach to Naming and Accessibility in Refactoring Java Programs; In: IEEE
Transactions on SOFTWARE ENGINEERING Volume 38, Issue 6, November 2012,
S. 1233 – 1257
Unter : https://cs.uwaterloo.ca/~ftip/pubs/tse2012JL.pdf
Letzter Zugriff: 13.06.2013
TB12
Andreas Thies, Eric Bodden: Safer Refactorings for Reflective Java Programs;
In: Proceedings of the 21th International Symposium on Software Testing and
Analysis (ISSTA) 2012, S. 1-11 ACM New York, NY, USA, 2012
(ACM SIGSOFT Distinguished Paper Award)
Unter: http://www.feu.de/ps/prjs/rf/issta12.pdf
Letzter Zugriff: 13.06.2013
TH12
Marc Teufel, Dr. Jonas Helming: Eclipse 4 Rich Clients mit dem Eclipse SDK 4.2
entwickler.press,Frankfurt am Main, 1. Auflage 2012
TR10
Andreas Thies, Christian Roth: Recommending Rename Refactorings; In: Proceedings of the 2nd International Workshop on Recommendation Systems for Software Engineering 2010, S. 1-5, ACM New York, NY, USA, 2010
Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/rsse2010.pdf
Letzter Zugriff: 22.06.2013
Wa12
Mirco Wagner: Sprachübergreifendes Java-XML-Refaktorisieren mit Refacola;
Bachelorarbeit FernUniversität in Hagen, April 2012
Unter: http://www.fernuni-hagen.de/imperia/md/content/ps/bachelorarbeitwagner.pdf
Letzter Letzter Zugriff 30.05.2013
We06
Frank Westphal: Testgetriebene Entwicklung - mit JUnit & FIT Wie Software änderbar bleibt; dpunkt.verlag Heidelberg, 1. Auflage 2006
WR11
Henning Wolf, Arne Roock: Agile Softwareentwicklung – Ein Überblick
dpunkt.verlag GmbH Heidelberg, 3. Auflage 2011
SMG11
Gustavo Soares, Melina Mongiovi, and Rohit Gheyi: Identifying overly strong conditions in refactoring implementations; In: Proceedings of the 27th IEEE International Conference on Software Maintenance, September 2011, Seite 173 - 182
Unter: http://www.dsc.ufcg.edu.br/~spg/uploads/icsm2011.pdf
Zugriff 24.06.2013
Anhang
Anhang
60
A) Referenz Constraintregeln
61
A) Referenz Constraintregeln
Names.ruleset.refacola
UniqueMemberTypeNames1
for all
t: Java.MemberType
do
if
all(t)
then
t.identifier != t.enclosingNamedTypes.identifier ,
t.identifier != t.tlowner.identifier
end
UniqueTopLevelTypeIdentifier
for all
tlt1: Java.TopLevelType
tlt2: Java.TopLevelType
do
if
tlt1 != tlt2
then
(tlt1.hostPackage = tlt2.hostPackage)
-> (tlt1.identifier != tlt2.identifier)
end
PackageDeclarationNames
for all
packageDeclaration: Java.PackageDeclaration
typeRoot: Java.TypeRoot
do
if
Java.packageDeclarationInTypeRoot(
packageDeclaration, typeRoot)
then
packageDeclaration.identifier
= typeRoot.hostPackage.identifier
end
ImportDeclarationNames1
for all
importDeclaration: Java.ImportDeclaration
clazz: Java.NamedType
do
if
Java.importDeclarationInTypeRoot(
importDeclaration, clazz)
then
(importDeclaration.typeBinding = clazz)
->(importDeclaration.identifier=clazz.identifier)
end
ImportDeclarationNames2
for all
importDeclaration: Java.ImportDeclaration
tlt: Java.TopLevelType
do
if
all(importDeclaration),
all(tlt)
then
(importDeclaration.typeBinding != tlt)
->(importDeclaration.identifier != clazz.identifier)
end
UniqueMemberTypeNames2
for all
memberType1: Java.MemberType
memberType2: Java.MemberType
do
if
memberType1 != memberType2
then
(memberType1.owner = memberType2.owner)
-> (memberType1.identifier
!= memberType2.identifier)
end
TopLevelTypeNames
for all
tlt: Java.TopLevelType
tr: Java.TypeRoot
do
if
all(tlt), all(tr)
then
(tlt.typeRoot = tr and tlt.accessibility = #public)
-> (tlt.identifier = tr.identifier)
end
ReferenceIdentifier
for all
reference: Java.NamedTypedReference
entity: Java.NamedEntity
do
if
Java.binds(reference, entity)
then
reference.identifier = entity.identifier
end
TypeReferenceIdentifier
for all
type: Java.NamedType
ref: Java.TypeReference
do
if
all(type), all(ref)
then
(type = ref.typeBinding)
-> (type.identifier = ref.identifier)
end
UniqueFieldIdentifier
for all
field1: Java.Field
field2: Java.Field
do
if
field1 != field2
then
field1.owner = field2.owner
->(field1.identifier != field2.identifier)
end
A) Referenz Constraintregeln
UniqueMethodIdentifier
for all
method1: Java.RegularTypedMethod
method2: Java.RegularTypedMethod
do
if
method1 != method2
then
(method1.owner = method2.owner)
and(method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType)
-> (method1.identifier != method2.identifier)
end
AccidentalHidingFinalMethod
for all
method1: Java.Method
method2: Java.Method
do
if
Java.finalMethod(method2), all(method1)
then
(method1.identifier != method2.identifier)
or (not Java.sub(method1.owner,
method2.owner))
or
(not(method1.parameters.declaredParameterType
= method2.parameters.declaredParameterType))
or ((method1.tlowner != method2.tlowner)
and (method2.accessibility = #private))
or ((method1.hostPackage
!= method2.hostPackage)
and (method2.accessibility <= #package))
end
UniqueLocalVariableNames1
for all
parameter1: Java.FormalParameter
parameter2: Java.FormalParameter
method: Java.MethodOrConstructor
do
if
Java.declaresLocalVariableOrParameter(
method, parameter1),
Java.declaresLocalVariableOrParameter(
method, parameter2),
(parameter1 != parameter2)
then
(parameter1.identifier != parameter2.identifier)
end
UniqueLocalVariableNames2
for all
parameter: Java.FormalParameter
localVariable: Java.LocalVariable
methodOrConstructor: Java.MethodOrConstructor
do
if
Java.declaresLocalVariableOrParameter(
methodOrConstructor, parameter),
Java.declaresLocalVariableOrParameter(
methodOrConstructor, localVariable)
then
(parameter.identifier != localVariable.identifier)
end
62
EagerInterfaceMethodNames
for all
type: Java.Class
typeMethod: Java.InstanceMethod
interfaceMethod: Java.InstanceMethod
do
if
Java.eagerInterfaceImplementation(
type, typeMethod, interfaceMethod)
then
typeMethod.identifier = interfaceMethod.identifier
end
OverridingMethodNames
for all
superMethod: Java.InstanceMethod
subMethod: Java.InstanceMethod
do
if
Java.overrides(subMethod, superMethod)
then
subMethod.identifier = superMethod.identifier
end
UniqueEnclosingMemberTypeNamesInAssignment1
for all
field1: Java.InstanceField
field2: Java.RegularTypedInstanceField
fieldReference: Java.FieldReference
expression: Java.Expression
do
if
field1 != field2,
Java.binds(fieldReference, field1),
Java.isExpression(fieldReference, expression),
Java.initialAssignment(field2, expression)
then
field1.tlowner = field2.tlowner
and field1.owner != field2.owner
and Java.sub!(field2.owner,field1.owner)
->fieldReference.identifier != field2.identifier
end
UniqueEnclosingMemberTypeNamesInAssignment2
for all
field1: Java.InstanceField
field2: Java.InstanceField
fieldReference1: Java.FieldReference
fieldReference2: Java.FieldReference
expression1: Java.Expression
expression2: Java.Expression
do
if
field1 != field2,
Java.binds(fieldReference1, field1),
Java.binds(fieldReference2, field2),
Java.isExpression(fieldReference1, expression1),
Java.isExpression(fieldReference2, expression2),
Java.assignment(expression2, expression1)
then
field1.tlowner = field2.tlowner
and field1.owner != field2.owner
and Java.sub!(field2.owner,field1.owner)
->fieldReference2.identifier
!= fieldReference1.identifier
end
A) Referenz Constraintregeln
UniqueLocalVariableNames3
for all
localVariable: Java.LocalVariable
do
if
Java.hasEnclosingVariables(localVariable)
then
localVariable.identifier
!= localVariable.enclosingLocalVariables.identifier
end
63
ConstructorNames
for all
type: Java.NamedType
constructor: Java.Constructor
do
if
Java.instantiates(constructor, type)
then
type.identifier = constructor.identifier
end
Regelset Accessibility (Autor A.Thies)
AccidentalOverriding
for all
superMethod: Java.InstanceMethod
subMethod: Java.InstanceMethod
do
if
all (subMethod), all(superMethod)
then
Java.overrides(subMethod, superMethod)
or (not Java.sub(subMethod.owner,
superMethod.owner))
or (superMethod.identifier != subMethod.identifier)
or (not (subMethod.parameters.declaredParameterType
= superMethod.parameters.declaredParameterType))
or (superMethod.accessibility = #private)
or ((superMethod.accessibility = #package)
and (superMethod.hostPackage
!= subMethod.hostPackage))
end
InstanceMethodOverridingStaticMethod
for all
staticMethod: Java.StaticMethod
instanceMethod: Java.InstanceMethod
do
if
all(staticMethod), all(instanceMethod)
then
(staticMethod.identifier != instanceMethod.identifier)
or (not Java.sub(instanceMethod.owner,
staticMethod.owner))
or (not(
staticMethod.parameters.declaredParameterType =
instanceMethod.parameters.declaredParameterType))
or ((staticMethod.tlowner != instanceMethod.tlowner)
and (staticMethod.accessibility = #private))
or ((staticMethod.hostPackage
!= instanceMethod.hostPackage)
and (staticMethod.accessibility <= #package))
end
StaticMethodHidesInstanceMethod
for all
instanceMethod: Java.InstanceMethod
staticMethod: Java.StaticMethod
do
if
all(instanceMethod), all(staticMethod)
then
(instanceMethod.identifier != staticMethod.identifier)
or (not Java.sub(staticMethod.owner,
instanceMethod.owner))
or (not(
instanceMethod.parameters.declaredParameterTyp
= staticMethod.parameters.declaredParameterType))
or ((instanceMethod.tlowner != staticMethod.tlowner)
and (instanceMethod.accessibility = #private))
or ((instanceMethod.hostPackage
!= staticMethod.hostPackage)
and (instanceMethod.accessibility <= #package))
end
B) Refaktorisierungsdefinition RenameMember
64
B) Refaktorisierungsdefinition RenameMember
Im Rahmen dieser Arbeit wurde auch die Refaktorisierungsdefinition für das
RenameMember erweitert. Die vollständige Datei ist hier abgebildet.
/******************************************************************
* Copyright (c) 2012 FernUniversitaet in Hagen
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*****************************************************************/
import "Java.language.refacola"
import "Accessibility.ruleset.refacola"
import "Types.ruleset.refacola"
import "Locations.ruleset.refacola"
import "Names.ruleset.refacola"
import "Reflection.ruleset.refacola"
refactoring RenameMember
languages Java
uses Accessibility, Types, Names, Locations, Reflection
forced changes
identifier of Java.NamedEntity as NewName
allowed changes
/* rename NamedEntity -> update reference-Identifier */
identifier of Java.NamedReference {initial, NewName @ forced}
/* rename type <-> update Constructor-Identifier */
identifier of Java.Constructor {initial, NewName @ forced}
/* rename method */
identifier of Java.Method
{initial, NewName @ forced}
/* rename static method*/
identifier of Java.StaticMethod
{initial, NewName @ forced}
/* rename importdeclaration */
identifier of Java.ImportDeclaration
/* rename Method-Parameter */
identifier of Java.FormalParameter
/*rename memberType */
identifier of Java.MemberType
{initial, NewName @ forced}
{initial, NewName @ forced}
{initial, NewName @ forced}
// rename *.java file if top level type is renamed
identifier of Java.TypeRoot {initial, NewName @ forced}
// for the case that a type root is renamed
identifier of Java.TopLevelType {initial, NewName @ forced}
C) JUnit-Testsuite
65
C) JUnit-Testsuite
Das UML-Diagramm stellt eine Reihe von Testklassen vor, die in der Testsuite
AllReanameMemberTestsuite zusammengefasst sind. Die JUnit-Tests befinden
sich im Paket de.feu.ps.refacola.lang.java.jdt.ui.tests.renamemember
und wurden zur besseren Übersichtlichkeit nach einzelnen Schwerpunktthemen
aufgeteilt.
de.feu.ps.refacola.lang.java.jdt.ui.tests.renamemember
AllRenameMemberTestsuite
RenameMemberNestedMemberTests
RenameMemberAbstractTests
RenameMemberInterfaceTests
RenameMemberControlFlowTests
RenameMemberTopLevelTypeTests
RenameMemberExceptionTests
RenameMemberStaticsTests
RenameMemberEnumTest
RenameMemberLegalIdentifierTests
RenameMemberTests
RenameMemberEagerInterfaceTests
RenameMemberOperatorTests
RenameMemberUniqueIdentifierTests
RenameParameterTests
RenameLocalVariableTests
RenameMemberVariousTests
RenameMemberQualifiedNameTests
RenameMemberHidingTests
RenameMemberInheritanceTests
RenameMemberOverrideTests
RenameMemberShadowingTests
RenameMemberOverloadTests
RenameMemberErlandMuellerProblemTests
RenameMemberWithThisOrSuperReference
RenameLocalElementsTests
RenameLocalVariableInInitializerTests
RenameMemberEnclosingTypVariableNamesTests
offene Themen
RenameMemberAnonymousTests
RenameMemberAnnotationTests
RenameMemberImportTest
RenamePackageTests
RenameMemberGenericsTests
dienen der Dokumentation
Referenztests für Bugfixes
RenameMemberKnownBugsTests
RenameMemberReproduceRttBugs
Tests für diverse
Refaktorisierungswerkzeuge
RenameRulesForOtherRefactoringsTests
Abbildung C.1 Übersicht UML – Junit Tests
RenameMemberBugtrackTests
D) Inhalt der CD
D) Inhalt der CD
./dokumente
Enthält
die Bachlorarbeit als PDF
Installationsanleitung.pdf
./entwicklungsumgebung
Enthält
die Entwicklungsumgebung eclipse.juno inklusive aller Plugins
jdk1.6.0_27
Generierte Plugin-Versionen
./projektverzeichnis
Enthält
Workspace aller Refacola-Projekte mit dem lokalen Entwicklungsstand
vom 07.07.2013
Workspace RTT-Testprojekte
./teststatistiken
Enthält
Statistiken der RTT-Tests
66
Eidesstattliche Versicherung
67
Eidesstattliche Versicherung
„Hiermit versichere ich an Eides statt, dass ich die Bachelorarbeit selbstständig und
ohne Inanspruchnahme fremder Hilfe angefertigt habe. Ich habe dabei nur die angegebenen Quellen und Hilfsmittel verwendet und die aus diesen wörtlich oder inhaltlich entnommenen Stellen als solche kenntlich gemacht. Die Arbeit hat in gleicher oder ähnlicher Form noch keiner anderen Prüfungsbehörde vorgelegen.
Ich erkläre mich damit einverstanden, dass die Arbeit mit Hilfe eines Plagiatserkennungsdienstes auf enthaltene Plagiate überprüft wird.“49
Ried, den 09.07.2013
49
Formulierung lt. Prüfungsordnung (Stand vom 14.07.2010) - §15 Bachlorarbeit (7) (vgl. [FU10])
Herunterladen