Wizards and warriors, part one
A common problem I see in object-oriented design is:
- A wizard is a kind of player.
- A warrior is a kind of player.
- A staff is a kind of weapon.
- A sword is a kind of weapon.
- A player has a weapon.
But before we get into the details, I just want to point out that I am not really talking about anything specific to the fantasy RPG genre here. Everything in this series applies equally well to Papers and Paychecks, but wizards and warriors are more fun to write about, so there you go.
OK, great, we have five bullet points so let's write some classes without thinking about it! What could possibly go wrong?
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 { }
Designing good class hierarchies is all about capturing the semantics of the business domain in the type system, right? And we've done a great job here. If there is behavior common to all players, that goes in the abstract base class. If there is behavior unique to wizards or warriors, that can go in the derived classes. Clearly we're on track for success here.
Right until we add"
- A warrior can only use a sword.
- A wizard can only use a staff.
What an unexpected development!
(As I have often pointed out, foreshadowing is the sign of a quality blog.)
Now what do we do? Readers familiar with type theory will know that the highfalutin name for the problem is that we're in violation of the Liskov Substitution Principle. But we don't need to understand the underlying type theory to see what's going horribly wrong. All we have to do is try to modify the code to support these new criteria.
Attempt #1
abstract class Player { public abstract Weapon Weapon { get; set; }}sealed class Wizard : Player{ public override Staff Weapon { get; set; }}
Nope, that's illegal in C#. An overriding member must match the signature (and return type) of the overridden member.
Attempt #2
abstract class Player { public abstract Weapon { get; set; }}sealed class Wizard : Player{ private Staff weapon; public override Weapon Weapon { get { return weapon; } set { weapon = (Staff) value; } } }
Now we've turned violations of the rule into runtime exceptions. This is incredibly error-prone; a caller could easily have a Wizard in hand and assign a Sword to Weapon. The whole point of capturing this thing in the type system is that the violation gets discovered at compile time.
Next time on FAIC: what other techniques do we have to try to represent this rule in the type system?