Objektorientierte Entwicklung (OOE): Splitter II
Geschwurbel von Daniel Schwamm (05.08.1994 bis 09.08.1994)
Inhalt
Der Model View Controller (MVC) von James Rumbaugh ist ein objektorientiertes
Framework, welches ähnlich wie das Seeheim-Modell (Trygve Reenskaug, 1979) vorsieht,
das User Interface (UI) getrennt von der eigentlichen Applikation zu modellieren.
Schematisch baut sich der Model View Controller folgendermassen auf:
User
Controller
View1 View2 View3 View4 View5
Model (Subjekt, Problemdomäne)
Auf ein Beispiel bezogen sieht obiges Schema folgendermassen aus:
User
Steuerung
Mausklick Tastatur Joystick
Cockpit Sound Landkarten Widgets
Flugsimulator
Flugzeug Ort Atmosphäre
Parametrisierte Funktionen: Nachfolgende Funktion erlaubt es,
beliebige Objekte auszugeben. Dazu muss nur eine Funktionsdefinition
mit dem Schlüsselwort "template" angegeben werden, weil der Compiler dann
an den Funktionsaufrufen (im main()-Teil) erkennt, für welche Typen er
eine eigene Funktionsinstanz implementieren muss.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
template <class T>
void f(T &t)
{
cout << t << endl;
};
struct X
{
char *txtp;
friend ostream* operator<<(ostream &os, X &x)
{
os << x.txtp;
return os;
};
X(char *tp) { txtp=tp; };
};
void main()
{
int i=100;
double d=1.2;
char str[]="Hallo";
X x("Hurz");
f(i);
f(d);
f(str);
f(x);
};
Folgende Punkte sind bei Funktionstemplates zu beachten:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
template <class T>
T f(T x, T y) {...};
void main()
{
int i=f(10, 1.2); // ERROR, weil 1.2 kein int!
};
inline template <class T> // ERROR: inline/extern/static
void f(T x) {...}; // müssen hinter template <> stehen
template <class T, class S>
void f(T &t) {...}; // ERROR, weil S kein Argument!
Klassen-Templates: Über normale Klassen kann man nur
Objekte des gleichen Typs erzeugen. Mithilfe der nachfolgenden drei Methoden
kann man sich Klassen-Instanzen für jeden beliebigen Typ erzeugen:
-
void-Pointer: Um z.B. einen Stack zu erzeugen, der Objekte beliebigen Typs
verwalten kann, eignet sich der void-Pointer. Statt direkt die Objekte zu
verwalten, werden auf dem Stack nur void-Pointer abgelegt, die auf beliebige
Objekte zeigen können.
Beispiel Void_StackC: Problematisch an solch einem "generischen" Stack
ist allerdings, dass auf ein und demselben Stack Objekte verschiedenen
Typs verwaltet werden können, was bezüglich Iteratoren u.ä. zu
Inkonsistenzen führen könnte.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
class Void_StackC {
public:
void **Buffer;
int Size;
int Pos;
Void_StackC(const int &i=3) {
Size=i;
Pos=0;
if(!(Buffer=new void*[Size])) {
cout << "*** Not enough memory! ***" << endl;
exit(0);
};
};
~Void_StackC() { delete [Size] Buffer; };
void Push(void *vp) {
if(!Full())
Buffer[Pos++]=vp;
};
void *Pop() {
if(!Empty())
return Buffer[--Pos];
return NULL;
};
int Empty() {return (Pos<=0)?1:0;};
int Full() {return (Pos>=Size)?1:0;};
};
void main() {
Void_StackC vi(3); // offen für alle Arten Pointer
for(int i=0; i<3; i++)
vi.Push(new int(i)); // Ablage von int-Pointern
while(!vi.Empty())
cout << *((int*) vi.Pop()) << endl;
};
Beispiel Void_QueueC: Der "Trick" bei Queues ist, immer ein
Element mehr zu erlauben, als der Anwender wünscht, weil dann durch
"Pos==Front" zu erkennen ist, dass die Schlange leer ist und nicht voll.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
class Void_QueueC {
public:
void **Buffer;
int Size;
int Pos;
int Front;
Void_QueueC(int &i=10) {
Size=i+1;
Pos=Front=0;
if(!(Buffer=new void*[Size])) {
cout << "*** Not enough memory! ***" << endl;
exit(0);
};
};
~Void_QueueC() { delete [Size] Buffer; };
void Push(void *vp) {
if(!Full()) {
Buffer[Pos]=vp;
Pos=(Pos+1)%Size;
};
};
void *Pop() {
void *vp;
if(!Empty()) {
vp=Buffer[Front];
Front=(Front+1)%Size;
return vp;
};
return NULL;
};
int Empty() { return (Pos==Front)?1:0; };
int Full() { return (Pos-Front==Size-1 || Front-Pos==1)?1:0; };
};
void main() {
int i;
Void_QueueC v(5); // offen für alle Arten Pointer
cout << "Queuefront (0 bis 4): ";
cin >> v.Front;
v.Pos=v.Front;
while(!v.Full()) {
cout << "Integer: ";
cin >> i;
v.Push(new int(i)); // Ablage von int-Pointern
};
while(!v.Empty())
cout << *((int*)v.Pop()) << endl;
};
-
Ableitung: Ein Stack, der verschiedene Objekttypen verwalten kann,
lässt sich auch durch Ableitung realisieren. Wir entwickeln die
abstrakte Basisklasse "Stack" mit pure-virtual Methoden, von dem dann die
benötigten Stacks, z.B. "IntStack", abgeleitet werden können. Diese
Methode verlangt einiges an Arbeitsaufwand für den Programmierer,
muss er doch für jeden neuen Typ eine eigene Stack-Klasse entwickeln.
-
Template-Klassen: Der effektivste Weg, eine generische Stack-Klasse zu
implementieren, stellt die Benutzung von Template-Klassen dar. Bei dieser
Methode überlassen wir es dem Compiler, je nach Anforderung die
nötigen typspezifischen Stack-Klassen-Instanzen zu erzeugen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
template <class T>
class X {
int *ip;
void f(T &t) { cout << t << endl; };
T g(T &t);
X() { ip=NULL; };
X(X<T>&);
};
template <class T> T X<T>::g(T &t) { return ++t; };
template <class T> X<T>::X(X<T> &x) { ip=x.ip; return *this; };
template <class T> void ff(X<T> &x, T &t) { x.f(t); };
void main() {
X<int> x, y(x);
X<double> z;
x.f(7);
cout << y.g(7) << endl;
ff(z, 1.4);
};
Eine von X abgeleitete Klasse, die selbst nicht parametrisiert
ist, muss den Basisklassen-Typ angeben, und hätte dadurch folgendes
Aussehen:
00001
class Y:puplic X<int> {...};
Die Elementfunktionen von parametrisierten Klassen sind immer
parametrisierte Funktionen, weil der implizite this-Zeiger parametrisiert ist.
Für friend-Funktionen dagegen trifft dies nicht zu; hier sind drei
Fälle denkbar:
-
Die friend-Funktion ist nicht parametrisiert: Daraus folgt,
dass jede Klassen-Instanz für jeden Typ die gleiche friend-Funktion
verwendet.
-
Die friend-Funktion enthält parametrisierte Argumente:
Daraus folgt, dass pro aufgerufenem Typ eine eigene friend-Funktion vom
Compiler erzeugt wird.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
template <class T>
class X {
T tt;
friend void f(X<T>&);
public:
X(T &t) { tt=t; };
};
template <class T> void f(X<T> &x) {
cout << x.tt << endl;
};
void main() {
X<char> x('a');
f(x);
};
-
Ein friend-Funktion enthält zwar parametrisierte Argumente, diese stammen
jedoch von einer anderen Klasse: Dies bewirkt das Gleiche wie der erste Fall.
Achtung: Eingebettete Klassen können nicht parametrisiert werden,
da Templates immer global zu deklarieren sind.
Es gilt:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
template <class T, int i>
class X {...};
void main() {
X<int, 10> a;
X<int, 2*5> b;
X<int, 11> c;
b=a; // OK, weil a und b gleichen Typ X<int, 10> haben!
a=c; // ERROR, weil c den Typ X<int, 11> hat!
};
In C bzw. C++ wurden Fehler üblicherweise folgendermassen abgefangen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
int* reserviere(int i) {
if(i>100) {
cout << "Index zu gross" << endl;
exit(1);
};
int *ip=new int[i];
if(ip==NULL) {
cout << "Kein Speicherplatz mehr" << endl;
exit(1);
};
return ip;
};
void main() {
int *ip=reserviere(10);
};
Seit einiger Zeit können Fehler auch folgendermassen
behandelt werden (dies erlaubt ein effektiveres Abfangen von Ausnahmen):
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
struct Ausnahme {
char *txtp; // für Fehlertext
Ausnahme(const char *tp) { txtp=tp; };
};
struct ZuGross:public Ausnahme {
int size; // für zu korrigierende Grösse
ZuGross(int i):Ausnahme("Index zu gross"), size(i) {};
};
struct SpeicherMangel:public Ausnahme {
ZuGross:Ausnahme("Kein Speicherplatz mehr") {};
};
int* reserviere(int i) {
if(i>100)
throw ZuGross(i); // Übergabe von size!
int *ip=new int[i];
if(ip==NULL)
throw SpeicherMangel; // ruft alle Destruktoren des
return ip; // try-Blocks auf!
};
void main() {
int *ip;
try {
ip=reserviere(101);
}
catch (ZuGross &zg) { // catch(...) würde alles Abfangen
// Fehlerausgabe
cout << zg.txtp << endl;
// Fehlerkorrekturmassnahmen vor Ort, weil ein
// Rücksprung zum Fehlerort nicht möglich ist.
// Der nächste catch-Block wird nicht untersucht!
int i;
if(sz.size>200)
i=100;
else i=50;
ip=reserviere(i); // Grössen-Kontrolle müsste in einem
} // umschliessenden try-Block stattfinden
catch (Ausnahme &a) { // Basiklasse erfasst auch
cout << a.txtp << endl; // abgeleitete Fehlerobj.
return; // Abbruch ==> dürfen nicht am
}; // Anfang stehen!
// wird ein Fehlerobjekt nicht gefunden,
// bricht das Programm ab.
// Ansonsten: Fortsetzung des Programms ...
};
Wenn ein statisches OOA-Modell anzugeben ist, schliesst das die Klassen-Spezifikation
nicht mit ein; diese wird erst beim erweiterten statischen Modell relevant!
Entscheidungsfolge-Diagramme sind in ausreichender Grösse anzufertigen, sodass jeder
Pfeil beschriftet werden kann! Zu beachten ist, dass jede Linie ein Objekt repräsentiert
und nicht nur eine Klasse!
Es gilt: Real World > Problembereich > Systembereich!
Normalerweise sind die User ausserhalb des Systembereichs anzusiedeln.
Der jeweilige Zustand bei Zustandsdiagrammen pro Objekt ist
durch eine geeignete Variable anzugeben!
exec() zum Aufruf eines eigenständigen, kompilierten
Programms ist nach fork() nur nötig, wenn der Sohn-Code nicht im
Vater-Code integriert worden ist.
Signalsetzung:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
void fA(){...};
void fB(){...};
void fC(){...};
void g1() {
signal(sigint, fA());
...;
};
void g2() {
g1();
signal(sigint, fB());
...;
};
void main() {
signal(sigint, fC());
g2();
signal(sigint, fA());
while(1)
;
};
Wird bei obigem Programm in der while-Schleife ein sigint
ausgelöst, z.B. durch ein Control-C-Ereignis, so wird der Signal-Stack
abgebaut, d.h., die Signal-Funktion fA(), fB() und fC() werden in folgender
Reihenfolge abgearbeitet:
00001
fA() -> fB() -> fA() -> fC()
NIH-Besonderheiten:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
Task::Task(...):HeapProc(...) {
if(FORK() != 0) { // UNIX-fork() genauso!
// Vater-Prozedur
...
};
// Sohn-Prozedur oder exec()
...
};
void main() {
MAIN_PROZESS(priorität); // erhält i.d.R. Priorität=0
...
};
Funktionsaufruf über Funktionspointer:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
int f1() { return 1; };
int f2() { return 2; };
void g1(int &i) { cout << i << endl; };
void g2(int &i) { cout << i*2 << endl; };
typedef int (*fp)(); // Funktionspointer
typedef void (*gp)(int&);
void main() {
fp f[2]; gp g[2]; // Felder mit Funktionspointern
f[0]=&f1; f[1]=&f2;
g[0]=&g1; g[1]=&g2;
for(int i=0; i<2; i++) {
cout << (*f[i])() << endl; // Ausgabe von f1 und f2
(*g[i])(i); // Ausgabe von g2 und g2
};
};
Überladen von Operatoren. Beispiel einer booleschen Klasse:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
enum bool {FALSE, TRUE};
struct boolean {
bool value;
friend ostream& operator<<(ostream &os, boolean &b) {
os << (int)b.value;
return os;
};
boolean operator&&(boolean &b) {
if(value==TRUE && b.value==TRUE)
value=TRUE;
else value=FALSE;
return *this;
};
boolean operator||(boolean &b) {
if(value==TRUE || b.value==TRUE)
value=TRUE;
else value=FALSE;
return *this;
};
boolean operator=(bool &b) { value=b; return *this;};
boolean operator=(boolean &b) { value=b.value; return *this;};
};
void main() {
boolean a, b;
a=FALSE;
b=TRUE;
cout << a << " " << b << endl;
cout << (a && b) << endl;
cout << (a || b) << endl;
cout << endl;
};