Wie löse ich die Interdependenz von Klassen in meinem C ++ - Code?

10

In meinem C ++ - Projekt habe ich zwei Klassen Particleund Contact. In der ParticleKlasse habe ich eine Mitgliedsvariable, std::vector<Contact> contactsdie alle Kontakte eines ParticleObjekts sowie die entsprechenden Mitgliedsfunktionen getContacts()und enthält addContact(Contact cont). Daher füge ich in "Partikel.h" "Kontakt.h" ein.

In der ContactKlasse möchte ich dem Konstruktor Code für Contactdiesen Aufruf hinzufügen Particle::addContact(Contact cont), damit dieser contactsfür beide ParticleObjekte aktualisiert wird , zwischen denen das ContactObjekt hinzugefügt wird. Daher müsste ich "Particle.h" in "Contact.cpp" aufnehmen.

Meine Frage ist, ob dies eine akzeptable / gute Codierungspraxis ist oder nicht, und wenn nicht, was wäre ein besserer Weg, um das zu implementieren, was ich erreichen möchte (einfach ausgedrückt, die Liste der Kontakte für ein bestimmtes Partikel wird bei jedem neuen Kontakt automatisch aktualisiert geschaffen).


Diese Klassen werden durch eine NetworkKlasse mit N Partikeln ( std::vector<Particle> particles) und Nc Kontakten ( std::vector<Contact> contacts) miteinander verbunden. Aber ich wollte in der Lage sein, Funktionen zu haben wie particles[0].getContacts()- ist es Particlein diesem Fall in Ordnung, solche Funktionen in der Klasse zu haben , oder gibt es in C ++ zu diesem Zweck eine bessere Assoziationsstruktur (von zwei verwandten Klassen, die in einer anderen Klasse verwendet werden)? .


Möglicherweise brauche ich hier einen Perspektivwechsel in meiner Herangehensweise. Da die beiden Klassen durch ein NetworkKlassenobjekt verbunden sind, ist es eine typische Code- / Klassenorganisation, Konnektivitätsinformationen vollständig vom NetworkObjekt steuern zu lassen (insofern sollte ein Partikelobjekt seine Kontakte nicht kennen und folglich kein getContacts()Mitglied haben Funktion). Um dann zu wissen, welche Kontakte ein bestimmtes Teilchen hat, müsste ich diese Informationen über das NetworkObjekt erhalten (z network.getContacts(Particle particle). B. mit ).

Wäre es weniger typisch (vielleicht sogar entmutigt), ein C ++ - Klassendesign für ein Partikelobjekt zu haben, dieses Wissen ebenfalls zu haben (dh mehrere Möglichkeiten zu haben, auf diese Informationen zuzugreifen - entweder über das Netzwerkobjekt oder das Partikelobjekt, je nachdem, was bequemer erscheint )?

AnInquiringMind
quelle
4
Hier ist ein Vortrag von cppcon 2017 - Die drei Ebenen der Überschriften: youtu.be/su9ittf-ozk
Robert Andrzejuk
3
Fragen, die Wörter wie "am besten", "besser" und "akzeptabel" enthalten, sind nicht zu beantworten, es sei denn, Sie können Ihre spezifischen Bewertungskriterien angeben.
Robert Harvey
Vielen Dank für die Bearbeitung, obwohl die Änderung Ihres Wortlauts in "typisch" nur eine Frage der Popularität darstellt. Es gibt Gründe, warum das Codieren auf die eine oder andere Weise erfolgt, und während Popularität ein Hinweis darauf sein kann, dass eine Technik "gut" ist (für eine Definition von "gut"), kann sie auch ein Hinweis darauf sein, dass Fracht kultiviert wird.
Robert Harvey
@ RobertHarvey Ich habe in meinem letzten Abschnitt "besser" und "schlecht" entfernt. Ich nehme an, ich frage nach dem typischen (vielleicht sogar bevorzugten / ermutigten) Ansatz, wenn Sie ein NetworkKlassenobjekt haben, das ParticleObjekte und ContactObjekte enthält . Mit diesem Basiswissen kann ich dann versuchen zu beurteilen, ob es meinen spezifischen Bedürfnissen entspricht oder nicht, die im Verlauf des Projekts noch erforscht / entwickelt werden.
AnInquiringMind
@RobertHarvey Ich nehme an, dass ich neu genug bin, um C ++ - Projekte komplett von Grund auf neu zu schreiben, damit ich gut lernen kann, was "typisch" und "beliebt" ist. Hoffentlich bekomme ich irgendwann genug Einsicht, um zu erkennen, warum eine andere Implementierung tatsächlich besser ist, aber im Moment möchte ich nur sicherstellen, dass ich mich dem nicht völlig knochenköpfig nähere!
AnInquiringMind

Antworten:

17

Ihre Frage besteht aus zwei Teilen.

