Qual è un buon modo per rappresentare una relazione molti-a-molti tra due classi?


10

Diciamo che ho due tipi di oggetti, A e B. Il rapporto tra loro è molti-a-molti, ma nessuno dei due è il proprietario dell'altro.

Entrambe le istanze A e B devono essere consapevoli della connessione; non è solo un modo.

Quindi, possiamo fare questo:

class A
{
    ...

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

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

La mia domanda è: dove inserisco le funzioni per creare e distruggere le connessioni?

Dovrebbe essere A :: Attach (B), che quindi aggiorna A :: Bs e B :: Come vettori?

O dovrebbe essere B :: Attach (A), che sembra ugualmente ragionevole.

Nessuno di questi si sente bene. Se smetto di lavorare con il codice e torno dopo una settimana, sono sicuro che non sarò in grado di ricordare se dovrei fare A.Attach (B) o B.Attach (A).

Forse dovrebbe essere una funzione come questa:

CreateConnection(A, B);

Ma creare una funzione globale sembra anche indesiderabile, dato che è una funzione specifica per lavorare solo con le classi A e B.

Un'altra domanda: se incontro spesso questo problema / requisito, posso in qualche modo crearne una soluzione generale? Forse una classe TwoWayConnection da cui posso derivare o utilizzare all'interno di classi che condividono questo tipo di relazione?

Quali sono alcuni buoni modi per gestire questa situazione ... So come gestire abbastanza bene la situazione "C possiede D", ma questa è più complicata.

Modifica: solo per renderlo più esplicito, questa domanda non comporta problemi di proprietà. Sia A che B appartengono a qualche altro oggetto Z e Z si occupa di tutti i problemi di proprietà. Sono interessato solo a come creare / rimuovere i collegamenti molti-a-molti tra A e B.


Vorrei creare una Z :: Attach (A, B), quindi, se Z possiede i due tipi di oggetti, "si sente bene", per lui creare la connessione. Nel mio esempio (non così buono), Z sarebbe un repository per A e B. Non riesco a vedere un altro modo senza un esempio pratico.
Machado,

1
Per essere più precisi, A e B possono avere proprietari diversi. Forse A è di proprietà di Z, mentre B è di proprietà di X, che a sua volta è di proprietà di Z. Come puoi vedere, diventa davvero contorto cercando di gestire il collegamento altrove. A e B devono poter accedere rapidamente a un elenco delle coppie a cui è collegato.
Dmitri Shuralyov,

Se sei curioso di conoscere i veri nomi di A e B nella mia situazione, lo sono Pointere GestureRecognizer. I puntatori sono di proprietà e gestiti dalla classe InputManager. GestureRecognizer è di proprietà di istanze Widget, che a loro volta sono di proprietà di un'istanza Screen che è di proprietà di un'istanza App. I puntatori vengono assegnati ai GestureRecognizer in modo che possano fornire loro dati di input non elaborati, ma i GestureRecognizer devono essere consapevoli del numero di puntatori attualmente associati (per distinguere i gesti da 1 dito a 2 dita, ecc.).
Dmitri Shuralyov,

Ora che ci penso di più, sembra che sto solo cercando di fare un riconoscimento gestuale basato su frame (piuttosto che su un puntatore). Quindi potrei anche semplicemente passare gli eventi a gesti fotogramma alla volta anziché puntatore alla volta. Hmm.
Dmitri Shuralyov,

Risposte:


2

Un modo è quello di aggiungere un Attach()metodo pubblico e anche un AttachWithoutReciprocating()metodo protetto a ogni classe. Fai amicizia Ae fai Bamicizia reciproca in modo che i loro Attach()metodi possano chiamare l'altro AttachWithoutReciprocating():

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

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

Se si implementano metodi simili per B, non sarà necessario ricordare su quale classe chiamare Attach().

Sono sicuro che potresti avvolgere quel comportamento in una MutuallyAttachableclasse che eredita Ae sia B, evitando così di ripeterti e di segnare punti bonus nel Giorno del Giudizio. Ma anche l'approccio non sofisticato implementa in entrambi i posti farà il lavoro.


1
Questo suona esattamente come la soluzione a cui stavo pensando nella parte posteriore della mia mente (cioè mettere Attach () sia in A che in B). Mi piace molto anche la soluzione più ASCIUTTA (non mi piace molto la duplicazione del codice) di avere una classe reciprocamente accessibile da cui è possibile derivare ogni volta che questo comportamento è necessario (prevedo abbastanza spesso). Il solo fatto di vedere la stessa soluzione offerta da qualcun altro mi rende molto più fiducioso, grazie!
Dmitri Shuralyov,

Accettarlo perché l'IMO è la migliore soluzione offerta finora. Grazie! Ho intenzione di implementare quella classe reciprocamente accessibile ora e riferire qui se incontro problemi imprevisti (alcune difficoltà possono anche sorgere a causa dell'eredità ormai multipla con cui non ho ancora molta esperienza).
Dmitri Shuralyov,

