¿Cómo resolver la interdependencia de clases en mi código C ++?

10

En mi proyecto C ++, tengo dos clases, Particley Contact. En la Particleclase, tengo una variable miembro std::vector<Contact> contactsque contiene todos los contactos de un Particleobjeto, y las funciones miembro correspondientes getContacts()y addContact(Contact cont). Por lo tanto, en "Particle.h", incluyo "Contact.h".

En la Contactclase, me gustaría agregar código al constructor para Contactque se llame Particle::addContact(Contact cont), de modo que contactsse actualice para los dos Particleobjetos entre los que Contactse agrega el objeto. Por lo tanto, tendría que incluir "Particle.h" en "Contact.cpp".

Mi pregunta es si esto es aceptable o buena práctica de codificación y, de lo contrario, cuál sería una mejor manera de implementar lo que estoy tratando de lograr (simplemente, actualizar automáticamente la lista de contactos para una partícula específica cada vez que un nuevo contacto es creado).


Estas clases estarán unidas por una Networkclase que tendrá N partículas ( std::vector<Particle> particles) y Nc contactos ( std::vector<Contact> contacts). Pero quería poder tener funciones como particles[0].getContacts(): ¿está bien tener tales funciones en la Particleclase en este caso, o hay una mejor "estructura" de asociación en C ++ para este propósito (de dos clases relacionadas que se utilizan en otra clase)? .


Es posible que necesite un cambio de perspectiva aquí en cómo me estoy acercando a esto. Dado que las dos clases están conectadas por un Networkobjeto de clase, es típico que la organización de código / clase tenga información de conectividad totalmente controlada por el Networkobjeto (en el sentido de que un objeto Particle no debe conocer sus contactos y, en consecuencia, no debe tener un getContacts()miembro función). Entonces, para saber qué contactos tiene una partícula específica, necesitaría obtener esa información a través del Networkobjeto (por ejemplo, usando network.getContacts(Particle particle)).

¿Sería menos típico (quizás incluso desalentado) el diseño de clase C ++ para que un objeto Particle también tenga ese conocimiento (es decir, tenga múltiples formas de acceder a esa información, ya sea a través del objeto Network o el objeto Particle, lo que sea más conveniente) )?

AnInquiringMind
fuente
44
Aquí hay una charla de cppcon 2017 - Las tres capas de encabezados: youtu.be/su9ittf-ozk
Robert Andrzejuk
3
Las preguntas que contienen palabras como "mejor", "mejor" y "aceptable" no tienen respuesta a menos que pueda establecer sus criterios específicos de evaluación.
Robert Harvey
Gracias por la edición, aunque cambiar su redacción a "típico" simplemente lo convierte en una cuestión de popularidad. Hay razones por las cuales la codificación se realiza de una forma u otra, y si bien la popularidad puede ser una indicación de que una técnica es "buena" (para alguna definición de "buena"), también puede ser una indicación de desecho de carga.
Robert Harvey
@RobertHarvey Eliminé "mejor" y "malo" en mi sección final. Supongo que estoy pidiendo el enfoque típico (quizás incluso favorecido / alentado) cuando tienes un Networkobjeto de clase que contiene Particleobjetos y Contactobjetos. Con ese conocimiento básico, puedo intentar evaluar si se ajusta o no a mis necesidades específicas, que todavía se están explorando / desarrollando a medida que avanzo en el proyecto.
AnInquiringMind
@RobertHarvey Supongo que soy lo suficientemente nuevo como para escribir proyectos C ++ completamente desde cero y estoy bien aprendiendo lo que es "típico" y "popular". Espero obtener suficiente información en algún momento para poder darme cuenta de por qué otra implementación es realmente mejor, pero por ahora, ¡solo quiero asegurarme de que no me estoy acercando a esto de una manera completamente descabellada!
AnInquiringMind

Respuestas:

17

Hay dos partes en su pregunta.

