Accerti l'incapsulamento e il principio Aperto / Chiuso con lo schema Comando?

1

Sto sviluppando una piccola partita multiplayer. Sarà servito da un server websockets e consumato da più utenti. In quanto tale, devo essere cauto riguardo agli errori di concorrenza.

L'architettura generale del software che ho creato è: Incapsula tutto lo stato di gioco nella classe Game di livello più alto e non consente ad altri thread di accedere agli oggetti gamestate. Invece, i client (thread Websocket sul server) inviano comandi alla classe Game dove vengono aggiunti a un elenco di thread-safe e successivamente elaborati dal metodo Game.Tick che viene chiamato in un thread separato. Ad esempio:

Game.AddCommand assegna un comando alla lista dei comandi in arrivo:

    public void AddCommand(Guid id, ICommand command)
    {
        lock (_commandsLock)
        {
            _commands[id] = command;
        }
    }

Game.Tick elabora ciascun comando e cancella l'elenco:

    public void Tick()
    {
        IList<ICommand> commands;
        lock (_commandsLock)
        {
            commands = _commands.Values.ToList();
            _commands = new Dictionary<Guid, ICommand>();
        }

        foreach (var command in commands)
        {
            command.Resolve();
            Observer.Update(_map);
        }
    }

Il problema è questo: per risolvere se stessi ogni Command ha bisogno di accedere agli interni di Game . Intendo utilizzare un numero di servizi tale che un metodo Resolve possa essere molto limitato o anche una singola riga, ad esempio movementService.Move(entity, target) . Se avessi trasmesso questi servizi nel metodo Resolve , ogni% diCommand sarebbe diventato gonfio di cose di cui non ha bisogno.

Invece, mi piacerebbe usare DI. Ma poiché ogni Command viene creato da un cliente, non possono accedere ai servizi che appartengono a Game . Questi servizi devono essere esposti, e se il client fa qualcosa di sbagliato (cioè chiama Resolve se stessi), potrebbero cambiare gamestate e causare errori di concorrenza.

Ho cercato di risolvere questo problema creando la nozione di CommandResolver , nel qual caso un Command sarebbe un modello semplice con alcuni parametri e nient'altro, risolto da un CommandResolver interno che ha accesso al gamestate. Ma questo richiedeva una mappatura elaborata (ad es. Map MoveCommand a MoveCommandResolver - ogni comando è un tipo diverso con dipendenze diverse) che sfugge al principio Open / Closed- devi modificare il mapping, probabilmente un lunga dichiarazione di switch awkward, piuttosto che semplicemente estendere la funzionalità con una nuova Command & %codice%.

Ora sto scartando il resolver e considerando una delle due possibilità: a) Esporre da CommandResolver a Game che può essere usato per generare comandi, con una sintassi come CommandFactory . I comandi quindi hanno accesso al gamestate e si risolvono da soli. b) I comandi sono costruiti dai client, ma prima di essere risolti viene chiamato un metodo game.GetCommandFor(entity).Move(target) che dà l'accesso Initialise(IUnityContainer) a gamestate. In questo modo ottiene l'accesso al gamestate solo quando il comando è pronto per essere rilasciato. Naturalmente, il client potrebbe ancora mantenere tecnicamente il comando e chiamarlo in un secondo momento.

Qualche idea su quale sia la soluzione migliore? Le mie preoccupazioni principali sono la facile estensibilità e l'integrità dello stato interno di Command .

    
posta Jansky 15.07.2018 - 11:26
fonte

1 risposta

2

Non è possibile creare una soluzione perfettamente sicura qui. O si astraggono i comandi e quindi si sacrifica la flessibilità o si concede maggiore flessibilità, ma si allenta l'incapsulamento del proprio stato di gioco.

La domanda qui è se gli oggetti comando possono essere considerati attendibili per non alterare lo stato del gioco. Se non ci si può fidare di loro, è possibile utilizzare una fabbrica di comandi che autorizzi in modo efficace alcuni comandi noti. I metodi di fabbrica possono eseguire la validazione per rifiutare i comandi illegali.

Ma la maggior parte delle volte, puoi fidarti del tuo codice. Sacrificare un po 'di incapsulamento per fare le cose va bene. Ad esempio, potremmo avere un progetto come questo:

interface IGameCommand
{
  void Resolve(GameState game);
}

Quindi in Game.Tick() :

foreach (var command in commands)
{
  command.Resolve(state);
  ...
}

Ciò richiede che lo stato del gioco esponga le operazioni e i servizi necessari. Questo oggetto di stato del gioco non deve essere uguale all'interfaccia pubblica ordinaria di Game - può essere una classe separata. Questo potrebbe essere utile per evitare di chiamare accidentalmente metodi sul gioco invece di mettere in coda un comando per dopo. Tecnicamente questo potrebbe essere usato per mantenere un riferimento oltre la durata del comando, ma se tutto questo è il tuo codice, allora semplicemente non farlo.

Per una partita multiplayer, pensa attentamente al tuo modello di sicurezza. Una soluzione orientata ai messaggi può essere una buona idea perché puoi convalidare i messaggi e puoi scambiare messaggi su una connessione sicura (per determinati valori di "sicuro"). Il sistema di tipi del tuo linguaggio di programmazione non è una barriera di sicurezza. Non è possibile eseguire codice non affidabile e assumere che non possa accedere ai dettagli privati. Pertanto, i messaggi non devono contenere codice. Invece, dovrai usare un approccio risolutore come hai discusso per mappare i messaggi sul comportamento.

    
risposta data 15.07.2018 - 14:11
fonte