Softwaretechnik WS2012
Abgaben
Danke für eure Evaluation (Gruppe B und Gruppe D)
Präsentation für Projektaufgabe R ist am 30.01.2013, Abgabe am 3.2. Falls sich allerdings noch grundlegende Fragen ergeben,
dann können wir die Abgabe um einige Tage verschieben.
Nachbesserungen für Aufgabe Q ebenfalls bitte bis 3.2. abgeben.
Nachbesserungen für Aufgabe R bitte bis 14.2. abgeben.
Und ein paar Infos zu den Fragen aus dem heutigen (30.1.) Praktikum:
"Fachmodell" (englisch: "domain model") enthält nur die Klassen, die "Objekte der Anwendungsdomäne" darstellen, also nur Klassen, die
das Spiel selbst darstellen (Figur, Gegner, Schuss, Hindernis, Bonus-Gegenstand, ...), aber keine Klassen, die sich aus der Anwendungslogik ergeben
(Controller, GUI). In unserem Fall ist die Controllerklasse auch gleichzeitig Container für die Daten des Spiels
(ich habe nicht daran gedacht, dass man das eigentlich ebenfalls trennen müsste). Deshalb könnte man auch
die Controllerklasse ins Fachmodell aufnehmen - dann aber nur die Membervariablen eintragen, die sich auf das Fachmodell beziehen, aber nichts
aus der Logik (Timer, Referenzen zu Hilfsklassen, ...).
Repräsentationsinvariante: Siehe Dossier 6 vom 29.11.2012 - Seite 7 (Beispiel 6.7) für ein minimales Beispiel einer Repräsentationsinvariante.
Spätestens für die letzte Hausübung gilt: bitte JavaDoc-Warnungen gemäß diesen Einstellungen konfigurieren:
Literatur
Sehr empfehlenswertes UML 2-Buch:
http://www.hanser-fachbuch.de/buch/UML+2+glasklar/9783446430570
Buch über Design Patterns: Head First Design Patterns
Eric Freeman, Elisabeth Freeman, Bert Bates, Kathy Sierra
ISBN englische Version: ISBN-10: 0596007124, ISBN-13: 978-0596007126
ISBN deutsche Version ("Entwurfsmuster von Kopf bis Fuß"): ISBN-10: 3897214210, ISBN-13: 978-3897214217
JUnit
Hier findet ihr einen Schnelleinstieg in JUnit.
Unit-Tests und Timer
Wie im Praktikum bereits angesprochen sind Unit-Tests sehr problematisch, wenn die Spiellogik auf einem Timer basiert. In Unit-Tests
ist es im Prinzip unmöglich, Bedingungen der Form "nach 3 Sekunden hat das Spiel diesen Zustand erreicht" abzubilden. Deshalb schlage ich einen Trick
vor: für den Unit-Test wird der Timer ausgetauscht und durch einen "Manuellen Timer" ersetzt. Dieser manuelle Timer läuft nicht automatisch, sondern
seine Ticks werden vom Unit-Test ausgelöst. Die Spiellogik erhält jeden dieser vom Unit-Test angestoßenen Timer-Ticks und verarbeitet
ihn so, als würde ein "echter" Timer laufen. Würde ein echter Timer z.B. alle 100 Millisekunden ticken, und der Unit-Test soll den Spielzustand
nach 5 Sekunden prüfen, müsste der "Manuelle Timer" also 50 mal vom Unit-Test ausgelöst werden.
Ein solches Konzept würde ich unter dem Begriff "Mock Object" einordnen.
Hier ein Beispielprojekt, das solch ein Verhalten zeigt (UnitTestMitTimer.zip): die GUI ("UnitTestMitTimerMainFrame")
ist minimalistisch und enthält nur einen Start-Button. Wird er geklickt, läuft in der "Logik"-Klasse ("UnitTestMitTimerLogik"), die hier als
Controller dient, ein Timer los. Er tickt alle 3 Sekunden. Die GUI erhält ein Event
und aktualisiert jedesmal ein Label, das die Anzahl der Timer-Ticks aus der Logik-Klasse holt. Man beachte das Design der Anwendung: der Controller
weiß nichts von der GUI, er gibt nur seine Statusänderung über einen ActionListener weiter.
Im Unit-Test wird ebenfalls die Logik-Klasse erzeugt und der Timer gestartet. Nach einer bestimmten Zeit wird geprüft, dass die Logik jetzt
im Zustand "Timer hat dreimal ausgelöst" ist.
Für die Kapselung der Timer gibt es das Interface "ITimer". Es bietet Methoden, um einen Timer zu initialisieren (wobei der ActionListener, der
beim Timer-Tick aufgerufen wird, übergeben wird), und um ihn zu starten (für die volle JavaDoc-Kommentierung des Code siehe Zip-Datei).
public interface ITimer
{
void addTimerListener ( ActionListener actionListener);
void start();
}
Es gibt zwei Interface-Implementierungen: "SwingTimer" erzeugt intern einen "javax.swing.Timer", der nach einem bestimmten Intervall auslöst.
Die "start"-Methode startet hier den Timer
public class SwingTimer implements ITimer
{
private Timer timer;
private ActionListener timerTickListener;
public SwingTimer (int intInterval)
{
this.timer = new Timer(intInterval, new ActionListener ()
{
@Override
public void actionPerformed(ActionEvent e)
{
tick();
}
});
}
private void tick()
{
if (this.timerTickListener != null)
{
this.timerTickListener.actionPerformed(null);
}
}
@Override
public void addTimerListener(ActionListener actionListener)
{
this.timerTickListener = actionListener;
}
@Override
public void start()
{
if (this.timerTickListener == null)
{
throw new IllegalStateException("Kein Tick-Listener registriert - Timer kann nicht gestartet werden");
}
this.timer.start();
}
}
Die Klasse "ManuellerTimer" ist für den Unit-Test gedacht: sie macht nichts automatisch, sondern bietet eine public Methode "tick": bei jedem Aufruf
dieser Methode wird der "Timer hat getickt"-Actionlistener aufgerufen. Die Interface-Methode "start" ist deshalb leer implementiert.
public class ManuellerTimer implements ITimer
{
private ActionListener timerTickListener;
@Override
public void addTimerListener(ActionListener actionListener)
{
this.timerTickListener = actionListener;
}
@Override
public void start()
{
//Nix zu tun!
}
public void tick()
{
if (this.timerTickListener != null)
{
this.timerTickListener.actionPerformed(null);
}
}
}
Das Erzeugen des zu verwendenden Timers erfolgt nicht in der Logik, sondern entweder im Hauptfenster der Anwendung (dann wird der "SwingTimer"
verwendet) oder im Unit-Test (hier wird der "ManuellerTimer" verwendet)
Im Unit-Test muss entsprechend dreimal die Methode "tick" von "ManuellerTimer" aufgerufen werden, um drei Timer-Ticks zu simulieren.
Anschließend kann der Wert "Anzahl an Timer-Ticks" aus der Logik geprüft werden:
@Test
public void test()
{
//Timer dreimal ticken lassen!
this.timerManuell.tick();
this.timerManuell.tick();
this.timerManuell.tick();
//Check: der interne Status der Logik-Klasse sollte jetzt besagen "Timer hat dreimal ausgelöst"
assertEquals(3, this.logik.getTimerTickCount());
}
Kommentierung
Ein paar Beispiele zum Thema "Sauberes Kommentieren" (zum Teil aus
einem früheren Praktikum, das das Thema "Enterprise Java Beans (JavaEE5)" hatte - ignoriert einfach Beispiele, die von "Kuchen" oder "Zutat" reden ;-))
UML-Tool
Ich empfehle ArgoUML: http://argouml.tigris.org
(unter http://argouml-downloads.tigris.org/argouml-0.34/ findet man auch einen Zip-Download,
der ohne Installation funktioniert).
Aufgaben
Aufgabe P1
Bei einer der Projektaufgaben gab es folgenden Ausgangslage: ein Gegner kann sich auf den Spieler zubewegen und dabei schießen. Der Spieler muss
den Gegner mehrfach treffen, um ihn zu besiegen. Der Spieler kann ein Powerup sammeln, um den Gegner kurzzeitig zu verlangsamen.
Mein Vorschlag war: vier Zustände für den Gegner: "gesund/bewegt sich normal", "verwundet/bewegt sich normal", "gesund/bewegt sich langsam"
und "verwundet/bewegt sich langsam" (also alle Kombinationen von Gesundheit und Geschwindigkeit). Hr. Igler hat mich auf eine andere Idee gebracht:
parallele Zustände. Ein Teil des Diagramms handelt die Gesundheit des Gegners ab, der zweite Teil die Geschwindigkeit, und im dritten Teil geht es um "Gegner schießt".
Alle drei Teile des Zustandsdiagramms erfolgen parallel.
Der parallele Zustand wird betreten nach dem Erzeugen des Gegners, über eine "Gabelung" beginnen alle drei Zustände.
Teil "Gesundheit": der Gegner startet mit voller Gesundheit. Wenn er getroffen wird, reduziert sich seine Gesundheit. Nach mehreren Teffern
wird er zerstört - dies führt zum Endzustand.
Teil "Bewegung": je nachdem, ob der Spieler ein Powerup gesammelt hat oder nicht, bewegt er sich schnell oder langsam. Zwischen diesen Zuständen
kann gewechselt werden. Hat der Spieler zum Zeitpunkt der Erzeugung des Gegners das Powerups bereits, startet der Gegner im Zustand "langsam", sonst
im Zustand "schnell" (über eine "Kreuzung"). Erreicht der Gegner den Bildrand oder berührt den Spieler, beendet das sein Dasein - der Endzustand ist
erreicht.
Teil "Schießen": der Gegner schießt regelmäßig, dadurch ändert sich sein Zustand aber nicht. Das Schießen wird über einen Timer-Trigger modelliert:
nach jeweils 2 Sekunden (nur ein Beispielwert) wird ein Schuss abgefeuert. Dieser Teil des Zusammengesetzten Zustands führt nicht zu einem Endzustand,
es endet also erst, wenn einer der beiden anderen Teile endet.
Umsetzung eines zusammengesetzten Zustands in ArgoUML:
Man fügt einen "Zusammengesetzten Zustand" ein und wählt in dessen Kontextmenü den Punkt "Neuer nebenläufiger Bereich":
Und noch ein ArgoUML-Tipp: eine Gabelung wird per Default waagrecht ins Diagramm gesetzt. Um sie vertikal auszurichten:
http://argouml-stats.tigris.org/documentation-de/manual-0.34/ch20s16.html
(Dieser Balken kann vertikal ausgerichtet werden, indem Sie die Gabelung markieren und diese mit der Taste 1 in eine seiner Ecken ziehen.)
Aufgabe A13: Zustandsdiagramm:
Hier eine Musterlösung und ein paar weitere Notationselemente aus dem Zustandsdiagramm.
Aufgabe A12: Zustandsdiagramm:
Zur Frage "wie modelliert man Exceptions im Zustandsdiagramm?"
Hier kommt es darauf an, ob durch die Exception eine komplette Verarbeitung abgebrochen wird (z.B. beim Parsen einer XML-Datei, wo ein Fehler vermutlich
den gesamten Parse-Vorgang ohne Möglichkeit auf "Reparatur" beenden würde), oder ob "nur" die aktuell aufgerufene Methode (und damit: Transition)
einen Fehler wirft, der Zustand des Gesamtsystems sich aber nicht ändert.
Im Beispiel der Klasse "MyStack" ist letzteres der Fall. Deshalb könnte eine Modellierung von "push im Falle eines vollen Stacks aufrufen" so aussehen:
Man beachte, dass der Guard hier den die Exception auslösenden Zustand "size == 4" definiert, und das Verhalten ist "Exception werfen".
Aufgabe A.10
Kommunikationsdiagramm: Rückgabenachrichten werden wohl modelliert, indem man einen Pfeil in umgekehrter Richtung zeichnet,
und die Beschriftung ist "Nummer.1:Name()", also wird an die Nummer des Aufrufs ein ".1" angehängt.
http://de.wikipedia.org/wiki/Kommunikationsdiagramm_%28UML%29
Auch Bedingungen und Schleifen sind möglich:
http://www.dipl-inf.de/downloads/UML%202%20Lerneinheit%202.pdf
Aufgabe A.9 ("Sequenzdiagramm")
Zwei Links zu erweiterten Syntaxelementen eines UML-Diagramms:
https://www.fbi.h-da.de/labore/case/uml/sequenzdiagramm.html
http://www.ibm.com/developerworks/rational/library/3101.html
Gemäß dem zweiten Link kann man an den Rückgabepfeil eines Funktionaufrufs (einer "Nachricht") wohl auch statt des Funktionsnamens
die Bedeutung der Rückgabe, also z.B. der Name einer Variablen, der der Wert zugewiesen wird, schreiben.
Relevante Elemente:
- "alt" für Abbildung von if/else-Entscheidungen
- "opt" für Abbildung von if-Entscheidungen ohne else
- "loop" für Abbildung von Schleifen
- "break" für Darstellung von Exceptions: wenn der Abbruchfall eintritt (die Exception ausgelöst wird), dann wird der Ablauf innerhalb
des "break"-Segments ausgeführt.
Aufgabe A.9 ("Probleme im Klassendiagramm")
Hier (unkommentiert) ein Klassendiagramm, dass die drei genannten Probleme umgeht:
Aufgabe A.7/A.8
Navigierbarkeit von Assoziationen:
Eine Assoziation ganz ohne Pfeile ist identisch mit einer Assoziation mit Pfeilspitzen an jedem Ende: sie ist in beide Richtungen navigierbar.
Eine Assoziation mit nur Pfeilspitze ist nur in diese Richtung navigierbar, aber nicht umgekehrt. Optional kann man die nicht-Navigierbarkeit
des Endes ohne Pfeilspitze durch ein "X" am Ende der Linie verdeutlichen.
Navigierbarkeit von Aggregationen:
"UML 2 glasklar" sagt: ...die gleichzeitige Angabe zur Navigierbarkeit am Assoziationsende des Ganzen ist ausgeschlossen. Dies liegt an der fehlenden
Darstellungsmöglichkeit, da der Pfeil oder das Kreuzsymbol die Begrenzungslinien des Diamanten verdecken würde als auch an der problematischen
semantischen Interpretation, da kein Teil zu einem ganzen gehören kann, ohne dieses zu kennen (bei Angabe eines Kreuzes).
Am Assoziationsende des Teiles sollte jedoch ein Navigationspfeil gesetzt werden, um explizit auszudrücken, dass das Ganze Zugriff auf seine Teile
besitzt.
Hr. Igler sieht das allerdings etwas anders:
Diese Vereinbarung kann man natürlich treffen. Das ist durch UML aber
nicht zwingend so vorgegeben. Zunächst mal gibt es in dieser Hinsicht
überhaupt Abhängigkeit zwischen Aggregation und Navigierbarkeit.
Eine Aggregation ist also immer in beide Richtungen navigierbar. Ich kann mir aber auch Fälle vorstellen, in denen es von der Programmierung
her irrelevant ist, zu welchem Ganzen ein Teil gehört. Hier kann man vermutlich in der Programmierung diese Richtung der Navigation weglassen.
Codebeispiele A.7
Teil 1:
public class Dictionary
{
private DictEntry[] entries = new DictEntry[3];
}
public class DictEntry
{
}
That's all. Probleme hierbei:
- Problem 1: Die Membervariable "entries" ist hier ein leeres Array mit Platz für drei Elemente. Also wäre das aktuell eine "0..3"-Kardinalität.
Möglichkeit 1: im Konstruktor drei DictEntry erzeugen:
public Dictionary()
{
entries[0] = new DictEntry();
entries[1] = new DictEntry();
entries[2] = new DictEntry();
}
Nachteil: jetzt sind zwar drei Objekte da, aber da die alle leer sind, kann man nicht wirklich sagen, dass man eine "zu drei"-Assoziation hat.
Die eigentlichen Daten für den DictEntry müsste man später von außen setzen.
Möglichkeit 2: im Konstruktor drei Entries übergeben:
public Dictionary(DictEntry entry1, DictEntry entry2, DictEntry entry3)
{
if (entry1 == null)
{
throw new IllegalArgumentException("entry1");
}
...mehr Null-Checks...
entries[0] = entry1;
entries[1] = entry2;
entries[2] = entry3;
}
Nachteil: relativ unflexibel - wenn man irgendwann auf eine höhere Kardinalität erweitert, klappt es nicht mehr.
- Problem 2: Ein DictEntry soll in genau einem Dictionary sein. Wie bildet man das ab? Das geht in meinen Augen nur, wenn der DictEntry weiß,
zu welchem Dictionary er gehört - das wäre Teilaufgabe 2.
Teil 2:
public class Dictionary
{
private DictEntry[] entries = new DictEntry[3];
}
public class DictEntry
{
private Dictionary dictionary;
public DictEntry (Dictionary dict)
{
if (dict == null)
{
throw new IllegalArgumentException("dict");
}
this.dictionary = dict;
}
}
Problem hier: da dem DictEntry das Dictionary übergeben wird, in dem er steckt, muss man das Dictionary erzeugen, bevor man den Entry erzeugen kann.
In dem Lösungsvorschlag zu Teil 1 habe ich bei Problem 1, Lösungsmöglichkeit 2 einen Konstruktor mit drei DictEntry skizziert.
Wenn man diese Variante verwendet, stellt sich folgendes Problem: man kann kein Dictionary erzeugen, ohne vorher drei DictEntry zu bauen. Und man
kann keinen DictEntry erzeugen, ohne das Dictionary zu haben.
Deshalb wäre hier z.B. folgende Variante denkbar:
public class Dictionary
{
private DictEntry[] entries = new DictEntry[3];
public DictEntry getEntry(int index)
{
//TODO: index-Check!
return this.entries[index];
}
public DictEntry setEntry(int index, DictEntry entry)
{
//TODO: index-Check! Prüfung auf "darf nicht überschrieben werden".
this.entries[index] = entry;
//Entry auf aktuelles Dictionary setzen:
entry.setDictionary (this);
}
}
public class DictEntry
{
private Dictionary dictionary;
public DictEntry()
{
}
public void setDictionary (Dictionary dict)
{
if (dict == null)
{
throw new IllegalArgumentException("dict");
}
//Darf nicht bereits in Dictionary vorhanden sein!
if (this.dictionary != null)
{
throw new IllegalOperationException ("Entry ist bereits in Dictionary vorhanden");
}
this.dictionary = dict;
}
}
Hier wird der Entry ohne Dictionary erzeugt, und im Dictionary gibt es ein Array mit drei leeren Einträgen. Sobald ein Eintrag über
"setEntry" zugefügt wird, wird ihm das zugehörige Dictionary gegeben. Hier kümmert sich die Klasse "Dictionary" also primär um die Konsistenz
der Relation. Aber es sind trotzdem Fälle denkbar, wo die Assoziation verletzt wird.
Teil 3:
public class Dictionary
{
private Vector<DictEntry> entries = new Vector<DictEntry>();
public Dictionary()
{
}
public void addEntry (DictEntry entry)
{
if (entry == null)
{
throw new IllegalArgumentException("entry");
}
this.entries.add(entry);
}
}
Die Klasse "DictEntry" sieht hier aus wie in Teil 1 (also keine Navigierbarkeit zum Dictionary).
Teil 4:
Hier sieht das "Dictionary" wie in Teil 3 aus, der DictEntry wie in Teil 2 (bei Aggregation ist immer zum Parent navigierbar).
Teil 5:
Komposition ist in Java nicht umsetzbar - wird wie Aggregation behandelt.
Einen Teil der Anforderungen an die Komposition übernimmt in Java der GarbageCollector: wenn der Parent das einzige Objekt ist, das eine
Referenz auf das Child hält, dann wird bei Garbage Collection des Parent auch das Child freigegeben:
http://www.coderanch.com/how-to/java/AssociationVsAggregationVsComposition
Modellierung des Datentyps einer Assoziation
Um bei einer Assoziation den Datentyp zu deklarieren, kann man sich "Stereotypen" bedienen. Einen Stereotypen kann man sich als "ein Bezeichner,
den man auf ein Modellelement anwendet" vorstellen. Es ist also nicht mehr als ein frei wählbarer Text, der zusätzliche Informationen zum Modell
enthält. Ein Stereotyp kann auf jedes Element der UML angewendet werden. Er enthält eine Zusatzinformation, die sich aus der Verwendung gibt.
Zum Beispiel könnte man Besonderheiten einer konkreten Programmiersprache darin modellieren. Diese Information ist für den Leser des Diagramms relevant,
und eventuell steuern sie Tools, die z.B. Quellcode aus einem Klassendiagramm erzeugen.
Mehrere Stereotypen kann man zu einem "Profil" zusammenfassen. Profile sind eine leichtgewichtige Möglichkeit, UML zu erweitern. Ein solches
Profil könnte z.B. alle für die Programmiersprache Java relevanten Stereotypen bündeln.
In unserem Beispiel könnte man damit modellieren, dass eine Assoziation den Datentyp java.util.Vector
haben soll.
Verwendung von Stereotypen mit ArgoUML: argouml.html
Zum Thema "void über Stereotyp modellieren" und allgemein zu Stereotypen schrieb mir Hr. Igler:
Im Prinzip: ja. Dafür braucht man aber keinen Stereotyp, weil es in UML
schon ein Ausdrucksmittel für void gibt: "Typ weglassen". (Das ist aber
leider nicht eindeutig, da man Typen ja auch einfach so weglassen kann.)
Etwas formaler betrachtet: Es gibt im wesentlichen drei Möglichkeiten,
die Sprache UML zu erweitern:
1) Stereotypen: z.B. <<interface>>
2) Properties (sehen aus und wirken im Prinzip wie Einschränkungen:
{...}): z.B. {abstract}
3) Meta-Modell verwenden
(1) und (2) setzen auf vorhandenen Modell-Elementen auf. Mit (3) kann
man komplett neue Diagrammarten erfinden.
Zu (1) und (2): Es gibt vordefinierte Stereotypen und Properties, z.B.:
1) Stereotypen: z.B. <<interface>>
2) Properties: z.B. {abstract}
Im Prinzip unterscheidet sich die Bedeutung des Einsatzes von
Stereotypen von der von Properties. Für unsere Zwecke ist das aber egal,
da es für uns vor allem auf eines ankommt: Durch die Angabe von
<<vector>> (vorangestellt) oder {vector} (nachgestellt) können wir
verbindliche Zusatzangaben für die Implementierung vorsehen. Es muss nur
sichergestellt sein, dass selbsterfundene Stereotypen und Properties vor
der ersten Verwendung eingeführt und erläutert werden. (Es gibt auch
dafür ein eigenes Diagramm, das Profildiagramm. Wir machen es uns aber
einfach -- das ist zulässig -- und erklären die Bedeutung in natürlicher
Sprache.)
Aufgabe A.4
Es kam im Praktikum die Frage auf "wie werden Vererbungen im Objektdiagramm dargestellt".
Antwort von http://mbse.se-rwth.de/book1/index.php?c=chapter4-1
In einem Objektdiagramm werden keine Vererbungsbeziehungen dargestellt. Stattdessen können die aus Oberklassen geerbten Attribute
in der Unterklasse explizit aufgelistet werden. Die Vererbungsstruktur zwischen Klassen wird also in einem Objektdiagramm „expandiert“.
Aufgabe A.1
Hier ein Link zu einer detaillierten Ariane5-Analyse:
http://userpages.uni-koblenz.de/~beckert/Lehre/Seminar-Softwarefehler/Ausarbeitungen/weyand.pdf
Und zu Therac-25:
http://www5.in.tum.de/lehre/seminare/semsoft/unterlagen_02/therac/website/index.html
Sowie ein nicht ganz so tiefgehender Wikipedia-Artikel:
http://de.wikipedia.org/wiki/Therac-25
Und zum Abschluss ein Artikel über statistische Erhebungen zur Anzahl von Bugs in einer Software:
http://swreflections.blogspot.de/2011/08/bugs-and-numbers-how-many-bugs-do-you.html
Am relevantesten ist dieser Abschnitt:
If 85% of bugs are hopefully found and fixed before the code is released, this leaves 0.75 bugs per Function Point (für den Autor bedeutet das ca. 50 Zeilen)
unfound (and obviously unfixed) in the code when it gets to production. Which means that for a small application of 1,000 Function Points
(50,000 or so lines of Java code), you could expect around 750 defects at release.
Stand 18.02.2013
Historie:
18.10.2012: Erstellt
24.10.2012: Aufgabe A.4
01.11.2012: Aufgabe A.7/A.8: Navigierbarkeit von Assoziationen, Stereotypen
04.11.2012: Codebeispiele zu A.7
06.11.2012: Hr. Iglers Hinweise zu Aggregation + Stereotypen
08.11.2012: Hinweise zu A.9 und A.10
13.11.2012: Link zu einer Bugstatistik
14.11.2021: Hinweis zu A.12
15.11.2012: Hinweis P.1, Details zu Sequenzdiagramm
21.11.2012: Link zu Musterlösung A.13
03.12.2012: Parallele Zustände, Rahmen für Bewertung P1
05.12.2012: div. Gruppenkorrekturen
06.12.2012: Hinweise zur Bewertung Abgabe 1
10.12.2012: Überarbeitung des Zustandsdiagramms "Parallele Zustände" nach Feedback Hr. Igler
12.12.2012: Präsentationstermin Q, Bonus-/Malus-Punkte B.3/B.4
15.12.2012: Finales Diagramm "Parallele Zustände"
18.12.2012: Link JUnit
19.12.2012: 360689 "entfernt"
25.12.2012: Punktzahlen P
26.12.2012: Update D.1
03.01.2013: Beispiel für "Unit-Test mit Timer", Link zu "Kommentieren"
10.01.2013: Evaluationsergebnisse
23.01.2013: Zeitplan Projektaufgabe R
25.01.2013: Bewertung Abgabe Q
30.01.2013: Infos zur Abgabe R
31.01.2013: erste finale Punktzahlen Q + R
01.02.2013: erste finale Punktzahlen Q + R
04.02.2013: mehr finale Punktzahlen Q
07.02.2013: "Bugfix" an A.7-Codebeispiel
18.02.2013: finale Punktzahlen