Reputation: 5615
I'm stuck in this situation where:
Ammo
, with AmmoBox
and Clip
as children.Weapon
, with Firearm
and Melee
as children.Firearm
is abstract, with ClipWeapon
and ShellWeapon
as children.Firearm
, there's a void Reload(Ammo ammo);
The problem is that, a ClipWeapon
could use both a Clip
and an AmmoBox
to reload:
public override void Reload(Ammo ammo)
{
if (ammo is Clip)
{
SwapClips(ammo as Clip);
}
else if (ammo is AmmoBox)
{
var ammoBox = ammo as AmmoBox;
// AddBullets returns how many bullets has left from its parameter
ammoBox.Set(clip.AddBullets(ammoBox.nBullets));
}
}
But a ShellWeapon
, could only use an AmmoBox
to reload. I could do this:
public override void Reload(Ammo ammo)
{
if (ammo is AmmoBox)
{
// reload...
}
}
But this is bad because, even though I'm checking to make sure it's of type AmmoBox
, from the outside, it appears like a ShellWeapon
could take a Clip
as well, since a Clip
is Ammo
as well.
Or, I could remove Reload
from Firearm
, and put it both ClipWeapon
and ShellWeapon
with the specific params I need, but doing so I will lose the benefits of Polymorphism, which is not what I want to.
Wouldn't it be optimal, if I could override Reload
inside ShellWeapon
like this:
public override void Reload(AmmoBox ammoBox)
{
// reload ...
}
Of course I tried it, and it didn't work, I got an error saying the signature must match or something, but shouldn't this be valid 'logically'? since AmmoBox
is a Ammo
?
How should I get around this? And in general, is my design correct?
(Note I was using interfaces IClipWeapon
and IShellWeapon
but I ran into trouble, so I moved to using classes instead)
Thanks in advance.
Upvotes: 15
Views: 12505
Reputation: 726599
The problem with which you are wrestling comes from the need to call a different implementation based on the run-time types of both the ammo and the weapon. Essentially, the action of reloading needs to be "virtual" with respect to two, not one, object. This problem is called double dispatch.
One way to address it would be creating a visitor-like construct:
abstract class Ammo {
public virtual void AddToShellWeapon(ShellWeapon weapon) {
throw new ApplicationException("Ammo cannot be added to shell weapon.");
}
public virtual void AddToClipWeapon(ClipWeapon weapon) {
throw new ApplicationException("Ammo cannot be added to clip weapon.");
}
}
class AmmoBox : Ammo {
public override void AddToShellWeapon(ShellWeapon weapon) {
...
}
public override void AddToClipWeapon(ClipWeapon weapon) {
...
}
}
class Clip : Ammo {
public override void AddToClipWeapon(ClipWeapon weapon) {
...
}
}
abstract class Weapon {
public abstract void Reload(Ammo ammo);
}
class ShellWeapon : Weapon {
public void Reload(Ammo ammo) {
ammo.AddToShellWeapon(this);
}
}
class ClipWeapon : Weapon {
public void Reload(Ammo ammo) {
ammo.AddToClipWeapon(this);
}
}
"The magic" happens in the implementations of Reload
of the weapon subclasses: rather than deciding what kind of ammo they get, they let the ammo itself do "the second leg" of double dispatch, and call whatever method is appropriate, because their AddTo...Weapon
methods know both their own type, and the type of the weapon into which they are being reloaded.
Upvotes: 3
Reputation: 1500665
but shouldn't this be valid 'logically'?
No. Your interface says that the caller can pass in any Ammo
- where you're restricting it to require an AmmoBox
, which is more specific.
What would you expect to happen if someone were to write:
Firearm firearm = new ShellWeapon();
firearm.Reload(new Ammo());
? That should be entirely valid code - so do you want it to blow up at execution time? Half the point of static typing is to avoid that sort of problem.
You could make Firearm
generic in the type of ammo is uses:
public abstract class Firearm<TAmmo> : Weapon where TAmmo : Ammo
{
public abstract void Reload(TAmmo ammo);
}
Then:
public class ShellWeapon : Firearm<AmmoBox>
That may or may not be a useful way of doing things, but it's at least worth considering.
Upvotes: 19
Reputation: 4500
You can use composition with interface extensions instead of multiple-inheritance:
class Ammo {}
class Clip : Ammo {}
class AmmoBox : Ammo {}
class Firearm {}
interface IClipReloadable {}
interface IAmmoBoxReloadable {}
class ClipWeapon : Firearm, IClipReloadable, IAmmoBoxReloadable {}
class AmmoBoxWeapon : Firearm, IAmmoBoxReloadable {}
static class IClipReloadExtension {
public static void Reload(this IClipReloadable firearm, Clip ammo) {}
}
static class IAmmoBoxReloadExtension {
public static void Reload(this IAmmoBoxReloadable firearm, AmmoBox ammo) {}
}
So that you will have 2 definitions of Reload() method with Clip and AmmoBox as arguments in ClipWeapon and only 1 Reload() method in AmmoBoxWeapon class with AmmoBox argument.
var ammoBox = new AmmoBox();
var clip = new Clip();
var clipWeapon = new ClipWeapon();
clipWeapon.Reload(ammoBox);
clipWeapon.Reload(clip);
var ammoBoxWeapon = new AmmoBoxWeapon();
ammoBoxWeapon.Reload(ammoBox);
And if you try pass Clip to AmmoBoxWeapon.Reload you will get an error:
ammoBoxWeapon.Reload(clip); // <- ERROR at compile time
Upvotes: 3
Reputation: 25927
I think, that it's perfectly fine to check, whether passed Ammo
is of valid type. The similar situation is, when function accepts a Stream
, but internally checks, whether it is seekable or writeable - depending on its requirements.
Upvotes: 1