Rahmenbedingungen un..

Werbung
Das Java-Projekt
Einführung
Das Ziel des vorliegenden Projekts ist es, Programme in der Programmiersprache Java analysieren
und modifizieren bzw. erstellen zu können. Dies ist u. a. nützlich
• beim Erstellen von Metriken (Operationen/Methoden pro Klasse, Klassen pro Package, Anweisungen pro Methode u. s. w.)
• beim Finden von angewandten Entwurfs-Mustern in fremdem Code
• beim Finden von Fehlern, etwa im Bereich Nebenläufigkeit (Verklemmungen, fehlende Synchronisation u. s. w.)
• zur Generierung von Programmen oder Programmteilen aus formalen Spezifikationen (vgl. Modell-getriebene Entwicklung!), etwa Datenbank-Anbindungen
• zum Einbauen von Idiomen (etwa Getter/Setter statt Direktzugriff auf Attribute)
• zum Einbauen von Entwurfs-Mustern (etwa das Befehls-Muster durch Kapselung von MethodenAufrufen in Befehlsklassen)
• zur Refaktorisierung (also Veränderung innerer Struktur unter Beibehaltung äußerer Schnittstellen) von Programmen unter Berücksichtigung und Mit-Veränderung vorhandener Datenbestände
Der letzte Punkt ist aktives Forschungsthema an der FHDW Hannover. Im Rahmen des Forschungsprojekts soll das hier entwickelte Programm bzw. Rahmenwerk seine Anwendung finden in der
praktischen Validierung theoretischer Erkenntnisse im Bereich Refaktorisierung von Informationssystemen.
Das Dokument ist folgendermaßen gegliedert: Die Kapitel sind den einzelnen Entwicklungsteams
zugeordnet und beinhalten in den untergeordneten Abschnitten allgemeine Rahmenbedingungen,
die einzelnen Aufgaben der Entwickler und Ausbaustufen des Projekts sowie die Zuordnung der
Entwickler zu den Aufgaben.
Entwicklungsumgebung
Das Projekt wird in Java entwickelt, vorzugsweise in der Entwicklungsumgebung Eclipse. Die Dokumentation und Quelltexte sind auf dem FHDW-eigenen Entwicklungsserver versioniert abgelegt
und können mit Hilfe eines Subversion-Klienten unter der URL https://svn.ha.bib.de/svn/repos/programgeneration/ abgerufen werden; dieser Server ist auch von außen erreichbar. Ein SubversionKlient ist in der von der FHDW bereitgestellten Eclipse-Umgebung bereits enthalten. Nähere Informationen zum Umgang mit der Versionsverwaltung werden in der Veranstaltung mitgeteilt.
Ebenfalls Teil des Projekts ist ein Fehlerverfolgungssystem (Bugzilla), das den Entwicklern behilflich sein soll, gefundene Fehler zu kategorisieren, an Entwickler zu verteilen und deren Behebung
nachzuhalten. Das System ist unter der URL https://svn.ha.bib.de/bugzilla/ zu erreichen. Auch hierzu erfolgen in der Veranstaltung gesonderte Hinweise.
Gruppe HFI402
Rahmenbedingungen für das Java-Projekt
Generelle Vorgabe: Jedes Programm, das von dem zu entwickelnden Java-Framework als
gültig akzeptiert wird, muss auch den Regeln der Java-Programmiersprache entsprechen!
(Das Umgekehrte ist nicht der Fall: Nicht jedes korrekte Java-Programm muss auch vom Framework als korrekt erkannt werden. Insbesondere wird zuerst nur eine Teilmenge der Java-Programmiersprache unterstützt werden.)
Diese Regel gilt selbstverständlich nur insoweit, als dass entsprechende Komponenten existieren,
welche die erforderlichen Überprüfungen durchführen können. Solange bspw. keine Typüberprüfung entwickelt ist, können Java-Programme akzeptiert werden, welche die Typregeln verletzen.
Jede Gruppe ist angehalten, ihren Quellcode ausreichend zu kommentieren. Insbesondere sollten für jede Operation/Methode JavaDoc-Kommentare geschrieben werden. Absolutes Minimum ist
die Dokumentation der öffentlichen Schnittstellen einer Komponente.
Gruppe “Scanner”
•
Die von der Scanner-Komponente erkannten lexikalischen Elemente sollen in strukturierten Token-Objekten gekapselt werden, erweitert um eine Positionsangabe (Zeile und Spalte des TokenAnfangs innerhalb der untersuchten Datei).
•
Der erzeugte Token-Strom soll mit einem expliziten Ende-Token abgeschlossen sein.
•
Die Scanner-Komponente soll keine Ausnahmen werfen, vielmehr sollen Fehler über spezielle
(Fehler-)Token weiteren Komponenten signalisiert werden.
•
Zum Erkennen von lexikalischen Elementen bieten sich u. U. verschiedene Methoden der JavaKlasse Character an, etwa isJavaIdentifierStart, isJavaIdentifierPart
oder isDigit.
•
Die Token-Schnittstelle muss mit der Parser-Gruppe abgesprochen werden!
Gruppe “Parser”
•
Die syntaktischen Elemente sollten über entsprechende Methoden der Parser-Komponente erkannt werden, etwa parseClass für das Parsen von Klassen oder parseMember für das Parsen von Elementen innerhalb der Klasse. Diese Methoden sollen entweder das entsprechende
Objekt der Modell-Komponente zurückliefern oder eine geeignete Ausnahme auswerfen.
•
Ausnahmen können vorerst unstrukturiert bleiben. Es bietet sich an, die grundlegende Ausnahmeklasse ParserException zu nennen und von Exception abzuleiten. Es sollte ein Konstruktor vorhanden sein, der eine Nachricht sowie ein Token-Objekt (zur Ermittlung des Fehlerkontextes) entgegennimmt.
•
Die Nachrichten für die verschiedenen Ausnahmen, die während des Parsens auftreten können,
sollen in der Parser-Komponente an zentraler Stelle gekapselt werden.
•
Die Modell-Schnittstelle muss mit der Modell-Gruppe abgesprochen werden!
Gruppe “Modell”
•
Die Klassen, die syntaktische Elemente von Java repräsentieren, sollen Konstruktoren enthalten,
die alle zur Erzeugung benötigten Informationen als Argumente entgegennehmen. (Objekte des
Modells werden also über genau einen Konstruktor-Aufruf komplett erzeugt.)
•
Es bietet sich an in Hinblick auf zukünftige Erweiterungen, sich Gedanken über eine geeignete
Abbildung des Java-Typ-Mechanismus in die Modellklassen zu machen. (Eine Basisklasse Type
mit einer Spezialisierung PrimitiveType sollte eine gute Basis für das weitere Vorgehen
sein.)
•
Es müssen die notwendigen Voraussetzungen geschaffen werden, um Besucher des Modells (im
Sinne des Visitor-Patterns) zu unterstützen. Die Zusammenarbeit mit der Drucker-Gruppe muss
gewährleistet sein!
Gruppe “Drucker”
•
Es muss lediglich ein Layout unterstützt werden.
Gruppenzusammensetzung der Studenten:
Gruppe 1:
Sascha Meyer, Bert Peters, Lisa Shekhter
Gruppe 2:
Lars Ente, Carsten Feldhaus, Marcel Lohmann, Michael Peters
Gruppe 3:
Axel Evers, Daniel Gattermann, Olaf Strauß, Tobias Jüttner
Gruppe 4:
Mario Moldenhauer, Dennis Linke, Maxim Astafev, Michael Krüger
1. Ausbaustufe: Klassendefinition mit Attributen
•
nur eine Datei
•
nur eine Klasse pro Datei
•
nur Sichtbarkeit public für die Klassendefinition
•
nur Attribut-Definitionen innerhalb der Klasse (keine Operationen/Methoden, keine inneren
Klassen)
•
nur Basistypen int und boolean
•
nur Attribut-Definitionen ohne Initialisierung
•
nur package-scoped Attribut-Definitionen (ohne explizit angegebene Sichtbarkeit)
Der zu akzeptierende Code hat also folgendes Aussehen:
public class class-name {
type1 attr-name1;
type2 attr-name2;
// ... (beliebig viele (*) Attribut-Definitionen)
}
Zieltermin: 02.08.2004
Gruppenzuordnung:
Scanner:
Parser:
Modell:
Drucker:
Gruppe 4
Gruppe 3
Gruppe 2
Gruppe 1
2. Ausbaustufe: Abstrakte Operationen
•
Operationen ohne explizit angegebene Sichtbarkeit
•
void
•
Parameterlisten
•
Attribute und Operationen können gemischt auftreten
•
abstract im Klassenkopf, darf vor und hinter public stehen
Beispiel:
public abstract class class-name {
abstract int getSomething();
abstract void setSomething (int something);
abstract void doSomething
(int first, boolean second, int third);
}
Zieltermin: 23.08.2004
Gruppenzuordnung:
Scanner:
Parser:
Modell:
Drucker:
Gruppe 3
Gruppe 2
Gruppe 1
Gruppe 4
3. Ausbaustufe: Initialisierung von Attributen
In dieser Ausbaustufe soll es möglich sein, Attribute zu initialisieren. Beispiel:
public class SectorCache {
int items = 0;
int itemSize = 512;
int maxSize = 1024 * 1024;
int maxItems = (maxSize+itemSize–1) / itemSize;
boolean haveFewMemory = false;
boolean preallocMemory =
(maxSize <= 64 * 1024) ||
(!haveFewMemory && 2048 * 1024 >= maxSize)
? true : false;
}
Folgende Einschränkungen sollen dabei gelten:
•
Ausdrücke können folgende unäre Operatoren enthalten: ! (nicht), + (Plus), - (Minus)
•
Ausdrücke können folgende binäre Operatoren enthalten: + (Addition), - (Subtraktion), * (Multiplikation), / (Division), % (Modulo), == (gleich), != (ungleich), < (kleiner), > (größer), <=
(kleiner oder gleich), >= (größer oder gleich), && (logisches UND), || (logisches ODER)
•
Ausdrücke können folgende ternäre Operatoren enthalten: ?: (entweder-oder)
•
Die Rangordnung der Operatoren ist wie folgt:
(höchste)
!, + (unär), - (unär)
*, /, %
+ (binär), - (binär)
<, >, <=, >=
==, !=
&&
||
(niedrigste)
?:
•
(Runde) Klammern ((, )) erlauben das Gruppieren von (Unter-)Ausdrücken
•
An Literalen sollen unterstützt werden: ganze Zahlen ohne Vorzeichen (z. B. 0, 1234) und boolesche Literale (true, false)
Zieltermin: 13.09.2004
Gruppenzuordnung:
Scanner:
Parser:
Modell:
Drucker:
Gruppe 2
Gruppe 1
Gruppe 4
Gruppe 3
4. Ausbaustufe: Methoden und einfache Anweisungen
Diese Ausbaustufe erweitert das System um die Verarbeitung einfacher Methoden. Insbesondere
sollen folgende Konstrukte unterstützt werden:
•
Methoden(-rümpfe)
•
(eventuell verschachtelte) Anweisungsblöcke
•
Definitionen von lokalen Variablen
•
return-Anweisungen
•
Zuweisungen
•
Methodenaufrufe
•
public-, protected- und private-Modifizierer bei Attribut- und Operations-/MethodenDefinitionen
Beispiel 1 (Einfacher Zähler):
public class Counter
{
private int counter = 0;
public void increment (int delta) {
int newValue = counter + delta;
set (newValue);
}
public void decrement (int delta) {
int newValue = counter – delta;
set (newValue);
}
public void set (int value) {counter = value;}
public int get () {return counter;}
}
Beispiel 2 (Wochentagsbestimmung; 0=Sonntag, 1=Montag usw.):
public class WeekDay
{
public int fromDate (int day, int month, int year) {
int k = month <= 2 ? 1 : 0;
int l = year – k;
int o = k * 12 + month;
int p1 = l / 400;
int p2 = l / 100;
int p3 = 5 * l / 4;
int p4 = 13 * (o + 1) / 5;
return (p4 + p3 – p2 + p1 + day – 1) % 7;
}
}
Zieltermin: 01.10.2004
Gruppenzuordnung:
Scanner:
Parser:
Modell:
Drucker:
Gruppe 1
Gruppe 4
Gruppe 3
Gruppe 2
5. Ausbaustufe (Praxisphase): Erweiterte Syntaxprüfung, mehrere Klassen und
Kommentare
In dieser Ausbaustufe wird das System um die folgenden Eigenschaften erweitert:
•
Verarbeitung (Ignorieren bzw. Speichern) von (normalen bzw. JavaDoc-)Kommentaren
•
Neue Komponente “Identifier” führt die Namensauflösung von Objekt-, Operations- und TypBezeichnern durch (z. B. ordnet sie einem Operationsaufruf die zugehörige Operation zu)
•
Eine oder mehrere neue Komponenten zum Prüfen syntaktischer und semantischer Regeln, die
bisher unberücksichtigt geblieben sind (s. u.)
•
Erweiterung des Treiber-Programms um die Fähigkeit, alle Java-Dateien in einem Verzeichnis
und mehrere Klassen pro Java-Datei in einem Aufruf verarbeiten zu können (d. h. eine ModellInstanz überdauert eventuell mehrere Scanner- und Parser-Durchläufe, s. u.)
Es werden wieder vier Gruppen gebildet. Jede Gruppe bearbeitet eine der oberen Aufgaben.
Die zusätzlich zu prüfenden syntaktischen und semantischen Eigenschaften von Java-Programmen
sind von der jeweiligen Gruppe zu erarbeiten und sinnvoll auf Komponenten zu verteilen. Es sind
jeweils zwei syntaktische und zwei semantische Bedingungen auszuwählen und zu implementieren.
Es folgt jeweils ein Beispiel zur Verdeutlichung:
•
syntaktisch: Prüfung auf eine verbotene mehrfache Anwendung von Modifizierern:
public public class X { // zweimal public
public private int x; // public und private
}
•
semantisch: Prüfung der Angemessenheit des abstract-Modifizierers:
// Klasse hat abstrakte Operationen, aber keinen abstract-Modifier im Klassenkopf
public class Y { public abstract void op(); }
Die Gruppe, die das Treiber-Programm erweitert, muss dafür Sorge tragen, dass folgende Java-Regeln eingehalten werden:
•
höchstens eine public-Klasse pro Datei
•
der Name einer eventuell enthaltenen public-Klasse muss mit dem Dateinamen (abgesehen
von der Dateinamen-Erweiterung) übereinstimmen
•
weitere Klassen innerhalb einer Datei müssen package-scoped sein (d. h. nicht public)
•
auf oberster Ebene sind “sporadische” Semikola erlaubt
Beispiel 1: A.java:
;;class B{};
public class A {};;
class C{}
Beispiel 2: B.java:
class B {};
Beispiel 3: C.java:
(Leere Dateien sind auch erlaubt!)
Zieltermin: Erster Projekttermin in der Woche 03.01.–07.01.2005
Gruppenzuordnung:
Kommentare:
Identifier:
Erweiterte Regelprüfung:
Erweiterung des Treibers:
Gruppe 2
Gruppe 3
Gruppe 1
Gruppe 4
6. Ausbaustufe: Refaktorisierung und Code-Review
Die entwickelte Anwendung soll gründlich überarbeitet werden. Ziel ist es, gemäß definierter Vorgaben ein “einheitliches Bild” aller Komponenten zu erhalten. Um das zu erreichen, wollen wir
Konventionen festlegen, die wir in drei Gruppen einteilen wollen: Kommentare, syntaktische Richtlinien und semantische/strukturelle Vorgaben.
1. Kommentare
•
durchgehend englisch
•
kommentiert werden:
➔
alle Operationen (sowohl öffentliche als auch private)
➔
Klassen und Schnittstellen
➔
Attribute
•
der Kommentar einer Operation dokumentiert die Schnittstelle so, dass die Benutzung der
Operation ohne ein Studium des Quelltextes möglich ist, d. h. Eingabe, Ausgabe, Funktionsweise und Ausnahmen sind ausreichend dokumentiert (letzteres beinhaltet auch Beschränkungen oder Annahmen, die bei bestimmten Parametern gelten!)
•
die Kommentare erfüllen die JavaDoc-Syntax (Tags verwenden wenn angebracht: @param, @return, @exception)
•
innerhalb von Methoden werden keine Kommentare geschrieben; wenn sie notwendig erscheinen → Code in neue Methode verschieben
•
verwendete Muster werden explizit dokumentiert (beteiligte Klassen, Operationen, Attribute; Rollen-Zuordnung); Referenz ist dabei “Design-Patterns” von Gamma et al.
•
triviale get- und set-Operationen (s. u.) müssen nicht kommentiert werden
•
bei redefinierenden Methoden ohne Ergänzung: Kommentar weglassen; ansonsten @seeTag benutzen
2. Syntaktische Richtlinien
•
Bezeichner durchgehend englisch
•
die üblichen Java-Benennungs-Konventionen beachten:
➔
Klassen mit Großbuchstaben beginnen
➔
Konstanten durchgängig groß, bei mehreren Wörtern durch Unterstriche trennen
➔
Operations- und Attribut-Namen klein beginnen
➔
bei mehreren Wörtern innerhalb eines Bezeichners: jedes Wort groß beginnen
•
die Länge einer Methode sollte etwa 25-30 Zeilen nicht überschreiten
•
maximal vier Einrückungs-Ebenen sind erlaubt
•
keine geschachtelten Iterationen
•
geschachtelte Fallunterscheidung höchstens bis zur dritten Ebene
•
keine leeren catch-Blöcke; wenn doch notwendig (etwa Parser), Methode aufrufen, die
“nichts tut”, um Code einfacher erweitern zu können
•
keine protected- und public-Attribute
•
Konstanten werden in einer eigenen Klasse gesammelt (Eigenschaften: public abstract; Bennenung: ...Constants; enthält nur Konstanten-Definitionen)
•
Konstruktor-Parameter, die zur Initialisierung von Attributen gedacht sind, heißen genauso wie die Attribute
•
eine triviale get-Operation für ein Attribut x vom Typ T wird folgendermaßen realisiert:
T getX () {
return this.x;
}
•
eine triviale set-Operation für ein Attribut x vom Typ T wird folgendermaßen realisiert:
void setX (T x) {
this.x = x;
}
•
auf Attribute und Operationen der Klasse wird generell über this zugegriffen; der direkte Zugriff auf Attribute sollte dabei nur innerhalb der entsprechenden get- und set-Methoden erfolgen
•
boolesche Operationen heißen vorzugsweise is... oder has...; Ausnahmen müssen
entsprechend begründet werden!
3. Semantische/strukturelle Vorgaben
•
•
Ausnahmen
➔
Ausnahme-Klassen existieren in einer einheitlichen Klassen-Hierarchie mit geeigneten Abstraktionen
➔
jede konkrete Ausnahme wird über eine eigene Klasse abgebildet
➔
jede Ausnahme-Klasse enthält so viel Kontext-Information wie nur möglich
Visitoren
➔
Typ-Abfrage und behandelnden Code in Visitoren auslagern; keine is...-Operationen verwenden (Ausnahme: Token-Hierarchie)
➔
Visitoren verwenden Attribut als Rückgabetyp (mit zugehöriger Zugriffs-Operation) und verwenden für jeden rekursiven Aufruf ein neues Objekt. Beispiel:
public abstract class Component {
public abstract void accept (CountVisitor v);
}
public class Leaf extends Component {
public void accept (CountVisitor v) {
v.accept (this);
}
}
public class Composite extends Component {
public void accept (CountVisitor v) {
v.accept (this);
}
public Iterator iterator () {
// ...
}
}
public class CountVisitor {
private int count; // Rückgabewert des Visitors
CountVisitor () {
this.count = 0; // Initialisierung
}
int getCount () { // Operation zum Zugriff auf das Ergebnis
return this.count;
}
void handleLeaf (Leaf leaf) {
this.count = this.count + 1;
}
void handleComposite (Composite composite) {
Iterator it = composite.iterator ();
while (it.hasNext ()) {
// Rekursion über neues Objekt
CountVisitor v = new CountVisitor ();
// accept liefert nichts direkt zurück
it.next ().accept (v);
// Ergebnis wird aus dem verwendeten Visitor gelesen
this.count += v.getCount ();
}
}
}
➔
Visitoren kapseln Ausnahmen aus besuchten Objekten in einer Wrapper-Klasse
namens ...VisitorException. Alle Operationen in einer abstrakten
...Visitor-Klasse haben eine entsprechende throws-Klausel, ebenso die
accept-Operation in der abstrakten Modell-Klasse. Beispiel:
// kapselt Modell-Ausnahmen für das Tunneln aus dem Visitor heraus
public class ModelVisitorException {
private ModelException exception; // gekapselte Ausnahme
public ModelVisitorException (ModelException exception) {
this.exception = exception; // Ausnahme speichern
}
public ModelException getException () {
return this.exception:
}
}
// abstraktes Modell-Objekt
public abstract class ModelObject {
public abstract void accept (ModelVisitor v)
throws ModelVisitorException;
}
// Ausdruck als konkretes Modell-Objekt
public class Expression extends ModelObject {
public String evaluate ()
throws DivisionByZeroModelException {
// ...
}
public void accept (ModelVisitor v)
throws ModelVisitorException {
v.handleExpression (this);
}
}
// abstrakter Visitor
public abstract class ModelVisitor {
public abstract void handleExpression (Expression expression)
throws ModelVisitorException;
}
// konkreter Visitor
public class EvaluateModelVisitor extends ModelVisitor {
private String result;
public String getResult () {
return this.result;
}
public void handleExpression (Expression expression)
throws ModelVisitorException {
try {
this.result = expression.evaluate ();
}
catch (DivisionByZeroModelException x) {
// Ausnahme wird eingepackt und getunnelt
throw new ModelVisitorException (x);
}
}
}
// Benutzung:
try {
EvaluateModelVisitor v = new EvaluateModelVisitor ();
expression.accept (v);
return v.getResult ();
}
catch (ModelVisitorException e) {
// Modell-Ausnahme wird ausgepackt
ModelException modelException = e.getException ();
// Beispiel 1: Ausgabe
System.out.println
("Modell-Ausnahme: " + modelException.getMessage ());
// Beispiel 2: Weiterreichen
throw modelException;
}
Zieltermin: 21.01.2005
Gruppenzuordnung:
Scanner:
Parser:
Modell + Visitor-Schnittstellen:
Treiber + “Rest”:
Gruppe 4
Gruppe 3
Gruppe 2
Gruppe 1
7. Ausbaustufe: Anweisungen
In dieser Ausbaustufe wird der potentielle Inhalt von Methoden um die folgenden Anweisungen erweitert:
•
die leere Anweisung (;)
•
Fallunterscheidung (if mit optionalem else)
•
Schleifen (while-, do-, for-Schleife)
•
break und continue (ohne Sprungmarken)
Beispiel:
public class FactorialProvider {
public int facIter1 (int n) {
int result = 1;
while (n > 0) {
result = result * n;
n = n – 1;
}
return n;
}
public int facIter2 (int n) {
int result = 1;
for (int i = n; i > 0; i = i - 1)
result = result * n;
return n;
}
public int facIter3 (int n) {
int result = 1;
do {
if (n == 0)
break;
result = result * n;
n = n – 1;
} while (n > 0);
return result;
}
public int facRec (int n) {
if (n == 0)
return 1;
else
return n * facRec (n – 1);
}
}
In for-Schleifen sind im Initialisierungs-Teil sowohl ein Ausdruck als auch die Definition einer lokalen Variable erlaubt, die nur innerhalb der for-Schleife existiert. Beispiel:
{
int i;
for (i = 1; i < 10; i = i + 1) // for mit Ausdruck
handle (i);
reached (i); // OK, i ist bekannt
}
{
for (int i = 1; i < 10; i = i + 1) // for mit lokaler Definition
handle (i);
// reached (i); // falsch, i hier unbekannt
}
Zieltermin: 04.02.2005
Gruppenzuordnung:
Scanner:
Parser:
Modell + Identifier-Visitor:
Drucker:
Gruppe 3
Gruppe 2
Gruppe 1
Gruppe 4
Analyse-Zusatzaufgabe für die Drucker-Truppe:
Es sollen alle Verstöße der derzeitigen Implementierung gegen die Java-Syntax und -Semantik systematisch analysiert und katalogisiert werden. Ziel ist es, die Abweichungen zu dokumentieren, um
sie in einer späteren Version der Software leichter korrigieren zu können.
Beispiel: Der Ausdruck
boolean b = 1 + 2;
wird akzeptiert, obwohl die Typen nicht zueinander passen → Kategorie “Typ-Kompatibilität”, Eintrag “Zuweisungen werden nicht auf Typ-Kompatibilität geprüft”.
8. Ausbaustufe: Gültigkeitsbereiche und Typen
Diese Ausbaustufe erweitert das System um grundlegende Fähigkeiten im Bereich der Objekttypen.
Weiterhin wird die Identifier-Komponente in der Funktionalität verbessert und für weitere Ausbaustufen entsprechend vorbereitet.
Die einzelnen Aufgaben sind zum Teil eng aneinander gekoppelt und erfordern deshalb eine schnelle Absprache der Schnittstellen in der Anfangsphase der Entwicklung, damit danach die Aufgaben
unabhängig voneinander gelöst werden können.
8.1 Gültigkeitsbereiche
Die Aufgabe ist, den Identifier so zu verändern, dass die Auflösung von Namen über mehrere Gültigkeitsbereiche hinweg einheitlich funktioniert. Dazu muss ein geeignetes Scope-Konzept (Scope =
Gültigkeitsbereich) entwickelt werden. Jede Deklaration findet innerhalb eines Gültigkeitsbereiches
statt; das Suchen einer passenden Deklaration findet “aufwärts” vom “aktuellen” Scope (Ort der
Verwendung) bis hin zum “globalen” Scope (außerhalb aller Top-Level-Klassen) statt.
Gegeben sei folgendes Beispiel:
public class A
{
private int x; // scope(x) == "A"
private int y; // scope(y) == "A"
public int f ( // scope(f) == "A"
int x
// scope(x) == "A.f"
)
{
int z = y + x; // scope(z) == "A.f"; y --> A.y, x --> A.f.x
int y = z + x; // scope(y) == "A.f"; z --> A.f.z, x --> A.f.x
return z * y; // z --> A.f.z, y --> A.f.y
}
public int g () { // scope(g) == "A"
return x;
// x --> A.x
}
public int h () { // scope(h) == "A"
int result = 0;
// scope(result) == "A.h"
for (int x = 1;
// scope(x) == "A.h.for"
x < 10; x = x + 1) // ; x --> A.h.for.x
{
int y
// scope(y) == "A.h.for.{"
= x * x; // x --> A.h.for.x
result = result // result --> A.h.result
+ y;
// y --> A.h.for.{.y
}
return result;
// result --> A.h.result
}
}
In diesem Beispiel werden die Namen x und y je nach Kontext (= je nach Scope) unterschiedlich
aufgelöst. Die hypothetische scope-Funktion weist beispielhaft jeder Deklaration den zugehörigen
Gültigkeitsbereich zu; die Namensauflösung wird über Pfeile symbolisiert.
8.2 Typ-Referenzierung und Typen von Ausdrücken
So wie bisher Namen von Variablen, Parametern, Attributen und Operationen aufgelöst wurden,
sollen jetzt Typ-Namen aufgelöst werden, d. h. die Verwendung eines Typs (z. B. einer Klasse) soll
der entsprechenden Definition zugeordnet werden. Dazu muss die bestehende Identifier-Komponente um die Behandlung von Typ-Verwendungen ergänzt werden.
Danach soll das System um die Funktionalität erweitert werden, zu jedem bisher unterstützten Ausdruck den passenden Typ zu ermitteln. Wir benötigen also (abstrakt betrachtet) die Funktion:
getType : Expression -> Type
die beispielsweise folgende Ergebnisse zurückliefert:
// Beispiel-Klasse zur Verdeutlichung
class A {
private int i;
public A f (int x, int y) {
// hier werden die getType-Aufrufe (s.u.) durchgeführt
}
}
getType
getType
getType
getType
(i) == "int"
(2 + 3) == "int"
(2 == i ? true : false) == "boolean"
(f (42, i * 7)) == "A"
8.3 Typ-Kompatibilität
Aufbauend auf der Schnittstelle zum Ermitteln des Typs eines Ausdrucks soll hier an allen Stellen
im Java-System, an denen eine Typ-Prüfung notwendig ist, diese durchgeführt und bei Nicht-Einhaltung der Java-Regeln zur Typ-Kompatibilität ein entsprechender Fehler generiert werden. TypPrüfungen sind unter anderem bei der Argument-Übergabe an Methoden, bei Zuweisungen und anderen Ausdrücken notwendig.
Zuerst soll analysiert werden, was Typ-Kompatibilität in Java bedeutet (natürlich im Rahmen des
bisher implementierten Sprachumfangs). Danach sollen alle Fälle lokalisiert werden, in denen eine
Typ-Prüfung notwendig ist. Schließlich soll die Typ-Überprüfung an diesen Stellen implementiert
werden.
8.4 Korrekturen
Diese Gruppe hat die Aufgabe, Schwächen im Modell und anderen Komponenten des Systems auszumerzen und eine stabile Grundlage für die weitere Erweiterung des Systems zu schaffen. Grundlage für die Änderungen ist zum einen die Liste der Syntax- und Semantik-Verletzungen, die in der
letzten Ausbaustufe von der Drucker-Gruppe erarbeitet wurde. Zum anderen ist Folgendes zu korrigieren:
•
Modell: EmptyExpression: Diese Abstraktion ist problematisch und sollte eliminiert werden.
•
Scanner: Der Scanner darf gemäß einer Design-Entscheidung keine Ausnahmen auswerfen. Es
ist zu prüfen, ob diese Entscheidung konsequent beibehalten wurde, da zumindest eine Ausnahme-Klasse in dem Scanner-Paket existiert und benutzt wird.
•
Parser: Der Parser ist auf fehlerhafte Gruppierungen von Operatoren zu überprüfen. In Ausbaustufe 7 verarbeitete der Parser Ausdrücke mit mehreren relationalen Operatoren nicht korrekt
(etwa 2 > 3 < true <= false).
•
System: Die Entscheidung der 5. Ausbaustufe, leere Dateien zu akzeptieren, ist nicht Java-konform. Das System ist so umzubauen, dass mindestens eine Klasse in einer Datei enthalten sein
muss.
Analyse-Zusatzaufgabe:
Die Korrekturen-Gruppe soll sich zusätzlich mit dem Thema “Überladung von Operationen” beschäftigen. Insbesondere soll geklärt werden, welche Komponenten des Systems angepasst werden
müssen, um Überladung zu ermöglichen. Bei diesen Überlegungen soll von einem System der 8.
Ausbaustufe ausgegangen werden (also mit angepasstem Identifier, Typ-Identifizierung, Typ-Zuweisung zu Ausdrücken und Typ-Kompatibilität)!
Zieltermin: 18.02.2005
Gruppenzuordnung:
Gültigkeitsbereiche:
Typ-Zuordnung:
Typ-Kompatibilität:
Korrekturen + Analyse “Überladung”:
Gruppe 3
Gruppe 4
Gruppe 1
Gruppe 2
8.5 Allgemeine Hinweise
8.5.1 Richtlinien
1. Allgemeine Entwurfsentscheidungen werden für das Gesamtprojekt festhalten!
2. Jeder entdeckte “Fehler” wird festgehalten! (ToDo-Liste für “Korrekturen”)
3. Jeder Punkt auf dieser Liste wird in der Architektur-Gruppe diskutiert und entschieden!
•
kein Problem → erledigt mit Datum und Begründung
•
ein Problem! → Lösungsstrategie und Verteilung von Unteraufgaben mit Fertigstellungskontrolle mit abschließender Erledigung (Datum)
8.5.2 Sanktionen
1. Entdeckt die annehmende Gruppe oder die Leitung einen Fehler in einem Systemteil, erhält die
abgebende Gruppe Punktabzug: –5 %
2. Entdeckt die annehmende Gruppe oder die Leitung Verstöße gegen unsere Richtlinien, erhält die
abgebende Gruppe Punktabzug: –3 %
3. Bei kriminellen, subversiven Fehlern erhält die abgebende Gruppe Punktabzug: –20 %
9. Ausbaustufe: Überladung und Vererbung
In dieser Ausbaustufe wird das System um die Fähigkeiten zur Überladung und Vererbung erweitert. Dies umfasst im Detail:
•
Erweiterungen des Scanners, Parsers und Modells
•
Erweiterung der Scope-Komponente
•
Anpassung des Typ-Systems und der Typ-Verträglichkeit
•
Anpassung der Identifier-Komponente
Beispiel:
class A {
public int i = 5;
private int j = 1;
public void f () {}
}
class B extends A {
private int j = 2;
protected int f (int i) {return i;}
public int f (boolean b) {return b ? i : j;}
public int g () {f (); return f (1) + f (false) * f (true) – i ();}
public int i () {return i + 1;}
}
Eine (hypothetische) Auswertung des Ausdrucks new B ().g () ergibt 5.
Zusätzlich wird das System um Zeichen und Zeichenketten erweitert.
9.1 Erweiterungen des Scanners, Parsers, Modells und Typ-Systems
Der Scanner und Parser müssen angepasst werden, um das neue Schlüsselwort extends bzw. die
hierarchische Ordnung der Vererbung auf Klassen verarbeiten bzw. abbilden zu können. Im gleichen Zug muss das Modell um die Typ-Ordnung erweitert werden. Schließlich müssen die Komponenten, die das Typ-System überwachen, angepasst werden. Die Regeln der Typ-Kompatibilität
können nun nämlich durch die Definition entsprechender Klassen und extends-Beziehungen verändert werden. Es ist sinnvoll, Operationen wie isAssignableTo und isPassableTo für andere Komponenten zur Verfügung zu stellen (etwa für die Scope-Komponente, die einen Operationsaufruf auflösen muss, dessen Name überladen ist). Zum Unterschied zwischen isAssignableTo und isPassableTo sollten die Abschnitte 5.2 und 5.3 der Java-Spezifikation zu Rate gezogen werden.
Alle equals-Aufrufe auf Typen müssen daraufhin überprüft werden, ob nicht isAssignableTo o. ä. verwendet werden muss!
9.2 Erweiterung der Scope-Komponente
Die Scope-Komponente muss angepasst werden, um Überladung und Vererbung unter einen Hut zu
bringen. Im obigen Beispiel sind einige Stellen gezeigt, an denen die Scope-Komponente “mehr tun
muss als bisher”. Insbesondere muss Überladung über alle Scopes einer Vererbungslinie hinweg
funktionieren (siehe Operation f in den Klassen A und B). Die Auswahl der korrekten Operation bei
der Anwendung erfordert die Ermittlung der “spezifischsten” Operation. (Achtung: Analysebedarf!
Siehe hierzu die Ergebnisse der letzten Analyse-Aufgabe und Abschnitt 15.12.2 in der Java-Spezifikation!)
Das Scope-Konzept soll auch so umgebaut werden, dass die Modell-Objekte ihren Scope kennen
(wie im Review besprochen).
9.3 Anpassung der Identifier-Komponente
Die Identifier-Komponente muss angepasst werden, um Gebrauch von der neuen Funktionalität in
den Scope- und Typ-Komponenten zu machen. Außerdem muss in diesem Zusammenhang auch die
getType-Operation auf Ausdrücken aktualisiert werden, damit diese auch im Kontext von Überladung korrekt funktioniert. (Je nach aktuellem Ausbau-Stand des Systems ist dazu keine bis viel Arbeit erforderlich!) Die zugehörige Gruppe übernimmt dabei die “Regie”-Funktion, d. h. sie ist für
die Integration der einzelnen Komponenten zu einem “sinnvollen Ganzen” verantwortlich.
Anwendungen von instanceof werden eliminiert!
9.4 Erweiterung der Basistypen
Das System soll um die Basistypen char und String sowie die passenden Operatoren erweitert
werden. Dabei sollen auch Zeichen- und Zeichenketten-Literale verarbeitet werden können. Weiterhin sollen bei den Basis-Operationen Definition und Applikation wie besprochen getrennt werden.
Analyse-Zusatzaufgabe “Interfaces”:
Die Gruppe analysiert, wie das System erweitert werden muss, um Interfaces zu unterstützen. Insbesondere sollen die notwendigen Erweiterungen der Regel zur Typ-Kompatibilität sowie die Namensauflösung in Interfaces (auch in Hinblick auf Überladung!) unter die Lupe genommen werden.
Analyse-Zusatzaufgabe “Konstruktoren”:
Die Gruppe analysiert, welche Änderungen am System vorgenommen werden müssen, um Konstruktoren zu unterstützen. Dabei sollen unter anderem auch super- und this-Aufrufe von Konstruktoren, Überladung und Vererbung von Konstruktoren sowie Referenzierung von Konstruktoren
durch new-Ausdrücke betrachtet werden.
Zieltermin: 08.04.2005, 16.30 Uhr
Gruppenzuordnung:
Scanner + Parser + Modell + Typ-Kompatibilität: Gruppe 4
Scope-Komponente:
Gruppe 2
Identifier-Komponente + Regie:
Gruppe 1 (+ Analyse “Interfaces”)
Erweiterung der Basistypen:
Gruppe 3 (+ Analyse “Konstruktoren”)
10. Ausbaustufe: Konsolidierung und Konstruktoren
Diese “Ausbaustufe” ist hauptsächlich der Fehlerbeseitigung und Restrukturierung des Programms
gewidmet. Sie umfasst die folgenden Aufgaben:
•
Restrukturierung und Vervollständigung der Komponenten zur Prüfung von Syntax und Semantik (gemeinhin als “Checker” bekannt)
•
Korrektur der Behandlung von String und char (insbesondere werden Literale nicht korrekt
erkannt)
•
Konstruktoren (erst einmal ohne super- und this-Aufrufe)
Es wird von den bearbeitenden Gruppen erwartet, dass das übliche Maß an Qualität eingehalten
wird (JavaDoc-Kommentare, eventuell zusätzliche Dokumentation und vor allem Testfälle!)
10.1 Restrukturierung und Vervollständigung der Checker-Komponenten
Auszug aus einer Analyse der Checker-Komponenten (Referencer-Komponente = Identifier-Komponente):
In der ursprünglichen Konzeption war der Checker dafür vorgesehen Konsistenzbedingungen im Modell zu prüfen und sicherzustellen. Da der Scanner und der Parser nicht alle Konsistenzbedingungen erfüllen können und auch das Modell inkonsistente Zustände erlaubt, ist
es notwendig eine Komponenten zu haben, die nach Aufbau des Modells überprüft, ob es
sich um ein standardkonformes Modell handelt.
Die Gruppe, die sich mit dem Entwurf des Checkers auseinandergesetzt hatte, machte bereits
zu Beginn den Fehler, Konsistenzbedingungen zu prüfen, die zu der Zeit gar nicht prüfbar
waren. So wurde zum Beispiel ein Modul für den Checker geschrieben, der den Rückgabetyp von return-Instruktionen prüft, ob er mit dem Rückgabe-Typ der Methode übereinstimmt. Da es allerdings zu dieser Zeit keinen Auswerter für Ausdrücke gab, konnten komplexe Ausdrücke gar nicht überprüft werden und führten unweigerlich zu Fehlern. Ausschließlich einfachste Ausdrücke wie return 4 konnten geprüft werden.
Weitere Module des Checkers haben keine Existenzberechtigung mehr, da die Aufgaben von
anderen Komponenten effektiver und schneller überprüft werden können. So gibt es ein Modul, welches dafür sorgt, dass es keine Attribute mit gleichem Namen in einer Klasse gibt,
und ein Modul, welches dafür sorgt, dass es keine Parameter von Operationen mit gleichem
Namen gibt. Diese beiden Aufgaben übernimmt nun der Scope-Builder, der beim erstellen
des Scopes sicherstellt, dass es keine doppelt definierten Namen gibt.
Die einzige Aufgabe, die der Checker derzeit noch sinnvoll erfüllt ist, dass er mehrfach vorhandene Modifikatoren von Attributen, Operationen und Klassen erkennt und eine Ausnahme generiert.
Da sich für die Weiterentwicklung des Checkers keine Entwicklergruppe verantwortlich
fühlte, befindet sich der Checker mittlerweile in einem rudimentären Zustand und benötigt
dringend eine Überarbeitung. Bei dieser Gelegenheit sollte der Checker insoweit angepasst
werden, dass er alle bisher bekannten Konsistenzbedingungen überprüft und deren Verstöße
meldet. Bei der Überarbeitung sollte man sich dann weiterhin Gedanken machen, ob es nicht
sinnvoll ist, den Checker zu unterschiedlichen Zeiten mit unterschiedlichen Überprüfungen
zu beauftragen, denn zur Zeit wird der Checker vor dem Referencer aufgerufen und anschließend nicht wieder. Einige Aufgaben des Checkers müssten vor dem Referencer aufgerufen
werden, da der Referencer ein Modell erwartet, bei dem zum Beispiel die Modifikatoren auf
Korrektheit geprüft wurden. Andere Aufgaben des Checkers dürfen erst nach dem Referencer aufgerufen werden, da erst dann die Typen aufgelöst und bestimmt wurden, um Rückgabewerte von return-Instruktionen auf Kompatibilität mit den Rückgabetypen von Operationen zu prüfen.
Die Konsistenzbedingungen, die zu Beginn der Diplomarbeit überprüft werden müssen, lassen sich wie folgt dokumentieren:
Vor dem Referencer:
•
Attribute dürfen keinen abstract-Modifikator besitzen
•
In Klassen ohne abstract-Modifikator (konkrete Klasse) dürfen keine Operationen mit
abstract-Modifikator existieren
•
Methoden dürfen keinen abstract-Modifikator besitzen
•
Operationen müssen einen abstract-Modifikator besitzen
•
Es darf keine zwei Modifikatoren von der gleichen Modifikator-Art vor Methoden, Operationen, Attributen und Klassen geben. (“public private” ist verboten, “abstract static” ist verboten, auch “public public” ist nicht gestattet, “public abstract” ist erlaubt)
•
Der private-Modifikator darf nicht vor Klassen existieren
•
Klassen dürfen nicht von Basistypen abgeleitet werden
•
Alle geerbten Operationen müssen in konkreten Klassen implementiert sein
•
Alle im Modell gekennzeichneten Komposita müssen zyklenfrei sein.
•
Definierende Modell-Objekte dürfen sich nicht selbst referenzieren. So ist beispielsweise
public class A extends A und public int a = a nicht erlaubt.
Nach dem Referencer:
•
Zuweisungen müssen auf Typkompatibilität geprüft werden. Einer Variablen darf nur ein
Wert zugewiesen werden, der in der Typhierarchie gleich oder spezieller ist. Bei den bisher vorhandenen Basistypen muss Typgleichheit herrschen.
•
Variablen- und Attributinitialisierungen müssen auch auf Typkompatibilität geprüft werden.
•
In Funktionsapplikationen muss sichergestellt werden, dass die Operanden-Typen zu dem
Operator passen. (Der Referencer bestimmt zur Zeit den Typ einer Funktionsapplikation
an Hand des Operators und nicht an Hand der Typen der Operanden. Dieses Verhalten
muss bei Einführung von Operatorüberladung geändert werden.)
•
In Schleifen-Anweisungen (wie while und for) muss der Bedingungsausdruck einen
boolschen Rückgabewert besitzen.
Auf Grund des Umfangs werden zwei Gruppen für diese Aufgabe angesetzt: Die eine Gruppe befasst sich mit Regelprüfungen vor dem Durchlauf der Identifier-Komponente und die andere mit Regelprüfungen danach.
10.2 String und char
Die letzte Umsetzung der entsprechenden Aufgabe war fehlerhaft. In dieser Ausbaustufe soll dies
korrigiert werden.
10.3 Konstruktoren
Das System soll um Konstruktoren und “alles drumherum” (etwa Überladung, new-Ausdrücke und
Konstruktor-Applikationen in new-Ausdrücken) erweitert werden. Die Behandlung von superund this-Aufrufen soll dabei in dieser Ausbaustufe außen vor bleiben.
Beispiel:
class A {
private int i;
public A () {
i = 0;
}
public A (int iValue) {
i = iValue;
}
public int getI () {return i;}
}
class B {
public B () {
A a1 = new A ();
A a2 = new A (42);
A a3 = new A (a1.getI () + a2.getI ());
}
}
Zieltermin: 13.07.2005, 08.00 Uhr (!)
Gruppenzuordnung:
String und char:
Konstruktoren:
Prüfungen vor dem Identifier-Durchlauf:
Prüfungen nach dem Identifier-Durchlauf:
Gruppe 3
Gruppe 2
Gruppe 1
Gruppe 4
11. Ausbaustufe: Refaktorisierungen (I)
Diese Ausbaustufe hat zum Ziel, eine Basis zu schaffen, auf deren Grundlage Refaktorisierungen
von Java-Programmen durchgeführt werden können. Weiterhin sollen einige Refaktorisierungen bereits umgesetzt werden.
11.1 Refaktorisierungs-Framework
Es soll ein Refaktorisierungs-Framework entwickelt werden, das die Grundlage für alle zukünftigen
Refaktorisierungen bilden soll und gemeinsam genutzte Dienste und Schnittstellen umfassen soll. In
dieser Ausbaustufe soll das Framework folgende Funktionalität enthalten:
•
Es soll eine Plug-in-Schnittstelle bereitgestellt werden, um Refaktorisierungs-Komponenten in
das Framework “einklinken” und nutzen zu können.
•
Ein Dienst zum Protokollieren der durchgeführten Aktivitäten der Refaktorisierungs-Komponenten ist ebenfalls Teil des Frameworks. Dabei sollen nicht nur durchgeführte Aktivitäten, sondern
auch unterlassene Aktivitäten protokolliert werden. Ein Beispiel für letzteres wäre bei einer Refaktorisierung, die für Attribute get- und set-Methoden hinzufügt, die Meldung, dass die hinzufügenden Methoden bereits unter diesem Namen in der jeweiligen Klasse existieren und somit
nicht erzeugt werden konnten.
•
Das Framework soll die Möglichkeit anbieten, durchgeführte Refaktorisierungen zurücknehmen
zu können (Undo-Funktionalität). Dabei sollen nach Möglichkeit die Refaktorisierungs-Komponenten entlastet werden und das Framework den Löwenanteil der Verwaltung und Durchführung
des Undo-Mechanismus übernehmen. Eine Möglichkeit ist, sich das Modell in seiner Gesamtheit
vor den Refaktorisierungen zu merken und hinterher wiederherzustellen. Eine sinnvolle Variante
ist, von den Refaktorisierungs-Komponenten zu fordern, vor irgendwelchen Änderungen an einem Modell-Objekt das Framework zu benachrichtigen, so dass das Framework dieses Objekt sichern und später wiederherstellen kann. (Letzteres erfordert natürlich ein Protokoll zum Austausch von Undo-Informationen zwischen Framework und Komponenten über die Plug-inSchnittstelle.)
11.2 R1: Zugriff auf Attribute über get- und set-Methoden umleiten
Diese Refaktorisierung hat zum Ziel, innerhalb einer Klasse alle direkten Zugriffe auf ein Attribut
über entsprechende get- und set-Methoden umzuleiten und nur innerhalb dieser beiden Methoden den direkten Zugriff aufs Attribut zu belassen. Beispiel:
public class A {
private int x = 0;
public int inc () {
return this.x = this.x + 1;
}
}
wird zu:
public class A {
private int x = 0;
public int getX () {
return this.x;
}
public void setX (int value) {
this.x = value;
}
public int inc () {
this.setX (this.getX () + 1);
return this.getX ();
}
}
Dabei soll die Komponente davon ausgehen, dass keine get- und set-Operationen existieren, die
bewahrt werden müssten. Es werden also immer neue get- und set-Operationen erzeugt und alle
bisherigen Attribut-Zugriffe darüber umgeleitet.
Wenn die neuen Operationen auf Grund von Namenskollisionen nicht erstellt werden können, soll
das protokolliert und die Refaktorisierung nur teilweise durchgeführt (wenn z. B. nur die set-Operation nicht generiert werden konnte) oder ggfs. ganz abgebrochen werden.
Die Wahl der Namen für die get- und set-Operation ist ggfs. genauer zu prüfen, um auch in Vererbungssituationen das Richtige zu tun, etwa bei diesem Beispiel:
class A {
public int x = 0;
public int Atest () {return x + 42;}
}
class B extends A {
private int x = 0;
public int Btest () {return x + 23;}
}
Ein Aufruf der Atest- oder Btest-Methode auf einem passenden A- oder B-Objekt soll nach der
Refaktorisierung dasselbe liefern wie davor.
11.3 R2: Schnittstellen extrahieren
Diese Refaktorisierung soll aus jeder Nicht-Schnittstellen-Klasse die enthaltene öffentliche Schnittstelle extrahieren und unter einem eigenen Namen als Schnittstellen-Klasse anbieten. Private, geschützte und package scoped-Operationen sollen dabei außen vor gelassen werden. Weiterhin sollen
nach Möglichkeit alle Referenzen auf die Klassen durch Referenzen auf die generierten Schnittstellen ersetzt werden. Bereits existierende Schnittstellen sowie Vererbungsbeziehungen sollen dabei
berücksichtigt werden. Beispiel:
public interface I1 {
void i1 (A1 a1);
}
public interface I2 {
void i2 (C1 c1);
}
public interface I3 {
void i3 ();
}
public interface I4 {
void i4 ();
}
public interface I12 extends I1, I2 {}
public abstract class A1 implements I1, I2, I3, I12 {
public abstract A1 a1 ();
private void p1 () {}
}
public class C1 extends A1 implements I1, I3, I4 {
public void i1 (A1 a1) {}
public void i2 (C1 c1) {}
public void i3 () {}
public void i4 () {}
public A1 a1 () {return new C1();}
public C1 c1 (C1 c) {return c;}
protected void g1 () {}
private void p2 () {}
}
führt zu folgenden zusätzlichen Schnittstellen und Änderungen in den Klassen-Köpfen:
public interface I1 {
void i1 (IA1 a1);
}
public interface I2 {
void i2 (IC1 c1);
}
public interface IA1 extends I3, I12 {
IA1 a1 ();
}
public abstract class A1 implements IA1 {
public abstract IA1 a1 ();
...
}
public interface IC1 extends IA1, I4 {
IC1 c1 (IC1 c);
}
public class C1 extends A1 implements IC1 {
public void i1 (IA1 a1) {}
public void i2 (IC1 c1) {}
...
public IA1 a1 () {return new C1();}
public IC1 c1 (IC1 c) {return c;}
...
}
Für die Bestimmung der Schnittstellen in der extends-Klausel einer generierten Schnittstelle (etwa IA1 im obigen Beispiel) soll ein Algorithmus entwickelt werden, um die minimale Anzahl an
Schnittstellen einfließen zu lassen. (Im obigen Beispiel konnten I1 und I2 in der extends-Klausel weggelassen werden, weil beide in der – ebenfalls verwendeten – Schnittstelle I12 bereits enthalten sind.)
Die im Beispiel verwendeten Namen für die Schnittstellen sind keine “offizielle” Vorgabe; andere,
sinnvollere Namenskonventionen sollten gewählt werden.
11.4 R3: Inline-Substitution von Methodenaufrufen
Diese Refaktorisierung soll nach Möglichkeit Methodenaufrufe eliminieren, indem deren Code in
den Aufruf “hineinsubstituiert” wird. Beispiel:
class A {
public int berechne (int x) {return x + this.f(x) + 42;}
public int f (int z) {return z + 23;}
}
class B {
A a = new A ();
public int test () {
int y = 23;
return this.a.berechne (this.y);
}
}
wird im ersten Schritt zu:
class A {
public int berechne (int x) {return x + (x + 23) + 42;}
public int f (int z) {return z + 23;}
}
class B {
A a = new A ();
public int test () {
int y = 23;
int r1 = (this.y + this.a.f(this.y) + 42);
return r1;
}
}
und in einem zweiten Schritt zu:
class A {
public int berechne (int x) {return x + (x + 23) + 42;}
public int f (int z) {return z + 23;}
}
class B {
A a = new A ();
public int test () {
int y = 23;
int r1 = this.y + (this.y + 23) + 42;
return r1;
}
}
Wie man sieht, muss eine solche Substitution eventuell mehrfach durchgeführt werden (natürlich
nicht unbedingt in dieser Reihenfolge). Wichtig ist nur, dass so viele Substitutionen wie nur möglich durchgeführt werden. (Zum Behandeln von Rekursion siehe unten.) Dabei muss die Substitution die Semantik erhalten. Das bedeutet, dass z. B. keine Substitution durchgeführt werden kann,
wenn die betrachtete Methode zu einer anderen Klasse gehört und auf deren private Elemente zugreift, auf welche die aufrufende Klasse keinen Zugriff hat. Vorsicht ist auch geboten beim Behandeln der Parameter: Weil Parameter immer kopiert werden, müssen eventuell temporäre Variablen
eingeführt werden. Beispiel:
class A {
public void tuNixSinnvolles (int n) {
n = n + 1;
}
public void aufrufTest () {
int z = 42;
tuNixSinnvolles (z);
}
}
Hier muss z nach der Substitution von tuNixSinnvolles beim Rücksprung aus der Methode
aufrufTest immer noch den Wert 42 enthalten.
Generell ist das Einführen temporärer Variablen erlaubt, auch wenn die temporären Variablen wegoptimiert werden könnten. Das Wichtige ist lediglich, dass die Semantik durch deren Einführung
nicht verändert wird. Insbesondere dürfen keine Namenskonflikte mit bereits existierenden Variablen auftreten. Es ist auch – entgegen der Programmierrichtlinien – erlaubt, derart eingeführte temporäre Variablen bei der Definition nicht zu initialisieren, sofern gewährleistet ist, dass sie vor ihrer
Verwendung einen Wert zugewiesen bekommen. (Siehe die temporären Variablen r1 in dem folgenden Beispiel für eine solche Situation.)
Direkte Rekursion soll verbleiben. Indirekte Rekursion soll in direkte aufgelöst werden. Beispiel:
class A {
public int fac(int n) {
if (n == 0)
return 1;
else
return n*fac2(n – 1);
}
public int fac2(int n) {
if (n == 0)
return 1;
else
return n*fac(n – 1);
}
}
wird zu:
class A {
public int fac(int n) {
if (n == 0)
return 1;
else {
int r1;
if ((n – 1) == 0)
r1 = 1;
else
r1 = (n – 1)*fac((n – 1) – 1);
return n*r1;
}
}
public int fac2(int n) {
if (n == 0)
return 1;
else {
int r1;
if ((n – 1) == 0)
r1 = 1;
else
r1 = (n – 1)*fac2((n – 1) – 1);
return n*r1;
}
}
}
Zieltermin: 15.08.2005
Gruppenzuordnung:
Framework:
R1 (Umleiten des Zugriffs auf Attribute):
R2 (Schnittstellen extrahieren):
R3 (Inline-Substitution):
Gruppe 2
Gruppe 1
Gruppe 4
Gruppe 3
12. Ausbaustufe: Refaktorisierungen (II) und Erweiterungen
In dieser Ausbaustufe sollen zum einen die Refaktorisierungen vorangetrieben und zudem eine Programm-Komponente entwickelt werden, mit der man benutzerfreundlich über eine graphische
Schnittstelle die Funktionalität der Refaktorisierungs-Komponenten und des Frameworks nutzen
kann. Zum anderen wird das Java-Modell um weitere syntaktische und semantische Elemente ergänzt. Weiterhin soll eine komplette Dokumentation des Ist-Zustandes erarbeitet bzw. zusammengestellt werden. Schließlich sollen einige Fehler, die im Laufe der Entwicklung “verbrochen” aber
noch nicht korrigiert worden sind, ausgemerzt werden.
12.1 Modell-Browser und Anbindung an das Refaktorisierungs-Framework
Es soll ein System entwickelt werden, mit dem ein Java-Objektmodell dargestellt und bearbeitet
werden kann. Das Java-Objektmodell wurde dabei vorher über einen Durchlauf des Java-Systems
erzeugt. Die Funktionalität zur Bearbeitung des Java-Modells soll sich dabei bewusst auf Refaktorisierungs-Komponenten stützen, die das Refaktorisierungs-Framework anbietet.
Die Darstellung und Bearbeitung des Modells soll mit Hilfe eine graphischen Benutzungsoberfläche
erfolgen; die strikte Trennung von Modell- und Controller/View-Funktionalität soll besonders berücksichtigt werden. Eine einfache Navigation zwischen den einzelnen Modell-Objekten ist ausreichend; weiter reichende Funktionalität (etwa Suchen von Objekten über deren Namen o. ä.) ist nicht
notwendig.
12.2 Refaktorisierungen (II) und super/this
Das Refaktorisierungs-System soll um grundlegende Refaktorisierungen erweitert werden, die das
Arbeiten mit dem Modell-Browser sinnvoll ergänzen. Wichtig sind insbesondere Erstellung, Umbenennung und Löschung (sofern möglich) von den möglichen Modell-Objekten (Klassen, Operationen, Methoden, Attribute u. s. w.)
Des Weiteren soll das Java-System syntaktisch und semantisch in allen Komponenten um die
Schlüsselwörter super und this erweitert werden, sofern dies noch nicht geschehen ist. Ziel dabei ist es, mit Hilfe von super Elemente der Basisklasse(n) und mit this Elemente der eigenen
Klasse referenzieren zu können.
Schließlich soll erreicht werden, dass vom Identifier identifizierte Attribute, auf die ohne this zugegriffen wird, im Modell zu AttributeSelectionExpression-Ausdrücken umgewandelt
werden.
12.3 Ausnahme-Behandlung
Das Java-Modell soll syntaktisch und semantisch um Elemente zur Ausnahme-Behandlung ergänzt
werden. Hierzu gehören:
•
Scannen und Parsen der Schlüsselwörter try, catch, throw, throws (finally soll dabei
außen vor bleiben)
•
Abbildung von Java-Klassen, die für die Java-Ausnahme-Behandlung eine Rolle spielen (Throwable, Exception, RuntimeException, Error)
•
Semantische Überprüfung der Korrektheit von throws-Klauseln bei Methoden und im Falle
von Vererbung (Fragestellungen sind z. B.: Sind alle Ausnahmen, die eine Methode verlassen
können, durch die throws-Klausel abgedeckt (abgesehen von den in der Java-Spezifikation erlaubten Abweichungen zu dieser Regel)? Ist eine throws-Klausel einer spezielleren Operation
unerlaubterweise allgemeiner als die der allgemeineren Operation?)
•
Semantische Überprüfung der Korrektheit von catch-Blöcken (Fragestellungen sind z. B.:
Kann eine durch einen catch-Block aufgefangene Ausnahme wirklich auftreten? Ist der deklarierte Typ des catch-Parameters vom Typ Throwable oder spezieller?)
•
Semantische Überprüfung der Korrektheit von throw-Anweisungen (eine Fragestellung ist
z. B., ob der Typ des Arguments Zuweisungs-kompatibel zu Throwable ist)
12.4 Dokumentation
Es soll eine Dokumentation des Ist-Zustandes angefertigt werden, aus der ersichtlich ist, welche Eigenschaften und Funktionen das Java-System und das Refaktorisierungs-Framework (samt Komponenten) zur Zeit beinhalten. Außerdem soll die Aufstellung derjenigen Java-Sprachelemente bzw.
-Konzepte, die vom System noch nicht bzw. unzulänglich unterstützt werden, auf den neuesten
Stand gebracht werden. Die Grundlage für die Dokumentation ist dabei der erreichte Stand dieser
Ausbaustufe.
12.5 Offene Punkte
In dieser Ausbaustufe sollen schließlich einige der noch offenen Punkte behoben werden. Details zu
den Fehlern sind im Bugzilla des Projekts zu finden. Die Zuordnung der Gruppen zu den einzelnen
Code-Teilen bei Projekt-übergreifender Fehlerbehebung (Fehler 57 & 58) ist wie folgt:
Scanner, Scope-Builder, Refakt.-Framework:
Parser, Checker, R2:
Treiber, Printer, R1:
Referencer, Modell & Modelldienste, R3:
Gruppe 1
Gruppe 2
Gruppe 3
Gruppe 4
•
Fehler 13: wird in der Dokumentation als “nicht erledigt” vermerkt
•
Fehler 32: wird vorerst so belassen und entsprechend dokumentiert
•
Fehler 42: Dokumentation des gewählten Work-arounds durch die Dokumentationsgruppe
•
Fehler 52: Gruppe 4 + Marcel
•
Fehler 56: Gruppe 3
•
Fehler 57: betrifft gesamtes Projekt, Aufteilung der Gruppen siehe oben
•
Fehler 58: betrifft gesamtes Projekt, Aufteilung der Gruppen siehe oben; bis zum 29.08.2005
Prüfung, ob vorgeschlagene Vorgehensweise (Entfernung sämtlichen “No-Operation”-Codes in
catch-Blöcken außer im Parser) möglich, danach Implementierung der Lösung
Zieltermin: 05.09.2005 (Dokumentation: 12.09.2005)
Gruppenzuordnung:
Modell-Browser:
Refaktorisierungen (II) und super/this:
Ausnahme-Behandlung:
Dokumentation:
Gruppe 1
Gruppe 4
Gruppe 2
Gruppe 3
Gruppe HFI404 / HFW404
Organisatorische Rahmenbedingungen
Das Integrationsprojekt findet innerhalb der drei Theoriequartale des Hauptstudiums durchgängig
statt. Es findet keine Klausur statt, vielmehr errechnet sich die Benotung aus den Einzelbewertungen der einzelnen Aufgaben pro Quartal. Da keine Klausur geschrieben wird, werden volle zwölf
Wochen des Quartals genutzt; die letzte Aufgabe reicht also in die Klausurphase hinein, was bei der
Zeiteinteilung beachtet werden sollte.
Die Entwicklung wird von vier Gruppen durchgeführt, wobei jede Gruppe aus vier bis fünf Entwicklern besteht. Im ersten Quartal werden die Gruppen nach der Hälfte des Quartals neu kombiniert, ab dem zweiten Quartal wird gelost. Ganz im Sinne von “Pair Programming” im Rahmen von
Extreme Programming soll dies erreichen, dass sich keine “eingefahrenen Gleise” aufbauen und
dass durch den Kontakt mit verschiedenen Entwicklern das gegenseitige Lernen voneinander gefördert und Synergie-Effekte ausgenutzt werden.
Jede Gruppe bekommt für den Zeitraum von zwei bis drei Wochen – je nach Schwierigkeitsgrad –
eine Aufgabe zugeteilt. Diese Aufgabe besteht in der Regel aus dem Anfertigen von Programmcode
zum Lösen einer bestimmten Problemstellung und dem Erstellen entsprechender Testfälle und Dokumentation. Am Ende des Zeitraums präsentiert jede Gruppe ihre Ergebnisse vor allen versammelten Entwicklern. Die (Teil-)Note berechnet sich dann aus der Qualität der Lösung inklusive der Dokumentation sowie aus der Präsentation. Gelegentlich sind die Aufgaben analytischer Natur, so dass
kein Programmcode geschrieben werden muss; stattdessen muss eine Problemstellung oder vorhandener Programmcode gründlich unter die Lupe genommen werden.
1. Aufgabe: Einarbeitung
Diese Aufgabe hat das Ziel, dass alle Entwickler sich in das bestehende Projekt einarbeiten. Dabei
soll jede Gruppe einen bestimmten Teilaspekt des Projekts untersuchen und ihn den anderen Entwicklern präsentieren. Dadurch soll jeder Entwickler am Ende einen Überblick über das Gesamtsystem bekommen und grob wissen, welche Funktionalität an welcher Stelle angesiedelt ist und wie
der allgemeine Programm-Ablauf aussieht.
Zusätzlich wird jede Gruppe sich in bestimmte (Entwurfs-)Muster einarbeiten, die in ihrem Kontext
von Bedeutung sind, und diese Entwurfs-Muster den anderen präsentieren. Dadurch soll erreicht
werden, dass alle Entwickler die verwendeten Entwurfs-Muster durchschauen und sie in späteren
Aufgaben problemlos einsetzen können.
Es werden vier Referate verteilt:
1.1 Scanner + Parser (Entwurfsmuster: State + Grammatik-Umsetzung)
Diese Gruppe hat die Aufgabe, sich in die Komponenten Scanner und Parser des Java-Projekts einzuarbeiten und über die wesentlichen Strukturen zu referieren, insbesondere die Funktionsweise des
Scanners und Parsers. Für ersteren ist das State-Muster wesentlich, für letzteren ist wichtig zu verstehen, nach welchem Rezept die formale Grammatik der Programmiersprache Java in Programmkonstrukte umgesetzt wird.
1.2 Modell (Entwurfsmuster: Proxy, Composite, Singleton)
Diese Gruppe soll die Zusammenhänge zwischen den verschiedenen Klassen aufzeigen, die zum
Modell gehören, also die Informationen des analysierten Programmcodes kapseln. Den in diesem
Zusammenhang verwendeten Muster Proxy, Composite und Singleton ist besondere Beachtung zu
schenken.
1.3 Scope-Builder + Referencer (Entwurfsmuster: Visitor + Programmier-Richtlinien)
Diese Gruppe beschäftigt sich mit den wichtigen Komponenten Scope-Builder und Referencer, deren Aufgabe es ist, in einem geparsten Programm Namen korrekt aufzulösen und ihnen die richtige
Bedeutung zuzuweisen. Hierbei soll das Visitor-Muster behandelt werden, da es wesentlich für das
Arbeiten am vorhandenen Modell ist, wie es der Referencer tut, um im Programm verwendete Namen zu finden und diese geeignet aufzulösen. Im Zusammenhang mit Visitoren sind auch einige
spezielle Richtlinien einzuhalten, über die ebenfalls referiert werden soll.
1.4 Refaktorisierung + Drucker (Entwurfsmuster: Observer)
Diese Gruppe soll sich mit dem Drucker sowie dem Refaktorisierungs-Rahmenwerk inklusive zweier Refaktorisierungen (Getter/Setter einbauen + Schnittstellen herausziehen) auseinandersetzen. Zusätzlich ist das Observer-Muster abzuhandeln, das im Refaktorisierungs-Rahmenwerk und den entsprechenden Plug-ins Verwendung findet.
Zieltermin: 25.07.2006
Gruppenzuordnung:
Scanner + Parser:
Modell:
Scope + Referencer:
Refaktorisierung + Drucker:
Dennis, Christoph, Markus S., Stefan Hu.
Markus K., Hendrik, Julia, Stefanie
Tobias, Artur, Michael, Stefan R.
Stefan Ho., Johannes, Sebastian, Janko, Norman
2. Aufgabe: 13. Ausbaustufe: Refaktorisierung, Pakete und Analysen
Diese Ausbaustufe erweitert die Fähigkeiten des Java-Werkzeugs um die folgenden Punkte:
1. Vereinheitlichung von Operatoren und Operationen
2. Unterstützung von Paketen und qualifizierten Namen
3. Kontrollflussanalyse (I)
4. Analyse-Plug-ins
Die einzelnen Themen werden in den nächsten Abschnitten detaillierter erläutert.
2.1 Vereinheitlichung von Operatoren und Operationen
Das bisherige Konzept, (eingebaute) Operatoren und (Benutzer-definierte) Operationen voneinander
zu trennen, bewährt sich nicht, wenn die Vielfalt der eingebauten Java-Operatoren ins Spiel kommt.
Die Trennung sorgt für Probleme bei der semantischen Analyse, da pro Operator-Symbol nur eine
Operator-Definition existiert, auch wenn der Operator semantisch mehrere Funktionen besitzt (Beispiele: “+” dient der Addition von Zahlen und der Verkettung von Zeichenketten; “==” vergleicht
entweder zwei Zahlen oder zwei Wahrheitswerte oder zwei Objekt-Referenzen). Besser ist es, die
eingebauten Operatoren als spezielle (freie) Funktionen im globalen Gültigkeitsbereich zu sehen,
die entsprechend typisierte Parameter und einen passenden Rückgabetyp besitzen; dies erlaubt es
nun, die obigen Beispiele als entsprechende Überladungen desselben Operators anzusehen. Dadurch
können Sonderbehandlungen von eingebauten Operatoren gänzlich vermieden werden. Zum Zwecke der Vereinheitlichung bietet es sich an, bestehende Operationen, Methoden und Konstruktoren
ebenfalls als freie Funktionen anzusehen, die einen zusätzlichen Parameter, das Empfänger-Objekt,
erhalten. Somit kann die gesamte Namensauflösung und Auflösung der Überladung für Operatoren,
Operationen und Konstruktoren über einen einheitlichen Mechanismus geregelt werden.
Beispiel: Der Additions-Operator “+” bekommt die Signaturen
int +(int x, int y);
String +(String x, String y);
Der Vergleichsoperator “==” erhält die Signaturen:
boolean ==(int x, int y);
boolean ==(boolean x, boolean y);
boolean ==(Object x, Object y);
In den Klassen
public class Number {
private int x;
public Number (int x) {this.x = x;}
public int value () {return this.x;}
public Number square () {return new Number(this.x * this.x);}
public Number add (int delta) {return new Number(this.x + delta);}
public Object clone() {return new Number(this.x);}
}
public class MyNumber extends Number {
public MyNumber (int x) {super(x);}
public Object clone() {return new MyNumber(this.value());}
}
bekommen die Operationen und der Konstruktor die folgenden Signaturen:
/* class Number */
public Number(int x);
public int value(Number this);
public Number square(Number this);
public Number add (Number this, int delta);
public Object clone(Number this);
/* class MyNumber */
public MyNumber(int x);
public Object clone(MyNumber this);
wobei this der neue, implizite Empfänger-Parameter ist, den nur Operationen und Methoden, nicht
aber Konstruktoren erhalten.
2.2 Unterstützung von Paketen und qualifizierten Namen
Das Java-Werkzeug soll um Pakete und qualifizierte Namen erweitert werden. Ein qualifizierter
Name ist ein Name, der mindestens einen “.”-Separator enthält, etwa java.lang.String. Generell haben Namen folgenden Aufbau:
name ::= simple-name + qualified-name
simple-name ::= identifier
qualified-name ::= name "." identifier
Die Schwierigkeit hierbei ist es, zum einen zu entscheiden, ob ein qualifizierter Name oder ein
“.”-Operator vorliegt:
a.B b; // ein qualifizierter Name: die Klasse B des Pakets a
b.c = 5; // der Punkt-Operator: das Attribut c des Objekts b (zur Klasse B)
Zum anderen können Teile eines qualifizierten Namens unterschiedliche Entitäten sein: Bei dem
Namen a.b.c.d können die einzelnen Teile folgendermaßen zusammenhängen:
• a.b.c.d ist ein Paket
• a.b.c ist ein Paket, d ist eine Klasse
• a.b ist ein Paket, c ist eine Klasse, d ist ein statisches Attribut
• a.b ist ein Paket, c und d sind Klassen (letztere ist eine geschachtelte oder innere Klasse)
• a ist ein Paket, b und c sind Klassen, d ist ein statisches Attribut
• a, b, c und d sind alles statische Attribute vom Typ einer Klasse
In dieser Ausbaustufe müssen nur die ersten beiden Verwendungen unterstützt werden; es soll aber
versucht werden, die notwendigen Änderungen am Modell, am Parser und an den zugehörigen Algorithmen der Namensauflösung möglichst allgemein zu halten, um die späteren Verwendungsweisen später leicht integrieren zu können.
Die Unterstützung von Paketen ist dahingehend anzufertigen, als dass jede definierte globale Komponente (Klasse, Schnittstelle) über eine package-Klausel “ihr” Paket zugewiesen bekommt und
deren Name über einen entsprechend qualifizierten Namen aufgelöst werden kann. Auch das Konzept des Standard-Paketes soll umgesetzt werden; das bedeutet, dass eine package-Angabe möglich, aber nicht erforderlich ist. Es ist aber nicht erforderlich, import-Klauseln zu unterstützen.
Gegebenenfalls ist zu analysieren, welche Modell-Elemente für die Aufgabe bereits zur Verfügung
stehen und welche Komponenten – wenn auch nur teilweise – bereits jetzt Teile der Aufgabenstellung unterstützen.
2.3 Kontrollflussanalyse (I)
Der bisher vernachlässigte Bereich der Kontrollflussanalyse soll vorangetrieben werden. In dieser
Ausbaustufe lautet die Zielsetzung, folgende ungültige Konstrukte zu erkennen:
• “Toter” Code: Code, der nie ausgeführt werden kann, weil bestimmt werden kann, dass die Methode vorher auf jeden Fall verlassen wird. Beispiel:
int f ()
{
return 5;
return 6; // toter Code, wird nie erreicht!
}
• Fehlende return-Anweisungen. Beispiel:
boolean greater(int x, int y)
{
if (x > y)
return true;
// Achtung: return fehlt!
}
In jedem Fall sind in dieser Ausbaustufe die Werte von (konstanten) Ausdrücken nicht in die Analyse einzubeziehen. Das bedeutet beispielsweise, dass der tote Code im folgenden (inkorrekten) JavaCode von der Kontrollflussanalyse nicht erkannt wird:
void doNothing()
{
while (false) {
int i = 1; // toter Code, wird aber nicht erkannt
}
}
Hier wird der tote Code nicht erkannt, weil der Wert der Bedingung (false) nicht in die Analyse
einfließt.
2.4 Analyse-Plug-ins
Das Java-Werkzeug unterstützt bisher Refaktorisierungen über sog. Refaktorisierungs-Plug-ins, die
mit Hilfe einer Browser-ähnlichen Oberfläche angestoßen und durchgeführt werden. Dieses Vorgehens-Konzept soll nun auf Analyse-Werkzeuge ausgeweitet werden. Das bedeutet im Einzelnen:
• die Entwicklung einer entsprechenden Analyse-Plug-in-Abstraktion, da die Plug-in-Abstraktion
für Refaktorisierungen auf Grund unterschiedlicher Anforderungen nicht geeignet ist,
• die Entwicklung einer passenden Plug-in-Verwaltung ähnlich dem bestehenden Plug-in-Framework für Refaktorisierungen,
• Einbindung der Verwaltung in die Browser-Anwendung und
• das Anfertigen von einigen ersten Analyse-Plug-ins.
Anzufertigen sind folgende Analysen:
1. Maximale und Ø Anzahl von Operationen und Methoden pro Klasse und Schnittstelle
2. Maximale Tiefe und Breite der Vererbungshierarchie
3. Maximale und Ø Anzahl von Anweisungen pro Methode
4. Maximale und Ø Schachtelungstiefe von Anweisungen
Zieltermin: 29.08.2006
Gruppenzuordnung:
Operatoren und Operationen:
Pakete und qualifizierte Namen:
Kontrollflussanalyse:
Analyse-Plug-ins:
Markus K., Hendrik, Julia, Stefanie
Dennis, Christoph, Markus S., Stefan Hu.
Stefan Ho., Johannes, Sebastian, Janko, Norman
Tobias, Artur, Michael, Stefan R.
3. Aufgabe: 14. Ausbaustufe: Importe, Operatoren, Analysen und Felder
In dieser Ausbaustufe soll hauptsächlich die in der letzten Ausbaustufe integrierte Funktionalität erweitert werden:
• zusätzliche Operatoren erweitern den Sprachumfang
• Importe vereinfachen die Benutzung von Namen aus anderen Paketen
• die Kontrollfluss-Analyse wird durch die Behandlung konstanter Ausdrücke verbessert
• der Sprach-Kern wird um eingebaute Felder (Arrays) erweitert
Die einzelnen Themen werden in den nächsten Abschnitten detaillierter erläutert.
3.1 Zusätzliche Operatoren + Koordination
Diese Aufgabe umfasst das Analysieren und Hinzufügen fehlender Signaturen zu bereits existierenden und erkannten Operatoren (relevante Teile von §15) sowie das Implementieren der Inkrementund Dekrement-Operatoren in den Präfix- und Postfix-Varianten (§15.14.1, §15.14.2, §15.15.1,
§15.15.2).
Beispiel zur ersten Teilaufgabe: Der Operator “+” wird unterstützt, allerdings fehlt beispielsweise
die Signatur
String +(String lhs, String rhs)
Beispiel zur zweiten Teilaufgabe: Unter Annahme der Definition
int i;
sollen die Ausdrücke
++i
i++
--i
i--
übersetzt und im Modell angemessen dargestellt werden.
Die Gruppe, welche die Umsetzung dieser Aufgabe übernimmt, soll zusätzlich als Koordinator für
die verschiedenen Gruppen fungieren und Arbeiten “neben der Reihe”, also dringende Fehler-Bereinigungen, Pflege der Bugzilla-Datenbank etc. übernehmen.
3.2 Importe
Das Programm soll in der Lage sein, Import-Deklarationen (§7.5) beim Referenzieren von Namen
zu berücksichtigen. Beispiel:
// Datei a/X.java
package a;
import b.T; // single-type import declaration
class X extends T {}
// Datei b/T.java
package b;
import a.*; // type-import-on-demand declaration
class T {
private X x;
}
3.3 Kontrollflussanalyse (II)
Bisher wurde die Kontrollfluss-Analyse (§14.20) ohne Berücksichtigung von Ausdrücken durchgeführt. Nun soll die Analyse durch die Betrachtung konstanter Ausdrücke verbessert werden. Beispiel 1: Die Schleife
int i = 0;
while (false) {
i = i + 1;
}
muss vom Übersetzer zurückgewiesen werden, weil der Inhalt der Schleife nicht erreichbar ist. Beispiel 2: Auch der Inhalt der Schleife
int i = 0;
while (2 – (4 > 5 ? 3 : 4) + 2 != 0) {
i = i + 1;
}
ist nicht erreichbar, obwohl es auf den ersten Blick nicht offensichtlich scheinen mag. In beiden Fällen ist jedoch der -Kontroll-Ausdruck ein sog. konstanter Ausdruck (§15.28), der zur Übersetzungszeit auswertbar ist und in die Analyse einfließen kann. Details zu dem Thema, bei welchen Konstrukten während der Kontrollfluss-Analyse Ausdrücke analysiert werden, sind §14.20 der JavaSpezifikation zu entnehmen.
Zusätzlich soll eine detaillierte Analyse von throw-Anweisungen und catch-Blöcken erfolgen.
Beispiel:
public class AException extends Exception { ... }
public class BException extends Exception { ... }
public class X {
public int f () throws AException { ... }
public int g () throws BException { ... }
public int test1 () throws Exception {
try {
return f () + g ();
}
catch (AException e) { return 42; }
return 5; // Unreachable Code
}
public int test2 () {
try {
return f () + g ();
}
catch (AException e) { return 42; }
catch (BException e) { }
return 5; // kein Unreachable Code
}
public int test3 () {
int result = 0;
try {
result = f () + g ();
}
catch (AException e) { return 42; }
catch (BException e) { return 43; }
return result; // kein Unreachable Code
}
public int test4 () {
try {
throw new AException ();
}
catch (Exception e) {
}
return 42; // kein Unreachable Code
}
public int test5 () {
try {
return f ();
}
catch (Exception e) {
return 42;
}
catch (AException e) { // Unreachable Code
return 43;
}
}
}
3.4 Unterstützung von Feldern (Arrays)
Diese Aufgabe umfasst das Umsetzen von eingebauten Feldern (Arrays), und zwar in folgendem
Umfang:
• Array-Typen (z.B. int[], aber auch mehrdimensionale Felder) sollen geparst und im Modell abgelegt werden können,
• Definitionen von Array-Variablen und -Attributen sollen erlaubt sein,
• Initialisierung von Arrays mit new-Ausdrücken soll möglich sein,
• Indizierung von Array-Objekten soll ermöglicht werden.
Folgende Funktionalität soll erst einmal nicht umgesetzt werden:
• das Feld length und die Methode clone sowie alle anderen Object-Methoden,
• Literale zur Initialisierung von Feldern (z.B. {1,2}),
• alternative Schreibweise von Feldern (int a[] statt int[] a).
Beispiel: Das folgende kleine Programm sollte fehlerfrei akzeptiert werden:
class Screen {
private int height;
private int width;
private char[][] buffer;
public Screen(int height, int width) {
this.height = height;
this.width = width;
this.buffer = new char[height][width];
for (int line = 0; line < height; line = line + 1)
for (int col = 0; col < width; col = col + 1)
this.set (width, col, ' ');
}
public int getWidth() {
return this.width;
}
public int getHeight() {
return this.height;
}
public char get(int line, int col) {
return this.buffer[line][col];
}
public void set(int line, int col, char c) {
this.buffer[line][col] = c;
}
}
Zieltermin: 12.09.2006
Gruppenzuordnung:
Operatoren + Koordination:
Importe:
Kontrollflussanalyse:
Stefanie, Markus K., Stefan Hu., Dennis
Janko, Stefan Ho., Markus S., Christoph
Sebastian, Norman, Johannes, Artur, Michael
Felder (Arrays):
Julia, Hendrik, Tobias, Stefan R.
Anmerkung: Wie angekündigt werden die Gruppen für die Bearbeitung dieser und der nächsten
Aufgaben rekombiniert. Dabei wird wie folgt vorgegangen:
1. Jede Gruppe teilt sich selbständig in zwei Teilgruppen à zwei Leute auf, mit Ausnahme der Fünfer-Gruppe, die sich in eine Zweier- und eine Dreier-Gruppe aufteilt.
2. Die Gruppe bestimmt unter sich, welche Teil-Gruppe bleibt (und somit die Bearbeitung der ursprünglichen Aufgabenstellung fortsetzt) und welche Teil-Gruppe sich einer anderen Aufgabe
anschließt. Nur entschärft gilt dies auch für die Analyse-Plug-in-Gruppe, da die Aufgabe ohnehin
wechselt (Arrays).
3. Die Teil-Gruppe, die zu einer anderen Aufgabe wechselt, sucht sich diese selbst aus. Bei mehreren Interessenten für eine Aufgabe wird wie bei der anfänglichen Aufgabeverteilung gelost.
4. Aufgabe: 15. Ausbaustufe: Konsolidierung
In dieser Ausbaustufe sind alle Entwickler aufgefordert, bestehende Fehler und Entwurfsschwächen
auszumerzen. Das Ziel ist, alle bekannten Fehler zu beheben. Dazu ist folgende Vorgehensweise
einzuhalten:
1. Alle Gruppen erarbeiten bis zum Ende der Vorlesungszeit eine Liste aller zu behebenden Fehler
zusammen mit einer groben Lösungsstrategie, die mit den jeweiligen Betreuern abgesprochen ist.
Das Nichtbearbeiten eines Fehlers muss mit beiden Betreuern abgesprochen sein.
2. Die Aufgaben werden von den Entwicklern selbständig und gerecht auf die vier Gruppen verteilt.
3. Die einzelnen Gruppen bearbeiten in der vorlesungsfreien Zeit alle ihnen zugeordneten Fehler.
Während der Bearbeitungsphase werden Besprechungstermine mit dem jeweiligen Betreuer abgesprochen. Die ersten drei Termine werden alle zwei Wochen, die folgenden alle drei Wochen durchgeführt. Wenn weiterer Gesprächsbedarf besteht, muss dies mit dem jeweiligen Betreuer abgesprochen werden.
Die Bewertung orientiert sich nach der erbrachten Leistung. Dazu werden die Fehler grob in drei
Schwierigkeitsgrade (leicht, mittelschwer, schwer) eingeordnet; diese Kategorien dienen der Gewichtung der einzelnen Aufgaben.
Zieltermin für Punkte (1) und (2):
29.09.2006
Zieltermin für Punkt (3):
01.01.2007
Gruppenzuordnung: Siehe Bugzilla
5. Aufgabe: 16. Ausbaustufe: Java ++: Pre-Prozessor für Java zur Unterstützung von
Standardmustern (1)
Viele objektorientierte Muster (OOM) sind soweit standardisiert, dass sie in konkrete Syntax gegossen und durch geeignete Werkzeuge weitgehend unterstützt werden können. Eine solche Erweiterung der Syntax erfordert keine semantische Anpassung, da die Semantik durch ein standardisiertes
Zusammenspiel verschiedener OOM's erklärt werden kann. Zur Verarbeitung bietet sich also ein
Pre-Prozessor an, der die erweiterte Syntax versteht, der dem Anwender viele hilfreiche Hinweise
zur korrekten Benutzung der Konzepte geben kann und der schließlich die erweiterte Syntax nach
Java übersetzt. In diesem Quartal wollen wir einen solchen Pre-Prozessor konzipieren und entwickeln.
Wir beginnen in dieser Aufgabe mit der Konzeption. Präsentation der Ergebnisse: 22. Januar 2007
5.1 Ereignisse
Java unterstützt Ausnahmeereignisse durch die throw-, throws- und catch-Klauseln. Wir wollen ein
ähnliches Konzept zur allgemeinen Kommunikation über Ereignisse entwickeln. Dazu sollen Objekte zu speziellen Ereignisklassen (analog zu Exception) die Ereignisse darstellen. Analoge Konzepte zu den throw-, throws- und catch-Klauseln sind zu konzipieren. Damit der Mechanismus ein
Kommunikationsmittel auch zwischen Objekten wird, sollen die Ereignisse, die Objekte einer Klasse erzeugen können, auch an der Schnittstelle der Klasse spezifiziert werden. Nutzer solcher Objekte können an der Nutzungsstelle (Attribute) auf diese Ereignisse reagieren. Dazu ist eine geeignete
Syntax zu entwickeln. Die Semantik für dieses Konzept soll durch die Übersetzung in geeignete
Observer-Muster bereitgestellt werden.
5.2 Spezialisierung/Generalisierung von Operationen in Argumenten und Rückgabetypen
Java lässt nur die Spezialisierung des Empfängers einer Nachricht zu. Wir wollen auch die Spezialisierung bzw. Generalisierung der Parameter und des Rückgabetyps von Operationen zulassen. Der
Pre-Prozessor muss dazu die Korrektheit der Spezialisierungsbeziehungen prüfen und ggf. Fehlermeldungen liefern. Die Semantik soll durch eine geeignete Übersetzung in Visitoren geliefert werden.
5.3 Mehrfachvererbung
Wir erweitern Java um Mehrfachvererbung. Der syntaktische Eingriff dazu ist relativ klein. Die Semantik soll durch eine Auflösung der Vererbung durch Delegation definiert werden. Achtung: Hier
ist insbesondere der Fall der Mehrfachvererbung einer Ressource über mehrere Erweiterungspfade
zu berücksichtigen!
5.4 Monitore und Synchronisationsbedingungen
Java verfügt über kein wirkliches Monitor-Konzept. Allerdings lassen sich das Monitorkonzept und
entsprechende Synchronisationsbedingungen mit den Primitiven von Java zur Synchronisation darstellen, siehe Skript zur Nebenläufigkeit von M. Löwe. Es soll eine geeignete Syntax für Monitore
entwickelt werden, in denen Synchronisationsbedingungsobjekte definiert werden können, die im
Wesentlichen aus der boole'schen Bedingung bestehen. Für diese Bedingungen soll es eine explizite
Operation zum Warten auf das Eintreffen geben. Das Eintreffen selber soll automatisch signalisiert
werden, wenn ein Thread den Monitor verlässt und die Bedingung gilt.
Zieltermin:
22.01.2007
Gruppenzuordnung:
Ereignisse:
Spezialisierung/Generalisierung:
Markus S., Christoph, Johannes, Norman, Sebastian
Hendrik, Tobias, Stefan R., Julia
Mehrfachvererbung:
Monitore:
Steffi, Markus, Dennis, Stefan Hu.
Stefan Ho., Janko, Michael, Artur
6. Aufgabe: 17. Ausbaustufe: Java-Komplettierung, Präprozessor und OO-Konzept
In dieser Ausbaustufe sind sowohl konzeptionelle als auch praktische Ausarbeitungen gefordert.
Ziel der praktischen Ausarbeitungen ist es, das Projekt näher an das Java-Sprachniveau zu bringen.
Ziel der konzeptionellen Ausarbeitungen ist es zum einen, vorbereitende Überlegungen zur Integration der Umsetzungen der Java++-Konzepte aus der vorhergehenden Ausbaustufe zu tätigen. Zum
anderen soll ein Papier über die verwendeten objektorientierten Konzepte erarbeitet werden, das als
Grundlage zur Behebung einiger Schwächen in der Umsetzung dieser Konzepte im Projekt dienen
soll.
6.1 OO-Konzept
Es soll ein Grundlagen-Papier verfasst werden, das zum einen die grundlegenden objektorientierten
Begriffe und Konzepte wie Operation, Methode, Nachricht, Klasse, Schnittstelle, statische und dynamische Bindung etc. definiert und in Beziehung setzt. Zum anderen sollen die variablen Anteile
der Konzepte herausgestellt werden. Das Ziel ist es, die Begriffe und Konzepte so zu verallgemeinern, dass dadurch eine Art Rahmenwerk entsteht, aus dem man durch Instantiierung die OO-Semantik verschiedener Programmiersprachen erhalten kann. Dabei ist es nur notwendig, sich auf
Java, Java++ und LOMF zu konzentrieren; das Einbringen von Kenntnissen über andere Sprachen
ist aber von Vorteil.
6.2 Präprozessor
Eine Konzeption soll erarbeitet werden, die aufzeigt, wie man das bisherige Projekt so um Java++
geeignet erweitern kann, dass keine Vermischung zwischen Java und Java++ im Quellcode auftritt.
Einerseits müssen die Komponenten identifiziert werden, die in Java und Java++ unterschiedliches
Verhalten aufweisen. Andererseits müssen Mechanismen entwickelt werden, um die Arbeitsweise
dieser neuen Java++-Komponenten so zu gestalten, dass nicht das Rad neu erfunden werden muss –
dass man also vieles von dem, was bereits da ist und sich nicht verändert hat, benutzen kann. Dabei
kann das auszuarbeitende OO-Konzept eventuell weiterhelfen. Das Ziel der Konzeption ist, den
Präprozessor so in das bestehende Projekt zu integrieren, dass man sowohl Java- als auch Java++Code verarbeiten kann, ohne dass der resultierende Projekt-Code sich verdoppelt und ohne dass der
Projekt-Code vor zusätzlichen booleschen Flags, expliziten Fallunterscheidungen und ähnlichem
strotzt.
6.3 Basistypen
Diese Aufgabe hat die Erweiterung der vom Projekt bisher erkannten Java-Teilmenge um die fehlenden Basistypen byte, short, long, float und double samt den zugehörigen Operationen
zum Ziel. Außerdem müssen alle von Java unterstützten Formen von Literalen für numerische Konstanten umgesetzt werden (hexadezimale und oktale Notation für Ganzzahlen, FließkommazahlKonstanten, Suffixe zur genaueren Spezifikation des Typs einer Konstante)
6.4 Sprungmarken (Labels)
Das Projekt soll um die Behandlung von Sprungmarken erweitert werden, die in Java von break
und continue benutzt werden, um gezielt die jeweils zu verlassende oder fortzusetzende Anweisung auswählen zu können. Ebenso soll das Schlüsselwort goto erkannt und mit derselben Semantik wie in Java versehen werden.
6.5 final
In dieser Aufgabe soll die Funktionalität des Schlüsselwortes final für Variablen, Parameter, Attribute, Methoden und Klassen in das Projekt integriert werden, zusammen mit den notwendigen
Komponenten zum Überprüfen der Einhaltung der geforderten Semantik (soweit innerhalb unseres
Projekts umsetzbar).
Zieltermin:
26.02.2007
Gruppenzuordnung:
OO-Konzept:
Präprozessor:
Basistypen:
Sprungmarken:
final:
Tobias, Hendrik
Stefan Ho., Stefan Hu., Dennis, Janko
Markus K., Stefanie, Julia
Artur, Michael, Norman, Stefan R.
Johannes, Sebastian, Christoph, Markus S.
7. Aufgabe: 18. Ausbaustufe: Java ++: Pre-Prozessor für Java zur Unterstützung von
Standardmustern (2)
In dieser Ausbaustufe sollen die Konzepte, die in der 16. Ausbaustufe erarbeitet wurden, umgesetzt
werden. Dabei soll beachtet werden, dass Java und Java++ in der Umsetzung getrennt werden (siehe
Präprozessor-Konzept aus Ausbaustufe 17!) Schließlich soll eine Gesamt-Dokumentation verfasst
werden, in welcher sowohl alle Java++-Erweiterungen erläutert werden als auch die Aktivierung
und Steuerung der Java++-Funktionalität “von außen” beschrieben wird.
Zieltermin für die Implementierung der Einzelkonzepte (inklusive Präsentation): 23.07.2007
Zieltermin für die Integration der Umsetzungen (inklusive Präsentation):
20.08.2007
Zieltermin für die Abschluss-Dokumentation:
14.09.2007
Gruppenzuordnung:
Ereignisse:
Spezialisierung/Generalisierung:
Mehrfachvererbung:
Monitore:
Julia, Stefan Hu., Norman, Artur
Stefanie, Tobias, Markus S., Janko, Sebastian
Hendrik, Stefan R., Johannes, Dennis
Markus K., Michael, Christoph, Stefan Ho.
Herunterladen