Dependência de classe circular

12

É um projeto ruim ter duas classes que precisam uma da outra?

Estou escrevendo um pequeno jogo no qual tenho uma GameEngineclasse que tem alguns GameStateobjetos. Para acessar vários métodos de renderização, esses GameStateobjetos também precisam conhecer a GameEngineclasse - portanto, é uma dependência circular.

Você chamaria isso de design ruim? Estou apenas perguntando, porque não tenho muita certeza e, neste momento, ainda sou capaz de refatorar essas coisas.

shad0w
fonte
2
Eu ficaria muito surpreso se a resposta fosse sim.
Doppelgreener
Como existem duas perguntas que são logicamente disjuntas. A resposta é sim para uma delas. Por que você deseja projetar um estado que realmente faça alguma coisa? você deve ter um gerente / controlador para verificar o estado e executar a ação. É um pouco confuso permitir que um tipo de objeto do estado assuma as responsabilidades de outra coisa. Se você quer fazer isso, C ++ é seu amigo .
Teodron # 30/12
Minhas aulas no GameState são como introdução, jogo atual, menu e assim por diante. O GameEngine armazena esses estados em uma pilha, para que eu possa pausar um estado e abrir um menu ou reproduzir uma cena, ... As classes GameState precisam conhecer o GameEngine, porque o mecanismo cria a janela e tem o contexto de renderização .
Shad0w
5
Lembre-se, todas essas regras visam rápido. Rápido para executar, Rápido para criar, Rápido para manter. Às vezes, essas facetas estão em desacordo e você precisa fazer uma análise de custo-benefício para decidir como proceder. Se você pensar muito sobre isso, e fizer uma tentativa, ainda estará fazendo melhor que 90% dos desenvolvedores. Não há monstro escondido que o mate se você fizer errado, ele é mais um operador de pedágio escondido.
DampeS8N

Respostas:

0

Não é um projeto ruim por natureza, mas pode facilmente sair do controle. Na verdade, você está usando esse design em seus códigos diários. Por exemplo, o vetor sabe que é o primeiro iterador e os iteradores têm ponteiro para o contêiner.

Agora, no seu caso, é muito melhor ter duas classes separadas para GameEnigne e GameState. Como basicamente esses dois estão fazendo coisas diferentes, e mais tarde você pode definir muitas classes que herdam o GameState (como um GameState para cada cena do jogo). E ninguém pode negar a necessidade de ter acesso um ao outro. Basicamente, o GameEngine está executando gamestates, portanto, ele deve ter um ponteiro para eles. E o GameState está usando os recursos definidos no GameEngine (como renderizados, gerente de física etc.).

Você não pode e não deve combinar essas duas classes uma com a outra, já que elas estão fazendo coisas diferentes por natureza e a combinação resultará em uma classe muito grande da qual ninguém gosta.

Até agora sabemos que precisamos de dependência circular no design. Existem várias maneiras de criar isso com segurança:

  1. Como você sugeriu, podemos colocar um ponteiro de cada um deles em outro, é a solução mais simples, mas pode facilmente sair do controle.
  2. Você também pode usar o design de singleton / multiton, com esse método você deve definir sua classe GameEngine como um singleton e cada um desses GameStates como um multiton. Embora muitos desenvolvedores considerem o Singleton e o Multiton como AntiPattern, eu prefiro esse tipo de design.
  3. Você pode usar variáveis ​​globais, elas são basicamente a mesma coisa que Singleton / multiton, mas elas têm uma pequena diferença no fato de não limitarem o programador a não criar instâncias à vontade.

Para concluir sua resposta, você pode usar qualquer um desses três métodos, mas precisa tomar cuidado para não usar nenhum deles, pois, como eu disse, todos esses designs são realmente perigosos e podem facilmente resultar em um código impossível de manter.

Ali1S232
fonte
6

É um projeto ruim ter duas classes que precisam uma da outra?

É um pouco de cheiro de código , mas pode-se sair com ele. Se essa é a maneira mais fácil e rápida de colocar seu jogo em funcionamento, siga em frente. Mas lembre-se disso, porque há uma boa chance de você precisar refatorá-lo em algum momento.

O problema do C ++ é que as dependências circulares não serão compiladas com tanta facilidade ; portanto, é uma idéia melhor se livrar delas, em vez de gastar tempo corrigindo sua compilação.

Vejo esta pergunta no SO para mais algumas opiniões.

Você chamaria [meu design] de design ruim?

Não, ainda é melhor do que colocar tudo em uma classe.

Não é tão bom, mas na verdade é bem próximo da maioria das implementações que já vi. Geralmente, você tem uma classe de gerente para os estados dos jogos ( cuidado! ) E uma classe de renderizador, e é bastante comum que eles sejam singletons. Portanto, a dependência circular é "oculta", mas é potencialmente lá.

Além disso, como você foi informado nos comentários, é um pouco estranho que as classes de estado do jogo executem algum tipo de renderização. Eles devem conter apenas informações de estado e a renderização deve ser manipulada por um renderizador ou por algum componente gráfico dos próprios objetos do jogo.

