Aggirare le regole in Wizards and Warriors


9

In questa serie di post sul blog , Eric Lippert descrive un problema nella progettazione orientata agli oggetti usando maghi e guerrieri come esempi, dove:

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.
  • Un mago può usare solo uno staff.

Quindi continua a dimostrare i problemi che si incontrano se si tenta di applicare queste regole utilizzando il sistema di tipo C # (ad esempio, rendere la Wizardclasse responsabile per assicurarsi che un mago possa usare solo uno staff). Si viola il principio di sostituzione di Liskov, si rischiano eccezioni di runtime o si finisce con un codice che è difficile da estendere.

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

player.Weapon = new Sword();

lo stato è modificato da se Commandsecondo Rules:

... creiamo un Commandoggetto chiamato Wieldche prende due oggetti di stato del gioco, a Playere a Weapon. Quando l'utente invia un comando al sistema "questa procedura guidata dovrebbe brandire quella spada", quel comando viene valutato nel contesto di un insieme di Rules, che produce una sequenza di Effects. Ne abbiamo uno Ruleche dice che quando un giocatore tenta di impugnare un'arma, l'effetto è che l'arma esistente, se ce n'è una, viene lasciata cadere e la nuova arma diventa l'arma del giocatore. Abbiamo un'altra regola che rafforza la prima regola, che dice che gli effetti della prima regola non si applicano quando un mago cerca di impugnare una spada.

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 aggirare il Commandse Rulesemplicemente impostando il parametro Weapona a Player. La Weaponproprietà deve essere accessibile dal Wieldcomando, quindi non può essere effettuata private set.

Allora, che cosa fa impedire che uno sviluppatore di fare questo? Devono solo ricordare di non farlo?


2
Non penso che questa domanda sia specifica del linguaggio (C #) in quanto è davvero una domanda sulla progettazione di OOP. Si prega di considerare di rimuovere il tag C #.
Maybe_Factor il

1
@maybe_factor Il tag c # va bene perché il codice pubblicato è c #.
Coding Yoshi,

Perché non chiedi direttamente a @EricLippert? Sembra apparire qui su questo sito di tanto in tanto.
Doc Brown,

@Maybe_Factor - Ho vacillato sopra il tag C #, ma ho deciso di tenerlo nel caso ci fosse una soluzione specifica per la lingua.
Ben L

1
@DocBrown - Ho pubblicato questa domanda sul suo blog (solo un paio di giorni fa, non ho aspettato così tanto una risposta). C'è un modo per portare la mia domanda qui alla sua attenzione?
Ben L

Risposte:


9

L'intero argomento a cui portano serie di post sul blog è nella parte cinque :

Non abbiamo motivo di credere che il sistema di tipo C # sia stato progettato per avere una generalità sufficiente per codificare le regole di Dungeons & Dragons, quindi perché ci stiamo provando?

Abbiamo risolto il problema di "dove va il codice che esprime le regole del sistema?" Va negli oggetti che rappresentano le regole del sistema, non negli oggetti che rappresentano lo stato del gioco; la preoccupazione degli oggetti di stato è mantenere il loro stato coerente, non nel valutare le regole del gioco.

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 di ciò. Anche l' Commandoggetto non sta facendo nulla con gli oggetti di 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 di gioco.

Se uno sviluppatore vuole creare un secondo sistema di regole che fa cose con personaggi e armi che il primo sistema di regole non consentirebbe, può farlo perché in C # non puoi (senza cattivi hack di riflessione) scoprire dove arriva una chiamata al metodo a partire dal.

Una soluzione alternativa che potrebbe funzionare in alcune situazioni consiste nel mettere gli oggetti di gioco (o le loro interfacce) in un assieme con il motore delle regole e contrassegnare tutti i metodi mutatore come internal. Tutti i sistemi che richiedono l'accesso in sola lettura agli oggetti di gioco si troverebbero in un assembly diverso, il che significa che sarebbero in grado di accedere solo ai publicmetodi. Questo lascia ancora la scappatoia degli oggetti di gioco che si chiamano reciprocamente i metodi interni. Ma farlo sarebbe evidente odore di codice, perché hai concordato che le classi di oggetti di gioco dovrebbero essere titolari di stato stupido.


4

L'ovvio problema del codice originale è che sta eseguendo la modellazione dei dati anziché la modellazione degli oggetti . Si prega di notare che non vi è assolutamente alcuna menzione dei requisiti aziendali reali 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 utilizzata nell'attacco, i Maghi possono impugnare uno Staff, Warriors a Sword":

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

"Ogni arma infligge danni 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 era richiesto che nessun giocatore potesse maneggiare un'arma.

Posso immaginare che ci sarebbe un requisito che dice: "Qualcuno Playerpuò provare a maneggiarne uno Weapon". Questa sarebbe comunque una cosa completamente diversa. Lo modellerei forse in questo modo:

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

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


1
Hai letto la serie? Forse vuoi dire all'autore di quella serie di non modellare i dati ma i requisiti. I rquirements che avete nella vostra risposta sono i tuoi confezionati requisiti meno dei requisiti l'autore aveva quando si costruisce il compilatore C #.
Coding Yoshi,

2
Eric Lippert sta descrivendo in dettaglio un problema tecnico in quella serie, il che va bene. Questa domanda riguarda tuttavia il problema reale, non le funzionalità di C #. Il mio punto è che, in progetti reali, dovremmo seguire i requisiti aziendali (per i quali ho fornito esempi inventati, sì), non assumere relazioni e proprietà. Ecco come ottenere un modello adatto. Qual era la domanda.
Robert Bräutigam,

Questa è la prima cosa che ho pensato leggendo quella serie. L'autore ha appena escogitato alcune astrazioni, senza mai valutarle ulteriormente, solo attenendosi a loro. Cercando di risolvere meccanicamente il problema, ancora e ancora. Invece di pensare a un dominio e alle astrazioni utili, ciò dovrebbe apparentemente andare per primo. Il mio voto.
Vadim Samokhin,

Questa è la risposta corretta L'articolo esprime requisiti contrastanti (un requisito afferma che un giocatore può impugnare un'arma [qualsiasi], mentre altri requisiti affermano che non è così.) E quindi specifica quanto sia difficile per il sistema esprimere correttamente il conflitto. L'unica risposta corretta è rimuovere il conflitto. In questo caso, ciò significa rimuovere il requisito che un giocatore può impugnare qualsiasi arma.
Daniel T.

