Qual é a maneira correta de modelar essa atividade do mundo real que parece precisar de referências circulares no OOP?


24

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!


7
E se o RuleBookexemplo, por exemplo, pegou Statecomo argumento e retornou o válido MoveList, ou seja, "aqui está onde estamos agora, o que pode ser feito a seguir?"
Jonrsharpe 29/05

O que @jonrsharpe disse. Ao jogar um jogo de tabuleiro real, o livro de regras também não sabe sobre nenhum jogo real sendo jogado. Eu provavelmente introduziria outra classe para calcular os movimentos, mas isso pode depender do tamanho dessa classe do RuleBook.
Sebastiaan van den Broek

4
Evitar o objeto god (BigClassThatDoesAlmostEverythingInTheGame) é muito mais importante do que evitar referências circulares.
User281377

2
@ user281377 eles não são necessariamente objetivos mutuamente exclusivos!
Jonrsharpe 29/05

11
Você pode mostrar as tentativas de modelagem? Um diagrama, por exemplo?
Utilizador

Respostas:


47

Eu tenho lutado com um problema em um projeto Java sobre referências circulares.

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.

Eu codifiquei isso, [...] mas o problema é que está cheio de referências circulares. Eu o implementei naquela época, enchendo todas as classes entrelaçadas em um único arquivo de origem , [...]

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.

Ou devo ficar com as classes interdependentes e convencer o compilador a compilá-las de alguma forma, [...]

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.


24
"Referências circulares não causam nenhum tipo de problema em Java." Em termos de compilação, isso é verdade. Referências circulares são consideradas um design ruim , no entanto.
Chop

22
As referências circulares são perfeitamente naturais em muitas situações, é por isso que Java e outras linguagens modernas usam um sofisticado coletor de lixo em vez de um simples contador de referências.
User281377

3
Java é capaz de resolver referências circulares é ótimo, e é definitivamente verdade que elas são naturais em muitas situações. Mas o OP apresentou uma situação específica , e isso deve ser considerado. O código de espaguete emaranhado provavelmente não é a melhor maneira de lidar com esse problema.
Matthew Leia

3
Por favor, não espalhe FUD sem fundamento sobre linguagens de programação não relacionadas. O Python suporta GC de ciclos de referência desde tempos antigos ( docs , também em SO: aqui e aqui ).
Christian Aichinger

2
IMHO esta resposta é apenas medíocre, uma vez que não há uma palavra sobre referências circulares sendo útil, por exemplo, no PO.
Doc Brown

22

É 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.)

  1. 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.

  2. 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,


11
Obrigado Mike. Você está certo sobre as desvantagens da entidade Jogo; com o antigo código do applet, pude criar novos jogos com pouco mais do que uma nova subclasse do RuleBook e o design gráfico apropriado.
Damian Walker

10

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


Isso tenderá a complicar a implementação do ValidMoves, o que tornará sua lógica mais lenta.
Taemyr

não, na verdade, estou assumindo que a configuração da placa é variável, então você precisa defini-la de alguma forma. Se você converter a instalação, moverá para outra estrutura ou objeto para ajudar no cálculo, poderá armazenar em cache o resultado, se necessário. Alguns jogos têm placas que mudam com o jogo e alguns movimentos válidos pode depender de movimentos anteriores ao invés de posição actual (por exemplo, o roque no xadrez.)
Ewan

11
Adicionar sinalizadores e outras coisas é a complexidade que você evita ao apenas ter o histórico de movimentos. não é caro de varrer dizer 100 xadrez move-se para obter a configuração atual conselho e você pode armazenar em cache o resultado entre movimentos
Ewan

11
você também evita alterar seu modelo de objeto para refletir regras. ou seja, no xadrez, se você fizer válidos movimentos -> peça + tabuleiro, falhará no jogo de castiçais, passivo, primeiro para peões e promoção de peças e precisará adicionar informações extras aos objetos ou fazer referência a um terceiro objeto. Você também perde a idéia de que é ir que é e conceitos como cheque descoberto
Ewan

