[Zurück] [Index] [Weiter]     Kuhlins: Objektorientiertes Design für C++


8.8.3

Bidirektionale Objektbeziehungen mit Kardinalität 0,1

Bei einer bidirektionalen Objektbeziehung mit Kardinalität 0,1 ist die referentielle Integrität zu sichern. Es dürfen daher keine Beziehungen einseitig auf- bzw. abgebaut werden. Der Symmetrie der Beziehung wird am besten eine globale Verbindungsfunktion gerecht, die friend beider Klassen ist und jeweils die private Hilfsfunktion _verbinde aufruft.

class X;

class Y {
public:
   friend void verbinde(X*, Y*);
   Y() : x(0) { }
   ~Y() { verbinde(x, 0); }
   X* gibX() const { return x; }
private:
   Y(const Y&);  // keine Kopien
   Y& operator=(const Y&);
   void _verbinde(X* xx) { x = xx; }
   X* x;
};

class X { /* beziehungstechnisch symmetrisch zu Y */ };

void verbinde(X* xx, Y* yy) {
   // vorher xx <-> yy' und yy <-> xx'
   if (xx != 0) {
      if (xx->gibY() != 0)
         xx->gibY()->_verbinde(0);  // yy' -> xx löschen
      xx->_verbinde(yy);            // xx -> yy aufbauen
   }
   if (yy != 0) {
      if (yy->gibX() != 0)
         yy->gibX()->_verbinde(0);  // xx' -> yy löschen
      yy->_verbinde(xx);            // yy -> xx aufbauen
   }
   // jetzt xx <-> yy, xx' -> 0 und yy' -> 0
}

inline void verbinde(Y* yy, X* xx) { verbinde(xx, yy); }

Durch die Definition der zweiten Verbindungsfunktion mit vertauschten Parametern ist ein sinnloser Aufruf verbinde(0, 0); mehrdeutig und somit fehlerhaft.

Beim Kopieren eines Objekts, das mit einem anderen Objekt in Beziehung steht, dürfen weder die Kardinalitäten noch die referentielle Integrität der Beziehung verletzt werden. Dies wird am einfachsten dadurch gewährleistet, daß keine Kopien zulässig sind. Andere Möglichkeiten wären:

Das Auf- und Abbauen von Beziehungen ist für const Objekte nicht möglich. Im Konstruktor darf keine Verbindung aufgebaut werden, weil sonst u. U. ein const Objekt modifiziert wird, z. B.

class Y {
public:
   explicit Y(X* xx) : x(0) { verbinde(xx, this); }
   // ...
private:
   X* x;
};

void test() {
   X xx;
   const Y yy(&xx);   // mit Y(X*)
   verbinde(&xx, 0);  // xx und yy trennen
}

Beim Lösen der Verbindung zwischen xx und yy wird das Datenelement x des const Objekts yy auf 0 gesetzt. Dies wird vom Compiler nicht moniert, da xx einen Zeiger auf Y und nicht auf const Y besitzt.

Bei dem gerade gezeigten Ansatz kann die referentielle Integrität der Beziehung über die public Schnittstelle der Klassen nicht gestört werden. Allerdings sind Programmierfehler bei der Implementation der Elementfunktionen nicht ausgeschlossen. Mit Hilfe einer parametrisierten Beziehungsklasse kann auch diese Fehlerquelle beseitigt werden. Beziehungsklassen dieser Art werden auch von objektorientierten Datenbanken eingesetzt (vgl. Schader 1996).

template<class Links, class Rechts> class B1zu1;

template<class Links, class Rechts>
class B1zu1 {
   friend class B1zu1<Rechts, Links>;
   typedef B1zu1<Rechts, Links> Rechts::*tr;
public:
   B1zu1(Links& li, tr zre) : l(li), r(0), zr(zre) { }
   ~B1zu1() { if (r != 0) (r->*zr).r = 0, r = 0; }
   B1zu1& operator=(Rechts*);
   Rechts* operator->() { return r; }
   const Rechts* operator->() const { return r; }
   Rechts& operator*() { return *r; }
   const Rechts& operator*() const { return *r; }
   operator bool() const { return r != 0; }
   operator Rechts*() { return r; }
   operator const Rechts*() const { return r; }
private:
   B1zu1(const B1zu1&);  // keine Kopien
   B1zu1& operator=(const B1zu1&);
   Links& l;     // Objekt, in dem das Beziehungsobjekt steckt
   Rechts* r;    // Partnerobjekt
   const tr zr;  // Position des Beziehungsobjekts im Partnerobjekt
};

