Article 84WM Wizards and warriors, part two

Wizards and warriors, part two

by
ericlippert
from Fabulous adventures in coding on (#84WM)

In this series we're exploring the problem "a player can use a weapon, a wizard is a kind of player, a staff is a kind of weapon, but a wizard can only use a staff". The best solution we've come up with so far is to throw a conversion violation at runtime if the developer makes a mistake, which seems less than optimal.

Attempt #3

abstract class Player { public Weapon Weapon { get; set; }}sealed class Wizard : Player{ public new Staff Weapon { get { return (Staff)base.Weapon; } set { base.Weapon = value; } }}

This has the nice property that if you have a Wizard in hand, the public surface area now gives you a Staff when you fetch the Weapon, and the type system prevents you from assigning a Sword to the Weapon of the Wizard.

But it has some not-so-nice properties. We're still in violation of the Liskov Substitution Principle: if we have a Player in hand then we can assign a Sword to its Weapon without failure:

Wizard wizard = new Wizard();Player player = wizard;player.Weapon = new Sword(); // FineStaff staff = wizard.Weapon; // Boom!

In this version the exception happens when we try to take the Wizard's staff! This sort of "time bomb" exception is really hard to debug, and it violates the guideline that a getter should never throw.

We could fix that problem like this:

abstract class Player { public Weapon Weapon { get; protected set; }}

Now if you want to set the weapon you need to have a Wizard in hand, not a Player, and the setter enforces type safety.

This is pretty good, but still not great; if we have a Wizard and a Staff in hand but the Wizard is in a variable of type Player then we need to know what to cast the Player to in order to do what is a legal property set, but is not allowed on Player. And of course the same problem exists if the Staff is in a variable of type Weapon; now we have to know what to cast it to in order to do the property set. We have not actually gained much here; there are still going to be conversions all over the show, some of which may fail, and then the failure case has to be considered.

Attempt #4

Interfaces! Yeah, that's the ticket. Every problem with class hierarchies can be solved by adding more abstraction layers, right? Except maybe the problem "I have too many abstraction layers."

interface IPlayer { Weapon Weapon { get; set; }}sealed class Wizard : IPlayer{ Weapon IPlayer.Weapon { get { return this.Weapon; } set { this.Weapon = (Staff) value; } } public Staff Weapon { get; set; }}

We've lost the nice property that we have a convenient container for code common to all players, but that's fixable by making a base class.

Though this is a popular solution, really we're just pushing the problem around rather than fixing it. Polymorphism is still totally broken, because someone can have an IPlayer in hand, assign a Sword to Weapon, and that throws. Interfaces as we've used them here are essentially no better than abstract base classes.

Attempt #5

It's time to get out the big guns. Generic constraints! Yeah baby!

abstract class Player<TWeapon> where TWeapon : Weapon{ TWeapon Weapon { get; set; }}sealed class Wizard : Player<Staff> { }sealed class Warrior : Player<Sword> { }

This is so awful I don't even know where to even begin, but I'll try.

First off, by adding parametric polymorphism we have lost subtype polymorphism completely. A method can no longer take a player; it must take a-player-that-can-use-a-particular-weapon, or it must be a generic method.

We could solve this problem by making the generic player type inherit from a non-generic player type that doesn't have a Weapon property, but now you cannot take advantage of the fact that players have weapons if you have a non-generic player in hand.

Second, this is a thorough abuse of our intuition about generic types. We want subtyping to represent the relationship "a wizard is a kind of player" and we want generics to represent the relationships "a box of fruit and a box of biscuits are both kinds of boxes, but not kinds of each other". I know that a fruit basket is a basket of fruit, but I see "a wizard is a player of staffs" and I do not know what that means. I have no intuition that the generics as we have used them here mean "a wizard is a player that is restricted to using a staff."

Third, this seems like maybe it will not scale well. If I want to also say "a player can wear armor, but a wizard can only wear robes", and "a player can read books, but a warrior can only read non-magical books", and so on, are we going to end up with a half-dozen type arguments to Player?

Attempt #6

Let's combine attempts 4 and 5. The problem arises when we try to mutate the weapon. We could make the player classes immutable, construct a new player every time the weapon changes, and now we can make the interface covariant. Though I am a big fan of immutable programming, I don't much like this solution. It seems like we are trying to model something that is really mutable in code, and so I like having that mutation represented in the code. Also, it's not 100% clear how this solves the problem; if we have a player in hand that we are cloning then we still need some way to prevent a new wizard from being created with a sword.

Oh, yeah, one other thing. Seeing Aragorn with his arsenal there reminded me, did I mention earlier that wizards and warriors can both use daggers?

Maybe your users always tell you the exact business domain constraints perfectly accurately before you start coding every project and they never change in the future. Well, I'm not that kind of user. I need the system to handle the fact that wizards and warriors can both use daggers, and no, daggers are not "a special kind of sword", so don't even ask.

Attempt #7

Plainly what we need to do here is combine all the techniques we've seen so far in this episode:

sealed class Wizard : IPlayer<Staff, Robes> , IPlayer<Dagger, Robes>{ ...

I can't bear to finish. I think just writing that little bit took an extra few minutes off my life.

Next time on FAIC: We'll leave the staves-and-swords problem aside for the moment and consider some related problem in class hierarchy design: suppose a paladin swings a sword at a werewolf in a church after midnight. Is that a concern of the player, monster, weapon or location classes? Or maybe some combination of them? And if the types of none of them are known at compile time, what then?


2639 b.gif?host=ericlippert.com&blog=67759120
External Content
Source RSS or Atom Feed
Feed Location http://ericlippert.com/feed
Feed Title Fabulous adventures in coding
Feed Link https://ericlippert.com/
Reply 0 comments