Comment résoudre l'interdépendance des classes dans mon code C ++?


10

Dans mon projet C ++, j'ai deux classes, Particleet Contact. Dans la Particleclasse, j'ai une variable membre std::vector<Contact> contactsqui contient tous les contacts d'un Particleobjet, et les fonctions membres correspondantes getContacts()et addContact(Contact cont). Ainsi, dans "Particle.h", j'inclus "Contact.h".

Dans la Contactclasse, je voudrais ajouter du code au constructeur pour Contactcet appel Particle::addContact(Contact cont), afin qu'il contactssoit mis à jour pour les deux Particleobjets entre lesquels l' Contactobjet est ajouté. Ainsi, je devrais inclure "Particle.h" dans "Contact.cpp".

Ma question est de savoir si cela est acceptable / bonne pratique de codage et, sinon, quelle serait une meilleure façon de mettre en œuvre ce que j'essaie d'atteindre (simplement mettre à jour automatiquement la liste des contacts pour une particule spécifique chaque fois qu'un nouveau contact est créé).


Ces classes seront liées entre elles par une Networkclasse qui aura N particules ( std::vector<Particle> particles) et Nc contacts ( std::vector<Contact> contacts). Mais je voulais pouvoir avoir des fonctions comme particles[0].getContacts()- est-ce correct d'avoir de telles fonctions dans la Particleclasse dans ce cas, ou y a-t-il une meilleure "structure" d'association en C ++ à cet effet (de deux classes liées utilisées dans une autre classe) .


J'ai peut-être besoin d'un changement de perspective ici dans la façon dont j'aborde cette question. Étant donné que les deux classes sont connectées par un Networkobjet de classe, est-ce une organisation de code / classe typique d'avoir des informations de connectivité entièrement contrôlées par l' Networkobjet (en ce qu'un objet Particle ne devrait pas être au courant de ses contacts et, par conséquent, il ne devrait pas avoir de getContacts()membre une fonction). Ensuite, afin de savoir quels contacts a une particule spécifique, j'aurais besoin d'obtenir ces informations via l' Networkobjet (par exemple, en utilisant network.getContacts(Particle particle)).

Serait-il moins typique (peut-être même découragé) de concevoir une classe C ++ pour un objet Particle d'avoir également ces connaissances (c'est-à-dire, avoir plusieurs façons d'accéder à ces informations - via l'objet Network ou l'objet Particle, selon ce qui semble le plus pratique) )?


4
Voici une conférence de cppcon 2017 - Les trois couches d'en-têtes: youtu.be/su9ittf-ozk
Robert Andrzejuk

3
Les questions qui contiennent des mots comme «meilleur», «meilleur» et «acceptable» sont sans réponse, sauf si vous pouvez indiquer vos critères d'évaluation spécifiques.
Robert Harvey

Merci pour la modification, bien que changer votre libellé en "typique" en fasse juste une question de popularité. Il y a des raisons pour lesquelles le codage est fait d'une manière ou d'une autre, et bien que la popularité puisse être une indication qu'une technique est "bonne" (pour une certaine définition de "bonne"), elle peut aussi être une indication de culture de cargaison.
Robert Harvey

@RobertHarvey J'ai supprimé "mieux" et "mauvais" dans ma dernière section. Je suppose que je demande l'approche typique (peut-être même favorisée / encouragée) lorsque vous avez un Networkobjet de classe qui contient des Particleobjets et des Contactobjets. Avec cette connaissance de base, je peux ensuite essayer d'évaluer si elle correspond ou non à mes besoins spécifiques, qui sont toujours explorés / développés au fur et à mesure de l'avancement du projet.
AnInquiringMind

@RobertHarvey Je suppose que je suis assez nouveau pour écrire des projets C ++ complètement à partir de zéro que je suis d'accord pour apprendre ce qui est "typique" et "populaire". J'espère que j'aurai suffisamment d'informations à un moment donné pour être en mesure de comprendre pourquoi une autre implémentation est en fait meilleure, mais pour l'instant, je veux juste m'assurer que je n'approche pas cela d'une manière complètement osée!
AnInquiringMind

Réponses:


17

Votre question comporte deux parties.