La primera parte es la organización de los archivos de encabezado C ++ y los archivos fuente. Esto se resuelve mediante el uso de la declaración directa y la separación de la declaración de clase (colocándolos en el archivo de encabezado) y el cuerpo del método (colocándolos en el archivo fuente). Además, en algunos casos se puede aplicar el lenguaje Pimpl ("puntero a la implementación") para resolver casos más difíciles. Utilice punteros de propiedad compartida ( shared_ptr), punteros de propiedad única ( unique_ptr) y punteros no propietarios (puntero sin formato, es decir, el "asterisco") de acuerdo con las mejores prácticas.

La segunda parte es cómo modelar objetos que están interrelacionados en forma de gráfico . Los gráficos generales que no son DAG (gráficos acíclicos dirigidos) no tienen una forma natural de expresar la propiedad de un árbol. En cambio, los nodos y las conexiones son todos metadatos que pertenecen a un único objeto gráfico. En este caso, no es posible modelar la relación nodo-conexión como agregaciones. Los nodos no "poseen" conexiones; las conexiones no "poseen" nodos. En cambio, son asociaciones, y tanto los nodos como las conexiones son "propiedad" del gráfico. El gráfico proporciona métodos de consulta y manipulación que operan en los nodos y las conexiones.

rwong
fuente
¡Gracias por la respuesta! De hecho, tengo una clase de red que tendrá N partículas y contactos Nc. Pero quería poder tener funciones como particles[0].getContacts(): ¿sugiere en su último párrafo que no debería tener tales funciones en la Particleclase, o que la estructura actual está bien porque están inherentemente relacionadas / asociadas Network? ¿Existe una mejor "estructura" de asociación en C ++ en este caso?
AnInquiringMind
1
En general, la Red es responsable de conocer las relaciones entre los objetos. Si usa una lista de adyacencia, por ejemplo, la partícula network.particle[p]tendría un correspondiente network.contacts[p]con los índices de sus contactos. De lo contrario, la Red y la Partícula de alguna manera ambos rastrean la misma información.
Inútil
@ Inútil Sí, ahí es donde no estoy seguro de cómo proceder. Entonces, ¿estás diciendo que el Particleobjeto no debería ser consciente de sus contactos (por lo que no debería tener una getContacts()función miembro), y que esa información solo debería provenir del interior del Networkobjeto? ¿Sería un mal diseño de clase C ++ para un Particleobjeto tener ese conocimiento (es decir, tener múltiples formas de acceder a esa información, a través del Networkobjeto o del Particleobjeto, lo que parezca más conveniente)? Esto último parece tener más sentido para mí, pero tal vez necesito cambiar mi perspectiva sobre esto.
AnInquiringMind
1
@PhysicsCodingEnthusiast: El problema de Particlesaber algo acerca de Contacts o Networks es que te vincula a una forma específica de representar esa relación. Las tres clases pueden tener que estar de acuerdo. Si, en cambio, Networkes el único que sabe o le importa, esa es solo una clase que debe cambiar si decide que otra representación es mejor.
cHao
@ cHao Ok, esto tiene sentido. Así Particley Contactdebe ser totalmente independiente, y la asociación entre ellos se define por el Networkobjeto. Solo para estar completamente seguro, esto es (probablemente) lo que @rwong quiso decir cuando escribió: "tanto los nodos como las conexiones son" propiedad del "gráfico. El gráfico proporciona métodos de consulta y manipulación que operan en los nodos y las conexiones". , ¿derecho?
AnInquiringMind
5

Si lo entendí bien, el mismo objeto de contacto pertenece a más de un objeto de partículas, ya que representa algún tipo de contacto físico entre dos o más partículas, ¿verdad?

Entonces, lo primero que creo que es cuestionable es ¿por qué Particletiene una variable miembro std::vector<Contact>? Debería ser a std::vector<Contact*>o a std::vector<std::shared_ptr<Contact> >en su lugar. addContactentonces debería tener una firma diferente como addContact(Contact *cont)o en su addContact(std::shared_ptr<Contact> cont)lugar.

Esto hace innecesario incluir "Contact.h" en "Particle.h", class Contactserá suficiente una declaración de "Particle.h" y una inclusión de "Contact.h" en "Particle.cpp".

