Eu tenho lutado com um problema em um projeto Java sobre referências circulares. Estou tentando modelar uma situação do mundo real na qual parece que os objetos em questão são interdependentes e precisam saber um do outro.
O projeto é um modelo genérico de jogar um jogo de tabuleiro. As aulas básicas são inespecíficas, mas são estendidas para lidar com detalhes de xadrez, gamão e outros jogos. Codifiquei isso como um applet há 11 anos com meia dúzia de jogos diferentes, mas o problema é que ele é cheio de referências circulares. Eu o implementei naquela época, reunindo todas as classes entrelaçadas em um único arquivo de origem, mas tenho a ideia de que essa é uma má forma em Java. Agora, quero implementar algo semelhante a um aplicativo Android e quero fazer as coisas corretamente.
As aulas são:
RuleBook: um objeto que pode ser interrogado para coisas como o layout inicial do tabuleiro, outras informações iniciais do estado do jogo, como quem se move primeiro, os movimentos disponíveis, o que acontece com o estado do jogo após um movimento proposto e uma avaliação de uma posição atual ou proposta do conselho.
Tabuleiro: uma representação simples de um tabuleiro de jogo, que pode ser instruído a refletir um Movimento.
MoveList: uma lista de movimentos. Isso é de dupla finalidade: uma escolha de jogadas disponíveis em um determinado ponto ou uma lista de jogadas que foram feitas no jogo. Pode ser dividido em duas classes quase idênticas, mas isso não é relevante para a pergunta que estou fazendo e pode complicar ainda mais.
Mover: um único movimento. Inclui tudo sobre o movimento como uma lista de átomos: pegue um pedaço daqui, coloque-o lá em baixo, remova um pedaço capturado de lá.
Estado: a informação completa do estado de um jogo em andamento. Não apenas a posição da Diretoria, mas também uma MoveList e outras informações de estado, como quem deve se mudar agora. No xadrez, seria registrado se o rei e as torres de cada jogador foram movidos.
Referências circulares são abundantes, por exemplo: o RuleBook precisa saber sobre o estado do jogo para determinar quais jogadas estão disponíveis em um determinado momento, mas o estado do jogo precisa consultar o RuleBook para obter o layout inicial inicial e quais efeitos colaterais acompanham uma jogada uma vez. é feito (por exemplo, quem se move a seguir).
Tentei organizar o novo conjunto de classes hierarquicamente, com o RuleBook na parte superior, pois ele precisa saber tudo. Mas isso resulta na necessidade de mover muitos métodos para a classe RuleBook (como fazer uma mudança), tornando-a monolítica e não particularmente representativa do que um RuleBook deve ser.
Então, qual é a maneira correta de organizar isso? Devo transformar o RuleBook em BigClassThatDoesAlmostEverythingInTheGame para evitar referências circulares, abandonando a tentativa de modelar o jogo do mundo real com precisão? Ou devo ficar com as classes interdependentes e convencer o compilador a compilá-las de alguma forma, mantendo meu modelo do mundo real? Ou há alguma estrutura válida óbvia que estou perdendo?
Obrigado por qualquer ajuda que você pode dar!
fonte
RuleBook
exemplo, por exemplo, pegouState
como argumento e retornou o válidoMoveList
, ou seja, "aqui está onde estamos agora, o que pode ser feito a seguir?"Respostas:
O coletor de lixo do Java não depende de técnicas de contagem de referência. Referências circulares não causam nenhum tipo de problema em Java. O tempo gasto na eliminação de referências circulares perfeitamente naturais em Java é desperdiçado.
Não é necessário. Se você apenas compilar todos os arquivos de origem de uma só vez (por exemplo,
javac *.java
), o compilador resolverá todas as referências futuras sem problemas.Sim. Espera-se que as classes de aplicativos sejam interdependentes. Compilar todos os arquivos de origem Java que pertencem ao mesmo pacote de uma vez não é um truque inteligente, é precisamente o modo como o Java deve funcionar.
fonte
É verdade que as dependências circulares são uma prática questionável do ponto de vista do design, mas não são proibidas e, do ponto de vista puramente técnico, nem sequer são necessariamente problemáticas , como você parece considerá-las: são perfeitamente legais. na maioria dos cenários, eles são inevitáveis em algumas situações e, em raras ocasiões, podem até ser considerados úteis.
Na verdade, existem muito poucos cenários em que o compilador java negará uma dependência circular. (Nota: pode haver mais, só posso pensar no seguinte agora.)
Na herança: você não pode ter uma classe A estender a classe B, que por sua vez estende a classe A, e é perfeitamente razoável que você não possa tê-lo, uma vez que a alternativa não faria absolutamente nenhum sentido do ponto de vista lógico.
Entre as classes locais do método: as classes declaradas em um método podem não se referir circularmente. Provavelmente, isso não passa de uma limitação do compilador java, possivelmente porque a capacidade de fazer isso não é útil o suficiente para justificar a complexidade adicional que seria necessária no compilador para suportá-lo. (A maioria dos programadores de Java nem sequer está ciente do fato de que você pode declarar uma classe dentro de um método, muito menos declarar várias classes e, em seguida, fazer com que essas classes se refiram circularmente.)
Portanto, é importante perceber e tirar do caminho que a busca para minimizar dependências circulares é uma busca pela pureza do design, não uma busca pela correção técnica.
Até onde eu sei, não existe uma abordagem reducionista para eliminar dependências circulares, o que significa que não há receita consistindo em nada além de simples etapas pré-determinadas e "não óbvias" para tomar um sistema com referências circulares, aplicá-las uma após a outra e terminar com um sistema livre de referências circulares. Você precisa colocar sua mente para trabalhar e executar etapas de refatoração que dependem da natureza do seu design.
Na situação específica que você tem em mãos, parece-me que o que você precisa é de uma nova entidade, talvez chamada "Game" ou "GameLogic", que conhece todas as outras entidades (sem que nenhuma das outras entidades saiba disso, ) para que as outras entidades não precisem se conhecer.
Por exemplo, parece-me irracional que sua entidade do RuleBook precise saber tudo sobre a entidade GameState, porque um livro de regras é algo que consultamos para reproduzir, não é algo que participa ativamente da reprodução. Portanto, é essa nova entidade "Jogo" que precisa consultar o livro de regras e o estado do jogo para determinar quais jogadas estão disponíveis e isso elimina as dependências circulares.
Agora, acho que posso adivinhar qual será o seu problema com essa abordagem: codificar a entidade "Game" de uma maneira independente do jogo será muito difícil, então é provável que você acabe não apenas com uma, mas com duas entidades que precisarão ter implementações personalizadas para cada tipo diferente de jogo: o "RuleBook" e a entidade "Game". O que, por sua vez, anula o propósito de ter uma entidade "RuleBook" em primeiro lugar. Bem, tudo o que posso dizer sobre isso é que talvez, apenas talvez, sua aspiração inicial de escrever um sistema que possa jogar muitos tipos diferentes de jogos possa ter sido nobre, mas talvez mal concebida. Se você estivesse no seu lugar, focaria em usar um mecanismo comum para exibir o estado de todos os jogos diferentes e um mecanismo comum para receber informações do usuário em todos esses jogos,
fonte
A teoria dos jogos trata os jogos como uma lista de movimentos anteriores (tipos de valor, incluindo quem os jogou) e uma função ValidMoves (previousMoves)
Eu tentaria seguir esse padrão para a parte não UI do jogo e tratar as coisas como a configuração do tabuleiro como movimentos.
a interface do usuário pode ser material OO padrão com uma maneira de refir a lógica
Atualizar para condensar comentários
Considere o xadrez. Jogos de xadrez são comumente registrados como listas de jogadas. http://en.wikipedia.org/wiki/Portable_Game_Notation
a lista de jogadas define o estado completo do jogo muito melhor do que uma figura do tabuleiro.
Digamos, por exemplo, que começamos a criar objetos para Board, Piece, Move etc e Métodos como Piece.GetValidMoves ()
primeiro, vemos que temos que ter uma referência da peça no tabuleiro, mas depois consideramos o roque. o que você só pode fazer se ainda não tiver movido seu rei ou torre. Então, precisamos de uma bandeira MovedAlready no rei e nas torres. Da mesma forma, os peões podem mover 2 quadrados no primeiro movimento.
Então vemos que, no jogo de castelos, a jogada válida do rei depende da existência e do estado da torre, de modo que o tabuleiro precisa ter peças e fazer referência a essas peças. estamos entrando no seu problema de referência circular.
No entanto, se definirmos Move como uma estrutura imutável e um estado de jogo como a lista de movimentos anteriores, descobriremos que esses problemas desaparecem. Para ver se o castling é válido, podemos verificar a lista de movimentos da existência de movimentos de castelo e rei. Para ver se o peão pode passar despercebido, podemos verificar se o outro peão fez um movimento duplo antes. Nenhuma referência é necessária, exceto Regras -> Mover
Agora, o xadrez tem um quadro estático, e as peças são sempre configuradas da mesma maneira. Mas digamos que temos uma variante em que permitimos uma configuração alternativa. talvez omitindo algumas peças como uma desvantagem.
Se adicionarmos os movimentos de configuração como movimentos, 'da caixa ao quadrado X' e adaptarmos o objeto Regras para entender esse movimento, ainda poderemos representar o jogo como uma sequência de movimentos.
Da mesma forma, se no seu jogo o tabuleiro não é estático, digamos que podemos adicionar quadrados ao xadrez ou remover quadrados do tabuleiro para que eles não possam ser movidos. Essas alterações também podem ser representadas como Moves sem alterar a estrutura geral do seu mecanismo de Regras ou ter que fazer referência a um objeto BoardSetup de similar
fonte
boardLayout
é uma função de todospriorMoves
(ou seja, se mantivéssemos como estado, nada seria contribuído além de cada umthisMove
). Portanto, a sugestão de Ewan é essencialmente "cortar o intermediário" - válido move uma função direta de todos os anteriores, em vez devalidMoves( boardLayout( priorMoves ) )
.A maneira padrão de remover uma referência circular entre duas classes na programação orientada a objetos é introduzir uma interface que possa ser implementada por uma delas. Portanto, no seu caso, você pode se
RuleBook
referir aoState
que se refere a umInitialPositionProvider
(que seria uma interface implementada porRuleBook
). Isso também facilita o teste, pois é possível criar umState
que use uma posição inicial diferente (presumivelmente mais simples) para fins de teste.fonte
Acredito que as referências circulares e o objeto deus no seu caso possam ser facilmente removidos separando o controle do fluxo do jogo dos modelos de estado e regra do jogo. Ao fazer isso, você provavelmente obteria muita flexibilidade e se livraria de uma complexidade desnecessária.
Eu acho que você deve ter um controlador ("um mestre do jogo", se desejar) que controla o fluxo do jogo e lida com as mudanças reais de estado, em vez de atribuir ao livro de regras ou ao estado do jogo essa responsabilidade.
Um objeto de estado do jogo não precisa mudar sozinho ou estar ciente das regras. A classe só precisa fornecer um modelo de objetos de estado de jogo facilmente manipulados (criados, inspecionados, alterados, persistentes, registrados, copiados, armazenados em cache etc.) e de estado de jogo eficientes para o restante do aplicativo.
O livro de regras não precisa saber ou mexer com nenhum jogo em andamento. Ele só precisa ter uma visão de um estado do jogo para saber quais movimentos são legais e precisa responder com um estado de jogo resultante quando perguntado o que acontece quando um movimento é aplicado a um estado do jogo. Também poderia fornecer um estado inicial do jogo quando solicitado um layout inicial.
O controlador deve estar ciente dos estados do jogo e do livro de regras e talvez de outros objetos do modelo, mas não deve mexer nos detalhes.
fonte
Acho que o problema aqui é que você não forneceu uma descrição clara de quais tarefas devem ser tratadas por quais classes. Descreverei o que considero uma boa descrição do que cada classe deve fazer; depois, darei um exemplo de código genérico que ilustra as idéias. Veremos que o código é menos acoplado e, portanto, não possui referências circulares.
Vamos começar descrevendo o que cada classe faz.
A
GameState
classe deve conter apenas informações sobre o estado atual do jogo. Não deve conter nenhuma informação sobre o que os estados passados do jogo ou quais movimentos futuros são possíveis. Ele deve conter apenas informações sobre quais peças estão em quais quadrados no xadrez, ou quantas e que tipo de damas existem em quais pontos do gamão. OGameState
arquivo deverá conter algumas informações extras, como informações sobre o jogo de xadrez no xadrez ou sobre o cubo dobrado no gamão.o
Move
aula é um pouco complicada. Eu diria que posso especificar uma jogada para jogar especificando osGameState
resultados da jogada. Então você pode imaginar que uma mudança pode ser implementada como umaGameState
. No entanto, em go (por exemplo), você pode imaginar que é muito mais fácil especificar uma jogada especificando um único ponto no tabuleiro. Queremos que nossaMove
classe seja flexível o suficiente para lidar com qualquer um desses casos. Portanto, aMove
classe realmente será uma interface com um método que realiza uma pré-movimentaçãoGameState
e retorna uma nova pós-movimentaçãoGameState
.Agora a
RuleBook
turma é responsável por saber tudo sobre as regras. Isso pode ser dividido em três coisas. Ele precisa saber qual é a inicialGameState
, precisa saber quais jogadas são legais e precisa saber se um dos jogadores venceu.Você também pode fazer uma
GameHistory
aula para acompanhar todos os movimentos que foram feitos e tudo oGameStates
que aconteceu. Uma nova classe é necessária porque decidimos que uma únicaGameState
não deveria ser responsável por conhecer todos osGameState
s que vieram antes dela.Isso conclui as classes / interfaces que discutirei. Você também tem uma
Board
aula. Mas acho que tabuleiros em jogos diferentes são diferentes o suficiente para que seja difícil ver o que genericamente poderia ser feito com tabuleiros. Agora vou dar interfaces genéricas e implementar classes genéricas.Primeiro é
GameState
. Como essa classe depende completamente do jogo em particular, não háGamestate
interface ou classe genérica .O próximo é
Move
. Como eu disse, isso pode ser representado com uma interface que possui um único método que assume um estado de pré-movimentação e produz um estado pós-movimentação. Aqui está o código para esta interface:Observe que existe um parâmetro de tipo. Isso ocorre porque, por exemplo, um
ChessMove
precisará saber sobre os detalhes do movimento anteriorChessGameState
. Assim, por exemplo, a declaração de classe deChessMove
seriaclass ChessMove extends Move<ChessGameState>
,onde você já teria definido uma
ChessGameState
classe.A seguir, discutirei a
RuleBook
classe genérica . Aqui está o código:Novamente, há um parâmetro de tipo para a
GameState
classe. ComoRuleBook
é suposto saber qual é o estado inicial, criamos um método para fornecer o estado inicial. ComoRuleBook
é suposto que sabemos que movimentos são legais, temos métodos para testar se um movimento é legal em um determinado estado e fornecer uma lista de movimentos legais para um determinado estado. Finalmente, existe um método para avaliar oGameState
. ObserveRuleBook
que só deve ser responsável por descrever se um ou os outros jogadores já venceram, mas não quem está em uma posição melhor no meio do jogo. Decidir quem está em uma posição melhor é algo complicado que deve ser transferido para sua própria classe. Portanto, aStateEvaluation
classe é na verdade apenas uma enumeração simples, da seguinte maneira:Por fim, vamos descrever a
GameHistory
classe. Esta classe é responsável por lembrar todas as posições alcançadas no jogo, bem como os movimentos que foram jogados. A principal coisa que ele deve ser capaz de fazer é gravar umMove
como tocado. Você também pode adicionar funcionalidade para desfazerMove
s. Eu tenho uma implementação abaixo.Finalmente, poderíamos imaginar fazer uma
Game
aula para amarrar tudo.Game
Supõe-se que esta classe exponha métodos que possibilitem às pessoas ver qual é a correnteGameState
, ver quem, se alguém tiver uma, ver quais movimentos podem ser executados e executar uma jogada. Eu tenho uma implementação abaixoObserve nesta classe que
RuleBook
não é responsável por saber qual é a correnteGameState
. Esse é oGameHistory
trabalho do. Então,Game
pergunta oGameHistory
que é o estado atual e fornece essas informações paraRuleBook
quando éGame
necessário dizer quais são os movimentos legais ou se alguém ganhou.De qualquer forma, o objetivo desta resposta é que, depois de determinar com precisão o que cada classe é responsável e concentrar cada classe em um pequeno número de responsabilidades, e atribuir cada responsabilidade a uma classe única, as classes tendem a ser dissociados e tudo fica fácil de codificar. Espero que isso seja aparente nos exemplos de código que eu dei.
fonte
Na minha experiência, referências circulares geralmente indicam que seu design não é bem pensado.
No seu projeto, não entendo por que o RuleBook precisa "saber" sobre o Estado. Pode receber um Estado como parâmetro para algum método, com certeza, mas por que ele precisa saber (por exemplo, manter como variável de instância) uma referência a um Estado? Isso não faz sentido para mim. Um RuleBook não precisa "saber" sobre o estado de nenhum jogo em particular para fazer seu trabalho; as regras do jogo não mudam dependendo do estado atual do jogo. Então, você o projetou incorretamente ou o projetou corretamente, mas está explicando incorretamente.
fonte
A dependência circular não é necessariamente um problema técnico, mas deve ser considerado um cheiro de código, que geralmente é uma violação do Princípio da Responsabilidade Única .
Sua dependência circular vem do fato de que você está tentando fazer muito com seu
State
objeto.Qualquer objeto com monitoração de estado deve fornecer apenas métodos diretamente relacionados ao gerenciamento desse estado local. Se requer algo além da lógica mais básica, provavelmente deve ser dividida em um padrão maior. Algumas pessoas têm opiniões diferentes sobre isso, mas, como regra geral, se você estiver fazendo algo além de getters e setters em dados, estará fazendo muito.
Nesse caso, seria melhor ter um
StateFactory
, que poderia saber sobre aRulebook
. Você provavelmente teria outra classe de controle que usa o seuStateFactory
para criar um novo jogo.State
definitivamente não deveria saberRulebook
.Rulebook
pode saber sobre aState
dependendo da implementação de suas regras.fonte
Existe alguma necessidade de um objeto do livro de regras ser vinculado a um estado específico do jogo ou faria mais sentido ter um objeto do livro de regras com um método que, dado um estado do jogo, relate quais movimentos estão disponíveis nesse estado (e, tendo relatado isso, não se lembra de nada sobre o estado em questão)? A menos que haja algo a ser ganho com o objeto perguntado sobre os movimentos disponíveis, retenha uma memória do estado do jogo, não há necessidade de persistir uma referência.
É possível, em alguns casos, haver vantagens em manter o objeto de avaliação de regras mantendo o estado. Se você acha que essa situação pode surgir, sugiro adicionar uma classe "árbitro" e fazer com que o livro de regras forneça um método "createReferee". Ao contrário do livro de regras, que não se importa em saber se é perguntado sobre um jogo ou cinquenta, um objeto de árbitro esperaria oficiar um jogo. Não se espera que encapsule todo o estado relacionado ao jogo que está oficiando, mas poderia armazenar em cache qualquer informação sobre o jogo que considerasse útil. Se um jogo suportar a funcionalidade "desfazer", pode ser útil pedir ao árbitro que inclua um meio de produzir um objeto "instantâneo" que possa ser armazenado junto com os estados anteriores do jogo; esse objeto deve,
Se algum acoplamento for necessário entre os aspectos de processamento de regras e processamento de estado do jogo, o uso de um objeto de árbitro permitirá manter esse acoplamento fora do livro de regras principal e das classes de estado do jogo. Também pode ser possível que novas regras considerem aspectos do estado do jogo que a classe de estado do jogo não consideraria relevante (por exemplo, se uma regra foi adicionada dizendo "O objeto X não pode fazer Y se já esteve no local Z ", o árbitro pode ser alterado para acompanhar quais objetos estiveram no local Z sem precisar alterar a classe de estado do jogo).
fonte
A maneira correta de lidar com isso é usar interfaces. Em vez de ter duas classes conhecidas uma da outra, faça com que cada classe implemente uma interface e faça referência a essa na outra classe. Digamos que você tenha as classes A e B que precisam fazer referência uma à outra. Como a interface de implementação da classe A e a interface de implementação da classe B, você pode fazer referência à interface B da classe A e à interface A da classe B. A classe A pode estar em seu próprio projeto, assim como a classe B. As interfaces estão em um projeto separado que os outros dois projetos fazem referência.
fonte