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:
    1. 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.
    2. Ich bin ein PDCObject. Wenn ich mich selbst laden soll, dann sage ich meinem korrespondierenden PDCObjectTagFormat, es soll sich selbst laden.
    3. 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:

  1. physische Gesamtheit-Teil-Struktur => keine 0-Kardinalitäten.
  2. Container-Inhalt-Struktur => meist beidseitige 0-Kardinalitäten.
  3. 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:

  1. Die Template-Argumente <x, y, z> müssen alle als Argumente der Funktion Verwendung finden. Die Funktion dagegen kann noch weitere Argumente führen.
  2. 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:

  1. 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.
  2. Dynamisches Modell: Szenarien pro Objekt, Ereignis-Folge-Diagramme pro Szenario und Zustands-Diagramme pro Objekt.
  3. 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