2

In Part 5 of Wizards and Warriors, the blog proposes the concept of a "Rule" and rulebook to address the issues with solutions in the previous parts.

One of the issues was the need for double dispatch using a Visitor pattern:

Player player = new Wizard();
Monster monster = new Vampire();
player.Attack(monster);

Player.Attack(Monster) does virtual dispatch to Wizard.Attack(Monster), which calls Monster.ResolveAttack(Wizard), which does virtual dispatch. Vampire did not override that method, so we get the base class implementation, which in turn does a call to Monster.ResolveAttack(Player), and we get the no-special-rules logic.

This was necessary because the Monster is an abstract type, and thus double dispatch is needed to perform the Attack logic on a derived class of Monster.

With the introduction of Rules, I don't understand how the complexity of multi-dispatch is avoided. A Rule contains a Player member, and a Weapon member. These are both abstract base classes. How would a Rule then perform the logic specific to derived classes like "Wizard" and "Sword"? Would it need to use a multi-dispatch pattern to avoid downcasting?

2 Answers 2

3

What you may have missed is that together with the introduction of Rules, Eric Lippert let go of the idea that a Wizard must be a sub-class of Player and Sword a sub-class of Weapon.

A Rule instance would ask the passed in Player and Weapon what type of player and weapon the Rule is dealing with to determine what action, if any, the Rule must take.
Player and Weapon can still be abstract base classes, but they don't need to be. The information about the kind of player or weapon could also be encoded in some other way, like an enumeration member.

The asking part seems to fly in the face of the "tell, don't ask" principle of OO design, but as the article illustrates, for many (most?) of these Rules, there is not an object in the system where it is natural to tell it to perform that Rule. So, that principle leads to sub-optimal designs in this case and you should look beyond it.

2
  • "A Rule instance would ask the passed in Player and Weapon what type of player and weapon the Rule is dealing with to determine what action, if any, the Rule must take." - well, that is an approach to implement this, but I am pretty sure that is not what Eric Lippert had in mind: he wrote "rules are data, not code" in his blog.
    – Doc Brown
    Aug 27, 2021 at 7:58
  • @DocBrown, you are right, it is actually the Command objects that ask the Player and Weapon about their state while interpreting the Rule data. The point remains that neither the Player nor the Weapon objects know which type of player can handle which type of weapon as that is encoded in the Rules. Aug 27, 2021 at 8:38
3

Your confusion comes from the fact Eric did not say much about the actual execution model (or evaluation model) for his proposed system. He leaves this to some degree "as an exercise to the reader". In his blog, I found this sentence about it:

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.

so the question here is - what does that actually mean, and where does this kind of command evaluation will take place?

Of course, I don't know how Eric would implement this, but here is my approach for it. I would start with some kind of command interpreter, together with some rule evaluation logic inside. Players, monsters and weapons are somewhere stored in a GameStateObject, and the set of all rules is stored in some container (maybe some kind of graph, Eric mentioned this in a reply to a comment to his blog). My idea about a command interpreter looks roughly like this:

 while(true)
 {   
     var cmd = GetNextCommandFromUser();
     if(cmd.ExitGame())
          break;
     cmd.Execute(subsetOfRules,gameState);
 }

Now imagine an Attack command, and lets say players and monsters have a common base class Character (or maybe there is only a Character class with no derivations). The Attack command will have at least two attributes AttackingCharacter and AttackedCharacter. Then, a potential implementation of the Execute method inside an AttackCommand could look like this:

  virtual void Execute(rules, gameState)
  {
       var attackRules = rules
             .Where(r => r.IsApplicableForAttacks(AttackingCharacter,AttackedCharacter));

       if(attackRules.Count()==0)
       {
            // maybe update some stats in gameState ...
            return;
       }
       var damage = CalculateTotalDamage(attackRules);
       var costs = CalculateCosts(attackRules);
       AttackingCharacter.ReduceStrength(costs);
       AttackedCharacter.IncreaseDamage(damage);
       // do additional things to gameState
  }

(Of course, this is pretty simplified for the sake of this example, but I guess you get the idea.)

Now let us come to your core question: where is the "multi-dispatch" complexity?

Parts of it is already handled in IsApplicableForAttacks: if the two characters are a Wizard and a Vampire, for example, this filter will make sure the remaining rules will only fit to this specific combination of characters (or at least to all combinations of players and monsters).

Note Eric Lippert might have had a different implementation in mind, with a rule being a plain data object, maybe reprented by some DSL, and a some kind of generic rule interpreter, but let us keep things simple here.

Taking a closer look into CalculateTotalDamage, there should exist a rule which contains the amount of damage for the given combination of attacker and attacked character (and maybe there is rule which applies to whole character classes, or combinations of them, or additional rules which increase or decrease the damage under certain conditions in the game state.) And if there is no such specific rule, maybe there is a default rule which applies to all remaining cases.

Hence CalculateTotalDamage will have to

  • filter the rules if they contribute to the damage
  • sort and/or prioritize them, and
  • sum up or combine the damage fractions of the applicable rules

And that's where the complexity goes: partially into the rules, partially into the commands, and partially into their interaction.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.