Quelle est la bonne façon de représenter une relation plusieurs-à-plusieurs entre deux classes?

10

Disons que j'ai deux types d'objets, A et B. La relation entre eux est plusieurs-à-plusieurs, mais aucun d'eux n'est propriétaire de l'autre.

Les instances A et B doivent être conscientes de la connexion; ce n'est pas qu'une façon.

Donc, nous pouvons le faire:

class A
{
    ...

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

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

Ma question est: où dois-je mettre les fonctions pour créer et détruire les connexions?

Faut-il que ce soit A :: Attach (B), qui met alors à jour A :: Bs et B :: As vecteurs?

Ou devrait-il être B :: Attach (A), ce qui semble tout aussi raisonnable.

Aucun de ceux-là ne se sent bien. Si j'arrête de travailler avec le code et que je reviens après une semaine, je suis sûr que je ne pourrai pas me rappeler si je devrais faire A.Attach (B) ou B.Attach (A).

Peut-être que cela devrait être une fonction comme celle-ci:

CreateConnection(A, B);

Mais créer une fonction globale semble également indésirable, étant donné qu'il s'agit d'une fonction spécifique pour travailler uniquement avec les classes A et B.

Une autre question: si je rencontre souvent ce problème / cette exigence, puis-je trouver une solution générale? Peut-être une classe TwoWayConnection dont je peux dériver ou utiliser au sein de classes qui partagent ce type de relation?

Quels sont les bons moyens de gérer cette situation ... Je sais comment gérer très bien la situation un-à-plusieurs "C possède D", mais celle-ci est plus délicate.

Edit: Juste pour le rendre plus explicite, cette question n'implique pas de problèmes de propriété. A et B appartiennent à un autre objet Z, et Z s'occupe de tous les problèmes de propriété. Je veux seulement savoir comment créer / supprimer les liens plusieurs-à-plusieurs entre A et B.

Dmitri Shuralyov
la source
Je créerais un Z :: Attach (A, B), donc, si Z possède les deux types d'objets, "se sent bien", pour lui de créer la connexion. Dans mon exemple (pas si bon), Z serait un référentiel pour A et B. Je ne peux pas voir une autre façon sans un exemple pratique.
Machado
1
Pour être plus précis, A et B peuvent avoir des propriétaires différents. Peut-être que A appartient à Z, tandis que B appartient à X, qui à son tour appartient à Z. Comme vous pouvez le voir, il devient alors vraiment compliqué d'essayer de gérer le lien ailleurs. A et B doivent pouvoir accéder rapidement à une liste des paires auxquelles il est lié.
Dmitri Shuralyov
Si vous êtes curieux de connaître les vrais noms de A et B dans ma situation, ce sont Pointeret GestureRecognizer. Les pointeurs sont détenus et gérés par la classe InputManager. GestureRecognizers appartiennent à des instances de Widget, qui sont à leur tour détenues par une instance d'écran qui appartient à une instance d'application. Les pointeurs sont assignés à GestureRecognizers afin qu'ils puissent leur fournir des données d'entrée brutes, mais GestureRecognizers doit être conscient du nombre de pointeurs qui leur sont actuellement associés (pour distinguer 1 doigt contre 2 gestes du doigt, etc.).
Dmitri Shuralyov
Maintenant que j'y pense davantage, il semble que j'essaie simplement de faire une reconnaissance gestuelle basée sur le cadre (plutôt que sur le pointeur). Donc, je ferais aussi bien de passer des événements à un geste image par image plutôt que pointeur à la fois. Hmm.
Dmitri Shuralyov

Réponses:

2

Une façon consiste à ajouter une Attach()méthode publique et également une AttachWithoutReciprocating()méthode protégée à chaque classe. Faites A-vous Bdes amis communs pour que leurs Attach()méthodes puissent appeler les autres AttachWithoutReciprocating():

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

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

Si vous implémentez des méthodes similaires pour B, vous n'aurez pas à vous souvenir de la classe à appeler Attach().

Je suis sûr que vous pouvez envelopper ce comportement dans une MutuallyAttachableclasse à la fois Aet BHériter de, évitant ainsi de vous répéter et marquer des points bonus le jour du jugement. Mais même l'approche peu sophistiquée de l'implémentation aux deux endroits fera le travail.

Caleb
la source
1
Cela ressemble précisément à la solution à laquelle je pensais au fond de mon esprit (c'est-à-dire de mettre Attach () à la fois en A et en B). J'aime aussi beaucoup la solution plus DRY (je n'aime vraiment pas la duplication de code) d'avoir une classe MutualAttachable dont vous pouvez dériver chaque fois que ce comportement est nécessaire (je prévois assez souvent). Le simple fait de voir la même solution offerte par quelqu'un d'autre me rend d'autant plus confiant, merci!
Dmitri Shuralyov
Accepter cela parce que l'OMI est la meilleure solution offerte jusqu'à présent. Merci! Je vais implémenter cette classe MutuallyAttachable maintenant et signaler ici si je rencontre des problèmes imprévus (certaines difficultés peuvent également survenir en raison de l'héritage désormais multiple avec lequel je n'ai pas encore beaucoup d'expérience).
Dmitri Shuralyov
Tout s'est bien passé. Cependant, selon mon dernier commentaire sur la question d'origine, je commence à réaliser que je n'ai pas besoin de cette fonctionnalité pour ce que j'avais envisagé à l'origine. Au lieu de garder une trace de ces connexions bidirectionnelles, je vais simplement passer chaque paire en paramètre à la fonction qui en a besoin. Cependant, cela peut encore être utile ultérieurement.
Dmitri Shuralyov
Voici la MutuallyAttachableclasse que j'ai écrite pour cette tâche, si quelqu'un veut la réutiliser: goo.gl/VY9RB (Pastebin) Edit: Ce code pourrait être amélioré en en faisant une classe modèle.
Dmitri Shuralyov,
1
J'ai placé le MutuallyAttachable<T, U>modèle de classe à Gist. gist.github.com/3308058
Dmitri Shuralyov
5

Est-il possible que la relation elle-même ait des propriétés supplémentaires?

Si c'est le cas, ce devrait être une classe distincte.

Si ce n'est pas le cas, tout mécanisme de gestion de liste réciproque suffira.

Steven A. Lowe
la source
S'il s'agit d'une classe distincte, comment A connaîtrait-il ses instances liées de B et B connaît-il ses instances liées de A? À ce stade, A et B devraient tous deux avoir une relation 1 à plusieurs avec C, n'est-ce pas?
Dmitri Shuralyov
1
A et B auraient tous deux besoin d'une référence à la collection d'instances C; C sert le même but qu'une «table de pont» dans la modélisation relationnelle
Steven A. Lowe
1

Habituellement, lorsque je me retrouve dans des situations comme celle-ci qui me semblent gênantes, mais je ne peux pas vraiment dire pourquoi, c'est parce que j'essaie de mettre quelque chose dans la mauvaise classe. Il y a presque toujours un moyen de sortir certaines fonctionnalités de la classe Aet Bde rendre les choses plus claires.

Un candidat pour déplacer l'association vers est le code qui crée l'association en premier lieu. Peut-être qu'au lieu d'appeler, attach()il stocke simplement une liste de std::pair<A, B>. S'il y a plusieurs endroits créant des associations, il peut être résumé en une seule classe.

Un autre candidat pour un mouvement est l'objet qui possède les objets associés, Zdans votre cas.

Un autre candidat commun pour déplacer l'association est le code qui est souvent appeler des méthodes sur Aou Bobjets. Par exemple, au lieu d'appeler a->foo(), ce qui fait en interne quelque chose avec tous les Bobjets associés , il connaît déjà les associations et appelle a->foo(b)pour chacun. Encore une fois, si plusieurs endroits l'appellent, il peut être résumé dans une seule classe.

Souvent, les objets qui créent, possèdent et utilisent l'association se trouvent être le même objet. Cela peut rendre le refactoring très facile.

La dernière méthode consiste à dériver la relation à partir d'autres relations. Par exemple, les frères et sœurs sont une relation plusieurs-à-plusieurs, mais plutôt que de conserver une liste de sœurs dans la classe des frères et vice versa, vous dérivez cette relation de la relation parentale. L'autre avantage de cette méthode est qu'elle crée une source unique de vérité. Il n'y a aucun moyen possible pour un bug ou une erreur d'exécution de créer une différence entre les listes frère, sœur et parent, car vous n'avez qu'une seule liste.

Ce type de refactoring n'est pas une formule magique qui fonctionne à chaque fois. Vous devez encore expérimenter jusqu'à ce que vous trouviez un bon ajustement pour chaque circonstance individuelle, mais je l'ai trouvé fonctionner environ 95% du temps. Les temps restants, il vous suffit de vivre avec la maladresse.

Karl Bielefeldt
la source
0

Si j'implémentais cela, je mettrais Attach à la fois A et B. À l'intérieur de la méthode Attach, j'appellerais alors Attach sur l'objet transmis. De cette façon, vous pouvez appeler A.Attach (B) ou B.Attach ( UNE). Ma syntaxe n'est peut-être pas correcte, je n'ai pas utilisé le c ++ depuis des années:

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

Une autre méthode consisterait à créer une 3e classe, C qui gère une liste statique de paires AB.

briddums
la source
Juste une question concernant votre suggestion alternative d'avoir un 3e classe C pour gérer une liste de paires AB: Comment A et B connaîtraient-ils les membres de leur paire? Est-ce que chaque A et B aurait besoin d'un lien vers (unique) C, puis demander à C de trouver les paires appropriées? B :: Foo () {std :: vector <A *> As = C.GetAs (this); / * Faites quelque chose avec As ... * /} Ou avez-vous envisagé autre chose? Parce que cela semble terriblement alambiqué.
Dmitri Shuralyov