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?

Advertisements

103 thoughts on “Wizards and warriors, part one

  1. Pingback: Использование Strategy Pattern в случае, если конкретные стратегии зависят от типа использующего их объекта - c# разработка-игр проектирование

    • I seal every class unless I have specifically designed it to be extensible. It’s easy enough to un-seal a class if you need to, but it’s hard to seal a class when you discover a security defect in your app because someone is providing their own implementation of one of your classes.

      • That makes sense. The one problem is that it makes it more annoying for external extensions. (Although your code *might* not be able to handle them anyways.)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s