Tutto ha funzionato senza intoppi. Tuttavia, come per il mio ultimo commento sulla domanda originale, sto iniziando a rendermi conto che non ho bisogno di questa funzionalità per quello che inizialmente immaginavo. Invece di tenere traccia di queste connessioni a 2 vie, passerò ogni coppia come parametro alla funzione che ne ha bisogno. Tuttavia, questo potrebbe tornare utile in un secondo momento.
Dmitri Shuralyov,

Ecco la MutuallyAttachableclasse che ho scritto per questo compito, se qualcuno vuole riutilizzarlo: goo.gl/VY9RB (Pastebin) Modifica: questo codice potrebbe essere migliorato rendendolo una classe modello.
Dmitri Shuralyov

1
Ho posizionato il MutuallyAttachable<T, U>modello di classe su Gist. gist.github.com/3308058
Dmitri Shuralyov il

5

È possibile che la relazione stessa abbia proprietà aggiuntive?

In tal caso, dovrebbe essere una classe separata.

In caso contrario, sarà sufficiente qualsiasi meccanismo di gestione delle liste alternativo.


Se è una classe separata, come farebbe A a conoscere le sue istanze collegate di B e B a conoscere le sue istanze collegate di A? A quel punto, sia A che B dovrebbero avere una relazione 1-a-molti con C, no?
Dmitri Shuralyov

1
A e B avrebbero entrambi bisogno di un riferimento alla raccolta di istanze C; C ha lo stesso scopo di un "tavolo a ponte" nella modellazione relazionale
Steven A. Lowe,

1

Di solito quando mi trovo in situazioni come queste che mi fanno sentire imbarazzante, ma non riesco proprio a capire perché, è perché sto cercando di mettere qualcosa nella classe sbagliata. C'è quasi sempre un modo per spostare alcune funzionalità fuori dalla classe Ae Bper rendere le cose più chiare.

Un candidato per spostare l'associazione è il codice che crea l'associazione in primo luogo. Forse invece di chiamarlo attach()semplicemente memorizza un elenco di std::pair<A, B>. Se ci sono più posizioni che creano associazioni, possono essere astratte in una classe.

Un altro candidato per una mossa è l'oggetto che possiede gli oggetti associati, Znel tuo caso.

Un altro candidato comune per lo spostamento dell'associazione è il codice che spesso chiama metodi Ao Boggetti. Ad esempio, invece di chiamare a->foo(), che internamente fa qualcosa con tutti gli Boggetti associati , conosce già le associazioni e chiama a->foo(b)per ognuna. Ancora una volta, se più posti lo chiamano, può essere astratto in una singola classe.

Molte volte gli oggetti che creano, possiedono e usano l'associazione capita di essere lo stesso oggetto. Ciò può rendere il refactoring molto semplice.

L'ultimo metodo è derivare la relazione da altre relazioni. Ad esempio, i fratelli e le sorelle sono una relazione molti-a-molti, ma piuttosto che tenere un elenco di sorelle nella classe dei fratelli e viceversa, si ricava quella relazione dalla relazione dei genitori. L'altro vantaggio di questo metodo è che crea un'unica fonte di verità. Non è possibile che un errore o un errore di runtime crei una discrepanza tra gli elenchi fratello, sorella e genitore, poiché hai un solo elenco.

Questo tipo di refactoring non è una formula magica che funziona ogni volta. Devi ancora sperimentare fino a quando non trovi una buona misura per ogni singola circostanza, ma ho scoperto che funziona circa il 95% delle volte. Le restanti volte devi solo convivere con l'imbarazzo.


0

Se lo stavo implementando, inserivo sia Attach in A che B. All'interno del metodo Attach, chiamerei Attach sull'oggetto passato. In questo modo puoi chiamare A.Attach (B) o B.Attach ( UN). La mia sintassi potrebbe non essere corretta, non uso c ++ da anni:

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

Un metodo alternativo sarebbe quello di creare una terza classe, C che gestisce un elenco statico di accoppiamenti AB.


Solo una domanda riguardante il tuo suggerimento alternativo di avere una 3a classe C per gestire un elenco di coppie AB: come farebbero A e B a conoscere i membri della sua coppia? Ogni A e B avrebbero bisogno di un collegamento a (singolo) C, quindi chiedere a C di trovare le coppie appropriate? B :: Foo () {std :: vector <A *> As = C.GetAs (this); / * Fai qualcosa con As ... * /} O hai immaginato qualcos'altro? Perché questo sembra terribilmente contorto.
Dmitri Shuralyov,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.