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:
JavaDoc


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
Head First Design Patterns


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.
Parallele Zustände
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":
Parallele Zustände in ArgoUML

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:
Zustandsdiagramm
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:


Aufgabe A.9 ("Probleme im Klassendiagramm")

Hier (unkommentiert) ein Klassendiagramm, dass die drei genannten Probleme umgeht:
Aufgabe A.9


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:
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