Qual é uma boa maneira de representar um relacionamento muitos-para-muitos entre duas classes?

10

Digamos que eu tenha dois tipos de objetos, A e B. O relacionamento entre eles é muitos-para-muitos, mas nenhum deles é o proprietário do outro.

As instâncias A e B precisam estar cientes da conexão; não é apenas uma maneira.

Então, podemos fazer isso:

class A
{
    ...

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

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

Minha pergunta é: onde coloco as funções para criar e destruir as conexões?

Deveria ser A :: Attach (B), que atualiza os vetores A :: Bs e B :: As?

Ou deveria ser B :: Attach (A), que parece igualmente razoável.

Nenhum desses parece certo. Se eu parar de trabalhar com o código e voltar depois de uma semana, tenho certeza de que não poderei me lembrar se devo fazer A.Attach (B) ou B.Attach (A).

Talvez deva ser uma função como esta:

CreateConnection(A, B);

Mas tornar uma função global também parece indesejável, já que é uma função específica para trabalhar apenas com as classes A e B.

Outra pergunta: se eu me deparar com esse problema / requisito com frequência, posso de alguma forma fazer uma solução geral para ele? Talvez uma classe TwoWayConnection da qual eu possa derivar ou usar dentro de classes que compartilhem esse tipo de relacionamento?

Quais são algumas boas maneiras de lidar com essa situação ... Eu sei como lidar com a situação um-para-muitos "C possui D" muito bem, mas essa é mais complicada.

Editar: Apenas para tornar mais explícito, esta pergunta não envolve problemas de propriedade. Ambos A e B pertencem a outro objeto Z, e Z cuida de todos os problemas de propriedade. Estou interessado apenas em como criar / remover os links muitos para muitos entre A e B.

Dmitri Shuralyov
fonte
Eu criaria um Z :: Attach (A, B), portanto, se Z possuir os dois tipos de objetos, "parece certo", para ele criar a conexão. No meu exemplo (não tão bom), Z seria um repositório para A e B. Não vejo outra maneira sem um exemplo prático.
Machado
11
Para ser mais preciso, A e B podem ter proprietários diferentes. Talvez A seja de propriedade de Z, enquanto B é de propriedade de X, que por sua vez é de Z. Como você pode ver, torna-se realmente complicado tentar gerenciar o link em outro lugar. A e B precisam ser capazes de acessar rapidamente uma lista dos pares aos quais estão vinculados.
Dmitri Shuralyov 10/10/12
Se você está curioso sobre os nomes reais de A e B na minha situação, eles são Pointere GestureRecognizer. Os ponteiros são de propriedade e gerenciados pela classe InputManager. Os GestureRecognizers pertencem a instâncias de Widget, que por sua vez pertencem a uma instância de Tela pertencente a uma instância de App. Os ponteiros são atribuídos aos GestureRecognizers para que eles possam fornecer dados brutos de entrada, mas os GestureRecognizers precisam estar cientes de quantos ponteiros estão atualmente associados a eles (para distinguir gestos de um dedo versus dois dedos, etc.).
Dmitri Shuralyov 10/10/12
Agora que penso mais nisso, parece que estou apenas tentando fazer o reconhecimento de gestos com base em quadros (em vez de em ponteiros). Assim, eu poderia apenas passar eventos para gesticular um quadro de cada vez, em vez de apontar de uma vez. Hmm.
Dmitri Shuralyov 10/10/12

Respostas:

2

Uma maneira é adicionar um Attach()método público e também um AttachWithoutReciprocating()método protegido a cada classe. Faça amizades Ae Bamigos em comum para que seus Attach()métodos possam chamar os de outros AttachWithoutReciprocating():

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

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

Se você implementar métodos semelhantes para B, não precisará se lembrar de qual classe chamar Attach().

Tenho certeza de que você poderia encerrar esse comportamento em uma MutuallyAttachableclasse que herdar Ae Bherdar, evitando repetir a si mesmo e marcar pontos de bônus no Dia do Julgamento. Mas mesmo a abordagem não sofisticada de implementar em ambos os lugares fará o trabalho.

Caleb
fonte
11
Isso soa exatamente como a solução que eu estava pensando no fundo da minha mente (ou seja, colocar Attach () em A e B). Eu também gosto muito da solução mais SECA (eu realmente não gosto de duplicar código) de ter uma classe MutuallyAttachable da qual você pode derivar sempre que esse comportamento for necessário (eu prevejo com frequência). Ver a mesma solução oferecida por outra pessoa me deixa muito mais confiante, obrigado!
Dmitri Shuralyov 10/10/12
Aceitando isso porque a IMO é a melhor solução oferecida até agora. Obrigado! Vou implementar essa classe MutuallyAttachable agora e reportar aqui se tiver problemas imprevistos (algumas dificuldades também podem surgir devido à herança agora múltipla com a qual ainda não tenho muita experiência).
Dmitri Shuralyov 10/10/12
Tudo deu certo. No entanto, conforme meu último comentário sobre a pergunta original, estou começando a perceber que não preciso dessa funcionalidade para o que eu originalmente imaginava. Em vez de acompanhar essas conexões bidirecionais, passarei todos os pares como parâmetro para a função que precisa. No entanto, isso ainda pode ser útil posteriormente.
Dmitri Shuralyov
Aqui está a MutuallyAttachableclasse que escrevi para esta tarefa, se alguém quiser reutilizá-la: goo.gl/VY9RB (Pastebin) Editar: Esse código pode ser aprimorado, tornando-a uma classe de modelo.
Dmitri Shuralyov
11
Eu coloquei o MutuallyAttachable<T, U>modelo de classe no Gist. gist.github.com/3308058
Dmitri Shuralyov
5

É possível que o próprio relacionamento tenha propriedades adicionais?

Nesse caso, deve ser uma classe separada.

Caso contrário, qualquer mecanismo de gerenciamento de lista recíproco será suficiente.

Steven A. Lowe
fonte
Se for uma classe separada, como A saberá suas instâncias vinculadas de B e B conhecerá suas instâncias vinculadas de A? Nesse ponto, A e B precisariam ter um relacionamento de 1 para muitos com C, não precisariam?
Dmitri Shuralyov
11
A e B precisariam de uma referência à coleção de instâncias C; C serve o mesmo propósito como uma 'mesa ponte' em modelagem relacional
Steven A. Lowe
1

Geralmente, quando entro em situações como essa que parecem estranhas, mas não sei exatamente por que, é porque estou tentando colocar algo na classe errada. Quase sempre há uma maneira de tirar algumas funcionalidades da classe Ae Btornar as coisas mais claras.

Um candidato para mover a associação para é o código que cria a associação em primeiro lugar. Talvez, em vez de chamá- attach()lo, simplesmente armazene uma lista de std::pair<A, B>. Se houver vários locais criando associações, ele poderá ser abstraído em uma classe.

Outro candidato a uma movimentação é o objeto que possui os objetos associados, Zno seu caso.

Outro candidato comum para mover a associação é o código que frequentemente chama métodos Aou Bobjetos. Por exemplo, em vez de chamar a->foo(), que faz algo internamente com todos os Bobjetos associados , ele já conhece as associações e chama a->foo(b)cada uma. Novamente, se vários locais o chamarem, ele poderá ser abstraído em uma única classe.

Muitas vezes, os objetos que criam, possuem e usam a associação são o mesmo objeto. Isso pode tornar a refatoração muito fácil.

O último método é derivar o relacionamento de outros relacionamentos. Por exemplo, irmãos e irmãs são um relacionamento de muitos para muitos, mas, em vez de manter uma lista de irmãs na classe dos irmãos e vice-versa, você deriva esse relacionamento do relacionamento dos pais. A outra vantagem desse método é que ele cria uma única fonte de verdade. Não é possível que um erro de bug ou tempo de execução crie uma discrepância entre as listas de irmãos, irmãs e pais, porque você possui apenas uma lista.

Esse tipo de refatoração não é uma fórmula mágica que funciona sempre. Você ainda precisa experimentar até encontrar um bom ajuste para cada circunstância individual, mas eu descobri que ele funciona cerca de 95% das vezes. Nos tempos restantes, você apenas tem que viver com o constrangimento.

Karl Bielefeldt
fonte
0

Se eu estivesse implementando isso, colocaria Attach em A e B. Dentro do método Attach, chamaria Attach no objeto transmitido. Dessa forma, você pode chamar A.Attach (B) ou B.Attach ( UMA). Minha sintaxe pode não estar correta, não uso c ++ há anos:

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

Um método alternativo seria criar uma terceira classe, C, que gerencia uma lista estática de pares AB.

briddums
fonte
Apenas uma pergunta sobre sua sugestão alternativa de ter uma terceira classe C para gerenciar uma lista de pares AB: Como A e B conheceriam seus membros? Cada A e B precisariam de um link para C (único) e depois pediriam que C encontrasse os pares apropriados? B :: Foo () {std :: vector <A *> As = C.GetAs (this); / * Faça algo com As ... * /} Ou você imaginou outra coisa? Porque isso parece terrivelmente complicado.
Dmitri Shuralyov 10/10/12