Objektorientierte Entwicklung (OOE): Splitter II

Geschwurbel von Daniel Schwamm (05.08.1994 bis 09.08.1994)

Inhalt

1. Model View Controller

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

2. Parametrisierte Funktionen bzw. Klassen

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:

  1. 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;
    };
  2. 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.
  3. 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:

  1. Die friend-Funktion ist nicht parametrisiert: Daraus folgt, dass jede Klassen-Instanz für jeden Typ die gleiche friend-Funktion verwendet.
  2. 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);
    };
  3. 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!
};

3. Fehler-Handling

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 ...
};

4. Sonstiges

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;
};