Evasione delle regole in Wizards and Warriors

9

In questa serie di post del blog , Eric Lippert descrive un problema nella progettazione orientata agli oggetti usando come esempi esempi di procedure guidate e guerrieri:

abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }

abstract class Player 
{ 
  public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }

e quindi aggiunge un paio di regole:

  • Un guerriero può usare solo una spada.
  • Una procedura guidata può utilizzare solo uno staff.

Quindi prosegue mostrando i problemi che si incontrano se si tenta di applicare queste regole utilizzando il sistema di tipo C # (ad esempio, rendendo la classe Wizard responsabile di assicurarsi che una procedura guidata possa utilizzare solo uno staff). Violi il Principio di sostituzione di Liskov, rischi le eccezioni di run-time o finisci con un codice che è difficile estendere.

La soluzione che ha in mente è che la classe Player non esegue alcuna convalida. Viene utilizzato solo per tenere traccia dello stato. Quindi, invece di dare a un giocatore un'arma di:

player.Weapon = new Sword();
Lo stato

è modificato da Command s e secondo Rule s:

...we make a Command object called Wield that takes two game state objects, a Player and a Weapon. When the user issues a command to the system “this wizard should wield that sword”, then that command is evaluated in the context of a set of Rules, which produces a sequence of Effects. We have one Rule that says that when a player attempts to wield a weapon, the effect is that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon. We have another rule that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.

Mi piace questa idea in linea di principio, ma ho una preoccupazione su come potrebbe essere utilizzata nella pratica.

Nulla sembra impedire a uno sviluppatore di eludere Commands e Rule s semplicemente impostando Weapon su Player . La proprietà Weapon deve essere accessibile dal comando Wield , quindi non può essere resa private set .

Quindi, cosa fa impedisce a uno sviluppatore di farlo? Devono solo ricordarsi di non farlo?

    
posta Ben L 23.11.2017 - 00:03
fonte

7 risposte

9

L'intera argomentazione a cui si riferisce la serie di post del blog è Parte 5 :

We have no reason to believe that the C# type system was designed to have sufficient generality to encode the rules of Dungeons & Dragons, so why are we even trying?

We have solved the problem of “where does the code go that expresses rules of the system?” It goes in objects that represent the rules of the system, not in the objects that represent the game state; the concern of the state objects is keeping their state consistent, not in evaluating the rules of the game.

Armi, personaggi, mostri e altri oggetti di gioco non sono responsabili del controllo di ciò che possono o non possono fare. Il sistema di regole è responsabile per questo. L'oggetto Command non sta facendo nulla con gli oggetti del gioco. Rappresenta solo il tentativo di fare qualcosa con loro. Il sistema di regole controlla quindi se il comando è possibile, e quando è il sistema di regole esegue il comando chiamando i metodi appropriati sugli oggetti del gioco.

Se uno sviluppatore vuole creare un sistema di regole secondo che faccia cose con personaggi e armi che il primo sistema di regole non permetterebbe, può farlo perché in C # non è possibile (senza spiacevoli riflessioni hack): scopri da dove proviene una chiamata di metodo.

Una soluzione che potrebbe funzionare in alcune situazioni sta mettendo gli oggetti di gioco (o le loro interfacce) in un assembly con il motore di regole e contrassegnare qualsiasi metodo di mutatore come% codice%. Tutti i sistemi che necessitano dell'accesso in sola lettura agli oggetti di gioco si trovano in un assembly diverso, il che significa che sarebbero in grado di accedere solo ai metodi internal . Ciò lascia ancora la scappatoia degli oggetti di gioco che chiamano i metodi interni l'uno dell'altro. Ma fare questo sarebbe ovvio odore di codice, perché hai convenuto che le classi di oggetti del gioco dovevano essere mute titolari di stato.

    
risposta data 23.11.2017 - 17:15
fonte
4

L'ovvio problema del codice originale è che sta facendo Data Modeling invece di Modeling degli oggetti . Si prega di notare che non vi è alcuna menzione degli effettivi requisiti aziendali nell'articolo collegato!

Vorrei iniziare cercando di ottenere effettivi requisiti funzionali. Ad esempio: "Qualsiasi giocatore può attaccare qualsiasi altro giocatore, ...". Qui:

interface Player {
    void Attack(Player enemy);
}

"I giocatori possono impugnare un'arma che viene usata nell'attacco, i maghi possono impugnare un bastone, i guerrieri una spada":

public class Wizard: Player {
    ...
    public void Wield(Staff weapon) { ... }
    ...
}
public class Warrior: Player {
    ...
    public void Wield(Sword sword) { ... }
    ...
}

"Ogni arma infligge danno al nemico attaccato". Ok, ora dobbiamo avere un'interfaccia comune per Weapon:

interface Weapon {
    void dealDamageTo(Player enemy);
}

E così via ... Perché non c'è Wield() in Player ? Perché non c'era alcun requisito che qualsiasi giocatore possa brandire qualsiasi arma.

Posso immaginare che ci sarebbe un requisito che dice: "Qualsiasi Player può provare a brandire qualsiasi Weapon ." Questa sarebbe una cosa completamente diversa comunque. Lo modellerei forse in questo modo:

