Contourner les règles chez les magiciens et les guerriers

Dans cette série de billets de blog , Eric Lippert décrit un problème dans la conception orientée objet utilisant des sorciers et des guerriers comme exemples, où:

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 { }

et ajoute ensuite quelques règles:

  • Un guerrier ne peut utiliser qu'une épée.
  • Un assistant ne peut utiliser qu'une portée.

Il expose ensuite les problèmes que vous rencontrez si vous essayez d'appliquer ces règles à l'aide du système de type C# (par exemple, en chargeant la classe Wizard de vérifier qu'un assistant ne peut utiliser qu'un personnel). Vous violez le principe de substitution de Liskov, vous risquez des exceptions d'exécution ou vous vous retrouvez avec un code difficile à étendre.

La solution qu'il propose est qu'aucune validation n'est effectuée par la classe Player. Il n'est utilisé que pour suivre l'état. Ensuite, au lieu de donner une arme à un joueur par:

player.Weapon = new Sword();

l'état est modifié par les commandes Command et selon les règles Rule :

... nous créons un objet Command appelé Wield qui prend deux états de jeu   objets, un Player et un Arme . Lorsque l'utilisateur lance une commande au   système "cet assistant doit utiliser cette épée", alors cette commande est   évalué dans le contexte d'un ensemble de Rule s, qui produit une séquence   des effets s. Nous avons un Rule qui dit que lorsqu'un joueur tente de   manier une arme, l'effet est que l'arme existante, s'il y a   une, est larguée et la nouvelle arme devient l’arme du joueur. nous   avoir une autre règle qui renforce la première règle, qui dit que le   Les effets de la première règle ne s’appliquent pas quand un sorcier essaie de   épée.

J'aime cette idée en principe, mais je me demande comment elle pourrait être utilisée dans la pratique.

Rien ne semble empêcher un développeur de contourner les Commandes et les Règles en définissant simplement Arme sur un Player . La propriété Weapon doit être accessible à l'aide de la commande Wield . Elle ne peut donc pas être configurée ensemble privé .

Alors, qu'est-ce que empêche un développeur de faire cela? Doivent-ils juste se rappeler de ne pas le faire?

