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

10

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

Entrambe le istanze A e B devono essere a conoscenza 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 aggiorna i vettori A :: Bs e B :: As?

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

Nessuno di questi sembra giusto. Se smetto di lavorare con il codice e torno dopo una settimana, sono sicuro che non potrò ricordare se dovrei fare A.Attach (B) o B.Attach (A).

Forse dovrebbe essere una funzione come questa:

CreateConnection(A, B);

Ma anche rendere una funzione globale non è auspicabile, 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 creare una soluzione generale per questo? Forse una classe TwoWayConnection che posso derivare da o utilizzare all'interno di classi che condividono questo tipo di relazione?

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

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

    
posta Dmitri Shuralyov 10.05.2012 - 21:52
fonte

5 risposte

2

Un modo è aggiungere un metodo pubblico Attach() e anche un metodo AttachWithoutReciprocating() protetto per ogni classe. Crea A e B amici comuni in modo che i loro metodi Attach() possano chiamare AttachWithoutReciprocating() dell'altro:

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

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

Se implementi metodi simili per B , non dovrai ricordare su quale classe chiamare Attach() .

Sono sicuro che potresti integrare questo comportamento in una classe MutuallyAttachable che eredita sia A che B , evitando così di ripetere te stesso e di guadagnare punti bonus nel giorno del giudizio. Ma anche il semplice approccio implement-it-in-both-places completerà il tuo lavoro.

    
risposta data 10.05.2012 - 22:36
fonte
5

È possibile che la relazione stessa abbia proprietà aggiuntive?

Se è così, allora dovrebbe essere una classe separata.

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

    
risposta data 10.05.2012 - 22:47
fonte
1

Di solito quando arrivo in situazioni come questa che mi sembrano imbarazzanti, ma non riesco a capire perché, è perché sto cercando di mettere qualcosa nella classe sbagliata. C'è quasi sempre un modo per spostare alcune funzionalità fuori dalla classe A e B per rendere le cose più chiare.

Un candidato per lo spostamento dell'associazione è il codice che crea l'associazione in primo luogo. Forse invece di chiamare attach() , semplicemente memorizza un elenco di std::pair<A, B> . Se ci sono più luoghi che creano associazioni, può essere estratto in una classe.

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

Un altro candidato comune per lo spostamento dell'associazione è il codice che frequentemente chiama metodi su A o B oggetti. Ad esempio, invece di chiamare a->foo() , che internamente fa qualcosa con tutti gli oggetti B associati, conosce già le associazioni e chiama a->foo(b) per ognuna. Di nuovo, se più luoghi lo chiamano, può essere astratto in una singola classe.

Molte volte gli oggetti che creano, possiedono e utilizzano l'associazione si presentano come lo stesso oggetto. Ciò può rendere il refactoring molto facile.

L'ultimo metodo è quello di derivare la relazione da altre relazioni. Ad esempio, i fratelli e le sorelle sono una relazione molti-a-molti, ma piuttosto che tenere una lista di sorelle nella classe di fratelli e viceversa, si ricava tale relazione dalla relazione genitoriale. L'altro vantaggio di questo metodo è che crea un'unica fonte di verità. Non c'è modo per un bug o un errore di runtime di creare una discrepanza tra il fratello, la sorella e gli elenchi dei genitori, perché hai solo una lista.

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

    
risposta data 10.05.2012 - 23:42
fonte
0

Se dovessi implementarlo, inserirò Attach in entrambi A e B. All'interno del metodo Attach, chiamerei Attach sull'oggetto passato. In questo modo puoi chiamare A.Attach (B) o B .Attach (A). La mia sintassi potrebbe non essere corretta, non ho usato c ++ in 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 abbinamenti A-B.

    
risposta data 10.05.2012 - 22:38
fonte
-1

Dato che stai inserendo C ++ 11 nell'elenco dei candidati ... Dai uno sguardo ai puntatori intelligenti.
link

Sembrano fornire più modi per affrontare questo problema esatto.

Modifica: Un frammento di ciò che pensavo fosse pertinente alla Q:

weak_ptr

Weak pointers are often explained as what you need to break loops in data structures managed using shared_ptrs. I think it is better to think of a weak_ptr as a pointer to something that you need access to (only) if it exists, and may get deleted (by someone else), and must have its destructor called after its last use (usually to delete anon-memory resource)
    
risposta data 10.05.2012 - 22:03
fonte

Leggi altre domande sui tag