Objektorientierte Entwicklung (OOE): Splitter II
von Daniel Schwamm (05.08.1994 bis 09.08.1994)
Aus "Heimat des Dilettantismus"
http://www.henrys.de/daniel/index.php?cmd=texte_objektorientierte-entwicklung-mix-2.htm
nach Schader, Rumbaugh (1994)
Inhalt
1. Model View Controler
2. Parametisierte Funktionen/Klassen
3. Fehler-Handling
4. Sonstiges
1. Model View Controler
Der Model View Controler von Rumbaugh ist eine
objektorientierte Framework, die ähnlich wie das Seeheim-Modell vorsieht,
das User Interface getrennt von der eigentlichen Applikation zu modellieren.
Schematisch baut sich der Model View Controller folgendermaßen auf:
User
Controller
View1 View2 View3 View4 View5
Model (Subjekt, Problemdomäne)
Auf ein Beispiel bezogen sieht obiges Schema
folgendermaßen aus:
User
Steuerung
Mausklick Tastatur Joystick
Cockpit Sound Landkarten Widgets
Flugsimulator
Flugzeug Ort Atmosphäre
2. Parametisierte Funktionen/Klassen
(1) Parametisierte Funktionen: Nachfolgende Funktion erlaubt
es, beliebige Objekte auszugeben. Dazu muß nur eine Funktions-Definition
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 muß.
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 Funktions-Templates zu beachten:
template <class T>
T f(T x, T y) {...};
void main() {
int i=f(10, 1.2); // ERROR, weil 1.2 kein int!
};
unline template <class T> // ERROR: unline/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!
(2) Klassen-Templates: Über normale Klassen kann man nur
Objekte des gleichen Typs erzeugen. Mit Hilfe der nachfolgenden drei Methoden
kann man sich Klassen-Instanzen für jeden beliebigen Typ erzeugen:
* 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. Problematisch an solch einem "generischen" Stack
ist allerdings, daß auf ein und demselben Stack Objekte verschiedenen
Typs verwaltet werden können, was bzgl. Iteratoren u.ä. zu
Inkonsistenzen führen könnte.
Beispiel Void_StackC:
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 enought 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, daß die Schlange leer ist und nicht voll.
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 enought 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;
};
* Ein Stack, der verschiedene Objekttypen verwalten kann,
läßt 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,
muß er doch für jeden neuen Typ eine eigene Stack-Klasse
entwicklen.
* 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.
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 abgleitete Klasse, die selbst nicht parametisiert
ist, muß den Basisklassen-Typ angeben, und hätte dadurch folgendes
Aussehen:
class Y:puplic X<int> {...};
Die Elementfunktionen von parametisierten Klassen sind immer
parametisierte Funktionen, weil der implizite this-Zeiger parametisiert ist.
Für friend-Funktionen dagegen trifft dies nicht zu; hier sind drei
Fälle denkbar:
* Die friend-Funktion ist nicht parametisiert. Daraus folgt,
daß jede Klassen-Instanz für jeden Typ die gleiche friend-Funktion
verwendet.
* Die friend-Funktion enthält parametisierte Argumente.
Daraus folgt, daß pro aufgerufenem Typ eine eigene friend-Funktion vom
Compiler erzeugt wird.
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 parametisierte
Argumente, diese stammen jedoch von einer anderen Klasse. Dies bewirkt das
gleiche wie der erste Fall.
Achtung: Folgende Dinge sind bei Klassentemplates zu
beachten:
* Eingebettete Klassen können nicht parametisiert werden,
da Templates immer global zu deklarieren sind.
* Es gilt:
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!
};
3. Fehler-Handling
In C bzw. C++ wurden Fehler üblicherweise
folgendermaßen abgefangen:
int* reserviere(int i) {
if(i>100) {
cout << "Index zu groß" << 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 folgendermaßen
behandelt werden (dies erlaubt ein effektiveres Abfangen von Ausnahmen):
struct Ausnahme {
char *txtp; // für Fehlertext
Ausnahme(const char *tp) { txtp=tp; };
};
struct ZuGross:public Ausnahme {
int size; // für zu korrigierende Größe
ZuGross(int i):Ausnahme("Index zu groß"), 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;
// Fehlerkorrekturmaßnahmen 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ößen-Kontrolle müßte in einem
} // umschließenden try-Block stattfinden
catch (Ausnahme &a) { // Basiklasse erfaßt 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 ...
};
4. Sonstiges
* Wenn ein statisches OOA-Modell anzugeben ist, schließt
das die Klassen-Spezifikation nicht mit ein; diese wird erst beim erweiterten
statischen Modell relevant!
* Entscheidungsfolge-Diagramme sind in aureichender
Größe anzufertigen, so daß jeder Pfeil beschriftet werden
kann! Zu beachten ist, daß jede Linie ein Objekt repräsentiert und
nicht nur eine Klasse!
* Es gilt: Real World > Problembereich > Systembereich!
Normalerweise sind die User außerhalb 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:
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:
fA() -> fB() -> fA() -> fC()
* NIH-Besonderheiten:
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:
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)(); // Funktions-Pointer
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 boolschen
Klasse:
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;
};
|