Luego la pregunta sobre el constructor. Quieres algo como

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

¿Derecho? Este diseño está bien, siempre que su programa siempre conozca las partículas relacionadas en el momento en que se debe crear un objeto de contacto.

Tenga en cuenta que si va por la std::vector<Contact*>ruta, debe invertir algunas ideas sobre la vida útil y la propiedad de los Contactobjetos. Ninguna partícula "posee" sus contactos, un contacto probablemente tendrá que eliminarse solo si Particlese destruyen ambos objetos relacionados . Usar en su std::shared_ptr<Contact>lugar resolverá este problema automáticamente. O deja que un objeto de "contexto circundante" tome posesión de partículas y contactos (como lo sugiere @rwong), y gestiona su vida útil.

Doc Brown
fuente
No veo el beneficio de addContact(const std::shared_ptr<Contact> &cont)más addContact(std::shared_ptr<Contact> cont)?
Caleth
@Caleth: esto se discutió aquí: stackoverflow.com/questions/3310737/… - "const" no es realmente importante aquí, pero pasar objetos por referencia (y escalares por valor) es el idioma estándar en C ++.
Doc Brown
2
Muchas de esas respuestas parecen ser de un pre moveparadigma
Caleth
@Caleth: ok, para mantener contentos a todos los quisquillosos, cambié esta parte sin importancia de mi respuesta.
Doc Brown
1
@PhysicsCodingEnthusiast: no, se trata principalmente de hacer particle1.getContacts()y particle2.getContacts()entregar el mismo Contactobjeto que representa el contacto físico entre particle1y particle2, y no dos objetos diferentes. Por supuesto, uno podría tratar de diseñar el sistema de una manera que no importe si hay dos Contactobjetos disponibles al mismo tiempo que representen el mismo contacto físico. Esto implicaría hacer Contactinmutable, pero ¿estás seguro de que esto es lo que quieres?
Doc Brown
0

Sí, lo que describe es una forma muy aceptable de garantizar que cada Contactinstancia esté en la lista de contactos de a Particle.

Bart van Ingen Schenau
fuente
Gracias por la respuesta. Había leído algunas sugerencias de que se debe evitar tener un par de clases interdependientes (por ejemplo, en "C ++ Design Patterns and Derivatives Pricing" de MS Joshi), pero aparentemente eso no es necesariamente correcto. Por curiosidad, ¿hay quizás otra forma de implementar esta actualización automática sin necesidad de interdependencia?
AnInquiringMind
44
@PhysicsCodingEnthusiast: Tener clases interdependientes crea todo tipo de dificultades y debes tratar de evitarlas. Pero a veces, dos clases están tan estrechamente relacionadas entre sí que eliminar la interdependencia entre ellas causa más problemas que la interdependencia misma.
Bart van Ingen Schenau
0

Lo que has hecho es correcto.

Otra forma ... Si el objetivo es asegurar que todos Contactestén en una lista, entonces usted podría:

  • creación de bloques de Contact(constructores privados),
  • adelante declarar Particleclase,
  • hacer que Particlela clase de un amigo Contact,
  • en Particlecrear un método de fábrica que crea unContact

Entonces no tienes que incluir particle.hencontact

Robert Andrzejuk
fuente
¡Gracias por la respuesta! Esa parece ser una forma útil de implementar esto. Me pregunto, con mi edición de la pregunta inicial sobre la Networkclase, ¿eso cambia la estructura sugerida, o seguiría siendo la misma?
AnInquiringMind
Después de haber actualizado su pregunta, está cambiando el alcance. ... Ahora está preguntando sobre la arquitectura de su aplicación, cuando anteriormente se trataba de un problema técnico.
Robert Andrzejuk
0

Otra opción que puede considerar es hacer que el constructor de contactos que acepte una referencia de partícula tenga una plantilla. Esto permitirá que un contacto se agregue a cualquier contenedor que implemente addContact(Contact).

template<class Container>
Contact(/*parameters*/, Container& container)
{
  container.addContact(*this);
}
Erróneo
fuente