Search code examples
c#classoopinterfaceabstract-class

Method which uses properties from both base class and interface it could potentially implement


I am trying to make a basic "Spell system" for a game, and trying to come up with a good way to implement "spell behaviour" in a modular way. I have a slight problem where I want certain methods to only be usable by spells of certain "types". Code example:

public abstract class Spell 
{
    public abstract void DoStuff();

    public void DoProjectileMovement() 
    {
        //Handle moving spell
        //Want to only allow calling from spells which implement "Projectile" interface
    }
}

public interface Projectile 
{
    int projNum { get; set; }
    float projSpeed { get; set; }
}

public class Fireball : Spell, Projectile 
{
    public int projNum { get; set; }
    public float projSpeed { get; set; }

    public override void DoStuff()
    {
        DoProjectileMovement();
    }
}

As it is now, the DoProjectileMovement() method can't use properties from the Projectile interface, which I want it to be able to access. The obvious option seems to just pass it as a parameter:

public void DoProjectileMovement(Projectile proj) 
{
    //Handle moving spell
    //Now it can access projectile properties through proj
}

But then when calling this method in Fireball, I have to do like this:

public override void DoStuff()
{
    DoProjectileMovement(this);
}

Having the extra this feels superfluous. I know that the Projectile interface will never be implemented by any other base class than Spell. And if it is implemented by Spell, I want to be able to call DoProjectileMovement(). An alternative is to have the DoProjectileMovement method have a default implementation in the Projectile interface, and somehow specifying that Projectile is guaranteed to also be a Spell. Is this possible?

Also, the approach of having a separate abstract class "ProjectileSpell" doesn't work, as I want to be able to use multiple different interfaces at a time. (For example, Fireball might also be "Exploding", and then I can't inherit from the two abstract classes "ProjectileSpell" and "ExplodingSpell" at the same time).


Solution

  • A few possibilities come to mind.

    Option 1: Use an extension method

    public abstract class Spell
    {
        public abstract void DoStuff();
    }
    
    public static class ProjectileExtensions
    {
        public static void DoProjectileMovement<TSpell>(this TSpell projectile)
            where TSpell : Spell, Projectile
        {
            // Handle moving spell
            // (You can access members of Spell and Projectile here)
        }
    }
    
    public class Fireball : Spell, Projectile
    {
        public int projNum { get; set; }
        public float projSpeed { get; set; }
    
        public override void DoStuff()
        {
            this.DoProjectileMovement(); // 'this' qualifier is required
        }
    }
    

    You can even write extension methods that require a combination of interfaces to be implemented.

    public static class ProjectileExtensions
    {
        public static void DoExplodingProjectileMovement<TSpell>(this TSpell projectile)
            where TSpell : Spell, Exploding, Projectile
        {
            // Handle explosive moving spell
            // (You can access members of Spell, Exploding, and Projectile here)
        }
    }
    

    Option 2: Use a default interface implementation (.NET Core only)

    This option requires that you introduce an ISpell interface implemented by both Spell and Projectile.

    public interface ISpell
    {
        void DoStuff();
    }
    
    public abstract class Spell : ISpell
    {
        public abstract void DoStuff();
    }
    
    public interface Projectile : ISpell
    {
        int projNum { get; set; }
        float projSpeed { get; set; }
    
        void DoProjectileMovement()
        {
            // Handle moving spell
            // (You can access members of ISpell and Projectile here)
        }
    }
    
    public class Fireball : Spell, Projectile
    {
        public int projNum { get; set; }
        public float projSpeed { get; set; }
    
        public override void DoStuff()
        {
            ((Projectile)this).DoProjectileMovement(); // cast is required
        }
    }
    

    Option 3: Refactor to use composition instead of inheritance

    Change Projectile from an interface to a class.

    public class Projectile
    {
        private Spell Spell { get; }
    
        public Projectile(Spell spell)
        {
            Spell = spell;
        }
    
        public int projNum { get; set; }
        public float projSpeed { get; set; }
    
        public void DoProjectileMovement()
        {
            // Handle moving spell
            // (You can access members of Spell (via the Spell property) and Projectile here)
        }
    }
    
    public class Fireball : Spell
    {
        private Projectile Projectile { get; }
    
        public Fireball()
        {
            Projectile = new Projectile(this);
        }
    
        public int projNum { get; set; }
        public float projSpeed { get; set; }
    
        public override void DoStuff()
        {
            Projectile.DoProjectileMovement();
        }
    }