11
@ Gabriel A boardLayouté uma função de todos priorMoves(ou seja, se mantivéssemos como estado, nada seria contribuído além de cada um thisMove). Portanto, a sugestão de Ewan é essencialmente "cortar o intermediário" - válido move uma função direta de todos os anteriores, em vez de validMoves( boardLayout( priorMoves ) ).
OJFord

8

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 RuleBookreferir ao Stateque se refere a um InitialPositionProvider(que seria uma interface implementada por RuleBook). Isso também facilita o teste, pois é possível criar um Stateque use uma posição inicial diferente (presumivelmente mais simples) para fins de teste.


6

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.


4
EXATAMENTE meu pensamento. O OP está misturando muitos dados e procedimentos nas mesmas classes. É melhor dividir isso mais. Esta é uma boa conversa sobre o assunto. Aliás, quando leio "visualizar um estado do jogo", penso em "argumento da função". +100 se eu puder.
jpmc26

5

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 GameStateclasse 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. O GameStatearquivo 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 os GameStateresultados da jogada. Então você pode imaginar que uma mudança pode ser implementada como uma GameState. 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 nossa Moveclasse seja flexível o suficiente para lidar com qualquer um desses casos. Portanto, a Moveclasse realmente será uma interface com um método que realiza uma pré-movimentação GameStatee retorna uma nova pós-movimentação GameState.

Agora a RuleBookturma é responsável por saber tudo sobre as regras. Isso pode ser dividido em três coisas. Ele precisa saber qual é a inicial GameState, 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 o GameStatesque aconteceu. Uma nova classe é necessária porque decidimos que uma única GameStatenão deveria ser responsável por conhecer todos os GameStates que vieram antes dela.

Isso conclui as classes / interfaces que discutirei. Você também tem uma Boardaula. 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á Gamestateinterface 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:

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Observe que existe um parâmetro de tipo. Isso ocorre porque, por exemplo, um ChessMoveprecisará saber sobre os detalhes do movimento anterior ChessGameState. Assim, por exemplo, a declaração de classe de ChessMoveseria

class ChessMove extends Move<ChessGameState>,

onde você já teria definido uma ChessGameStateclasse.

A seguir, discutirei a RuleBookclasse genérica . Aqui está o código:

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

Novamente, há um parâmetro de tipo para a GameStateclasse. Como RuleBooké suposto saber qual é o estado inicial, criamos um método para fornecer o estado inicial. Como RuleBooké 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 o GameState. Observe RuleBookque 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, a StateEvaluationclasse é na verdade apenas uma enumeração simples, da seguinte maneira:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Por fim, vamos descrever a GameHistoryclasse. 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 um Movecomo tocado. Você também pode adicionar funcionalidade para desfazer Moves. Eu tenho uma implementação abaixo.

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

Finalmente, poderíamos imaginar fazer uma Gameaula para amarrar tudo. GameSupõe-se que esta classe exponha métodos que possibilitem às pessoas ver qual é a corrente GameState, ver quem, se alguém tiver uma, ver quais movimentos podem ser executados e executar uma jogada. Eu tenho uma implementação abaixo

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

Observe nesta classe que RuleBooknão é responsável por saber qual é a corrente GameState. Esse é o GameHistorytrabalho do. Então, Gamepergunta o GameHistoryque é o estado atual e fornece essas informações para RuleBookquando é Gamenecessá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.


3

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.


+1. Você compra um jogo de tabuleiro físico, recebe um livro de regras capaz de descrever as regras sem estado.
Unperson325680

1

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 a Rulebook. Você provavelmente teria outra classe de controle que usa o seu StateFactorypara criar um novo jogo. Statedefinitivamente não deveria saber Rulebook. Rulebookpode saber sobre a Statedependendo da implementação de suas regras.


0

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).


-2

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.


2
isso parece meramente repetir os pontos feitos e explicados em uma resposta anterior postada poucas horas antes desta
3900
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.