8.8.3 |
Bidirektionale Objektbeziehungen mit Kardinalität
|
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(); // ... };