Objektorientierte Entwicklung (OOE): Splitter I
Geschwurbel von Daniel Schwamm (04.07.1994 bis 01.08.1994)
Gesammeltes
Definitionen: Allokieren für Objekte den Speicherplatz und beinhalten
für Funktionen den Funktionsrumpf. Objekte und Funktionen (ausser
inline-Funktionen) werden nur einmal pro Programm definiert.
Default-Konstruktor: Ein Konstruktor, den man ohne Parameter anzugeben
aufrufen kann. Entweder erlaubt er keine Parameter ("Konstruktor();") oder die
Parameter führen Default-Werte ("Konstruktor(int i=10);"). Auch
Konstruktoren, die der Compiler automatisch generiert, sind
Default-Konstruktoren.
Der Präprozessor wurde bei C intensiv eingesetzt, sollte jedoch bei C++
vermieden werden. Statt "#define" sollte mit "const" und "inline" gearbeitet
werden. Makros lassen sich durch Templates simulieren. Beispiel:
template<class T> // Achtung! Kein ";" dahinter!
inline T& MAX(T &a, T &b) {
return a>b?a:b;
};
Die "stdio.h"-Bibliothek sollte durch die "iostream.h"-Bibliothek ersetzt
werden, da letztere die Ausgabe von benutzerdefinierte Typen erlaubt.
"operator<<" sollte in Klassen als "friend" aufgenommen werden. Dann
kann dieser überladene Operator in herkömmlicher Weise mit
benutzerdefinierten Typen eingesetzt werden. Implementierung:
friend ostream& operator<<(ostream &s, const classtype &t) {
s << t.daten;
return s;
};
Achtung! "ostream" darf nicht "const" sein. Überall muss referenziert
werden, bis auf "classtype". Dort kann auch gepointert oder mit
Temporär-Objekten gearbeitet werden.
HIC heisst Human Interaction Class. Sie wird im OOD-Prozess modelliert, u.a.
getrennt von der Problem Domain Class (PDC).
Bei der Analyse der Problembereichs-Komponente sollten bereits möglichst
allgemeine Klassennamen gebildet werden, um SW-Reuse zu ermöglichen.
Coad/Nicola stellen das Baseball-Modell vor: Zuerst macht man OOA, dann OOD
(z.B. die PDC), dann OOP (Implementierung von PDC als Prototyp), dann wieder
OOD (dieses Mal die HIC), dann OOP, dann OOD (Task Management Class; TMC), dann
OOP und dann OOD (Data Management Class; DMC). Schliesslich kann wieder mit OOA
fortgefahren werden und die Spirale des Baseball-Modells wiederholt sich.
Referenzattribute, häufig Surrogate, sollten im OOD-Modell (noch nicht
im OOA-Modell) mit hinten anstehendem "(d)" für Design gekennzeichnet
werden. Das Attribut selbst trägt den Namen der referenzierten Klassen
(möglicherweise allerdings im Plural). Referenzen in einem Objekt sind
immer dann nötig, wenn dem referenzierten Objekt mindestens eine Nachricht
zukommt, was durch einen Nachrichtenpfeil gekennzeichnet wird.
Wichtige Überladungsoperatoren sind z.B. "operator=" für
Zuweisungen (Konversionsfunktionen) und "operator==" für Objektvergleiche.
Es ist oft sinnvoll, C-Basistypen wie z.B. "char*" durch benutzerdefinierte
Typen wie z.B. "String" zu ersetzten, da die benutzerdefinierten Typen extern,
d.h. individuell vom Programmierer, gewartet und erweitert bzw. beschränkt
werden können.
Für Deep-Copy (Copy, bei dem auch Pointer mitkopiert werden) werden
Copy-Konstruktoren in den Klassen benötigt. Der Default-Copy-Konstruktor
kopiert nur die Ausprägungen der C-basierten Typen, sofern sie keine
Pointer sind. Allerdings: Auch Zuweisungskontruktoren können für
Deep-Copy benutzt werden. Beispiel:
class X {
char *sp;
X(X& x) { // Achtung! Kein X(X x);
sp=x.sp; //(weil sonst ewige Rekursion)
return *this; // dadurch a=b=c möglich!
};
};
OOE-Leute sollten sich an das "Continuum of Representation"-Prinzip halten,
d.h. ein durchgängiges Modell für die OOE benutzten, z.B. das
OOE-Modell von Coad/Yourdon. OOA, OOD und OOP können unabhängig
voneinander gewartet werden, ohne dass dabei Inkonsistenzen auftreten
müssen. Die Phasen sind jederzeit untereinander transferierbar.
HIC-Implementationen sollten so früh als möglich als
rudimentäre Version realisiert werden, um dem User ein Bild des
späteren Produkts zu geben (Prototyping-Prinzip).
Es hilft beim analysieren und designen, wenn man sich an das "I am
alive"-Prinzip hält, d.h. über Objekte in der Ich-Form nachdenkt,
z.B. "Ich bin ein Schalter. Wenn x passiert, dann mache ich y und z".
Sehr ähnliche Klassen sollten vermieden werden. Man sollte versuchen, ob
die Klasse nicht besser durch Aufnahme einer Funktion in die ähnliche
Klasse transferierbar ist. Beispiel: Es hat wenig Sinn, für eine
Zählerklasse für jede Basis eine eigene abgeleitete Zählerklasse
zu entwickeln, da eine allgemeine Transformation der Basen durch eine Methode
realisiert werden kann.
OOE wird in Projekten betrieben. 12 Mann gelten hier als Richtlinie für
grössere Projekte, wobei je nach Fähigkeit die Teilnehmer auf die
Phasen OOA, OOD und OOP verteilt werden, wo sie dann nach dem Baseball-Modell
in gegenseitiger Konkurrenz zueinander ihren Teil der OOE abarbeiten
können.
Die Klassen PDC, HIC, TMC und DMC des Designs sollten getrennt gehalten
werden, d.h. nur über Beziehungen und Nachrichtenaustausch zueinander im
Kontakt stehen, niemals aber über Vererbungsstrukturen oder Container-Strukturen.
Dadurch wirken sich Änderungen in einer Klasse nicht auf die anderen
Klassen aus (niedriges Coupling).
Werden bei der HIC Fensterklassen gebildet, sollten deren Namen ihre Ausgabe
wiedergeben, z.B. "Number", nicht ihre Funktion, z.B. "Counter of Terms". Dies
ist wichtig im Bezug auf SW-Reuse.
Die HIC sollte sich so weit wie möglich die Klassen des GUIs zunutze
machen, z.B. direkt einsetzen oder eigene Klassen davon ableiten.
Zusammengesetzte Klassennamen wie z.B. "Segelschiff" deuten oft an, dass die
Klasse aufspaltbar ist, hier z.B. in die "is a"-Struktur "Schiff" und
"Segelschiff".
Zu beachten: "class A{A();}; A *a=new A[10];" ruft insgesamt 10mal den
Default-Konstruktor "A()" auf! Einen anderen Konstruktor als einen
Default-Konstruktor einzusetzen, geht in diesem Falle nicht. Man sieht also,
dass man u.U. auf einen Default-Konstruktor nicht verzichten kann.
Ein Copy-Konstruktor sieht folgendermassen aus: "String(const String
&s);" Auf das "const" kann verzichtet werden, nicht aber auf das
Referenzzeichen (Gefahr der Ewigrekursion)! Dieser Copy-Konstruktor kann auf
zwei Arten eingesetzt werden: "String s1(s2)" oder "String s1=s2". I.d.R.
übernimmt der (Copy-)Konstruktor eine Überprüfung der
Gültigkeit, im Gegensatz zum Zuweisungsoperator. Wird der Zuweisungsoperator
überladen, wird statt des Copy-Konstruktors bei Operationen mit
"="-Verwendung immer der Zuweisungsoperator verwendet!
Der Zuweisungsoperator, der im Gegensatz zum Copy-Konstruktor keine
Gültigkeitskontrolle betreibt, kann folgendermassen aufgebaut werden:
String& String::operator=(const String &s){
if(&s==this)return *this;
daten=s.daten; // Deep-Copy möglich!
return *this; // Für a=b=c!
};
Es gilt: a+b <==> operator+(a, b); Wird der "+"-Operator
überladen, so sollte auch der "++" und +="-Operator überladen werden.
V.a. mit dem "++"-Operator gibt es Probleme, weil der Präfix oder Postfix
verwendet werden kann. Postfix erhält i.d.R. ein Dummy-"int" als Argument
(funktioniert bei Borland C++ 2.0 nicht!), um ihn vom
Präfix-Inkrementierer unterscheiden zu können. Beispiel für den
interessanteren Postfix-Inkrementierer:
X X::operator++(int) { // int steht für Postfix!
X *temp=*this; // aktuellen Zustand retten
i++; // Zustand inkrementieren
j++;
return temp; // vor aktuellen Zustand zurück
};
Konventionen für Variablennamen: "p" für Pointer, "r" für
Referenzen, "s" für statische Elemente oder Strings, "ch" für
Charakter, Grossbuchstaben für konstante Variablen.
"new" und "delete" sind "malloc" und "free" vorzuziehen, da letztere keinen
Aufruf des Konstruktors bzw. Destruktors impliziert haben.
Statische Elemente von Klassen sind ausserhalb der Klasse zu definieren.
class X { static int sx; static char sy; };
int X::sx; // Initiierung mit Null
char X::sy='a'; // Initiierung mit 'a'
"friends" befinden sich zwar innerhalb von Klassen, werden jedoch nicht zu
den Elementfunktionen dazugezählt!
Template-Funktionen innerhalb von Nicht-Template-Klassen sind verboten!
Ein einfaches "::new" sorgt dafür, dass alle klassenspezifischen "news"
umgangen werden und die globale "new"-Funktion aktiviert wird.
Referenzenzählen (!=Objektzählen): Buchführung darüber,
wie viele Objekte aktuell auf eine bestimmte Datenstruktur zeigen. Dadurch weiss
man z.B., ob die Datenressource noch benötigt wird oder aus dem Speicher
entfernt werden kann.
Konstruktoren sollten so aufgebaut sein, dass Initialisierungen über den
Funktionskopf den Zuweisungen im Funktionsrumpf vorgezogen werden. Bei der
Initialisierung von "const"- und "Referenz"-Variablen ist man sogar auf die
Initialisierungsliste des Konstruktors angewiesen. Beispiel:
struct X {
const char *s;
X(const char *sp) { s=sp; } // ERROR
X(const char *sp):s(sp) {}; // OK
};
Objektzählen-Implementation:
struct X {
static int z;
X() { z++};
~X() { z--};
};
int X::z; // Initiierung mit Null.
Destruktoren von Basisklassen sollten unbedingt virtuell deklariert werden.
Warum? Damit über einen "basierten" Zeiger eine Destruktion des
abgeleiteten Objekts auch alle Basisklassen-Destruktoren aktiviert (die im Gegensatz
zu allen anderen virtuellen Funktionen nicht in jeder Klasse die gleiche
Bezeichnung besitzen müssen). Nur so wird das Objekt vollständig aus
dem Speicher geräumt.
Bei Klassen, die nicht als Basisklassen dienen sollen, sollte auf
"virtual"-Deklarationen verzichtet werden, denn der virtuelle Mechanismus
verlangt Zusatzaufwand. So benötigt er z.B. pro Objekt einen "Virtual
Table Pointer", der auf die Zusatzstruktur "Virtual Table" zeigt, die pro
Klasse genau einmal angelegt wird, und die die Adressen der virtuellen
Funktionen in Abhängigkeit vom Objekt-Aufrufer führt.
Der Zuweisungsoperator "operator=" (wie überhaupt viele Operatoren)
sollte eine Referenz auf "this" zurückliefern, damit Kettenzuweisungen der
Form "a=b=c" möglich sind. Implementierung:
String& operator=(String *sp) { ... return *this; };
Der Hauptunterschied zwischen globalen Funktionen und Elementfunktionen ist
der, dass nur letztere virtuell sein können.
Wird der Operator "operator<<" der "iostream.h"-Bibliothek global
für eine benutzerdefinierte Ausgabe überschrieben, erhält er
u.U. keinen Zugriff auf alle Klassenelemente (z.B. PRIVATE/PROTECTED-Elemente).
Wird er als Elementfunktion überschrieben, sind auch Ausgaben der Form
"Variable << cout" möglich, was aber nicht erwünscht ist, da es
von der Norm abweicht. Aus diesem Grund ist dieser Operator als "friend" der
Klasse zu deklarieren.
Beim Design von Klassen sollte man sich an das Prinzip der funktionellen
Abstraktion halten, d.h. Klassenelemente PRIVATE bzw. PROTECTED gestalten und
für den Anwender eventuell Funktionen zur Manipulation bereitstellen.
Einen Zeiger auf eine Datenstruktur, die man nicht verändern soll,
erhält man z.B. durch "const char *sp". Der Zeiger "sp" bleibt dabei aber
veränderlich, kann also z.B. um einen Buchstaben hochgesetzt werden
("sp=&sp[1];"). Möchte man auch den Zeiger unveränderlich halten,
ist ein weiteres "const" in den Term einzufügen: "const char* const sp".
Man beachte dabei die Position von "*"! Möchte man nur den Pointer
konstant halten, den Datenteil aber veränderlich halten, so eignet sich
hierfür "char * const sp".
Es gilt: Elementfunktionen, die sich nur hinsichtlich ihrer
"const"-Eigenschaften unterscheiden (Rückgabewert oder Parameterliste),
können ohne Parameter-Matching-Probleme überladen werden.
In C wurde die Übergabe via Wert bevorzugt, und auch in C++ ist sie
Standard. Doch eine Übergabe via Referenz ist wesentlich effektiver,
sofern nicht nur sehr kleine Variablen wie z.B. "shorts" (1 Byte)
übergeben werden, denn Pointer benötigen 4 Bytes. Die Effizienz
rührt v.a. daher, dass Referenzaufrufe keine Konstruktoren und
Destruktoren bemühen müssen, um temporäre Objekte anzulegen.
Die "const"-Eigenschaft lässt sich "weg-casten". Das ist manchmal
notwendig, wenn Funktionen nicht-konstante Variablen wünschen, wo
eigentlich konstante Objekte die Regel sind. Beispiel: Die "strlen"-Funktion
(zählt übrigens den Terminator NICHT mit!) verlangt einen
nicht-konstanten Parameter (in UNIX, nicht bei Borland C++ 2.0). Ein
"weg-casten" der "const"-Eigenschaft lässt sich erreichen über:
const char *sp="test";
int i=strlen((char *)sp);
Beim Konvertieren eines Objektes auf seine Basisklasse muss auf das
Slicing-Problem geachtet werden: Im Gegensatz zu den Upcasts von Pointern verlieren
"basierte" Objekte (nicht Pointer!) ihre Fähigkeiten, die sie als
abgeleitete Objekte noch innehatten; sie werden zu normalen
Basisklassen-Objekte, der Virtual-Mechanismus ist lahm gelegt.
struct X { virtual f(); };
struct Y:public virtual X { f(); };
Y *yp=new Y;
Y y;
X *xp=&y;
X x=y; // Object-Upcasting!
xp->f(); // ruft virtuell Y::f() auf.
x.f(); // ruft (fälschlicherweise) X::f() auf.
Der Operator "operator+" verlangt eine Übergabe via Wert, da er ein neues
Objekt erzeugen muss.
Überladen funktioniert nicht immer, so gelingt z.B. bei Null-Werten kein
Parameter-Matching, da nicht zwischen "char", "int" und Pointern unterschieden
werden kann. Bisweilen ist auch ein Parameter-Defaulting der bessere Weg, wobei
aber sinnvolle Default-Werte existieren sollten.
Templates haben nur globalen Sicherheitsbereich, d.h. eine Typkonversion ist
nur innerhalb einer Datei möglich, ein Programm kann aber aus vielen
inkludierten Dateien bestehen.
Achtung! Ambiguitäten (Uneindeutigkeiten) können dem Compiler
entgehen, z.B. bei mehrfacher Vererbung (z.B. zwei Basisklassen füllen
eine abgeleitete Klasse mit Eigenschaften), sofern die Basisklassen
gleichnamige Funktionen bzw. Attribute haben.
Wie kann man verhindern, dass sich Anwender Kopien von Objekten erzeugen
können? Durch Weglassen eines Copy-Konstruktors gelingt das nur zum Teil,
denn ein Soft-Copy wird durch einen automatisierten Copy-Konstruktor gegeben.
Wie verhindere ich diesen automatisierten Copy-Konstruktor. Indem ich einen
Copy-Konstruktor vorgebe, diesen aber nur deklariere und nicht definiere, und
ihn am besten im PRIVATE-Teil ablege. Versucht ein Anwender nun eine Zuweisung,
so wird sich entweder der Linker oder der Compiler beschweren.
Bei grösseren Programmen kann es durch inkludierte Dateien zu
Namenskonflikten kommen. Designer von Bibliotheken können diesen Effekt
mildern, indem sie ihre Variablen in "struct" als "static"-Variablen
deklarieren und ausserhalb eines "struct" definieren. Der Anwender muss dann
über ein vorangestelltes "Struct-Name::" auf diese Variablen zugreifen.
Falls keine Namenskonflikte zu erwarten sind, kann man ein Header-File mit
lauter "typedef Varname Struct-Name::Varname;" bilden, das sich der Anwender
inkludieren kann.
Die 80-20-Regel besagt, dass 80% der Zeit nur 20% des Codes genutzt wird. Nur
diese 20% des Codes sind hauptsächlich zu optimieren.
Öffentliche Vererbung (über PUBLIC) bedeutet eine "is
a"-Vererbung. Wird diese virtuell realisiert, dann wird nur die Schnittstelle
vererbt, nicht aber die Implementationen; diese sind in der abgeleiteten Klasse
neu zu definieren. Anders bei nicht-virtueller Vererbung: Hier wird die
Schnittstelle samt ihren Implementationen vererbt; diese sollten nicht
überschrieben werden.
"pure virtual"-Funktionen sind definierbar (!), sie müssen allerdings
beim Aufruf vollständig qualifiziert werden (kein Parameter-Defaulting).
Nicht-virtuelle Klassen sind im Gegensatz zu virtuellen Klassen nicht dynamisch
gebunden, sondern statisch. Schon vor Laufzeit muss feststehen, von welchem
Objekttyp sie aufgerufen werden, daher sollten ihre Funktionen nicht
überladen werden, den "basierte" Pointer können nicht virtuell
arbeiten, d.h. die abgeleitete Funktion aufrufen (in Borland C++ 2.0 schon: Auf
Klassen-"virtual" kann verzichtet werden, nicht jedoch auf das
Funktionen-"virtual").
Default-Parameter einer virtuellen Funktion sollten in der abgeleiteten
Klasse nie geändert werden, denn Default-Parameter sind immer statisch
gebunden, d.h. "basierte" Pointer rufen immer nur die Basis-Defaults auf, nie
die abgeleiteten!
Alle Datenstrukturen, die gebräuchlich sind, sind potenzielle Kandidaten
für Templates. Spezialisierung ist nur nötig, falls die Typen
die Eigenschaften der Klasse beeinflussen, z.B. andere (adaptive) Funktionen
erfordern.
Downcasts sind Casts, bei denen ein Basisklassen-Pointer auf abgeleitete
Klassen konvertiert wird. Das ist eine unsichere Sache, da nicht definiert ist,
ob der gecastete Pointer erlaubt, auf Funktionen der abgeleiteten Klasse
zuzugreifen (wird erst während der Laufzeit festgestellt). Wann immer
möglich, sollte auf Downcasts verzichtet werden. Bei virtueller
Klassen-Vererbung sind Downcast sogar explizit verboten (Borland C++ 2.0 bricht
die Kompilierung ab, ohne den Fehler zu benennen)! Beispiel:
class X{ int virtual f(); }; // Funktion "virtual" geht!
class Y:public X{ int i; Y(int i){i=1;}; int f(); };
X *x=new X;
x->f(); // OK
((Y*)x)->f(); // Downcast: Konstruktor wird nicht
// aufgerufen. Daher führt "i" Zufallswert!
((Y)x).f(); // Downcast: Der Compiler verlangt einen
// Copy-Konstruktor!
"has a"- und "is implemented with"-Strukturen sind durch Layering
implementierbar, d.h. durch einen Pointer in der Basisklasse auf ein Feld von
Teileobjekten (klassenwertiges Attribut) .
"is implemented with" kann durch Layering realisiert werden oder im Notfall
auch durch PRIVATE-Vererbung (eine PROTECTED-Ableitung ist nicht
möglich). Dadurch kann man Eigenschaften von Basisklassen gewinnen, ohne
dadurch eine Beziehung zu modellieren, da dieser Code nur klassenintern nutzbar
ist und nicht für Anwender zugreifbar ist (private Vererbung). Beispiel:
class X{public: int f();};
class Y:private X{public: int g();};
Y *y=new Y;
y->f(); // ZUGRIFFSFEHLER, obwohl f() PUBLIC ist!
Globale Objekte und "static"-Variablen sollten vor "main()" initialisiert
sein, ansonsten setzt sie der Compiler automatisch auf null (daher bedeutet
"int i;" eine Deklaration UND eine Definition!). Die Reihenfolge der realen
Initialisierung hängt einzig und alleine von der Reihenfolge der
Deklarationen ab, nicht von der Reihenfolge der Initialisierungen!
Achtung! Statische Elemente und Funktionen verhalten sich wie Nicht-Objekte,
d.h. sie lassen sich nicht referenzieren wie die gewöhnlichen Objekte!
Ein Prefix-Inkrement (++i) verlangt den Operator "operator++()". Ein
Postfix-Dekrement (i--) den Operator "operator--(int)".
Beispiel für die Entwicklung einer Daten-Management-Komponente:
-
I. Szenarien in "I am alive"-Form:
-
Ich bin eine ObjectTable: Um ein Objekt zu laden, öffne ich die
Objektedatei, lade diverse Variablen, kreiere alle zum Objekt gehörenden
PDCObjects und sage den PDCObjects, sie sollen sich selbst laden. Danach
schliesse ich die Objektdatei wieder.
-
Ich bin ein PDCObject. Wenn ich mich selbst laden soll, dann sage ich meinem
korrespondierenden PDCObjectTagFormat, es soll sich selbst laden.
-
Ich bin ein PDCObjectTagFormat. Ich lade mich, indem ich meine Attribute, meine
Objekt-ID und meine Beziehungen einlese.
-
II. OOA-/OOD-Modell der DMC (plus angedeutete PDC):
ObjectTable ObjectTagFormat
PDCObjects (d) ObjectID
storeObjects storeAttributes
restoreObjects restoreAttributes
storeObjectID
...
PDCObject PDCObjectTagFormat
ObjectID (d)
storeUsingFormat store
restoreUsingFormat restore
Customer CustomerFormat ProductFormat
Name Name
Adress Price
storeAttributes storeAttr.
restoreAttributes restoreAttr.
... ...
Nimmt ein Objekt Kontakt auf zu einem Objekt der gleichen Klasse, wird dies
dadurch modelliert, dass eine Beziehung plus ein Nachrichtenpfeil links/rechts
aus dem Objektsymbol heraustritt und links/rechts wieder in dasselbe eintritt.
Während beim OOA-Modell noch alle Kardinalitäten angegeben werden,
könne sich diese im OOD-Modell auf diejenigen reduzieren, die unbedingt
nötig sind. Wenn ein Objekt einem anderen Objekt eine Nachricht zukommen
lässt, dass andere Objekt dies aber nicht tut (bis auf die
Rückantwort, die aber nicht durch einen Nachrichtenpfeil angedeutet wird),
dann genügt eine Kardinalität auf der Seite des Sendeobjekts.
Es gilt: x->f(); <==> (*x).f();
Die Kardinalität bei einem Objekt gibt an, wie oft es eine Beziehung mit
dem gegenüberliegendem Objekt eingeht. Bei
Aggregations-/Zerlegungsstrukturen gilt das Gleiche: Die Kardinalität
beim Aggregat gibt an, aus wie vielen der Zerlegungsobjekten es besteht
(meistens "0, m", während die Kardinalität der Zerlegung angibt,
wie viel Aggregaten eines davon angehören kann (meisten "1").
Beziehungen werden durch einfache Linien symbolisiert, die seitwärts aus
den Objektsymbolen führen. Die Pfeile der Nachrichten werden ebenfalls
seitwärts modelliert. Aggregations-/Zerlegungsstrukturen (eine Sonderform
von Mehrfachbeziehungen) verlaufen wie die "is a"-Vererbungsstruktur von oben
nach unten.
Beispiel für die Modellierung einer HIC:
ModelViewContainer | |
| |
layout | HIC | PDC
| |
DisplayBox | Button | XYZ
| |
| action |
| label |
| |
displayValue | displayLabel |
update | push |
ALGOL-60/68 (1960) war ein C++/Pascal-Vorläufer. Daher kommen
Blöcke, Prozesse, Rekursion, Operatoren-Überladung.
Der Adressoperator wird auch &-Operator oder Referenz-Operator genannt.
Es gibt kein Array mit Referenz-Elementen (aber Pointer-Elementen). Es gibt
auch kein Array mit Funktionen, aber mit Funktionspointern!
Die Assoziativität gibt Auskunft über die
Operator-Abarbeitungsreihenfolge. Rechts-assoziativ sind der
Zuweisungsoperator und alle UNÄREN Operationen (z.B. int **a <=>
int *(*a)). Links-assoziativ ist der Rest (z.B. a && b && c
=> (a && b ) && c).
Aufzählungen (Enumerations) DEKLARIEREN (keine Speicherplatz-Allokation)
eine Menge GANZZAHLIGER KONSTANTEN. Z.B. enum Tag { Montag, Dienstag=1,
Mittwoch }.
Die Fehler-Ausnahmebehandlung sieht folgendes vor:
Im Code:
if( BestimmterFehler==TRUE ) throw Fehlertyp();
Fehlerbehandlungsroutine:
try { vielleicht Fehler }
catch (Fehlertyp1) {ja, Fehler 1}
catch (Fehlertyp2) {...}
};
Klassen können direkte oder indirekte Basisklassen sein.
Die Bibliothek enthält eine Menge von compilierten Objekt-Klassen, deren
Deklarationen in Header-Dateien stehen. Der Linker liest nur die
benötigten Bibliotheksteile ein.
Die Bindung gibt den Geltungsbereich von Variablen vor. Globale Namen gelten
in jeder Datei eines Programms (externe Bindung), ausser sie wurden mit static,
const (ohne extern davor) oder inline spezifiziert (dann interne Bindung).
"typedef"-Anweisungen und Enumeratoren haben immer interne, Klassen dagegen
fast immer externe Bindung.
Ein 8-Bit-Feld lässt sich folgendermassen implementieren:
struct X {
unsigned int bit1:1;
...;
unsigned int lastbit:8;
};
Ein Zugriff ist mittels "X x; if(x.bit1){}" möglich. Zeiger bzw.
Referenzen sind nicht möglich, dafür aber Manipulation mit "->"
statt mit den "<<"- oder ">>"-Operatoren.
Ein Builder ist ein Programm, welches Modelle (z.B. ERM, X11-Widgets) in
Codeteile überführt (z.B. C++-Objects) und fertige Applikationen
daraus bildet. +: Änderungen am Modell bewirken dynamische Änderung
am Code.
C von Kernighan/Ritchie ist eine prozedurale Sprache, die noch nicht Klassen,
new, delete, throw, try kennt. C++ 1980 massgeblich von Stroustrup entwickelt
(ohne High-Level-Typen).
Array-Übergaben sind immer Call by Reference.
Containerklassen werden i.d.R. über Templates realisiert.
Die Dereferenzierung geschieht mittels des Inhaltsoperators "operator*", um
den Wert, auf den Typ zeigt, zu liefern. Achtung! Funktioniert nicht mit
void-Zeigern!
Destruktoren werden auch Cleanups genannt. Wenn sie "virtual" sind, kann eine
abgeleitete Klasse auch AUTOMATISCH die Basisklassen löschen.
Der Ellipse-Operator "(...)" ist für die Argument-Übergabe
nützlich, wenn die Anzahl der übergebenen Argumente variieren kann
oder unbekannt ist. Beispiel:
f( int a, ... ){};
f(1); // OK
f(1, "Text", 22 ); // OK
Die Escape-Sequenz wird benutzt zum Ausdruck nicht-ausdruckbarer Zeichen:
Z.B. '\?', '\t' oder '\ooo' für Oktal-Ziffern, '\xhhh' für
Hexadezimal-Ziffern.
Feldernamen (z.B. int i[]={1,2}) sind nicht mit Zeigern identisch, da sie
unveränderbar sind (keine L-Values!) und Speicherplatz reservieren.
Das Argument-Matching erfolgt über folgende Stufen:
(1) triviale Konversion (T&->T).
(2) Typ-Angleichung (char->int).
(3) Standardkonversion (int->long->const long).
(4) benutzerdefinierte Konversion.
Der Funktionsoperator "()" wird auch cast-Operator genannt. Er muss eine
Elementfunktion sein. Günstig für die Parameterübergabe "X
x=y(1,2)".
Die Header-Dateien enthalten nur EXTERN-DEKLARATIONEN (die Definitionen liegen
in den ".cpp"-Dateien). Sie enthalten nicht: Funktionsdefinitionen,
Datendefinitionen (wie int i;) oder Aggregate-Initialisierungen der Form
"t[]={...}".
Es gilt: a---b <=> a-- - b (-- bindet stärker als -). ++++a
(Achtung! "+" Vielfache von 2!) geht, aber a---- geht wegen der
Links-Assoziativität nicht. Ebenso führt ++i++ zu einem
L-Value-Error.
Konstanten können literal (selbst sprechend) sein, z.B.
143=0217=0x8f=0x8F oder '\"Zeichenkette\"' oder ""+Terminator oder .478e2. Sie
sind symbolisch, wenn ein "const" vor dem Bezeichner steht.
Der Konstruktor kann nicht virtual, const, volatile oder static sein.
Der Polymorphismus bietet generische Funktionen, d.h. syntaktisch gleiche
Namen rufen semantisch ähnliche Methoden auf, wodurch insgesamt weniger
Nachrichtennamen im System existieren.
Smart-Zeiger-Klassen sind Klassen, bei denen der "operator->"
überladen wird. Dadurch können bei jeder "->"-Anwendung
irgendwelche Funktionen ausgelöst werden.
Die Speicherbelegung von C++-Programmen sieht folgendermassen aus:
-
Code-Teil (fixiert): Enthält die Funktionen-Implementationen.
-
Daten-Teil (fixiert): Enthält globale und static-Objekte.
-
Stack (variabel): Enthält lokale Variablen während
Blockaktivität.
-
Heap=Freispeicher (variabel): Enthält die new- und delete-Objekte.
Überladen ist eine Polymorphismus-Form, Parameter-Defaulting eine
andere. Nicht überladbar sind die Operatoren: sizeof, ., .*, ::, ?:. Eine
weitere Polymorphismus-Form stellt schliesslich noch das OBJEKTABHÄNGIGE
Funktionsauswahl-Verhalten dar.
L(ocations)-Values beschreiben die Adresse einer Variablen (diese ist nicht
änderbar). R(ead)-Values beschreiben den änderbaren Wert einer
Variablen.
"inline" und "virtual" verträgt sich nicht zusammen!
Folgender Fall ist unmöglich, aber durch anschliessende substituierbar:
class X { X x; }; // ERROR: Compiler weiss Grösse von X nicht!
class X { X *x; }; // OK: Compiler kennt Pointer-Grösse!
class X { X &x; }; // OK: Compiler kennt Referenzgrösse!
Der Zuweisungsoperator "=" wird im Gegensatz zu allen anderen Operatoren NICHT
VERERBT!
Abstrakte Basisklassen benötigen mindestens eine "pure
virtual"-Elementfunktion!
Wegen ihrer Ähnlichkeit sind folgende Funktionen nicht überladbar:
- f(int&i) versus f(int i)
- f(int[]) versus f(int*)
- f(char*) versus f(char*const)
Trotz ihrer Ähnlichkeit sind folgende Funktionen überladbar:
- f(int[][10]) versus f(int[][3])
- f(const int*) versus f(int*)
- f()const versus f()
Das Schlüsselwort "extern" erlaubt die mehrfache DEKLARATION eines Typs,
z.B. extern int a; (KEINE DEFINITION wie int a;). Der Typ muss und darf aber in
einer Datei nur genau einmal definiert sein.
"friend"-Deklarationen sind NICHT TRANSITIV! Ausserdem besitzen sie keinen
IMPLIZITEN THIS-Zeiger, müssen also einen Pointer/eine Referenz als
Argument führen.
Mehrdimensionales Arrays können folgendermassen aufgebaut werden:
X x [a] [b]; // Array von a Arrays mit jeweils b X-Elementen.
int a[][2]={ {1,2}, {3,4}, {5,6} };
Die Grösse lässt sich über "a*b*sizeof(X);" oder "sizeof(x);"
ermitteln.
"friend" können in Template-Klassen benutzt werden, entweder in
parametrisierter Form (Klassen-Typ oder Funktionstyp möglich) oder in
nicht-parametrisierter Form.
"static" hat zwei Bedeutungen: Innerhalb von Klassen bedeuten
"static"-Deklarationen, dass die Variabel/Funktion von allen Instanzen
gemeinsam genutzt wird und ausserhalb der Klasse über "TYP
Klasse::Variable=x;" zu initialisieren ist. "static"-Elementfunktionen besitzen
keinen impliziten "this"-Pointer! Für "static"-Deklarationen ausserhalb
von Klassen gilt, dass diese Variablen/Funktionen nur INNERHALB eine Datei
erkannt werden (und nicht im ganzen Programm).
Der "union"-Befehl kann zum Speicherplatzsparen verwendet werden: Mehrere
Variablen teilen sich hiermit einen Speicherbereich. Beispiel:
struct X {
union {
char b;
int j;
}; // nur b oder j führt einen Wert, nie beide!
};
Virtuelle Destruktoren müssen im Gegensatz zu allen anderen virtuellen
Funktionen NICHT den Bezeichner für die abgeleitete Klassen vorgeben, was
aber auch logisch ist, da der Destruktor immer genauso wie die Klasse heissen
muss, die ihn besitzt (mit vorangestellter Tilde).
Bei "void"-Typen erlaubt C++ keine Zeigerarithmetik der Form "void *xp;
xp++;", da der Compiler die Grösse von "void" nicht kennt!
Eine reine Protokollklasse ist eine Klasse ohne Attribute; sie ist auch immer
abstrakt.
Aggregationsstrukturen werden bei der OOA ohne klassenwertige Attribute bzw.
Referenzen modelliert. Die Technikdetails werden erst ins OOD eingebaut.
Anders sieht es bei den Kardinalitäten aus: Beim OOD-Modell kann auf
dieselben verzichtet werden, wenn ein Teilobjekt nicht wissen muss, zu welcher
Gesamtheit es gehört.
(Broadcast-)Nachrichtenverbindungen zwischen Objekten benötigen
Beziehungen oder eine Aggregationsstruktur. Verbindungen bis an die Klasse
bedeuten, dass immer ein Konstruktoraufrufe dabei ist. Einzige Ausnahme:
Polymorphe Aufrufe von abstrakten Klassen-Funktionen.
Checkliste zum finden von Objekten: Rollen, Orte, Externes (aber zur
Systemverantwortung gehörendes), Interaktionen (z.B. Vertrag), Ereignisse,
Beschreibungen (z.B. Rezeptur) und Organisationseinheiten.
Ableitbare Informationen (aus Performancegründen) sollten erst im
OOD-Modell modelliert werden. Auch findet dort mit unter eine
OOA-Attribute-Aufschlüsselung statt, z.B. Anschrift ergibt Name, Ort, PLZ,
...
Für Vererbungsstrukturen gilt die Regel: Attribute so hoch wie
möglich (Abbau von Redundanzen) und so tief wie nötig (kein
Informations-Overloading) anordnen!
Sind Beziehung temporärer Natur, z.B. Bus<->Fahrer, so ist es
häufig sinnvoll, Ereignisklassen dazwischen einzufügen, z.B.
Fahrt(Datum). Eine solche Ereignisklasse eignet sich auch dann, wenn mehrere
Arten von Beziehungen zwischen Objekten vorliegen können, z.B. "Planung"
und "Aktuell". Die Beziehungen werden in der OOA-Attributeschicht modelliert,
weil im OOD entsprechende Attribute zur Erreichung der Beziehung gesetzt werden
müssen.
Die Klassenspezifikation verlangt ein Hinweis darauf, ob es sich um eine
Generalisierung oder Spezialisierung handelt. Wenn ersteres, dann sind auch die
möglichen Spezialisierungen aufzuzählen!
Wir unterscheiden drei Aggregationen:
-
physische Gesamtheit-Teil-Struktur => keine 0-Kardinalitäten.
-
Container-Inhalt-Struktur => meist beidseitige 0-Kardinalitäten.
-
konzeptionelle Gesamtheit-Teil-Struktur oder Gruppierung-Mitglied-Struktur
=> oft nur beim Teilobjekt eine 0-Kardinalität.
Die Objekt-ID gehört zum Design. Sie ist wichtig für OODB und
verteilte Objekte in Netzen.
Aggregierte Objekte sind Objekte, die andere Objekte enthalten (Layering oder
gelayerte Objekte). Strukturtyp ist Whole-Part, also Gesamtheit-Teil-Struktur.
Multiple Vererbung=mehrfache Vererbung, d.h. ein Objekt hat mehrere DIREKTE
Basisklassen. Achtung: Konflikte wegen Namensgleichheit möglich (Konflikt ist erst
im OOD zu lösen!) => Auflösung der Hierarchie und Netzwerkbildung.
Beispiel: Farbdrucker+Laserdrucker IS_A Farblaserdrucker.
Cluster-Modell von Meyer: Cluster=Subjekte=logisch zusammengehörende
Klassen. Cluster werden parallel in drei Phasen (SPEC, DESIMPL, VALGEN)
modelliert, wobei zuerst generalisierte Cluster entwickelt werden sollten
(für Reuse).
Das Fontänenmodell operiert interaktiv mit Überschneidungen
verschiedener Phasen und aber auch mit abgeschlossenen Phasen, so muss z.B. vor
dem SW-Einsatz der SW-Test komplett abgeschlossen sein. Ist auch für
nicht-objektorientierte SW geeignet.
Baseball-Modell von Coad/Nicola: Von jeder Phase ein bisschen. Schnell
Ergebnisse hervorbringen, aber Vorsicht: keine Vermengung von Designteilen
(HIC, Problembereich, Daten- und Task-Management) zu Beginn, die später
wieder getrennt werden müssen. Notfalls auch zunächst Klassen mit nur
einem Attribut entwerfen, die erst später angereichert werden.
OOA heisst: Modellierung des relevanten Realweltausschnitts. Betrachtet werden
Klassen, Attribute, Methoden, Beziehungen, Strukturen, Subjekte,
Kommunikationsverbindungen, Abstraktionen, Wertebereiche, Kardinalitäten,
Defaultwerte, Spezifikationen, Algorithmen, ... in einem statischen (nach Coad
bzw. Yourdon), einem dynamischen (nach Rumbaugh) und einem funktionellen
Modell.
Delegation heisst bei der OOE, dass ein Objekt aus Reuse-SW-ICs
konstruiert wird, die jeweils bestimmte Aufgaben zu erledigen haben. Die
Aggregation fungiert dabei häufig als Antenne: Sie empfängt, womit die
Teilobjekte weiterarbeiten können.
Implizite Methoden, die nicht angezeigt werden müssen: Konstruktor,
Destruktor, Zugriffsfunktionen, Beziehungsfunktionen (Auf- und Abbau).
Ereignisfolgediagramme gehören zum dynamischen OOA-Modell. Alle
Nachrichten für Externe, sowie Nachrichten von Externen werden modelliert,
und es wird der Nachrichtenaustausch zwischen den Systemobjekten betrachtet.
Die Diagramme bilden die diversen Szenarios ab (aber nicht 1:1 => ein
Szenarioschritt=x Ereignisfolgen). Wenn Methoden auf sich selbst zeigen, ist
dies im Diagramm entsprechend aufzunehmen. Konsistenzbedingung ist, dass nur
alle Objekte (nicht nur Instanzen!) betrachtet werden, die auch im statischen
OOA-Modell auftauchen (bis auf den externen User).
Zustandsdiagramme: Aktivitäten sind eine temporäre,
Timer-gesteuerte Angelegenheit, Aktionen dagegen immerzu möglich.
Zustandsdiagramme für ein Objekt orientieren sich an der Gesamtheit der
Ereignisfolgen, die dieses Objekt berühren. Aktivitäten und Aktionen
sind bei den Zustandsübergängen zu vermerken. Bei der verbalen
Spezifikation sind Wenn-dann-Bedingungen zu erläutern. Eine
Konsistenzbedingung ist, dass man - ausser vom Endzustand - immer zum
Startzustand zurückkommen kann.
Objektorientierte Datenflussdiagramme sind Strukturierte Analyse-Diagramme.
Sie gehören zum funktionalen Modell. Struktogramme und Pseudocodes sind
jedoch geeigneter zur Modellierung dieser Sicht.
Upper-CASE-Tools beachten im Gegensatz zu Low-CASE-Tools auch die Analysephase.
Einige wichtige Namen+Werke: Boehm=Spiralenmodell, Chen=ERM, Coad=OOE,
DeMarco=Strukturierte Analyse, Mellor=IM, Meyer=Cluster-Modell, Nicola=OOD/OOP,
Rumbaugh=OOE, Shlaer=IM, Stroustrup=C++ und Yourdon=OOA.
Zeiger auf Klassenelemente (Offset-Zeiger, die für alle Instanzen
genutzt werden können; funktioniert auch mit static-Elementen [nicht bei
Borland C++ 2.0]):
struct X {
int i; // wenn static, dann streikt Borland C++ 2.0
};
void main() {
int X::* I=&X::i; // I zeigt noch ins Leere
X x;
x.i=1;
X *xp=new X;
x->i=2;
cout << x.*I << xp->*I << endl;
};
Unspezifizierte Argumente über den Ellipse-Operator "...":
include <stdarg.h>
enum typ {ints, doubs}
void drucke(int anz, type x, ...) {
va_list zva; // Zusatzargumente-Pointer
va_start(zva, x); // Tabelle über Typ informieren
switch(x) {
case ints:
for(int i; i<anz; i++)
cout << va_arg(zva, int) << "\t";
break;
case doubs:
for(int i; i<anz; i++)
cout << va_arg(zva, doubs) << "\t";
break;
};
va_end(zva);
};
void main() {
drucke(2, ints, 1, 2);
drucke(3, doubs, 3.3, 4.4, 5.5);
};
Shift-Operatoren "<<" und ">>":
const int x=5; // in Borland C++ 2.0 KEIN modifizierbarer L-Wert
cout << (x << 5); // 5 * 2^5 = 5 * 32 = 160; x behält Wert=5
cout << (x >> 2); // 5 / 2^2 = 5 / 4 = 1
Bit-Operatoren "&", "^" und "|": Es gilt die Tabelle:
Bit1 Bit2 Bit1 & Bit2 Bit1 ^ Bit2 Bit1 | Bit2
-----------------------------------------------
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 0 1
Abgeleitete Datentypen: Alle Datentypen, die aus den vordefinierten
Datentypen wie z.B. "int", "double" und "char" gebildet werden, wobei die
Operatoren "&", "*", "[]" und "()" verwendet werden. Beispiele:
int i[2][3]={{1,2,3}, {4,5,6}}; // abgeleiteter Typ i[2][3]
char *txtp=new char[10]; // abgeleiteter Typ *txtp
int &ri=i;
int f(int i){i++; return i;};
Klassennamen sind nur dann extern gebunden, wenn folgende Bedingungen gegeben sind:
- Die Klasse enthält keine static-Variablen oder -Funktionen!
- Alle Funktionen müssen inline deklariert sein!
- Es darf in der Klasse kein extern gebundenes Objekt angesprochen werden!
Literalkonstanten sind im Gegensatz zu symbolischen Konstanten (const-Variablen)
"selbstsprechende" Konstanten wie "123", "0217" oder "0x8f".
Unvollständige Deklarationen sind Deklarationen der Art "class X;". Sie
sind evtl. nötig, damit doppelt verkettete Klassen realisiert werden
können. Beispiel:
class X;
class Y {
X *xp;
};
class X {
Y *yp;
};
Zugriffsdeklarationen der Form "Klasse::Variable" sind in private
abgeleiteten Klassen nötig, um Zugriff auf bestimmte Basisklassen-Elemente
zu erhalten, da sich diese Elemente dann wie public-abgeleitete Elemente
verhalten. Beispiel:
class X {
public:
int i;
};
class Y:private X {
public:
X::i; // Zugriffsdeklaration. Achtung: Kein Typ davor!
};
void main() {
Y y;
y.i=2; // ist möglich (ohne Zugriffsdeklaration nicht)!
};
Eingebettete Klassen haben keine besonderen Zugriffsrechte auf die
äussere Klasse (das gilt auch umgekehrt). Grund: Auf die interne Klasse
"Y" kann über "X::Y" zugegriffen werden, ohne dass ein "X" existieren
muss! Ausnahme: Auf static-Elemente, Enumeratoren und typedef-Namen kann immer
direkt zugegriffen werden (bei Borland C++ 2.0 kann eine Y-Klasse sogar im
private-Teil von X stehen, und dennoch direkt mit "Y y;" Objekte davon erzeugt
werden; dagegen streikt der Compiler beim Einsatz von static-Variablen)!
Speicheraufbau pro Programm:
- Codeteil: Enthält die Anweisungen.
- Datenteil: Enthält die globalen Variablen und die static-Variablen.
- Stack: Enthält die lokalen Variablen.
-
Heap: Enthält die benutzergesteuerten Variablen, für die ein Zeiger
im Datenteil oder Stack nötig ist, da sie namenlos sind.
L-Wert (L-Value): Das sind i.d.R. modifizierbare Variable-Inhalte. Bei
einigen Operatoren müssen zwingend L-Werte verwendet werden.
Unveränderliche L-Werte sind z.B. Namen für Felder, Funktionen
Literal-Konstanten und const-Variablen.
Leeranweisungen der Form ";" sind bisweilen aus syntaktischen Gründen nötig in C++.
Unbezeichnete Klassen: Das sind Klassen-Definitionen der Form:
class {
int i;
} x, y, z; // x, y, z nur als unspezifizierte Argumente übergebbar
Namen ohne Bindung sind Namen, für die der Compiler keinen Speicherplatz
anfordern muss. Dazu gehören Enumeratoren und typedef-Namen.
Zwei Dinge sind bei Template-Funktionen zu beachten:
-
Die Template-Argumente <x, y, z> müssen alle als Argumente der
Funktion Verwendung finden. Die Funktion dagegen kann noch weitere Argumente
führen.
-
Die Schlüsselwörter "static", "inline" und "extern" dürfen nur
hinter "template <...>" stehen, nicht davor.
Typ-Endungen: "l", "f" und "u" für Literal-Konstanten, wobei die
Reihenfolge von "f"/"l" und "u" unwichtig ist.
Typ-Umwandlungsformen:
- Cast-Notation: int i=(int)j;
- Funktionsnotation: int i=int(j);
Es gilt: Realität > Problembereich > System-Verantwortlichkeit =
Objekte, Strukturen, Beziehungen, Nachrichten-Verbindungen.
Aggregations-Strukturen sind folgendermassen zu überprüfen:
- In welche Teile kann die Aggregationsklasse zerlegt werden?
- Ist die Systemverantwortlichkeit für alle Teile gegeben?
- Enthalten die Teilklassen die nötigen Anforderungen?
Ereignisse sind zeitpunktabhängig, Zustände umfassen dagegen
Zeiträume. Es gilt die Kette: Ereignis -> Zustand -> Ereignis ->
Zustand ...
Aktionen bzw. Aktivitäten werden oft durch Ereignisse wie z.B.
eintreffende Nachrichten ausgelöst. Aktionen sind zeitunabhängige
Operationen von Objekten, wie z.B. "Lesen", "Schreiben", "Nachrichtenversand".
Aktivitäten, z.B. komplexe Methodenaufrufe, benötigen dagegen eine
bestimmte Zeit zur Ausführung, was i.d.R. durch Timer realisiert wird.
Attribute werden folgendermassen geprüft: Keine Redundanzen enthalten?
Keine ableitbaren Informationen enthalten? Existiert immer ein sinnvoller Wert
(wenn nicht, Spezialklasse bilden)? Einziges Attribut einer Klasse?
Die OOA besteht aus folgenden Komponenten:
-
Statisches Modell: Attribute-, Methoden-, Objekte-, Subjekte-,
Nachrichten-Verbindungsschicht zur Modellierung der Problembereichskomponente
im Bereich der System-Verantwortlichkeit mit interner Konsistenzprüfung
und den Klassen-Spezifikationen.
-
Dynamisches Modell: Szenarien pro Objekt, Ereignis-Folge-Diagramme pro Szenario
und Zustands-Diagramme pro Objekt.
-
Funktionales Modell: Pseudocodes und Struktogramme für alle komplexeren
Methoden. Evtl. auch noch Strukturierte Analysen und Fluss-Diagramme.
Die Klassen-Spezifikationen des statischen Modells sind folgendermassen aufgebaut:
KLASSE < Name, Spezialisierung, Generalisierung, Beschreibung
TEILKLASSE < Name, Kardinalität, Strukturtyp, Beschreibung > TEILKLASSE ...
BEZIEHUNG < Objektname, Kardinalität, Beschreibung > BEZIEHUNG ...
ATTRIBUT < Name, Typ, Wertebereich, Beschreibung > ATTRIBUT ...
METHODE < Name, Argumente, Rückgabewert, Beschreibung > METHODE ...
NACHRICHTENVERBINDUNG < Objektname, Methode, > NACHRICHTENVERBINDUNG ...
Argumente, Rückgabewert,
Beschreibung>
Gefundene Klassen sind vielfältig zu untersuchen: Klassenwissen
nötig? Klassenwissen ausreichend? Mehr als ein Attribut? Mehr als eine
Instanz? Gelten Attribute/Methoden für alle Objekte? Keine ableitbaren
Attribute enthalten? Wird Problembereich abgebildet (ohne OOA- und
OOP-Konstrukte)?
Konsistenzprüfungen sind nicht nur in den einzelnen OOA-Modellen
(statisch/dynamisch/funktional) vorzunehmen, sondern auch zwischen den
Modellen. Achtung: Implizite Methoden sind hiervon auszuklammern! Beim ersten
Durchlauf des Baseball-Modells muss jedoch z.B. keine statischen
Modell-Änderung vorgenommen werden, wenn im dynamischen Modell abweichende
Methoden im Ereignis-Folge-Diagrammen auftauchen.
Die Szenarien geben alle User-Aktionen und Systemreaktionen wieder. Nur wenn
die Reaktion dem User angezeigt wird, ist für sie ein Extra-Schritt
anzugeben; interne Prüfungen usw. werden im gleichen Schritt mit der
User-Aktion wiedergegeben.
Neben Klassen-Spezifikation des statischen Modells der OOA gibt es auch
Zustands-Spezifikationen der dynamischen Modells der OOA. Sie sind
folgendermassen aufgebaut:
ZUSTAND < Name, Beschreibung, Attributwert
EREIGNIS < Name, Sender, Argumente, Aktion, Folgezustand