Was ist ein guter Weg, um eine Viele-zu-Viele-Beziehung zwischen zwei Klassen darzustellen?

10

Angenommen, ich habe zwei Objekttypen, A und B. Die Beziehung zwischen ihnen ist viele-zu-viele, aber keiner von ihnen ist der Besitzer des anderen.

Sowohl A- als auch B-Instanzen müssen sich der Verbindung bewusst sein. Es ist nicht nur ein Weg.

Also können wir das tun:

class A
{
    ...

    private: std::vector<B *> Bs;
}

class B
{
    private: std::vector<A *> As;
}

Meine Frage ist: Wo platziere ich die Funktionen zum Erstellen und Zerstören der Verbindungen?

Sollte es A :: Attach (B) sein, das dann die Vektoren A :: Bs und B :: As aktualisiert?

Oder sollte es B :: Attach (A) sein, was ebenso vernünftig erscheint.

Keiner von beiden fühlt sich richtig an. Wenn ich aufhöre, mit dem Code zu arbeiten, und nach einer Woche zurückkomme, kann ich mich sicher nicht erinnern, ob ich A.Attach (B) oder B.Attach (A) ausführen soll.

Vielleicht sollte es eine Funktion wie diese sein:

CreateConnection(A, B);

Das Erstellen einer globalen Funktion erscheint jedoch auch unerwünscht, da es sich um eine Funktion handelt, die speziell für die Arbeit mit nur den Klassen A und B vorgesehen ist.

Eine andere Frage: Wenn ich häufig auf dieses Problem / diese Anforderung stoße, kann ich dann irgendwie eine allgemeine Lösung dafür finden? Vielleicht eine TwoWayConnection-Klasse, die ich von Klassen ableiten oder in Klassen verwenden kann, die diese Art von Beziehung teilen?

Was sind einige gute Möglichkeiten, um mit dieser Situation umzugehen ... Ich weiß, wie man mit der Eins-zu-Viele-Situation "C besitzt D" recht gut umgeht, aber diese ist schwieriger.

Bearbeiten: Nur um es deutlicher zu machen, beinhaltet diese Frage keine Eigentumsfragen. Sowohl A als auch B gehören einem anderen Objekt Z, und Z kümmert sich um alle Eigentumsfragen. Ich bin nur daran interessiert, wie man die vielen-zu-vielen-Verknüpfungen zwischen A und B erstellt / entfernt.

Dmitri Shuralyov
quelle
Ich würde einen Z :: Attach (A, B) erstellen. Wenn Z also die beiden Arten von Objekten besitzt, fühlt er sich "richtig", damit er die Verbindung herstellt. In meinem (nicht so guten) Beispiel wäre Z ein Repository für A und B. Ohne ein praktisches Beispiel kann ich keinen anderen Weg sehen.
Machado
1
Genauer gesagt können A und B unterschiedliche Eigentümer haben. Vielleicht gehört A Z, während B X gehört, das wiederum Z gehört. Wie Sie sehen können, wird es beim Versuch, den Link an anderer Stelle zu verwalten, sehr kompliziert. A und B müssen in der Lage sein, schnell auf eine Liste der Paare zuzugreifen, mit denen sie verknüpft sind.
Dmitri Shuralyov
Wenn Sie neugierig auf die richtigen Namen von A und B in meiner Situation sind, sind sie Pointerund GestureRecognizer. Zeiger gehören der InputManager-Klasse und werden von ihr verwaltet. GestureRecognizers gehören Widget-Instanzen, die wiederum einer Screen-Instanz gehören, die einer App-Instanz gehört. Zeiger werden GestureRecognizern zugewiesen, damit sie ihnen rohe Eingabedaten zuführen können. GestureRecognizer müssen jedoch wissen, wie viele Zeiger derzeit mit ihnen verknüpft sind (um zwischen 1-Finger- und 2-Finger-Gesten usw. zu unterscheiden).
Dmitri Shuralyov
Jetzt, wo ich mehr darüber nachdenke, scheint es, dass ich nur versuche, eine rahmenbasierte (und keine zeigerbasierte) Gestenerkennung durchzuführen. Ich kann also genauso gut Ereignisse an einzelne Gesten übergeben, anstatt jeweils nur einen Zeiger. Hmm.
Dmitri Shuralyov