interface Player {
    void Attack(Player enemy);
    void TryWielding(Weapon weapon); // Throws UnwieldableException
}

Riepilogo: modella i requisiti e solo i requisiti. Non eseguire la modellazione dei dati, che non è la modellazione oo.

    
risposta data 23.11.2017 - 13:28
fonte
2

Un modo sarebbe passare il comando Wield a Player . Il giocatore esegue quindi il comando Wield , che controlla le regole appropriate e restituisce Weapon , che Player imposta quindi con il proprio campo Arma. In questo modo, il campo Arma può avere un setter privato ed è impostabile solo passando un comando Wield al giocatore.

    
risposta data 23.11.2017 - 01:01
fonte
2

Nulla impedisce allo sviluppatore di farlo. In realtà, Eric Lippert ha provato molte tecniche diverse, ma tutte avevano dei punti deboli. Questo era il punto centrale di quella serie che impedire allo sviluppatore di farlo non è facile e tutto ciò che ha provato ha avuto degli svantaggi. Alla fine ha deciso che usare l'oggetto Command con le regole è la strada da percorrere.

Con le regole, puoi impostare Weapon proprietà di Wizard per essere Sword ma quando chiedi a Wizard di brandire l'arma (Spada) e attaccare, non avrà alcun effetto e quindi non cambierà nessuno stato. Come dice di seguito:

We have another rule that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword. The effects for that situation are “make a sad trombone sound, the user loses their action for this turn, no game state is mutated

In altre parole, non possiamo imporre tale regola attraverso relazioni type che ha provato in molti modi diversi, ma a entrambi non piaceva o non funzionava. Quindi l'unica cosa che ha detto che possiamo fare è fare qualcosa al riguardo in fase di runtime. Lanciare un'eccezione non era buona perché non la considera un'eccezione.

Finalmente ha scelto di andare con la soluzione di cui sopra. Questa soluzione dice fondamentalmente che puoi impostare qualsiasi arma, ma quando la cedi, se non l'arma giusta, sarebbe essenzialmente inutile. Ma nessuna eccezione sarebbe stata lanciata.

Penso che sia una buona soluzione. Anche se in alcuni casi vorrei andare con il modello di prova anche.

    
risposta data 23.11.2017 - 00:57
fonte
1

La prima soluzione scartata dall'autore era quella di rappresentare le regole in base al sistema dei tipi. Il sistema di tipi viene valutato al momento della compilazione. Se si scollegano le regole dal sistema di tipi, non vengono più controllate dal compilatore, quindi non c'è nulla che impedisca a uno sviluppatore di commettere un errore di per sé.

Ma questo problema è affrontato da ogni pezzo di logica / modellazione che non è controllato dal compilatore e la risposta generale a questo è il test (unità). Pertanto, la soluzione proposta dall'autore necessita di una robusta struttura di test per aggirare gli errori degli sviluppatori. Per sottolineare questo punto di aver bisogno di una strong bardatura di test per errori rilevati solo in fase di esecuzione, guarda questo articolo di Bruce Eckel, che argomenta che è necessario scambiare la tipizzazione strong per test più efficaci nei linguaggi dinamici.

In conclusione, l'unica cosa che può impedire agli sviluppatori di commettere errori consiste in una serie di test (unitari) che verificano il rispetto di tutte le regole.

    
risposta data 23.11.2017 - 17:22
fonte
0

Potrei aver perso una sottigliezza qui, ma non sono sicuro che il problema riguardi il sistema dei tipi. Forse è con convenzione in C #.

Ad esempio, puoi rendere completamente sicuro questo carattere rendendo il setter Weapon protetto in Player . Quindi aggiungi setSword(Sword) e setStaff(Staff) a Warrior e Wizard rispettivamente che chiamano il setter protetto.

In questo modo, la relazione Player / Weapon viene controllata staticamente e il codice a cui non importa può utilizzare solo Player per ottenere Weapon .

    
risposta data 23.11.2017 - 11:21
fonte
0

So, what does prevent a developer from doing this? Do they just have to remember not to?

Questa domanda è in effetti la stessa cosa con l'argomento "holy-war-ish" chiamato " dove mettere la convalida " (molto probabilmente notando anche ddd).

Quindi, prima di rispondere a questa domanda, dovresti chiederti: qual è la natura delle regole che vuoi seguire? Sono scolpiti nella pietra e definiscono l'entità? La rottura di quelle regole fa sì che un'entità smetta di essere ciò che è? In caso affermativo, oltre a mantenere queste regole nella validazione del comando , inserire anche loro in un'entità. Quindi, se uno sviluppatore si dimentica di convalidare il comando, le tue entità non saranno in uno stato non valido.

Se no - beh, implica implicitamente che queste regole siano specifiche del comando e non dovrebbero risiedere in entità di dominio. Pertanto, la violazione di queste regole comporta azioni che non dovrebbero essere consentite, ma non in stato di modello non valido.

    
risposta data 24.11.2017 - 09:43
fonte

Leggi altre domande sui tag