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!
How to Prevent Inheritance. In C# you can use the sealed keyword in order to prevent a class from being inherited. Also in VB.NET you can use the NotInheritable keyword to prevent accidental inheritance of the class.
Inheritance allows us to define a class that inherits all the methods and properties from another class. Parent class is the class being inherited from, also called base class. Child class is the class that inherits from another class, also called derived class.
Inheritance is a feature or a process in which, new classes are created from the existing classes. The new class created is called “derived class” or “child class” and the existing class is known as the “base class” or “parent class”. The derived class now is said to be inherited from the base class.
To get the higher design flexibility, the design principle says that composition should be favored over inheritance. Inheritance should only be used when subclass 'is a' superclass. Don't use inheritance to get code reuse. If there is no 'is a' relationship, then use composition for code reuse.
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.
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With