La première partie est l'organisation des fichiers d'en-tête C ++ et des fichiers source. Ceci est résolu en utilisant la déclaration directe et la séparation de la déclaration de classe (en les plaçant dans le fichier d'en-tête) et du corps de la méthode (en les plaçant dans le fichier source). De plus, dans certains cas, on peut appliquer l' idiome Pimpl ("pointeur vers l'implémentation") pour résoudre des cas plus difficiles. Utilisez des pointeurs de propriété partagée ( shared_ptr), des pointeurs de propriété unique ( unique_ptr) et des pointeurs non propriétaires (pointeur brut, c'est-à-dire «astérisque») conformément aux meilleures pratiques.

La deuxième partie est de savoir comment modéliser des objets qui sont interdépendants sous la forme d'un graphique . Les graphiques généraux qui ne sont pas des DAG (graphiques acycliques dirigés) n'ont pas un moyen naturel d'exprimer la propriété arborescente. Au lieu de cela, les nœuds et les connexions sont tous des métadonnées qui appartiennent à un seul objet graphique. Dans ce cas, il n'est pas possible de modéliser la relation nœud-connexion sous forme d'agrégations. Les nœuds ne "possèdent" pas de connexions; les connexions ne "possèdent" pas de nœuds. Au lieu de cela, ce sont des associations, et les nœuds et les connexions appartiennent au graphique. Le graphique fournit des méthodes de requête et de manipulation qui opèrent sur les nœuds et les connexions.


Merci pour la réponse! J'ai en fait une classe réseau qui aura N particules et Nc contacts. Mais je voulais pouvoir avoir des fonctions comme particles[0].getContacts()- suggérez-vous dans votre dernier paragraphe que je ne devrais pas avoir de telles fonctions dans la Particleclasse, ou que la structure actuelle est correcte parce qu'elles sont intrinsèquement liées / associées Network? Y a-t-il une meilleure "structure" d'association en C ++ dans ce cas?
AnInquiringMind

1
Généralement, le réseau est responsable de la connaissance des relations entre les objets. Si vous utilisez une liste d'adjacence, par exemple, la particule network.particle[p]aurait un correspondant network.contacts[p]avec les indices de ses contacts. Sinon, le réseau et la particule suivent tous les deux les mêmes informations.
inutile

@Useless Ouais, c'est là que je ne sais pas comment procéder. Vous dites donc que l' Particleobjet ne devrait pas être au courant de ses contacts (donc je ne devrais pas avoir de getContacts()fonction membre), et que ces informations ne devraient provenir que de l' Networkobjet? Serait-ce une mauvaise conception de classe C ++ pour un Particleobjet d'avoir cette connaissance (c'est-à-dire, avoir plusieurs façons d'accéder à cette information - via l' Networkobjet ou l' Particleobjet, selon ce qui semble le plus pratique)? Ce dernier semble avoir plus de sens pour moi, mais peut-être que je dois changer ma perspective à ce sujet.
AnInquiringMind

1
@PhysicsCodingEnthusiast: Le problème avec Particletout savoir sur Contacts ou Networks est qu'il vous lie à une manière spécifique de représenter cette relation. Les trois classes devront peut-être s'entendre. Si au lieu de cela, Networkc'est le seul qui sait ou se soucie, c'est seulement une classe qui doit changer si vous décidez qu'une autre représentation est meilleure.
cHao

@cHao D'accord, cela a du sens. Donc, Particleet Contactdoivent être complètement séparés, et l'association entre eux est définie par l' Networkobjet. Juste pour être complètement sûr, c'est (probablement) ce que @rwong voulait dire quand il / elle a écrit, "les nœuds et les connexions sont" possédés "par le graphique. Le graphique fournit des méthodes de requête et de manipulation qui opèrent sur les nœuds et les connexions." , droite?
AnInquiringMind

5

Si je vous comprends bien, le même objet de contact appartient à plus d'un objet de particules, car il représente une sorte de contact physique entre deux ou plusieurs particules, non?

La première chose que je pense est sujette à caution est pourquoi Particlea une variable membre std::vector<Contact>? Ce devrait être un std::vector<Contact*>ou un à la std::vector<std::shared_ptr<Contact> >place. addContactdevrait alors avoir une signature différente comme addContact(Contact *cont)ou à la addContact(std::shared_ptr<Contact> cont)place.

Il n'est donc pas nécessaire d'inclure "Contact.h" dans "Particle.h", une déclaration directe de class Contactdans "Particle.h" et une inclusion de "Contact.h" dans "Particle.cpp" seront suffisantes.

Ensuite, la question sur le constructeur. Tu veux quelque chose comme

 Contact(Particle &p1, Particle &p2)
 {
      p1.addContact(this);
      p2.addContact(this);
 }

Droite? Cette conception est correcte, tant que votre programme connaît toujours les particules associées au moment où un objet contact doit être créé.

Remarquez que si vous std::vector<Contact*> route, vous devez réfléchir à la durée de vie et à la propriété des Contactobjets. Aucune particule ne "possède" ses contacts, un contact ne devra probablement être supprimé que si les deux Particleobjets associés sont détruits. Utiliser à la std::shared_ptr<Contact>place résoudra automatiquement ce problème. Ou vous laissez un objet "contexte environnant" s'approprier les particules et les contacts (comme suggéré par @rwong) et gérer leur durée de vie.


Je ne vois pas l'avantage de addContact(const std::shared_ptr<Contact> &cont)plus addContact(std::shared_ptr<Contact> cont)?
Caleth

@Caleth: cela a été discuté ici: stackoverflow.com/questions/3310737/… - "const" n'est pas vraiment important ici, mais passer des objets par référence (et des scalaires par valeur) est l'idiome standard en C ++.
Doc Brown

2
Beaucoup de ces réponses semblent provenir d'un pré- moveparadigme
Caleth

@Caleth: ok, pour garder tous les nitpickers heureux, j'ai changé cette partie tout à fait sans importance de ma réponse.
Doc Brown

1
@PhysicsCodingEnthusiast: non, il s'agit avant tout de créer particle1.getContacts()et de particle2.getContacts()livrer le même Contactobjet représentant le contact physique entre particle1et particle2, et non deux objets différents. Bien sûr, on pourrait essayer de concevoir le système d'une manière peu importe s'il y a deux Contactobjets disponibles en même temps représentant le même contact physique. Cela impliquerait de rendre Contactimmuable, mais êtes-vous sûr que c'est ce que vous voulez?
Doc Brown

0

Oui, ce que vous décrivez est un moyen très acceptable de garantir que chaque Contactinstance figure dans la liste des contacts de a Particle.


Merci pour la réponse. J'avais lu quelques suggestions selon lesquelles il fallait éviter une paire de classes interdépendantes (par exemple, dans "C ++ Design Patterns and Derivatives Pricing" de MS Joshi), mais apparemment, ce n'est pas nécessairement correct? Par curiosité, existe-t-il peut-être une autre façon de mettre en œuvre cette mise à jour automatique sans avoir besoin d'interdépendance?
AnInquiringMind

4
@PhysicsCodingEnthusiast: Avoir des classes interdépendantes crée toutes sortes de difficultés et vous devriez essayer de les éviter. Mais parfois, deux classes sont si étroitement liées que l'élimination de l'interdépendance entre elles pose plus de problèmes que l'interdépendance elle-même.
Bart van Ingen Schenau,

0

Ce que vous avez fait est correct.

Une autre façon ... Si le but est de s'assurer que tout Contactest dans une liste, alors vous pourriez:

  • bloquer la création de Contact (constructeurs privés),
  • déclarer en avant Particle classe,
  • faire de la Particleclasse un ami deContact ,
  • à Particlecréer une méthode d'usine qui crée unContact

Ensuite, vous n'avez pas à inclure particle.hdanscontact


Merci pour la réponse! Cela semble être un moyen utile de mettre cela en œuvre. Je me demande simplement, avec ma modification de la question initiale concernant la Networkclasse, cela change-t-il la structure suggérée, ou serait-ce toujours la même chose?
AnInquiringMind

Une fois que vous avez mis à jour votre question, sa portée change. ... Maintenant vous vous interrogez sur l'architecture de votre application, alors qu'auparavant il s'agissait d'un problème technique.
Robert Andrzejuk

0

Une autre option que vous pourriez envisager est de faire du constructeur Contact qui accepte une référence de particule un modèle. Cela permettra à un contact de s'ajouter à n'importe quel conteneur qui implémente addContact(Contact).

template<class Container>
Contact(/*parameters*/, Container& container)
{
  container.addContact(*this);
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.