Design OO corretto per stato e comando

5

Sto lavorando (un po ') su un gioco di strategia (a turni). Ci sono due classi rilevanti per la domanda:

  • State : questa è una classe immutabile, che espone tutti i suoi campi (sia via Getter o in un altro modo, come mi sembrava appropriato). Lo stato è un po 'complicato, quindi l'ho scomposto in diverse classi nel pacchetto ...state
  • Command : questa è una classe astratta immutabile con un paio di sottoclassi come MoveCommand(Field from, Field to) , PassCommand() , BidCommand(int amount) , ecc. nel pacchetto ...command . Tutti i campi hanno getter pubblici.

Ho bisogno di uno dei due metodi

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

restituire il nuovo stato (ottenuto applicando il comando allo stato).

L'uso del primo metodo sembra migliore a prima vista, poiché invia a diverse implementazioni di applyTo nelle sottoclassi di Command . Sfortunatamente, mi costringe a giocherellare con i molti dettagli di State nella classe Command . Per farlo funzionare, ho bisogno di qualcosa come MutableState , o State.Builder , o un costruttore di molti-args di State , o qualsiasi altra cosa.

L'uso del secondo metodo sembra brutto, in quanto mi costringerebbe a usare instanceof (o qualche altro modo brutto per simulare il dispacciamento del metodo virtuale). OTOH, concentrerebbe il lavoro con State nella classe stessa.

Quindi penso che il primo metodo sia la strada da percorrere. Con Command e State ciascuno nel suo stesso pacchetto significa che MutableState (o qualunque cosa venga usato per creare il State risultante) deve essere una classe pubblica poiché non ci sono friend s in Java. Nessun problema reale, ma non bello, vero?

Quindi qual è il design corretto?

    
posta maaartinus 12.09.2011 - 23:34
fonte

5 risposte

2

Non lasciarti coinvolgere dal Pattern di stato. Questo ha un uso specifico che non penso necessariamente si applica al tuo gioco, almeno non quando stai manipolando lo stato di gioco.

Prendi, ad esempio, un'applicazione di disegno. In questo caso, il pulsante premuto nella barra degli strumenti influisce sul modo in cui il pannello di disegno reagisce a determinati stimoli - in questo caso, non vuoi che l'applicazione sappia cosa sta per fare, vuoi solo che passi lo stimolo all'oggetto State e lascia che capisca cosa fare.

Quindi, il pulsante di disegno del rettangolo è inattivo, l'applicazione contiene un RectangleDrawingState da cui può richiedere un tipo di puntatore del mouse sopra o un'azione del mouse verso il basso o qualsiasi altra cosa. Se premi il pulsante di disegno elipse, quel controllo sposterà RectangleDrawingState per un ElipseDrawingState e l'applicazione continua a chiedere un tipo di puntatore del mouse sopra e un'azione di ridimensionamento del mouse e ottiene risposte completamente diverse.

Questo è il tipo di situazione che rende un buon caso d'uso per il modello di stato.

Ora puoi vuoi questo nel tuo gioco di strategia a turni, per gestire se il pulsante di attacco viene premuto o se il pulsante di raccolta risorse viene premuto. Ma dovresti gestire lo stato del gioco in modo diverso.

Lo stato del gioco dovrebbe essere gestito come un singolo oggetto mutabile, che viene manipolato alla fine di ogni azione. Questo oggetto deve essere serializzabile in qualche modo.

Quello che dovresti fare è ottenere un oggetto Command con qualsiasi mezzo necessario e agire sull'oggetto mutabile dello stato di gioco (potrebbe essere che il tuo oggetto State immutabile, che dipende dall'azione selezionata in un dato momento, ritorni un comando che può agire sull'oggetto stato di gioco, ad esempio).

    
risposta data 13.09.2011 - 01:15
fonte
2

Sembra che tu sappia già che la Soluzione 2 è un cattivo piano. Ma la soluzione 1 ti sta dando dei problemi.

Considero che le tue classi finiscono per apparire in questo modo:

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

In primo luogo, hai progettato male i tuoi oggetti immutabili. L'idea è non di passare avanti e indietro tra oggetti immutabili e mutevoli. Piuttosto, se vuoi usare oggetti immutabili, i metodi sul tuo oggetto immutabile dovrebbero restituire nuovi oggetti. Quindi il tuo codice dovrebbe essere più simile a questo.

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

Ma l'immutabilità è ottima per le piccole cose come numeri, archi, ecc. Per gestire l'intero stato del gioco, tende ad essere ingombrante. Quindi, davvero, immagino che dovresti abbandonare l'aspetto immutabile e fare:

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

Tuttavia, è possibile creare un argomento per far sì che MoveCommand abbia una logica che appartiene a qualcun altro. Forse il codice dovrebbe apparire come:

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

Ma questo rende l'intera classe MoveCommand un po 'inutile perché sembra che non faccia nulla. Ma io dico: non ti preoccupare. Alcuni comandi saranno banalmente implementati usando gli oggetti dello stato del gioco. E questa è una buona cosa.

    
risposta data 13.09.2011 - 02:22
fonte
1

Penso che tu stia bene usando il tuo primo metodo. Probabilmente vorrai utilizzare un StateBuilder per consentire al comando di costruire il nuovo stato.

Così hai diviso il tuo stato, i tuoi comandi (permettendoti di tornare agli stati precedenti, suppongo) e i tuoi mezzi per costruire nuovi stati.

Detto questo, non sono sicuro di quale stato immutabile ti stia dando in questo caso, al di là di qualcosa con una maniglia che può essere rintracciata o ripristinata. Se questo non è il tuo scopo, non so se ne hai bisogno.

    
risposta data 13.09.2011 - 00:39
fonte
0

Forse dovresti separare Command (una richiesta) dai suoi affetti (una risposta). Hai una serie di classi di comando concrete e gli oggetti di stato si comportano in modo diverso per ognuno, quindi dovresti considerare l'utilizzo di metodi sovraccaricati per ciascun comando. Ciò consente di gestire specificamente ciascun comando in quanto solo lo Stato dovrebbe sapere come rispondere. Gli oggetti Command non hanno alcun comportamento intrinseco, solo i dati necessari per la richiesta.

void State.Handle(MoveCommand)

void State.Handle(PassCommand)

void State.Handle(BidCommand)

I metodi Handle sono responsabili della convalida del comando rispetto allo stato corrente e quindi del sollevamento degli eventi del dominio appropriati per influenzare lo stato del modello. Gli eventi di dominio contengono i nuovi dati da applicare alle parti interessate (ad esempio "MovedEvent" contiene una nuova posizione).

I metodi Apply accettano oggetti evento di dominio concreti e modificano lo stato di conseguenza:

void State.Apply(MovedEvent)

void State.Apply(PassedEvent)

void State.Apply(BiddedEvent)

Gli eventi di dominio possono anche essere indirizzati ad altri oggetti interessati.

    
risposta data 13.09.2011 - 10:29
fonte
0

Ho finito con un design misto:

  • Lo stato è composto da due parti:
    • StateData che rappresenta
      • la scheda
      • le pile di carte di entrambi i giocatori
      • e altre cose a lungo termine
    • StateControl che rappresenta i passaggi intermedi come
      • quando un giocatore ha selezionato una propria unità come attaccante, ma non ancora il bersaglio
      • quando si verifica una delle tante situazioni transitorie simili

Quest'ultimo usa il modello di stato. Tutto è immutabile e ho abbandonato la controparte mutabile. Con l'aiuto di Lombok Wither , funziona perfettamente.

Sto usando Command.applyTo(State) che a volte si limita a delegare metodi State.apply(Command) , quindi in realtà ci sono entrambe le varianti.

L'immutabilità porta ad allocazioni non necessarie come

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

ma gli oggetti a vita breve sono economici e l'oggetto è in realtà piuttosto piccolo in quanto consiste in un paio di riferimenti ad altri piccoli oggetti immutabili. A volte, ci sono più intermedi da buttare via, il che mi ha spinto a usare una controparte mutabile come costruttore, ma questo era troppo complicato per il design.

L'immutabilità ha anche reso il codice un po 'più lungo, ma sicuramente meno soggetto a errori. Mi ha anche salvato dalla codifica di qualsiasi metodo di copia e credo che mostrerà i suoi vantaggi quando / codificherò l'IA del gioco.

    
risposta data 30.08.2014 - 01:13
fonte

Leggi altre domande sui tag