template<class Links, class Rechts>
B1zu1<Links, Rechts>&
B1zu1<Links, Rechts>::operator=(Rechts* re) {
   if (r != 0)
      (r->*zr).r = 0;
   r = re;
   if (re != 0) {
      Links*const li = (re->*zr).r;
      if (li != 0)
         (li->*((re->*zr).zr)).r = 0;
      (re->*zr).r = &l;
   }
   return *this;
}

Die Operatoren sind für die Beziehungsklasse B1zu1 so überladen, daß sich ihre Objekte wie Zeiger verhalten. Im Gegensatz zu "gewöhnlichen" Zeigern können sie aber nicht kopiert werden. Außerdem zeichnen sie sich dadurch aus, daß sie die referentielle Integrität einer bidirektionalen 1:1 Beziehung sichern. Dazu wird eine ggf. bestehende Beziehung im Destruktor abgebaut. Bei Zuweisungen wird die alte Beziehung ab- und die neue aufgebaut.

Die gezeigte Implementation benutzt ein Datenelement l zum Speichern der Adresse des Objekts, in dem das Beziehungsobjekt, für das der Zuweisungsoperator aufgerufen wird, enthalten ist. Das Datenelement l wird nur im Zuweisungsoperator benötigt, um die Adresse des Objekts zu ermitteln. Diese Adresse ist prinzipiell auch berechenbar, so daß das Datenelement eingespart werden kann.

Links*const l = reinterpret_cast<Links*>(
   reinterpret_cast<unsigned long int>(this)
   -reinterpret_cast<unsigned long int>(
      &(static_cast<Links*>(0)->*((re->*zr).zr))
   )
);

Obwohl auf den meisten Systemen die Adresse korrekt berechnet wird, ist die Lösung i. a. nicht portabel. Denn es ist einerseits nicht gewährleistet, daß der größte ganzzahlige Datentyp (unsigned long int) groß genug ist, um die Adresse eines Zeigers zu speichern. Andererseits ist nur die Konvertierung eines Zeigers in einen ganzzahligen Wert, der groß genug ist, um den Wert des Zeigers zu speichern, und zurück in denselben Zeiger definiert (WP §5.2.9). In der fraglichen Anweisung ist dies aber nicht gegeben, so daß das Ergebnis implementationsabhängig ist. Die Dereferenzierung des Nullzeigers ist dagegen unbedenklich, da er lediglich in die Adreßberechnung eingeht, aber kein Speicherzugriff erfolgt.

Der bei der Implementation der Beziehungsklasse getriebene Aufwand zahlt sich bei der Benutzung der Klasse voll aus. Eine bidirektionale Objektbeziehung mit Kardinalität 0,1 zwischen den Klassen X und Y wird realisiert, indem jede der Klassen ein Objekt der Beziehungsklasse B1zu1 definiert. Das erste Template-Argument ist dabei die eigene Klasse und das zweite die andere Klasse. Im Konstruktor ist das Beziehungsobjekt korrekt zu initialisieren (siehe Listing).

class X;

class Y {
public:
   Y();
   B1zu1<Y, X> x;
   // ...
};

class X {
public:
   X();
   B1zu1<X, Y> y;
   // ...
   void bsp();
};

Y::Y() : x(*this, &X::y) { }
X::X() : y(*this, &Y::x) { }

Das Beziehungsobjekt ist jeweils public deklariert, damit Verbindungen einfach per Zuweisung etabliert werden können. Außerdem erlaubt dies einen direkten Zugriff auf das Partnerobjekt.

void test() {
   X x;
   Y y;
   y.x = &x;    // Verbindung zwischen x und y aufbauen
   y.x->bsp();  // entspricht x.bsp();
   y.x = 0;     // Verbindung zwischen x und y lösen
}

Dem Benutzer der Klasse werden dadurch nicht mehr Rechte eingeräumt als bei einer Implementation, die mit Zugriffsfunktionen arbeitet.

class Y {
public:
   void verbinde(X*);
   X* gibX();
   // ...
};