Dans cette série d'articles de blog , Eric Lippert décrit un problème de conception orientée objet utilisant des assistants 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 { }
puis ajoute quelques règles:
- Un guerrier ne peut utiliser qu'une épée.
- Un assistant ne peut utiliser qu'un bâton.
Il continue ensuite à démontrer les problèmes que vous rencontrez si vous essayez d'appliquer ces règles en utilisant le système de type C # (par exemple, rendre la Wizard
classe responsable de s'assurer qu'un assistant ne peut utiliser qu'un bâton). Vous violez le principe de substitution Liskov, vous risquez des exceptions d'exécution ou vous vous retrouvez avec du code difficile à étendre.
La solution qu'il propose est qu'aucune validation n'est effectuée par la classe Player. Il est uniquement utilisé pour suivre l'état. Ensuite, au lieu de donner une arme à un joueur:
player.Weapon = new Sword();
l'état est modifié par Command
s et selon Rule
s:
... nous créons un
Command
objet appeléWield
qui prend deux objets d'état de jeu, aPlayer
et aWeapon
. Lorsque l'utilisateur envoie une commande au système «cet assistant doit manier cette épée», cette commande est alors évaluée dans le contexte d'un ensemble deRule
s, qui produit une séquence deEffect
s. Nous en avons unRule
qui dit que lorsqu'un joueur tente de manier une arme, l'effet est que l'arme existante, s'il y en a une, est lâchée et que la nouvelle arme devient l'arme du joueur. Nous avons une autre règle qui renforce la première règle, qui dit que les effets de la première règle ne s'appliquent pas lorsqu'un sorcier essaie de brandir une é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 le Commands
et Rule
s en plaçant simplement le Weapon
sur a Player
. La Weapon
propriété doit être accessible par la Wield
commande, elle ne peut donc pas être créée private set
.
Alors, qu'est - ce qui empêche un développeur de faire cela? Doivent-ils simplement se rappeler de ne pas le faire?
Réponses:
L'argument entier auquel mène une série de billets de blog se trouve dans la cinquième partie :
Les armes, personnages, monstres et autres objets du jeu ne sont pas responsables de vérifier ce qu'ils peuvent ou ne peuvent pas faire. Le système de règles en est responsable. L'
Command
objet 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 lorsqu'elle l'est, le système de règles exécute la commande en appelant les méthodes appropriées sur les objets du jeu.Si un développeur veut créer un deuxième système de règles qui fait des choses avec des personnages et des armes que le premier système de règles ne permettrait pas, il peut le faire parce qu'en C # vous ne pouvez pas (sans hacks de réflexion désagréables) savoir d'où vient un appel de méthode de.
Une solution de contournement qui pourrait fonctionner dans certaines situations consiste à placer les objets de jeu (ou leurs interfaces) dans un seul assemblage avec le moteur de règles et à marquer les méthodes de mutation comme
internal
. Tous les systèmes qui ont besoin d'un accès en lecture seule aux objets du jeu se trouveraient dans un assemblage différent, ce qui signifie qu'ils ne pourraient accéder qu'auxpublic
méthodes. Cela laisse toujours la faille des objets de jeu appelant les méthodes internes des autres. Mais cela serait une odeur de code évidente, car vous avez convenu que les classes d'objets de jeu sont censées être des détenteurs d'état stupides.la source
Le problème évident du code d'origine est qu'il fait de 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: "N'importe quel joueur peut attaquer n'importe quel autre joueur, ...". Ici:
"Les joueurs peuvent manier une arme qui est utilisée dans l'attaque, les sorciers peuvent manier un bâton, les guerriers une épée":
"Chaque arme inflige des dégâts à l'ennemi attaqué". Ok, maintenant nous devons avoir une interface commune pour Weapon:
Et ainsi de suite ... Pourquoi n'y en a-t-il pas
Wield()
dans lePlayer
? Parce qu'il n'y avait aucune exigence selon laquelle tout joueur peut utiliser n'importe quelle arme.Je peux imaginer, qu'il y aurait une exigence qui dit: "N'importe qui
Player
peut essayer d'en utiliserWeapon
." Ce serait cependant une chose complètement différente. Je le modéliserais peut-être de cette façon: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 oo.
la source
Une façon serait de passer la
Wield
commande auPlayer
. Le joueur exécute ensuite laWield
commande, qui vérifie les règles appropriées et renvoie laWeapon
, avec laquellePlayer
il définit ensuite son propre champ d'arme. De cette façon, le champ Weapon peut avoir un setter privé et ne peut être réglé qu'en passant uneWield
commande au joueur.la source
Rien n'empêche le développeur de le faire. En fait, Eric Lippert a essayé de nombreuses techniques différentes mais elles avaient toutes des faiblesses. C'était tout l'intérêt de cette série, empêcher le développeur de le faire n'est pas facile et tout ce qu'il a essayé avait des inconvénients. Enfin, il a décidé que l'utilisation d'un
Command
objet avec des règles était la voie à suivre.Avec les règles, vous pouvez définir la
Weapon
propriété de aWizard
pour être un,Sword
mais lorsque vous demandezWizard
à l'arme (épée) et l'attaque, elle n'aura aucun effet et ne changera donc aucun état. Comme il le dit ci-dessous:En d'autres termes, nous ne pouvons pas appliquer une telle règle par le biais de
type
relations qu'il a essayées de différentes manières mais qui ne l'ont pas aimé ou qui n'ont pas fonctionné. Ainsi, la seule chose qu'il a dit que nous pouvons faire est de faire quelque chose à ce sujet lors de l'exécution. Lancer une exception n'était pas bon car il ne la considère pas comme une exception.Il a finalement choisi d'aller avec la solution ci-dessus. Cette solution dit essentiellement que vous pouvez définir n'importe quelle arme, mais si vous la cédez, si ce n'est la bonne arme, elle serait essentiellement inutile. Mais aucune exception ne serait levée.
Je pense que c'est une bonne solution. Bien que dans certains cas, j'irais aussi avec le modèle try-set.
la source
This solution basically says you can set any weapon but when you yield it, if not the right weapon, it would be essentially useless.
Je ne l'ai pas trouvé dans cette série, pourriez-vous s'il vous plaît me montrer où cette solution est proposée?that the existing weapon, if there is one, is dropped and the new weapon becomes the player’s weapon
. Bien que la deuxième règle soitthat strengthens the first rule, that says that the first rule’s effects do not apply when a wizard tries to wield a sword.
donc je pense qu'il existe une règle vérifiant si l'arme est une épée, elle ne peut donc pas être maniée par un sorcier, elle n'est donc pas définie. Au lieu de cela, un trombone triste sonne.Wield
ici. Je pense que c'est un nom un peu trompeur pour la commande. Quelque chose commeChangeWeapon
serait plus précis. Je suppose que vous pourriez avoir un modèle différent où vous pouvez définir n'importe quelle arme, mais lorsque vous la céderez, sinon la bonne arme, ce serait essentiellement inutile . Cela semble intéressant, mais je ne pense pas que ce soit ce que décrit Eric Lippert.La première solution rejetée par l'auteur a été de représenter les règles par le système de type. Le système de type est évalué au moment de la compilation. Si vous détachez les règles du système de type, elles ne sont plus vérifiées par le compilateur, donc rien n'empêche un développeur de faire une erreur en soi.
Mais ce problème est rencontré par 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 a besoin d'un faisceau de test solide pour contourner les erreurs des développeurs. Pour souligner ce point d'avoir besoin d'un harnais de test solide pour les erreurs qui ne sont détectées qu'au moment de l'exécution, consultez cet article de Bruce Eckel, qui fait valoir que vous devez échanger le typage fort pour des tests plus forts dans les langages dynamiques.
En conclusion, la seule chose qui peut empêcher les développeurs de faire des erreurs est d'avoir un ensemble de tests (unitaires) vérifiant que toutes les règles sont respectées.
la source
J'ai peut-être manqué une subtilité ici, mais je ne suis pas sûr que le problème soit avec le système de type. C'est peut-être avec la convention en C #.
Par exemple, vous pouvez rendre ce type complètement sûr en
Weapon
protégeant le setter dansPlayer
. Ensuite, ajoutezsetSword(Sword)
etsetStaff(Staff)
àWarrior
etWizard
respectivement qui appellent le setter protégé.De cette façon, la relation
Player
/Weapon
est vérifiée statiquement et le code qui ne se soucie pas peut simplement utiliser unPlayer
pour obtenir unWeapon
.la source
Weapon
à unPlayer
. Mais il n'y a pas de système de types où vous ne connaissez pas les types de béton au moment de la compilation qui peuvent agir sur ces types de béton au moment de la compilation. Par définition. Ce schéma signifie que seul ce cas doit être traité au moment de l'exécution, car en tant que tel, il est en fait meilleur que n'importe lequel des schémas d'Eric.Cette question est en fait la même avec un sujet assez saint-guerre-ish appelé " où mettre la validation " (notant probablement aussi ddd).
Donc, avant de répondre à cette question, il faut se demander: quelle est la nature des règles que vous souhaitez suivre? Sont-ils gravés dans la pierre et définissent l'entité? La violation de ces règles fait-elle en sorte qu'une entité cesse d'être ce qu'elle est? Si oui, en plus de conserver ces règles dans la validation de commande , placez-les également dans une entité. Donc, si un développeur oublie de valider la commande, vos entités ne seront pas dans un état invalide.
Si ce n'est pas le cas, 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. La violation de ces règles entraîne donc des actions qui n'auraient pas dû être autorisées, mais pas dans un état de modèle non valide.
la source