¿Cuál es una buena manera de representar una relación de muchos a muchos entre dos clases?

10

Digamos que tengo dos tipos de objetos, A y B. La relación entre ellos es de muchos a muchos, pero ninguno de ellos es el dueño del otro.

Ambas instancias A y B deben ser conscientes de la conexión; No es solo una forma.

Entonces, podemos hacer esto:

class A
{
    ...

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

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

Mi pregunta es: ¿ dónde pongo las funciones para crear y destruir las conexiones?

¿Debería ser A :: Attach (B), que luego actualiza A :: Bs y B :: Como vectores?

O debería ser B :: Adjuntar (A), que parece igualmente razonable.

Ninguno de los dos se siente bien. Si dejo de trabajar con el código y vuelvo después de una semana, estoy seguro de que no podré recordar si debería estar haciendo A.Attach (B) o B.Attach (A).

Quizás debería ser una función como esta:

CreateConnection(A, B);

Pero hacer una función global también parece indeseable, dado que es una función específica para trabajar solo con las clases A y B.

Otra pregunta: si me encuentro con este problema / requisito con frecuencia, ¿puedo de alguna manera hacer una solución general para ello? ¿Quizás una clase TwoWayConnection de la que puedo derivar o usar dentro de clases que comparten este tipo de relación?

¿Cuáles son algunas buenas maneras de manejar esta situación? Sé cómo manejar la situación de uno a muchos "C posee D" bastante bien, pero esta es más complicada.

Editar: solo para hacerlo más explícito, esta pregunta no involucra problemas de propiedad. Tanto A como B son propiedad de algún otro objeto Z, y Z se ocupa de todos los problemas de propiedad. Solo me interesa cómo crear / eliminar los enlaces de muchos a muchos entre A y B.

Dmitri Shuralyov
fuente
Crearía un Z :: Attach (A, B), por lo tanto, si Z posee los dos tipos de objetos, "se siente bien", para que él cree la conexión. En mi ejemplo (no tan bueno), Z sería un repositorio para A y B. No puedo ver otra forma sin un ejemplo práctico.
Machado
1
Para ser más precisos, A y B pueden tener diferentes propietarios. Quizás A es propiedad de Z, mientras que B es propiedad de X, que a su vez es propiedad de Z. Como puede ver, entonces se vuelve realmente complicado tratar de administrar el enlace en otro lugar. A y B deben poder acceder rápidamente a una lista de los pares a los que está vinculado.
Dmitri Shuralyov
Si tienes curiosidad sobre los nombres reales de A y B en mi situación, son Pointery GestureRecognizer. Los punteros son propiedad y están administrados por la clase InputManager. GestureRecognizers son propiedad de instancias de Widget, que a su vez son propiedad de una instancia de Screen que es propiedad de una instancia de aplicación. Los punteros se asignan a GestureRecognizers para que puedan enviarles datos de entrada sin procesar, pero GestureRecognizers deben saber cuántos punteros hay asociados actualmente con ellos (para distinguir los gestos de 1 dedo frente a 2 dedos, etc.).
Dmitri Shuralyov
Ahora que lo pienso más, parece que solo estoy tratando de hacer un reconocimiento de gestos basado en marcos (en lugar de puntero). Por lo tanto, también podría pasar los eventos a un marco de gesto a la vez en lugar de un puntero a la vez. Hmm
Dmitri Shuralyov

Respuestas:

2

Una forma es agregar un Attach()método público y también un AttachWithoutReciprocating()método protegido a cada clase. Hacer amigos Ay Bamigos mutuos para que sus Attach()métodos puedan llamar a los demás AttachWithoutReciprocating():

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

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

Si implementa métodos similares para B, no tendrá que recordar a qué clase recurrir Attach().

Estoy seguro de que podría concluir ese comportamiento en una MutuallyAttachableclase que herede Ay Bherede, evitando así repetirse y obtener puntos de bonificación en el Día del Juicio. Pero incluso el enfoque poco sofisticado de implementarlo en ambos lugares hará el trabajo.

Caleb
fuente
1
Esto suena precisamente como la solución en la que estaba pensando en el fondo de mi mente (es decir, poner Attach () en A y B). También me gusta mucho la solución más SECA (realmente no me gusta duplicar código) de tener una clase MutuallyAttachable de la que puedas derivar cuando sea necesario este comportamiento (lo preveo con bastante frecuencia). El solo hecho de ver la misma solución ofrecida por otra persona me da mucha más confianza, ¡gracias!
Dmitri Shuralyov
Aceptando esto porque IMO es la mejor solución ofrecida hasta ahora. ¡Gracias! Voy a implementar esa clase MutuallyAttachable ahora e informar aquí si me encuentro con problemas imprevistos (algunas dificultades también pueden surgir debido a la herencia ahora múltiple con la que aún no tengo mucha experiencia).
Dmitri Shuralyov
Todo salió bien. Sin embargo, según mi último comentario sobre la pregunta original, estoy empezando a darme cuenta de que no necesito esta funcionalidad para lo que imaginé originalmente. En lugar de hacer un seguimiento de estas conexiones bidireccionales, simplemente pasaré cada par como parámetro a la función que lo necesita. Sin embargo, esto puede ser útil en un momento posterior.
Dmitri Shuralyov
Aquí está la MutuallyAttachableclase que escribí para esta tarea, si alguien quiere reutilizarla: goo.gl/VY9RB (Pastebin) Editar: Este código podría mejorarse convirtiéndolo en una clase de plantilla.
Dmitri Shuralyov
1
Coloqué la MutuallyAttachable<T, U>plantilla de clase en Gist. gist.github.com/3308058
Dmitri Shuralyov
5

¿Es posible que la relación misma tenga propiedades adicionales?

Si es así, entonces debería ser una clase separada.

De lo contrario, cualquier mecanismo de gestión de listas recíprocas será suficiente.

Steven A. Lowe
fuente
Si es una clase separada, ¿cómo conocería A sus instancias vinculadas de B y B conocería sus instancias vinculadas de A? En ese punto, tanto A como B necesitarían tener una relación de 1 a muchos con C, ¿no?
Dmitri Shuralyov
1
A y B necesitarían una referencia a la colección de instancias de C; C tiene el mismo propósito que una "tabla puente" en el modelado relacional
Steven A. Lowe
1

Por lo general, cuando me meto en situaciones como esta que me parecen incómodas, pero no puedo decir por qué, es porque estoy tratando de poner algo en la clase incorrecta. Casi siempre hay una manera de mover algunas funciones fuera de clase Ay Bhacer las cosas más claras.

Un candidato para mover la asociación a es el código que crea la asociación en primer lugar. Quizás en lugar de llamarlo attach()simplemente almacene una lista de std::pair<A, B>. Si hay múltiples lugares creando asociaciones, se puede resumir en una clase.

Otro candidato para un movimiento es el objeto que posee los objetos asociados, Zen su caso.

Otro candidato común para mover la asociación es el código que con frecuencia invoca métodos Au Bobjetos. Por ejemplo, en lugar de llamar a->foo(), que internamente hace algo con todos los Bobjetos asociados , ya conoce las asociaciones y las llamadas a->foo(b)para cada uno. Nuevamente, si varios lugares lo llaman, se puede resumir en una sola clase.

Muchas veces los objetos que crean, poseen y usan la asociación resultan ser el mismo objeto. Eso puede hacer que la refactorización sea muy fácil.

El último método es derivar la relación de otras relaciones. Por ejemplo, los hermanos y hermanas son una relación de muchos a muchos, pero en lugar de mantener una lista de hermanas en la clase de hermanos y viceversa, deriva esa relación de la relación de los padres. La otra ventaja de este método es que crea una única fuente de verdad. No hay forma posible de que un error o error de tiempo de ejecución cree una discrepancia entre las listas de hermanos, hermanas y padres, porque solo tiene una lista.

Este tipo de refactorización no es una fórmula mágica que funcione siempre. Todavía tiene que experimentar hasta que encuentre una buena opción para cada circunstancia individual, pero he descubierto que funciona aproximadamente el 95% del tiempo. El resto del tiempo solo tienes que vivir con la incomodidad.

Karl Bielefeldt
fuente
0

Si implementara esto, pondría Attach en A y B. Dentro del método Attach, llamaría Attach en el objeto pasado. De esa manera puede llamar a A.Attach (B) o B.Attach ( UNA). Mi sintaxis puede no ser correcta, no he usado c ++ en años:

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 método alternativo sería crear una tercera clase, C, que administra una lista estática de emparejamientos AB.

briddums
fuente
Solo una pregunta con respecto a su sugerencia alternativa de tener una tercera clase C para administrar una lista de pares AB: ¿Cómo conocerían A y B sus miembros de par? ¿Cada A y B necesitarían un enlace a (único) C, y luego pedirle a C que encuentre los pares apropiados? B :: Foo () {std :: vector <A *> As = C.GetAs (esto); / * Haz algo con As ... * /} ¿O imaginaste algo más? Porque esto parece tremendamente complicado.
Dmitri Shuralyov