Reputation: 169
I’m trying to adhere to good OO design principles and design patterns and such. So while developing this C# application of mine I can often find multiple solutions to design and architecture issues, I always want to find and implement the more “canonical” one in the hopes of building highly maintainable and flexible software and also become a better OO programmer.
So suppose I have these two abstract classes: Character
and Weapon
.
abstract class Weapon
{
public string Name { get; set; }
}
abstract class Character
{
public Weapon weapon { get; set; }
}
Derived classes might be Sword
and Staff
from Weapon
, and Warrior
and Mage
from Character
, etc. (this is all hypothetical, not related to my actual software!).
Every Character
has a Weapon
, but for every implementation of Character
I know what implementation of Weapon
it will have. For instance, I know (and I want to enforce!) that at runtime every instance of Warrior
will have a Weapon
of type Sword
. Of course I could do this:
class Sword : Weapon
{
public void Draw() { }
}
class Warrior : Character
{
public Warrior()
{
weapon = new Sword();
}
}
But this way, every time I want to use my Weapon
object properly inside a Warrior
, I have to perform a cast, which I believe to be a not so great of a practice. Also I have no means to prevent myself from messing up, that is, there is no type safety!
An ideal solution would be to be able to override the Weapon weapon
property in the Warrior
class with a Sword weapon
property, that way I have type safety and if the user uses my Warrior
as a Character
, he can still use my Sword
as a Weapon
. Sadly, it doesn’t seem like C# supports this kind of construct.
So here are my questions: is this some kind of classical OO problem, with a name and a well-documented solution? In that case I would very much like to know the name of the problem and the solutions. Some links to good reading material would be very helpful! If not, what kind of class design would you propose in order to maintain functionality and enforce type safety in an elegant and idiomatic way?
Thanks for reading!
Upvotes: 6
Views: 783
Reputation: 13148
The problem is that the "warriors can only use swords" restriction is not something that should be modeled in the type system.
You have "characters are things that have weapons"
abstract class Character {
public Weapon weapon { get; set; }
}
but what you really want is to say something like "characters are things that have weapons, but with some restriction". I would suggest not having concrete types like "warrior" and "sword" at all, but rather those concrete types should be instances:
sealed class Weapon {
public string Name { get; set; }
}
sealed class Character {
const int sSkillToUseWeapon = 50;
readonly Dictionary<string, int> mWeaponSkills = new Dictionary<string, int>();
Weapon mCurrentWeapon;
public Weapon CurrentWeapon {
get {
return mCurrentWeapon;
} set {
if (!mWeaponSkills.ContainsKey(value.Name))
return;
if (mWeaponSkills[value.Name] < mSkillToUseWeapon)
return;
mCurrentWeapon = value;
}
}
public void SetWeaponSkill(string pWeaponName, int pSkill) {
if (mWeaponSkills.ContainsKey(pWeaponName))
mWeaponSkills[pWeaponName] = pSkill;
else
mWeaponSkill.Add(pWeaponName, pSkill);
}
}
Then you can declare instances of them to use and reuse:
var warrior = new Character();
warrior.SetWeaponSkill("Sword", 100);
warrior.SetWeaponSkill("Bow", 25);
var archer = new Character();
archer.SetWeaponSkill("Bow", 125);
archer.SetWeaponSkill("Sword", 25);
archer.SetWeaponSkill("Dagger", 55);
//etc...
A Warrior
is then an instance of Character with a particular set of weapon skills.
When you approach things this way, another issue you are bound to encouter is that there is a difference between, for example, "Rusty Sword" and "A Rusty Sword". In other words, even if "Rusty Sword" is an instance of "Weapon", there is still a difference between the general concept of "Rusty Sword", and an actual rusty sword that is lying on the ground. Same thing for "Goblin" or "Warrior".
The terms I use for these myself are "Stock____" and "Placed____", so there is a difference between a StockWeapon that defines a "class" of weapons, and a PlacedWeapon, which is an actual "instance" of a StockWeapon in the game world.
Placed objects then "have" instances of Stock objects, which define their common attributes (base damage, base durability, etc.), and have other attributes which define their current state independent of other similar items (location, durability damage, etc.)
(Here is a good site about using OOP patterns for game programming. (Oops I guess I forgot halfway through writing this that the game programming part was only an analogy. Nevertheless I did intend that hopefully the explanation would remain analogous.))
Upvotes: 1
Reputation: 660032
UPDATE: This question was the inspiration for a series of articles on my blog. Thanks for the interesting question!
is this some kind of classical OO problem, with a name and a well-documented solution?
The fundamental problem you're running into here is that it is difficult in an OO system to add a restriction to a subclass. Saying "a character can use a weapon" and then "a warrior is a kind of character that can only use a sword" is hard to model in an OO system.
If you're interested in the theory behind this, you'll want to look for the "Liskov Substitution Principle". (And also any discussion of why "Square" is not a subtype of "Rectangle" and vice versa.)
There are however ways to do it. Here's one.
interface ICharacter
{
public IWeapon Weapon { get; }
}
interface IWeapon { }
class Sword : IWeapon { }
class Warrior : ICharacter
{
IWeapon ICharacter.Weapon { get { return this.Weapon; } }
public Sword Weapon { get; private set; }
}
Now the public surface area of Warrior has a Weapon that is always a Sword, but anyone using the ICharacter
interface sees a Weapon
property that is of type IWeapon
.
The key here is to move away from the "is a special kind of" relationship and move towards a "can fulfill the contract of" relationship. Rather than saying "a warrior is a special kind of character", say "a warrior is a thing that can be used when a character is required".
All this said, you are embarking upon a whole world of fascinating problems where you'll discover just how bad at representing these sorts of things OO languages really are. You'll quickly find yourself deep in the double-dispatch problem and learning how to solve it with the Visitor pattern, once you start asking more complicated questions like "what happens when a warrior uses a sharp sword to attack an orc wearing leather armor?" and discover that you have four class hierarchies -- character, weapon, monster, armor -- all of which have subclasses that influence the outcome. Virtual methods only allow you to dispatch on one runtime type, but you'll find that you need two, three, four or more levels of runtime type dispatch.
It gets to be a mess. Consider not trying to capture too much in the type system.
Upvotes: 12
Reputation: 732
I think you are experiencing the lack of support in C# for Covariant return type. Note you can see the get
of a property as a special kind of method.
Upvotes: 0
Reputation: 7320
Just use generics type constraint ! Here's an example
public abstract class Weapon
{
public string Name { get; set; }
}
public interface ICharacter
{
Weapon GetWeapon();
}
public interface ICharacter<out TWeapon> : ICharacter
where TWeapon : Weapon
{
TWeapon Weapon { get; } // no set allowed here since TWeapon must be covariant
}
public abstract class Character<TWeapon> : ICharacter<TWeapon>
where TWeapon : Weapon
{
public TWeapon Weapon { get; set; }
public abstract void Fight();
public Weapon GetWeapon() { return this.Weapon; }
}
public class Sword : Weapon
{
public void DoSomethingWithSword()
{
}
}
public class Warrior : Character<Sword>
{
public override void Fight()
{
this.Weapon.DoSomethingWithSword();
}
}
Upvotes: 9
Reputation: 169
For the sake of documentation and future reference, I’m posting the solution that I’ll likely end up using:
abstract class Weapon {}
abstract class Character
{
public abstract Weapon GetWeapon();
}
class Sword : Weapon {}
class Warrior : Character
{
public Sword Sword { get; private set; }
public override Weapon GetWeapon()
{
return Sword;
}
}
As I see it, the only disadvantage is that objects of type Warrior have a Sword Sword
property and a Weapon GetWeapon()
method. In my case, this comes as a cosmetic problem only as both the property and the method return a reference to the same object when invoked.
Any comments are welcome!
Upvotes: 1