Der erste Teil ist die Organisation von C ++ - Header- und Quelldateien. Dies wird gelöst, indem die Vorwärtsdeklaration und die Trennung der Klassendeklaration (Einfügen in die Header-Datei) und des Methodenkörpers (Einfügen in die Quelldatei) verwendet werden. Darüber hinaus kann man in einigen Fällen das Pimpl-Idiom ("Zeiger auf die Implementierung") anwenden , um schwierigere Fälle zu lösen. Verwenden Sie Zeiger mit gemeinsamem Besitz ( shared_ptr), Zeiger mit einzelnem Besitz ( unique_ptr) und Zeiger ohne Besitz (Rohzeiger, dh das "Sternchen") gemäß den Best Practices.

Im zweiten Teil werden Objekte modelliert, die in Form eines Diagramms miteinander verknüpft sind . Allgemeine Diagramme, die keine DAGs sind (gerichtete azyklische Diagramme), haben keine natürliche Art, baumartigen Besitz auszudrücken. Stattdessen sind die Knoten und Verbindungen alle Metadaten, die zu einem einzelnen Diagrammobjekt gehören. In diesem Fall ist es nicht möglich, die Knoten-Verbindungs-Beziehung als Aggregationen zu modellieren. Knoten "besitzen" keine Verbindungen; Verbindungen "besitzen" keine Knoten. Stattdessen handelt es sich um Zuordnungen, und sowohl Knoten als auch Verbindungen gehören dem Diagramm. Das Diagramm enthält Abfrage- und Manipulationsmethoden, die auf den Knoten und Verbindungen ausgeführt werden.

rwong
quelle
Danke für die Antwort! Ich habe tatsächlich eine Netzwerkklasse, die N Partikel und Nc Kontakte hat. Aber ich wollte in der Lage sein, Funktionen wie particles[0].getContacts()- schlagen Sie in Ihrem letzten Absatz vor, dass ich solche Funktionen nicht in der ParticleKlasse haben sollte oder dass die aktuelle Struktur in Ordnung ist, weil sie von Natur aus miteinander verbunden sind Network? Gibt es in diesem Fall eine bessere Assoziationsstruktur in C ++?
AnInquiringMind
1
Im Allgemeinen ist das Netzwerk dafür verantwortlich, die Beziehungen zwischen Objekten zu kennen. Wenn Sie beispielsweise eine Adjazenzliste verwenden, hat das Partikel network.particle[p]eine Übereinstimmung network.contacts[p]mit den Indizes seiner Kontakte. Ansonsten verfolgen sowohl das Netzwerk als auch das Partikel die gleichen Informationen.
Nutzlos
@ Nutzlos Ja, da bin ich mir nicht sicher, wie ich vorgehen soll. Sie sagen also, dass das ParticleObjekt seine Kontakte nicht kennen sollte (also sollte ich keine getContacts()Mitgliedsfunktion haben) und dass diese Informationen nur aus dem NetworkObjekt stammen sollten? Wäre es ein schlechtes C ++ - Klassendesign für ein ParticleObjekt, über dieses Wissen zu verfügen (dh mehrere Möglichkeiten zu haben, auf diese Informationen zuzugreifen - entweder über das NetworkObjekt oder das ParticleObjekt, je nachdem, was bequemer erscheint)? Letzteres scheint mir sinnvoller zu sein, aber vielleicht muss ich meine Sichtweise dazu ändern.
AnInquiringMind
1
@PhysicsCodingEnthusiast: Das Problem mit dem ParticleWissen über Contacts oder Networks besteht darin, dass Sie an eine bestimmte Art der Darstellung dieser Beziehung gebunden sind . Möglicherweise müssen alle drei Klassen übereinstimmen. Wenn stattdessen Networknur der eine weiß oder sich darum kümmert, muss sich nur eine Klasse ändern, wenn Sie entscheiden, dass eine andere Darstellung besser ist.
CHao
@cHao Okay, das macht Sinn. Also Particleund Contactsollte völlig getrennt sein, und die Assoziation zwischen ihnen wird durch das NetworkObjekt definiert . Um ganz sicher zu sein, meinte @rwong (wahrscheinlich), als er / sie schrieb: "Sowohl Knoten als auch Verbindungen gehören dem Diagramm." Das Diagramm bietet Abfrage- und Manipulationsmethoden, die für die Knoten und Verbindungen gelten. " , Recht?
AnInquiringMind
5

Wenn ich Sie richtig verstanden habe, gehört dasselbe Kontaktobjekt zu mehr als einem Partikelobjekt, da es eine Art physischen Kontakt zwischen zwei oder mehr Partikeln darstellt, oder?

Das erste, was ich für fragwürdig halte, ist, warum Particlees eine Mitgliedsvariable gibt std::vector<Contact>. Es sollte stattdessen ein std::vector<Contact*>oder ein sein std::vector<std::shared_ptr<Contact> >. addContactdann sollte eine andere Signatur wie addContact(Contact *cont)oder addContact(std::shared_ptr<Contact> cont)stattdessen haben.

Dies macht es unnötig, "Contact.h" in "Particle.h" aufzunehmen, eine Vorwärtsdeklaration von class Contact"Particle.h" und ein Include von "Contact.h" in "Particle.cpp" sind ausreichend.

Dann die Frage nach dem Konstruktor. Du willst so etwas wie

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