Antworten:

2

Eine Möglichkeit besteht darin , jeder Klasse eine öffentliche Attach()Methode sowie eine geschützte AttachWithoutReciprocating()Methode hinzuzufügen . Machen Sie Aund Bgemeinsame Freunde, damit ihre Attach()Methoden die des anderen anrufen können AttachWithoutReciprocating():

A::Attach(B &b) {
    Bs.push_back(&b);
    b.AttachWithoutReciprocating(*this);
}

A::AttachWithoutReciprocating(B &b) {
    Bs.push_back(&b);
}

Wenn Sie ähnliche Methoden für implementieren B, müssen Sie sich nicht merken, welche Klasse aufgerufen werden soll Attach().

Ich bin sicher, Sie könnten dieses Verhalten in eine MutuallyAttachableKlasse einwickeln, die beides ist Aund von der Sie Berben, und so vermeiden, sich zu wiederholen und am Tag des Jüngsten Gerichts Bonuspunkte zu sammeln. Aber auch der unkomplizierte Ansatz, es an beiden Orten zu implementieren, wird die Arbeit erledigen.

Caleb
quelle
1
Dies klingt genau wie die Lösung, an die ich im Hinterkopf gedacht habe (dh Attach () sowohl in A als auch in B einzufügen). Ich mag auch die DRY-Lösung (ich mag es wirklich nicht, Code zu duplizieren), eine MutuallyAttachable-Klasse zu haben, von der Sie ableiten können, wann immer dieses Verhalten benötigt wird (ich sehe es ziemlich oft voraus). Allein die gleiche Lösung zu sehen, die von jemand anderem angeboten wird, macht mich umso sicherer, danke!
Dmitri Shuralyov
Akzeptiere dies, weil IMO die beste Lösung ist, die bisher angeboten wurde. Vielen Dank! Ich werde diese MutuallyAttachable-Klasse jetzt implementieren und hier berichten, wenn ich auf unvorhergesehene Probleme stoße (einige Schwierigkeiten können auch aufgrund der jetzt mehrfachen Vererbung auftreten, mit der ich noch nicht viel Erfahrung habe).
Dmitri Shuralyov
Alles hat reibungslos geklappt. Nach meinem letzten Kommentar zur ursprünglichen Frage wird mir jedoch langsam klar, dass ich diese Funktionalität nicht für das benötige, was ich mir ursprünglich vorgestellt hatte. Anstatt diese 2-Wege-Verbindungen zu verfolgen, übergebe ich einfach jedes Paar als Parameter an die Funktion, die es benötigt. Dies kann jedoch zu einem späteren Zeitpunkt noch nützlich sein.
Dmitri Shuralyov
Hier ist die MutuallyAttachableKlasse, die ich für diese Aufgabe geschrieben habe, wenn jemand sie wiederverwenden möchte: goo.gl/VY9RB (Pastebin) Bearbeiten: Dieser Code könnte verbessert werden, indem er zu einer Vorlagenklasse gemacht wird.
Dmitri Shuralyov
1
Ich habe die MutuallyAttachable<T, U>Klassenvorlage bei Gist platziert. gist.github.com/3308058
Dmitri Shuralyov
5

Kann die Beziehung selbst zusätzliche Eigenschaften haben?

Wenn ja, sollte es eine separate Klasse sein.

Wenn nicht, reicht jeder Mechanismus zur Verwaltung der hin- und hergehenden Listen aus.

Steven A. Lowe
quelle
Wenn es sich um eine separate Klasse handelt, wie würde A die verknüpften Instanzen von B und B die verknüpften Instanzen von A kennen? Zu diesem Zeitpunkt müssten sowohl A als auch B eine 1-zu-viele-Beziehung zu C haben, nicht wahr?
Dmitri Shuralyov
1
A und B würden beide einen Verweis auf die Sammlung von C-Instanzen benötigen; C dient dem gleichen Zweck wie eine 'Brückentabelle' in der relationalen Modellierung
Steven A. Lowe
1