Agora pode haver o design final . Estou curioso para ver se outras respostas trazem uma boa idéia. Ainda assim, você provavelmente é o único que pode encontrar o melhor design para o seu jogo.

Laurent Couvidou
fonte
Por que seria um cheiro de código? A maioria dos objetos com relacionamento pai-filho precisa se ver.
Kikaimaru
4
Porque é a maneira mais fácil de não definir qual classe é responsável por quê. Ele termina facilmente em duas classes que são tão profundamente acopladas que a adição de novo código pode ser feita em uma delas, para que elas não sejam mais separadas conceitualmente. É também uma porta aberta para o código espaguete: a classe A chama a classe B para isso, mas B precisa dessas informações de A, etc. Portanto, não, eu não diria que um objeto filho deve saber sobre seu pai. Se possível, é melhor que não.
Laurent Couvidou
Isso é uma falácia escorregadia. As classes estáticas também podem levar a coisas ruins, mas as classes util não têm cheiro de código. E eu realmente duvido que haja alguma maneira "boa" de fazer coisas como Scene has SceneNodes e SceneNode fazer referência a Scene, então você não pode adicioná-lo a duas cenas diferentes ... E onde você para? A é obrigatório B, B requer C e C requer A um cheiro de código?
Kikaimaru
De fato, isso é como classes estáticas. Manuseie com cuidado, use-o apenas se for necessário. Agora estou falando por experiência própria e não sou o único que pensa assim , mas, na verdade, isso é uma questão de opinião. Minha opinião é que, no contexto dado pelo OP, isso é realmente fedorento.
Laurent Couvidou
Não há menção desse caso nesse artigo da Wikipedia. E você não pode dizer que é um caso de "intimidade inadequada" até que você realmente tenha visto essas aulas.
Kikaimaru
6

Muitas vezes, é considerado um design ruim ter que ter duas classes que se referem diretamente uma à outra, sim. Em termos práticos, pode ser mais difícil seguir o fluxo de controle através do código, a propriedade dos objetos e sua vida útil podem ser complicadas. Isso significa que nenhuma classe é reutilizável sem a outra. Isso pode significar que o fluxo de controle deve realmente viver fora das duas dessas classes em uma terceira classe 'mediadora', e assim por diante.

No entanto, é muito comum a 2 objetos se referirem um ao outro, e a diferença aqui é que geralmente o relacionamento em uma direção é mais abstrato. Por exemplo, em um sistema Model-View-Controller, o objeto View pode conter uma referência ao objeto Model e saberá tudo sobre ele, podendo acessar todos os seus métodos e propriedades para que a View possa se preencher com dados relevantes . O objeto Model pode reter uma referência para o View para que ele possa fazer a atualização quando seus próprios dados forem alterados. Mas, em vez de o Modelo ter uma referência de Visualização - o que tornaria o Modelo dependente da Visualização - geralmente a Visualização implementa uma interface Observável, geralmente com apenas 1Update()e o Modelo mantém uma referência a um objeto Observável, que pode ser uma Visualização. Quando o Modelo muda, ele chama Update()todos os seus Observáveis, e a Visualização é implementada Update()retornando ao Modelo e recuperando qualquer informação atualizada. O benefício aqui é que o Modelo não sabe nada sobre Views (e por que deveria?) E pode ser reutilizado em outras situações, mesmo naquelas sem Views.

Você tem uma situação semelhante no seu jogo. O GameEngine normalmente conhece os GameStates. Mas o GameState não precisa saber tudo sobre o GameEngine - ele só precisa acessar certos métodos de renderização no GameEngine. Isso deve disparar um pouco de alarme em sua cabeça, dizendo que (a) o GameEngine está tentando fazer muitas coisas em uma classe e / ou (b) o GameState não precisa de todo o mecanismo do jogo, apenas da parte renderizável.

Três abordagens para resolver isso incluem:

  • Faça o GameEngine derivar de uma interface Renderable e passe essa referência para o GameState. Se você refatorar a parte de renderização, basta garantir que ela mantenha a mesma interface.
  • Fatore a parte de renderização em um novo objeto Renderer e passe para o GameStates.
  • Deixe como está. Talvez em algum momento você queira acessar toda a funcionalidade GameEngine do GameState, afinal. Mas lembre-se de que um software fácil de manter e estender geralmente requer que cada classe se refira o mínimo possível a parte externa, e é por isso que dividir as coisas em subobjetos e interfaces que executam um único e bem tarefa definida é preferível.
Kylotan
fonte
0

É geralmente considerado uma boa prática ter alta coesão com baixo acoplamento. Aqui estão alguns links sobre acoplamento e coesão.

acoplamento da wikipedia

coesão da wikipedia

baixo acoplamento, alta coesão

stackoverflow sobre as melhores práticas

Ter duas classes se referindo é ter alto acoplamento. A estrutura do google guice visa obter alta coesão com baixo acoplamento por meio de injeção de dependência. Eu sugiro que você leia um pouco mais sobre o assunto e faça sua própria ligação, de acordo com o seu contexto.

avanderw
fonte