Recht? Dieses Design ist in Ordnung, solange Ihr Programm die zugehörigen Partikel immer zu dem Zeitpunkt kennt, zu dem ein Kontaktobjekt erstellt werden muss.

Beachten Sie, dass Sie, wenn Sie den std::vector<Contact*>Weg gehen , einige Gedanken über die Lebensdauer und den Besitz der ContactObjekte investieren müssen . Kein Partikel "besitzt" seine Kontakte, ein Kontakt muss wahrscheinlich nur gelöscht werden, wenn beide verwandten ParticleObjekte zerstört sind. Wenn Sie std::shared_ptr<Contact>stattdessen verwenden, wird dieses Problem automatisch für Sie gelöst. Oder Sie lassen ein "umgebenden Kontext" -Objekt das Eigentum an Partikeln und Kontakten übernehmen (wie von @rwong vorgeschlagen) und deren Lebensdauer verwalten.

Doc Brown
quelle
Ich sehe den Nutzen von addContact(const std::shared_ptr<Contact> &cont)über nicht addContact(std::shared_ptr<Contact> cont)?
Caleth
@Caleth: Dies wurde hier besprochen: stackoverflow.com/questions/3310737/… - "const" ist hier nicht wirklich wichtig, aber das Übergeben von Objekten als Referenz (und Skalare nach Wert) ist die Standardsprache in C ++.
Doc Brown
2
Viele dieser Antworten scheinen von einer prä- zu sein moveParadigma
Caleth
@Caleth: ok, um alle Nitpicker bei Laune zu halten, habe ich diesen ziemlich unwichtigen Teil meiner Antwort geändert.
Doc Brown
1
@PhysicsCodingEnthusiast: Nein, hier geht es in erster Linie darum, dasselbe Objekt zu erstellen particle1.getContacts()und zu particle2.getContacts()liefern, das Contactden physischen Kontakt zwischen particle1und darstellt particle2, und nicht zwei verschiedene Objekte. Natürlich könnte man versuchen, das System so zu gestalten, dass es keine Rolle spielt, ob zwei ContactObjekte gleichzeitig verfügbar sind, die denselben physischen Kontakt darstellen. Dies würde bedeuten, Contactunveränderlich zu machen , aber sind Sie sicher, dass dies das ist, was Sie wollen?
Doc Brown
0

Ja, was Sie beschreiben, ist eine sehr akzeptable Methode, um sicherzustellen, dass jede ContactInstanz in der Kontaktliste von a enthalten ist Particle.

Bart van Ingen Schenau
quelle
Danke für die Antwort. Ich hatte einige Vorschläge gelesen, dass ein Paar voneinander abhängiger Klassen vermieden werden sollte (zum Beispiel in "C ++ Design Patterns and Derivatives Pricing" von MS Joshi), aber anscheinend ist das nicht unbedingt richtig? Gibt es aus Neugier vielleicht eine andere Möglichkeit, diese automatische Aktualisierung zu implementieren, ohne dass eine gegenseitige Abhängigkeit erforderlich ist?
AnInquiringMind
4
@PhysicsCodingEnthusiast: Interdependente Klassen verursachen alle Arten von Schwierigkeiten, und Sie sollten versuchen, sie zu vermeiden. Aber manchmal sind zwei Klassen so eng miteinander verwandt, dass das Entfernen der gegenseitigen Abhängigkeit zwischen ihnen mehr Probleme verursacht als die gegenseitige Abhängigkeit selbst.
Bart van Ingen Schenau
0

Was Sie getan haben, ist richtig.

Ein anderer Weg ... Wenn das Ziel darin besteht, sicherzustellen, dass jeder Contactin einer Liste enthalten ist, können Sie:

  • Blockerstellung von Contact(privaten Konstruktoren),
  • vorwärts deklarieren ParticleKlasse,
  • mach die ParticleKlasse zu einem Freund von Contact,
  • in Particleerstellen Sie eine Factory-Methode, die eine erstelltContact

Dann müssen Sie nicht enthalten particle.hincontact

Robert Andrzejuk
quelle
Danke für die Antwort! Das scheint ein nützlicher Weg zu sein, dies umzusetzen. Ich frage mich nur, Networkob sich mit meiner Bearbeitung der ersten Frage zur Klasse die vorgeschlagene Struktur ändert oder ob sie immer noch dieselbe ist.
AnInquiringMind
Nachdem Sie Ihre Frage aktualisiert haben, ändert sich der Umfang. ... Jetzt fragen Sie nach der Architektur Ihrer Anwendung, als es zuvor um ein technisches Problem ging.
Robert Andrzejuk
0

Eine weitere Option, die Sie in Betracht ziehen könnten, besteht darin, den Kontaktkonstruktor so zu gestalten, dass eine Vorlage mit Partikelreferenz akzeptiert wird. Auf diese Weise kann sich ein Kontakt zu jedem Container hinzufügen, der implementiert wird addContact(Contact).

template<class Container>
Contact(/*parameters*/, Container& container)
{
  container.addContact(*this);
}
Fehlerhaft
quelle