Normalerweise, wenn ich in Situationen wie diese gerate, die sich unangenehm anfühlen, aber ich kann nicht genau sagen warum, weil ich versuche, etwas in die falsche Klasse zu bringen. Es gibt fast immer eine Möglichkeit, einige Funktionen aus dem Unterricht zu entfernen Aund Bdie Dinge klarer zu machen.

Ein Kandidat für das Verschieben der Zuordnung ist der Code, der die Zuordnung überhaupt erst erstellt. Vielleicht attach()speichert statt es einfach eine Liste von std::pair<A, B>. Wenn mehrere Stellen Assoziationen erzeugen, kann diese in eine Klasse abstrahiert werden.

Ein weiterer Kandidat für einen Umzug ist Zin Ihrem Fall das Objekt, dem die zugehörigen Objekte gehören .

Ein weiterer häufiger Kandidat für das Verschieben der Zuordnung ist der Code, der häufig Methoden oder Objekte aufruft . Anstatt beispielsweise aufzurufen , was intern etwas mit allen zugeordneten Objekten tut , kennt es bereits die Zuordnungen und ruft für jedes einzelne auf. Wenn es an mehreren Stellen aufgerufen wird, kann es wiederum in eine einzelne Klasse abstrahiert werden.ABa->foo()Ba->foo(b)

Oft sind die Objekte, die die Zuordnung erstellen, besitzen und verwenden, dasselbe Objekt. Das kann das Refactoring sehr einfach machen.

Die letzte Methode besteht darin, die Beziehung aus anderen Beziehungen abzuleiten. Zum Beispiel sind Brüder und Schwestern eine Viele-zu-Viele-Beziehung, aber anstatt eine Liste der Schwestern in der Bruderklasse zu führen und umgekehrt, leiten Sie diese Beziehung aus der elterlichen Beziehung ab. Der andere Vorteil dieser Methode ist, dass sie eine einzige Quelle der Wahrheit schafft. Es gibt keine Möglichkeit für einen Fehler oder Laufzeitfehler, eine Diskrepanz zwischen der Bruder-, Schwester- und Elternliste zu erstellen, da Sie nur eine Liste haben.

Diese Art des Refactorings ist keine Zauberformel, die jedes Mal funktioniert. Sie müssen noch experimentieren, bis Sie für jeden einzelnen Umstand eine gute Passform gefunden haben, aber ich habe festgestellt, dass dies in etwa 95% der Fälle funktioniert. Die verbleibenden Zeiten muss man nur mit der Unbeholfenheit leben.

Karl Bielefeldt
quelle
0

Wenn ich dies implementieren würde, würde ich Attach sowohl in A als auch in B einfügen. Innerhalb der Attach-Methode würde ich dann Attach für das übergebene Objekt aufrufen. Auf diese Weise können Sie entweder A.Attach (B) oder B.Attach ( EIN). Meine Syntax ist möglicherweise nicht korrekt. Ich habe C ++ seit Jahren nicht mehr verwendet:

class A {
  private: std::vector<B *> Bs;

  void Attach (B* obj) {
    // Only add obj if it's not in the vector
    if (std::find(Bs.begin(), Bs.end(), obj) == Bs.end()) {
      //Add obj to the vector
      Bs.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

class B {
  private: std::vector<A *> As;

  void Attach (A* obj) {
    // Only add obj if it's not in the vector
    if (std::find(As.begin(), As.end(), obj) == As.end()) {
      //Add obj to the vector
      As.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

Eine alternative Methode wäre, eine 3. Klasse C zu erstellen, die eine statische Liste von AB-Paaren verwaltet.

Briddums
quelle
Nur eine Frage zu Ihrem alternativen Vorschlag, eine 3. Klasse C zur Verwaltung einer Liste von AB-Paaren zu haben: Wie würden A und B ihre Paarmitglieder kennen? Würden jedes A und B einen Link zu (einzelnem) C benötigen und dann C bitten, die geeigneten Paare zu finden? B :: Foo () {std :: vector <A *> As = C.GetAs (this); / * Mach etwas mit As ... * /} Oder hast du dir etwas anderes vorgestellt? Weil dies furchtbar verwickelt erscheint.
Dmitri Shuralyov