Design OO adequado para Estado e Comando

5

Estou trabalhando (um pouco) em um jogo (baseado em turnos). Existem duas classes relevantes para a questão:

  • State : Esta é uma classe imutável, que expõe todos os seus campos (seja por meio de getters ou de outra maneira, conforme me pareceu apropriado). O estado é um pouco complicado, então eu o decomponho em várias classes no pacote ...state
  • Command : esta é uma classe imutável abstrata com algumas subclasses como MoveCommand(Field from, Field to) , PassCommand() , BidCommand(int amount) , etc. no pacote ...command . Todos os campos têm getters públicos.

Eu preciso de um dos dois métodos

  • State Command.applyTo(State) ou
  • State State.apply(Command)

retornando o novo estado (obtido aplicando o comando ao estado).

O uso do primeiro método parece melhor à primeira vista, já que ele é enviado para diferentes implementações de applyTo nas subclasses de Command . Infelizmente, isso me obriga a mexer com os muitos detalhes de State na classe Command . Para que isso funcione, preciso de algo como MutableState , ou State.Builder , ou um construtor de muitos argumentos de State , ou o que for.

Usar o segundo método parece feio, já que ele me forçaria a usar instanceof (ou alguma outra maneira feia de simular o envio do método virtual). OTOH, concentraria o trabalho com State na própria classe.

Então eu acho que o primeiro método é o caminho a percorrer. Com Command e State cada um em seu próprio pacote, isso significa que MutableState (ou o que for usado para construir o State resultante) precisa ser uma classe pública, pois não há friend s em Java. Nenhum problema real, mas não legal, é isso?

Então, qual é o design adequado?

    
por maaartinus 12.09.2011 / 23:34
fonte

5 respostas

2

Não fique muito envolvido com o padrão de estado. Isto tem um uso específico que eu não acho que necessariamente se aplica ao seu jogo, pelo menos não quando você está manipulando o estado do jogo.

Tome, por exemplo, um aplicativo de desenho. Neste caso, o botão pressionado na barra de ferramentas afeta a maneira como o painel de desenho reage a certos estímulos - neste caso, você não quer que o aplicativo saiba o que ele vai fazer, você apenas quer que ele passe o estímulo. para o objeto State e deixe-o descobrir o que fazer.

Assim, o botão de desenho do retângulo está inativo, o aplicativo contém um RectangleDrawingState do qual ele pode solicitar um tipo de ponteiro com o mouse ou uma ação do mouse ou qualquer outra coisa. Se você pressionar o botão elipse-drawing, então o controle muda o RectangleDrawingState para um ElipseDrawingState e o aplicativo continua a pedir um tipo de ponteiro com o mouse e uma ação do mouse e obtém respostas completamente diferentes.

Este é o tipo de situação que faz um bom caso de uso para o Padrão de Estado.

Agora você pode querer isso em seu jogo de estratégia por turnos, para saber se o botão de ataque está pressionado ou se o botão reunir recursos está pressionado. Mas você deve lidar com o estado do jogo de maneira diferente.

O estado do jogo deve ser tratado como um único objeto mutável, que é manipulado no final de cada ação. Este objeto precisa ser serializável de alguma forma.

O que você deve fazer é obter um objeto Command por qualquer meio necessário e agir sobre o objeto mutável state-of-play (pode ser que seu objeto State imutável, que depende da ação selecionada a qualquer momento, retorne um comando que pode atuar no objeto state-of-play, por exemplo).

    
por 13.09.2011 / 01:15
fonte
2

Parece que você já sabe que a Solução 2 é um plano ruim. Mas a solução 1 está causando problemas também.

Eu entendo que suas aulas acabam parecendo assim:

class MoveCommand
{
   MoveCommand(Field from, Field to)
   {
      this->from = from;
      this->to = to;
   }

   State apply(State state)
   {
       MutableState mutable_state = new MutableState(state);
       Unit unit = mutable_state.GetUnit(from);
       mutable_state.RemoveUnit(from);
       mutable_state.PutUnit(to, unit);
       return mutable_state.AsState();
   }
}

Primeiramente, você projetou seus objetos imutáveis de forma errada. A idéia é não para alternar entre objetos imutáveis e mutáveis. Em vez disso, se você quiser usar objetos imutáveis, os métodos no seu objeto imutável devem retornar novos objetos. Então seu código deve se parecer mais com isso.