2

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


In realtà questo non risolve il problema. Lo sviluppatore che sta realizzando l'oggetto comando può passare qualsiasi arma e il giocatore lo imposterà. Vai a leggere la serie perché il problema è più difficile di quanto pensi. In realtà ha realizzato quella serie perché ha riscontrato questo problema di progettazione durante lo sviluppo del compilatore Roslyn C #.
Coding Yoshi,

2

Nulla impedisce allo sviluppatore di farlo. In realtà Eric Lippert ha provato molte tecniche diverse ma tutte avevano 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 un Commandoggetto con regole è la strada da percorrere.

Con le regole, puoi impostare la Weaponproprietà di Wizarda come a Swordma quando chiedi Wizarddi impugnare l'arma (Spada) e attaccarla non avrà alcun effetto e quindi non cambierà alcuno stato. Come dice di seguito:

Abbiamo un'altra regola che rafforza la prima regola, che dice che gli effetti della prima regola non si applicano quando un mago tenta di impugnare una spada. Gli effetti per quella situazione sono “emettono un suono trombone triste, l'utente perde la sua azione in questo turno, nessuno stato del gioco è mutato

In altre parole, non possiamo applicare tale regola attraverso typerelazioni che ha provato in molti modi diversi, ma o non gli piaceva o non funzionava. Quindi l'unica cosa che ha detto che possiamo fare è fare qualcosa al momento dell'esecuzione. Lanciare un'eccezione non era un bene perché non la considera un'eccezione.

Alla fine ha scelto di andare con la soluzione di cui sopra. Questa soluzione in sostanza dice che puoi impostare qualsiasi arma ma quando la produci, se non l'arma giusta, sarebbe essenzialmente inutile. Ma nessuna eccezione sarebbe generata.

Penso che sia una buona soluzione. Anche se in alcuni casi andrei anche con lo schema try-set.


This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.Non sono riuscito a trovarlo in quella serie, potresti indicarmi dove viene proposta questa soluzione?
Vadim Samokhin,

@zapadlo Lo dice indirettamente. Ho copiato quella parte nella mia risposta e l'ho citata. Eccolo di nuovo Nella citazione dice: quando un mago cerca di impugnare una spada. Come può un mago impugnare una spada se non è stata impostata una spada? Deve essere stato impostato. Quindi se un mago brandisce una spada Gli effetti per quella situazione sono "
emetti

Hmm, penso che brandire una spada sia fondamentalmente significa che dovrebbe essere impostato, no? Mentre leggo quel paragrafo, interpreto che l'effetto della prima regola è that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon. Mentre la seconda regola che that strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.quindi penso che ci sia una regola che controlla se l'arma è una spada, quindi non può essere esercitata da un wizzard, quindi non è impostata. Invece suona un trombone triste.
Vadim Samokhin,

