Search code examples
c#interfacetype-testing

How to restrict an object to one of its interfaces for type testing?


I have four interfaces:

public interface IBasicBoat
{
    Hull BoatHull { get; set; }
}

public interface ISailBoat : IBasicBoat
{
    List<Sail> Sails { get; set; }
}

public interface IMotorBoat : IBasicBoat
{
    Engine Motor { get; set; }
}

public interface IGenericBoat : ISailBoat, IMotorBoat
{
}

and one class:

public class GenericBoat : IGenericBoat
{
    public Hull BoatHull { get; set; }

    public List<Sail> Sails { get; set; }

    public Engine Motor { get; set; }
}

I want my algorithm to work differently on ISailBoats and IMotorBoats.

Say I have these two objects:

    ISailBoat z_objSailBoat = new GenericBoat()
    {
        BoatHull = new Hull(),
        Sails = new List<Sail>()
    };

    IMotorBoat z_objMotorBoat = new GenericBoat()
    {
        BoatHull = new Hull(), 
        Motor = new Engine()
    };

and this method:

    public void CheckBuoyancy(IBasicBoat p_objBoat)
    {
        // [...]
    }

that is called twice: CheckBuoyancy(z_objSailBoat) and CheckBuoyancy(z_objMotorBoat).

Inside CheckBuoyancy(), I want to be able to tell whether p_objBoat is an ISailBoat so I can check the sails.

I've tried

    if (p_objBoat is ISailBoat z_objSailBoatToCheck)
    {
        // do something with z_objSailBoatToCheck.Sails
    }

but both z_objSailBoat and z_objMotorBoat pass that test.

  1. Why does z_objMotorBoat is ISailBoat return true? (1a. Is it because z_objMotorBoat is created as a GenericBoat, or is it more complicated?)
  2. How can I check whether p_objBoat is an ISailBoat?

The obvious solution to 2. would be to create more classes like SailBoat and MotorBoat so I'd have two different constructors, but I'd like to avoid that. (If only because my actual case is noticeably more complicated.)


Solution

    1. Why does z_objMotorBoat is ISailBoat return true? (1a. Is it because z_objMotorBoat is created as a GenericBoat, or is it more complicated?)

    The reason is that GenericBoat implements both interfaces and runtime will always evaluate it that way.

    1. How can I check whether p_objBoat is an ISailBoat?

    You might check that by revisiting the code.

    Interfaces:

    public interface IBoat
    {
        Hull BoatHull { get; set; }
        Buoyancy CheckBuoyancy();
    }
    
    public interface ISailBoat : IBoat
    {
        List<Sail> Sails { get; set; }
    }
    
    public interface IMotorBoat : IBoat
    {
        Engine Motor { get; set; }
    }
    

    Classes:

    public abstract class Boat : IBoat
    {
        Hull BoatHull { get; set; }
    
        public virtual Buoyancy CheckBuoyancy() {
            // Calculate buoyancy if both boat types use same algorithm.
            // If not you might want to change virtual modifier to abstract to force each concrete type to implement its own calculation.
        }
    }
    
    public class MotorBoat : Boat, IMotorBoat
    {
        Engine Motor { get; set; }
    
        public override Buoyancy CheckBuoyancy() {
            // In case you need to use different way to calculate buoyancy
        }
    }
    
    public class SailBoat : Boat, ISailBoat
    {
        List<Sail> Sails { get; set; }
    
        public override Buoyancy CheckBuoyancy() {
            // In case you need to use different way to calculate buoyancy
        }
    }
    

    Usage:

    IBoat boat1 = new SailBoat()
    {
       BoatHull = new Hull(),
       Sails = new List<Sail>()
    };
    
    IBoat boat2 = new MotorBoat()
    {
        BoatHull = new Hull(), 
        Motor = new Engine()
    };
    
    var boatBuoyancy1 = boat1.CheckBuoyancy();
    var boatBuoyancy2 = boat2.CheckBuoyancy();
    
    if(boat1 is ISailBoat sailBoat) {
       var sailsCount = sailBoat.Sails.Count();
    }
    
    if(boat2 is IMotorBoat motorBoat) {
       var horsePower = motorBoat.Motor.HorsePower;
    }