9
L’important est d’empêcher les utilisateurs (joueurs) de faire des choses interdites pendant le jeu, mais cette tentative de maniement n’est même pas une situation exceptionnelle, pas plus que de ne pas pouvoir se déplacer à gauche quand il y a un mur - juste une partie du jeu normal. Il n'y a aucune raison d'essayer de les appliquer de manière statique (au moment de la compilation), et il n'y a pas non plus de bonne raison d'utiliser des exceptions sur le flux de contrôle normal pour tenter de les gérer.
ajouté l'auteur Ayyash, source
Je ne pense pas que cette question soit spécifique à la langue (C #), car il s’agit vraiment d’une question de conception POO. Veuillez envisager de supprimer la balise C #.
ajouté l'auteur Faisal, source
Pourquoi ne demandez-vous pas @EricLippert directement? Il semble apparaître ici sur ce site de temps en temps.
ajouté l'auteur Peter LeFanu Lumsdaine, source
@BenL: bien sûr, et vous le savez déjà: en utilisant le caractère "@". Depuis que je l'ai déjà fait pour vous, attendez.
ajouté l'auteur Peter LeFanu Lumsdaine, source
@maybe_factor La balise C# est correcte car le code envoyé est c #.
ajouté l'auteur user254582, source
J'ai posé une question presque similaire ici, à laquelle il a répondu. : D J'espère que c'est utile. stackoverflow .com/questions/46555530/& hellip;
ajouté l'auteur AdmiralThrawn, source
@DocBrown - J'ai posté cette question sur son blog (il y a à peine quelques jours, je l'avoue, je n'ai pas attendu longtemps pour obtenir une réponse). Est-il possible de porter ma question ici à son attention?
ajouté l'auteur kangear, source
@ Maybe_Factor - J'ai hésité sur la balise C#, mais j'ai décidé de la conserver au cas où il y aurait une solution spécifique à la langue.
ajouté l'auteur kangear, source

7 Réponses

Tout l'argumentation à laquelle aboutissent les séries d'articles de blog se trouve dans la Part Cinq :

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.

Les armes, personnages, monstres et autres objets du jeu ne sont pas tenus de vérifier ce qu’ils peuvent ou ne peuvent pas faire. Le système de règles est responsable de cela. L'objet Command ne fait rien non plus avec les objets du jeu. Cela représente simplement la tentative de faire quelque chose avec eux. Le système de règles vérifie ensuite si la commande est possible et, le cas échéant, exécute la commande en appelant les méthodes appropriées sur les objets de jeu.

Si un développeur souhaite créer un système de règles secondes faisant des choses avec des personnages et des armes que le premier système de règles ne permet pas, il peut le faire car en C#, vous ne pouvez pas (sans fioritures fictives) rechercher d'où vient un appel de méthode.

Une solution de contournement que pourrait fonctionner dans certaines situations consiste à placer les objets de jeu (ou leurs interfaces) dans un assemblage avec le moteur de règles et à marquer chaque méthode de mutateur comme interne. . Tous les systèmes nécessitant un accès en lecture seule aux objets de jeu se trouveraient dans un assemblage différent, ce qui signifie qu'ils ne pourraient accéder qu'aux méthodes public . Cela laisse toujours la faille des objets de jeu appelant les méthodes internes les uns des autres. Mais cela aurait une odeur évidente de code, car vous avez convenu que les classes d’objets du jeu sont supposées être des détenteurs d’États stupides.

9
ajouté

Le problème évident du code d'origine est qu'il utilise la modélisation de données au lieu de la modélisation d'objet . Veuillez noter qu'il n'y a absolument aucune mention des exigences commerciales réelles dans l'article lié!

Je commencerais par essayer d’obtenir les exigences fonctionnelles réelles. Par exemple: "Tout joueur peut attaquer tout autre joueur, ...". Ici:

interface Player {
    void Attack(Player enemy);
}

"Les joueurs peuvent manier une arme qui est utilisée dans l'attaque, les sorciers peuvent manier un bâton, des guerriers une épée":

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

"Chaque arme inflige des dégâts à l'ennemi attaqué". Ok, maintenant nous devons avoir une interface commune pour Weapon:

interface Weapon {
    void dealDamageTo(Player enemy);
}

Et ainsi de suite ... Pourquoi n'y a-t-il pas de Wield() dans Player ? Parce qu'il n'était pas nécessaire qu'un joueur puisse utiliser une arme.

Je peux imaginer qu’il y aurait une exigence qui dit: "Tout Lecteur peut essayer d’utiliser une Arme ". Ce serait une chose complètement différente cependant. Je modéliserais peut-être de cette façon:

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

Résumé: Modélisez les exigences et uniquement les exigences. Ne faites pas de modélisation de données, ce n'est pas de la modélisation.

4
ajouté
C'est la bonne réponse. L'article exprime des exigences contradictoires (une exigence stipule qu'un joueur peut utiliser une arme, alors que d'autres indiquent que ce n'est pas le cas.) Et explique ensuite combien il est difficile pour le système d'exprimer correctement le conflit. La seule réponse correcte est de supprimer le conflit. Dans ce cas, cela signifie qu’il faut supprimer la condition qu’un joueur puisse manier n’importe quelle arme.
ajouté l'auteur Gary, source
Avez-vous lu la série? Peut-être voudrez-vous dire à l'auteur de cette série de ne pas modéliser les données, mais les exigences. Les exigences que vous avez spécifiées dans votre réponse sont vos pré-requis , PAS les prérequis de l’auteur lors de la construction du compilateur C #.
ajouté l'auteur user254582, source
C'est la première chose à laquelle j'ai pensé en lisant cette série. L'auteur vient de proposer quelques abstractions, sans jamais les évaluer davantage, mais simplement en rester là. Essayer de résoudre le problème mécaniquement, encore et encore. Au lieu de penser à un domaine et à des abstractions utiles, cela devrait apparemment commencer en premier. Mon vote positif.
ajouté l'auteur glglgl, source
Eric Lippert détaille un problème technique dans cette série, ce qui est bien. Cette question concerne toutefois le problème actuel, pas les fonctionnalités C #. Mon point est que, dans les projets réels, nous sommes supposés suivre les exigences commerciales (pour lesquelles j’ai donné des exemples inventés, oui), et non pas supposer des relations et propriétés. C'est ainsi que vous obtenez un modèle qui convient. Quelle était la question?
ajouté l'auteur Robert Bräutigam, source

Une solution serait de passer la commande Wield au Player . Le lecteur exécute ensuite la commande Wield , qui vérifie les règles appropriées et renvoie le code Weapon , que le Player définit ensuite avec son propre champ Arme. De cette façon, le champ Weapon peut avoir un configurateur privé et ne peut être configuré qu'en transmettant une commande Wield au joueur.

2
ajouté
En réalité, cela ne résout pas le problème. Le développeur qui fabrique l'objet de commande peut passer n'importe quelle arme et le joueur le définira. Allez lire la série parce que le problème est plus difficile que vous ne le pensez. En fait, il a réalisé cette série car il avait rencontré ce problème de conception lors du développement du compilateur Roslyn C #.
ajouté l'auteur user254582, source

Rien n'empêche le développeur de le faire. En fait, Eric Lippert a essayé de nombreuses techniques différentes, mais toutes avaient des faiblesses. C’est tout l’intérêt de cette série que d'empêcher le développeur d'agir ainsi n'est pas facile et que tout ce qu'il a essayé avait des inconvénients. Enfin, il a décidé d'utiliser un objet Command avec des règles.

Avec les règles, vous pouvez définir la propriété Arme d'un assistant comme étant un épée , mais lorsque vous demandez à assistant de Manier l'arme (épée) et l'attaquer n'aura aucun effet et donc il ne changera aucun état. Comme il dit ci-dessous:

Nous avons une autre règle qui renforce la première règle, indiquant que les effets de la première règle ne s'appliquent pas lorsqu'un magicien essaie de brandir une épée. Les effets de cette situation sont les suivants: «produire un son de trombone triste, l’utilisateur perd son action pour ce tour, aucun état de jeu n’est muté

En d’autres termes, nous ne pouvons pas imposer une telle règle par le biais de relations type qu’il a essayées de différentes manières, mais ne l’a pas aimé ou n’a pas fonctionné. Ainsi, la seule chose qu'il puisse dire, c'est de faire quelque chose à ce sujet au moment de l'exécution. Lancer une exception n'était pas bon car il ne considère pas cela comme une exception.

Il a finalement choisi d'aller avec la solution ci-dessus. Cette solution dit fondamentalement que vous pouvez utiliser n'importe quelle arme, mais que si vous la cédez, sinon l'arme appropriée sera inutilisable. Mais aucune exception ne serait lancée.

Je pense que c'est une bonne solution. Bien que, dans certains cas, j'opterais également pour le motif try-set.

2
ajouté
Non, je doute fort que ce soit le sens. Cela signifie que si une arme est placée et que le joueur ne peut pas utiliser cette arme, émettez un son triste et perdez votre tour. Aucun état n'est changé. N'a aucun effet.
ajouté l'auteur user254582, source
@ zapadlo Il le dit indirectement. J'ai copié cette partie de ma réponse et l'ai citée. La voici encore. Dans la citation, il dit: quand un sorcier tente de brandir une épée. Comment un sorcier peut-il brandir une épée si une épée n'a pas été installée? Il doit avoir été réglé. Ensuite, si un sorcier brandit une épée Les effets de cette situation sont les suivants: «produire un son de trombone triste, l’utilisateur perd son action pour ce tour
ajouté l'auteur user254582, source
À mon avis, il serait étrange qu'une règle de commandement soit violée. C'est comme brouiller un problème. Pourquoi gérer ce problème après la violation d’une règle et définir un assistant dans un état non valide? Le sorcier ne peut pas avoir d'épée, mais c'est le cas! Pourquoi ne pas le laisser arriver?
ajouté l'auteur glglgl, source
Hmm, je pense que manier une épée signifie fondamentalement qu'il devrait être réglé, non? En lisant ce paragraphe, j’interprète que l’effet de la première règle est que l’arme existante, s’il en existe une, est abandonnée et que la nouvelle arme devient l’arme du joueur . Alors que la deuxième règle qui renforce la première règle, cela dit que les effets de la première règle ne s'appliquent pas lorsqu'un assistant essaie de brandir une épée. Je pense donc qu'il existe une règle permettant de vérifier si l'arme est bien épée, de sorte qu'il ne peut pas être brandi par un wizzard, donc il n'est pas défini. Au lieu de cela, un trombone triste sonne.
ajouté l'auteur glglgl, source
Cette solution dit fondamentalement que vous pouvez utiliser n'importe quelle arme, mais si vous la cédez, c’est inutile si vous la donnez. Je n’ai pas réussi à la trouver dans cette série. Pourriez-vous me signaler où cela se trouve? solution est proposée?
ajouté l'auteur glglgl, source
Je suis d'accord avec @Zapadlo sur la façon d'interpréter Wield ici. Je pense que c'est un nom légèrement trompeur pour la commande. Quelque chose comme ChangeWeapon serait plus précis. J'imagine que vous pourriez avoir un modèle différent où vous pouvez placer n'importe quelle arme, mais lorsque vous la cédez, si ce n'est pas la bonne arme, ce sera essentiellement inutile . Cela semble intéressant, mais je ne pense pas que ce soit ce que décrit Eric Lippert.
ajouté l'auteur kangear, source

La première solution mise au rebut de l'auteur consistait à représenter les règles par le système de types. Le système de types est évalué à la compilation. Si vous détachez les règles du système de types, elles ne sont plus vérifiées par le compilateur, donc rien n'empêche un développeur de commettre une erreur en tant que telle.

Mais ce problème est confronté à chaque élément de logique/modélisation qui n'est pas vérifié par le compilateur et la réponse générale à cela est le test (unitaire). Par conséquent, la solution proposée par l'auteur nécessite un test puissant pour contourner les erreurs commises par les développeurs. Pour souligner le fait qu’il est nécessaire de disposer d’un faisceau de test puissant pour les erreurs détectées uniquement à l’exécution, consultez la section cet article de Bruce Eckel, qui explique que vous devez échanger la dactylographie forte contre des tests plus rigoureux dans des langages dynamiques.

En conclusion, la seule chose qui puisse empêcher les développeurs de commettre des erreurs est de disposer d'un ensemble de tests (unitaires) vérifiant que toutes les règles sont respectées.

1
ajouté
Vous devez utiliser une API et cette dernière est censée vous assurer de faire des tests unitaires ou de faire cette hypothèse. Tout le défi consiste à modéliser pour que le modèle ne casse pas même si le développeur qui l'utilise est négligent.
ajouté l'auteur user254582, source
Ce que j’essayais de dire, c’est qu’il n’ya rien qui empêche le développeur de commettre des erreurs. Dans la solution proposée, les règles sont dissociées des données. Par conséquent, si vous ne créez pas vos propres contrôles, rien ne vous empêche d'utiliser les objets de données sans appliquer les règles.
ajouté l'auteur Macin, source

Alors, qu'est-ce qui empêche un développeur de faire cela? Doivent-ils juste se rappeler de ne pas le faire?

Cette question est en fait la même chose avec le sujet intitulé " où mettre la validation " (notant probablement ddd également).

Donc, avant de répondre à cette question, il convient de se demander: quelle est la nature des règles que vous souhaitez suivre? Sont-ils gravés dans la pierre et définissent-ils l'entité? La violation de ces règles a-t-elle pour conséquence qu'une entité cesse d'être ce qu'elle est? Si oui, conservez ces règles dans la validation de commande , mettez-les également dans une entité. Ainsi, si un développeur oublie de valider la commande, vos entités ne seront pas dans un état non valide.

Si non - bien, cela implique intrinsèquement que ces règles sont spécifiques à une commande et ne doivent pas résider dans des entités de domaine. En violant ces règles, vous obtiendrez des actions qui n'auraient pas dû être autorisées, mais pas dans un état de modèle non valide.

0
ajouté

J'ai peut-être manqué une subtilité ici, mais je ne suis pas sûr que le problème vient du système de types. Peut-être que c'est avec la convention en C #.

Par exemple, vous pouvez sécuriser complètement ce type en rendant le setter Arme protégé dans Player . Ajoutez ensuite setSword (Épée) et setStaff (Personnel) à Warrior et Assistant qui appellent respectivement le setter protégé.

De cette façon, la relation Lecteur / Arme est vérifiée de manière statique et le code non pertinent peut simplement utiliser un Lecteur pour obtenir un . Arme .

0
ajouté
Oui c'est cassé. Il va aussi plus loin en disant qu'un sorcier et un guerrier peuvent utiliser des dagues. Alors maintenant, vous devez ajouter une autre méthode et quand il y a une autre arme, une autre méthode, etc. Et même si un assistant ne peut pas utiliser d'épée, vous disposez de la méthode setSword définie dans l'assistant . .
ajouté l'auteur user254582, source
Désolé, je n'ai pas pu changer mon commentaire une fois que j'ai réalisé que vous ne lançiez pas une excuse. Cependant, vous avez brisé l'héritage en faisant cela. Voir le problème que l'auteur tentait de résoudre est que vous ne pouvez pas simplement ajouter une méthode comme vous l'avez fait car les types ne peuvent pas être traités de manière polymorphe.
ajouté l'auteur user254582, source
Eric Lippert n'a pas voulu lancer d'exceptions. Avez-vous lu la série? La solution doit répondre aux exigences et celles-ci sont clairement indiquées dans la série.
ajouté l'auteur user254582, source
@CodingYoshi Pourquoi cela jetterait-il une exception? Il est de type sûr, c’est-à-dire qu'il peut être vérifié au moment de la compilation.
ajouté l'auteur philm, source
@CodingYoshi L'exigence polymorphe est qu'un joueur possède une arme. Et dans ce schéma, le joueur a bien une arme. Aucun héritage n'est cassé. Cette solution ne compilera que si vous maîtrisez les règles.
ajouté l'auteur philm, source
@CodingYoshi Maintenant, cela ne signifie pas que vous ne pouvez pas écrire de code nécessitant une vérification à l'exécution, par exemple. si vous essayez d’ajouter une arme à un lecteur . Mais il existe un système de type no dans lequel vous ne connaissez pas les types concrets au moment de la compilation qui peuvent agir sur ces types concrets au moment de la compilation. Par définition. Ce schéma signifie que seul ce cas doit être traité à l'exécution. En tant que tel, il est en réalité meilleur que tous les schémas d'Eric.
ajouté l'auteur philm, source
ajouté l'auteur philm, source