State apply(State state)
{
    Unit unit = state.GetUnit(from);
    state = state.RemoveUnit(unit);
    state = state.PutUnit(to, unit);
    return state;
}

Mas a imutabilidade é ótima para pequenas coisas como números, cordas, etc. Para lidar com todo o estado do seu jogo, ele tende a ficar pesado. Então, realmente, eu acho que você deveria abandonar o aspecto imutável e fazer:

void apply(State state)
{
    Unit unit = state.GetUnit(from);
    state.RemoveUnit(unit);
    state.PutUnit(to, unit);
}

No entanto, pode-se argumentar que isso faz com que o MoveCommand tenha lógica que pertença a outro lugar. Talvez o código deva parecer:

void apply(State state)
{
    state.move(from, to);
}

Mas isso faz com que toda a classe MoveCommand pareça um pouco inútil, porque não parece realmente fazer nada. Mas eu digo: não se preocupe com isso. Alguns comandos serão implementados trivialmente usando os objetos de estado do jogo. E isso é uma coisa boa.

    
por 13.09.2011 / 02:22
fonte
1

Acho que você está bem usando seu primeiro método. Você provavelmente desejará utilizar um StateBuilder para permitir que o comando crie o novo estado.

Assim você dividiu seu estado, seus comandos (permitindo reverter para estados anteriores, eu suponho), e seus meios de construir novos estados.

Dito isso, não tenho certeza do que o estado imutável está dando a você neste caso, além de algo com um identificador que possa ser rastreado ou revertido. Se esse não é o seu propósito, eu não sei se você precisa disso.

    
por 13.09.2011 / 00:39
fonte
0

Talvez você deva separar o Comando (uma solicitação) de seus afetos (uma resposta). Você tem um conjunto de classes de Comando concretas e os objetos de Estado se comportarão de maneira diferente para cada um, portanto, você deve considerar o uso de métodos sobrecarregados para cada Comando. Isso permite que você manipule especificamente cada Comando, pois somente o Estado deve saber como responder. Você comando objetos não têm comportamento inerente, somente os dados necessários para a solicitação.

void State.Handle(MoveCommand)

void State.Handle(PassCommand)

void State.Handle(BidCommand)

Os métodos Handle são responsáveis por validar o comando em relação ao estado atual e, em seguida, elevar os eventos de domínio apropriados para afetar o estado do modelo. Os eventos de domínio contêm os novos dados a serem aplicados às partes interessadas (por exemplo, 'MovedEvent' contém nova posição).

Os métodos Apply aceitam objetos de evento de domínio concreto e alteram o estado de acordo:

void State.Apply(MovedEvent)

void State.Apply(PassedEvent)

void State.Apply(BiddedEvent)

Os eventos de domínio também podem ser roteados para outros objetos afetados.

    
por 13.09.2011 / 10:29
fonte
0

Acabei com um design misto:

  • O estado consiste em duas partes:
    • StateData representando
      • o quadro
      • as pilhas de cartas de ambos os jogadores
      • e outras coisas de longo prazo
    • StateControl representando os passos intermediários como
      • quando um jogador selecionou uma unidade como atacante, mas ainda não o alvo
      • quando ocorre uma das muitas situações transitórias semelhantes

O último usa o padrão de estado. Tudo é imutável e deixei de lado a contraparte mutável. Com a ajuda do Lombok Wither , funciona muito bem.

Estou usando Command.applyTo(State) , que às vezes apenas delega aos métodos auxiliares State.apply(Command) , então, na verdade, há duas variantes.

A imutabilidade leva a algumas alocações desnecessárias, como

State move(Field from, Field to) {
    Group g = getGroupFromField(from);
    return this
        .withField(from, EMPTY_GROUP) // throw-away state
        .withField(to, g);
}

mas objetos de vida curta são baratos e o objeto é realmente pequeno, pois consiste em algumas referências a outros pequenos objetos imutáveis. Às vezes, há mais intermediários descartáveis, o que me fez usar uma contraparte mutável como construtora, mas isso atrapalhou muito o design.

A imutabilidade também tornou o código um pouco mais longo, mas certamente menos propenso a erros. Isso também me salvou de codificar qualquer método de cópia e acredito que ele mostrará suas vantagens quando / se eu codificar o jogo AI.

    
por 30.08.2014 / 01:13
fonte