Secondo me sarebbe strano che una regola di comando venisse violata. È come sfocare su un problema. Perché gestire questo problema dopo che una regola è già stata violata e impostare una procedura guidata in uno stato non valido? Il mago non può avere una spada, ma ce l'ha! Perché non lasciarlo accadere?
Vadim Samokhin,

Sono d'accordo con @Zapadlo su come interpretare Wieldqui. Penso che sia un nome leggermente fuorviante per il comando. Qualcosa del genere ChangeWeaponsarebbe più preciso. Immagino che potresti avere un modello diverso in cui puoi impostare qualsiasi arma ma quando la produci, se non l'arma giusta, sarebbe essenzialmente inutile . Sembra interessante, ma non credo sia quello che descrive Eric Lippert.
Ben L

2

La prima soluzione scartata dall'autore era quella di rappresentare le regole dal sistema dei tipi. Il sistema di tipi viene valutato in fase di compilazione. Se si staccano le regole dal sistema di tipi, non vengono più verificate 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 è verificato dal compilatore e la risposta generale a questo è il test (unitario). Pertanto, la soluzione proposta dall'autore richiede un forte cablaggio di prova per eludere gli errori degli sviluppatori. Per sottolineare questo punto di necessità di un forte cablaggio di prova per errori che possono essere rilevati solo in fase di esecuzione, guarda questo articolo di Bruce Eckel, che sostiene che è necessario scambiare la tipizzazione forte per test più forti in linguaggi dinamici.

In conclusione, l'unica cosa che può impedire agli sviluppatori di commettere errori è avere una serie di test (unit) per verificare che tutte le regole vengano rispettate.


Devi usare un'API e l'API dovrebbe garantire che effettuerai test di unità o farai questo presupposto? L'intera sfida riguarda la modellazione, quindi il modello non si interrompe anche se lo sviluppatore che utilizza il modello è negligente.
Coding Yoshi,

1
Il punto che stavo cercando di sottolineare era che non c'è nulla che impedisca allo sviluppatore di commettere errori. Nella soluzione proposta, le regole sono separate dai dati, quindi se non si creano i propri controlli, non c'è nulla che ti impedisca di utilizzare gli oggetti dati senza applicare le regole.
Larsbe,

1

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, è possibile rendere completamente sicuro questo tipo rendendo Weaponprotetto il setter Player. Quindi aggiungere setSword(Sword)e setStaff(Staff)ad Warriore Wizard, rispettivamente, quella chiamata il setter protetto.

In questo modo, la relazione Player/ Weaponviene controllata staticamente e il codice a cui non importa può semplicemente usare a Playerper ottenere a Weapon.


Eric Lippert non voleva lanciare eccezioni. Hai letto la serie? La soluzione deve soddisfare i requisiti e questi requisiti sono chiaramente indicati nella serie.
Coding Yoshi,

@CodingYoshi Perché questo dovrebbe generare un'eccezione? È sicuro, ovvero controllabile al momento della compilazione.
Alex,

Spiacenti, non sono riuscito a modificare il mio commento una volta che mi sono reso conto che non stai generando scuse. Tuttavia, hai rotto l'eredità facendo così. Vedi il problema che l'autore stava cercando di risolvere è che non puoi semplicemente aggiungere un metodo come hai fatto perché ora i tipi non possono essere trattati polimorficamente.
Coding Yoshi,

@CodingYoshi Il requisito polimorfico è che un giocatore abbia un'arma. E in questo schema, il giocatore ha davvero un'arma. Nessuna eredità è rotta. Questa soluzione verrà compilata solo se si ottengono le regole giuste.
Alex,

@CodingYoshi Ora, ciò non significa che non puoi scrivere codice che richiederebbe un controllo di runtime, ad esempio se provi ad aggiungere Weapona a Player. Ma non esiste un sistema di tipi in cui non si conoscono i tipi concreti in fase di compilazione che possono agire su quei tipi concreti in fase di compilazione. Per definizione. Questo schema significa che è solo quel caso che deve essere affrontato in fase di esecuzione, in quanto tale è in realtà migliore di qualsiasi schema di Eric.
Alex,

0

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

Questa domanda è effettivamente la stessa con l'argomento piuttosto di guerra santa 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 violazione di queste regole fa sì che un'entità smetta di essere quello che è? Se sì, insieme a mantenere queste regole nella convalida dei comandi , inseriscile anche in un'entità. Pertanto, se uno sviluppatore dimentica di convalidare il comando, le entità non si troveranno in uno stato non valido.

Se no - beh, implica intrinsecamente che queste regole sono specifiche del comando e non devono risiedere in entità di dominio. Pertanto, la violazione di queste regole comporta un'azione che non avrebbe dovuto essere consentita, ma non in uno stato di modello non valido.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.