The question of when to use an interface versus an abstract class is a common topic in technical interviews and system design discussions. While many textbook definitions explain the differences between these two, I think it is easier to understand the concept by using a scenario plus sample code.
In this article, I’ll walk through two real-world scenarios with C# code examples to demonstrate when to choose an abstract class and when to prefer an interface.
Scenario 1. Abstract Class does work, Interface doesn’t
Consider we are creating an RPG game.
Where is the character?
- Has initial HP.
- Will sleep to restore HP and
- Will level up.
The player can choose an occupation to be either a Warrior or a Healer.
Where is a Warrior?
- Able to battle
- customized levels up for a warrior
And for a Healer
- able to self-healing
- customized level-up for a healer
We used abstract class for our RPG game character design, first we created an abstract-based class.
public abstract class Human
{
public int HP { get; set; }
public readonly int INITIAL_HEAL_POINT = 100;
public readonly int SLEEP_RESTORE_POINT = 20;
protected Human()
{
HP = INITIAL_HEAL_POINT;
}
public abstract void LevelUp();
public void Sleep()
{
HP += SLEEP_RESTORE_POINT;
Console.WriteLine($"Hero gets sleep and restores HP to: {HP}\n");
}
public virtual void SelfHealing(int healItem) { }
public virtual void Battle(int damage) { }
}
From the above base class, we can see.
- I used abstract for the LevelUp method, which enforces derived class to override the LevelUp method since level upgrading is unique by character’s occupation.
- For special skills like Battle and SelfHealing, I used virtual tools to give the derived class flexibility if they want to override it or just ignore it.
- The sleep method, since it is a shared method ( in our case, sleep just restores 20 HP regardless of the character occupation), does not need to be overridden; the method’s logic is complete and applies universally to all derived classes, so I just put void as a regular method, without abstract or virtual.
Here is the code of the Warrior class that inherited the Human base class.
public class Warrior : Human
{
public override void LevelUp()
{
Console.WriteLine("Some customized level upgrading mechanism for warrior type hero");
}
public override void Battle(int damage)
{
HP -= damage;
Console.WriteLine($"Hero takes {damage} HP from battle, and current HP is {HP}");
}
}
As you can see, in my Warrior class.
- I do not need to have a constructor to initialize my HP.
- I override the LevelUP method logic that is customized to Warrior level-up.
- I complete the logic of the Battle method for Warrior.
- I DO NOT need to do anything about the self-healing method.
And here is the code of the Healer class that inherited the Human base class.
public class Healer : Human
{
public override void LevelUp()
{
Console.WriteLine("Some customized level upgrading mechanism for healer type hero");
}
public override void SelfHealing(int healItem)
{
HP += healItem;
Console.WriteLine($"Hero perform self healing of {healItem} point, and current HP is {HP} ");
}
}
As you can see, in my Healer class.
- I do not need to have a constructor to initialize my HP.
- I override the LevelUP method logic that customizes to the Healer level-up.
- I complete the logic of the SelfHealing method for Healers.
- I DO NOT need to do anything about the Battle method.
The main program will look like this.
Warrior warrior1 = new Warrior();
Console.WriteLine($"Warrior1 intial HP: {warrior1.HP} ");
warrior1.Battle(30);
warrior1.Sleep();
Healer healer1 = new Healer();
Console.WriteLine($"Healer1 intial HP: {healer1.HP} ");
healer1.SelfHealing(30);
healer1.Sleep();
Output
Now, I will use Interface to do the exact same thing.
First, the Interface.
public interface IHuman
{
public int HP { get; set; }
void LevelUp();
void Sleep();
void Battle(int damage);
void SelfHealing(int healItem);
}
And then the WarriorOfInterface and HealerOfInterface class that implements IHuman.
// Code of Warrior
public class WarriorOfInterface : IHuman
{
public int HP { get; set; }
public readonly int INITIAL_HEAL_POINT = 100;
public readonly int SLEEP_RESTORE_POINT = 20;
public WarriorOfInterface()
{
HP = INITIAL_HEAL_POINT;
}
public void Battle(int damage)
{
HP -= damage;
Console.WriteLine($"Hero take {damage} HP from battle, and current HP is {HP}");
}
public void SelfHealing(int healItem)
{
// Do nothing since warrior does not have the ability to self-heal
}
public void Sleep()
{
HP += SLEEP_RESTORE_POINT;
Console.WriteLine($"Hero gets sleep and restores HP to: {HP}\n");
}
public void LevelUp()
{
Console.WriteLine("Some customized level upgrading mechanism for warrior type hero");
}
}
// Code of Healer
public class HealerOfInterface : IHuman
{
public int HP { get; set; }
public readonly int INITIAL_HEAL_POINT = 100;
public readonly int SLEEP_RESTORE_POINT = 20;
public HealerOfInterface()
{
HP = INITIAL_HEAL_POINT;
}
public void Battle(int damage)
{
// Do nothing since healer does not have to battle
}
public void SelfHealing(int healItem)
{
HP += healItem;
Console.WriteLine($"Hero performs self-healing of {healItem} points, and current HP is {HP}");
}
public void Sleep()
{
HP += SLEEP_RESTORE_POINT;
Console.WriteLine($"Hero gets sleep and restores HP to: {HP}\n");
}
public void LevelUp()
{
Console.WriteLine("Some customized level upgrading mechanism for healer type hero");
}
}
And the main program.
//Main program
WarriorOfInterface warrior2 = new WarriorOfInterface();
Console.WriteLine($"Warrior2 intial HP: {warrior2.HP} ");
warrior2.Battle(30);
warrior2.Sleep();
HealerOfInterface healer2 = new HealerOfInterface();
Console.WriteLine($"Healer2 intial HP: {healer2.HP} ");
healer2.SelfHealing(30);
healer2.Sleep();
The code runs perfectly, and the output is exactly the same as the example that uses an abstract class. Why do I endorse abstract class in this scenario?
For a few reasons.
- Code duplication: each occupation class has to reimplement the Sleep() logic and HP initialization in the constructor, which leads to code duplication.
- Opportunities for bug: each occupation has to implement a method that is not related to themselves, like the Warrior class needs to implement (although it is a dummy) the SelfHealing method. Imagine we have like ten occupations and each occupation has a few of its own special skillset; this creates opportunities for bugs due to inconsistent behavior.
- Inconsistent state management: HP is a state of this example. Imagine a method like Battle in Healer class that does nothing might affect the HP calculation bug in the future, should our code get complex.
- Maintenance Overhead: If in the future we need to add new common methods like Eating, Transportation, Respawn, etc, in Interface, we have to implement it in each class, but with an abstract class, we just need to implement this common logic in the base class, therefore prevent code duplication.
Although some might argue that I can change the interface to 3 interfaces by introducing IBattle and IHeal.
public interface IHuman_v2
{
int HP { get; set; }
void LevelUp();
void Sleep();
}
public interface IBattle
{
void Battle(int damage);
}
public interface IHeal
{
void SelfHealing(int healItem);
}
Where it only implemented by related classes, therefore it can reduce code duplication.
public class WarriorOfInterface_v2 : IHuman_v2, IBattle
{
public void Battle(int damage)
{
HP -= damage;
Console.WriteLine($"Hero took {damage} damage from battle, and current HP is {HP}");
}
// only Battle method, no more Self Healing method
// other codes
}
public class HealerOfInterface_v2 : IHuman_v2, IHeal
{
public void SelfHealing(int healItem)
{
HP += healItem;
Console.WriteLine($"Hero performed self healing of {healItem} points, and current HP is {HP}");
}
// only Self Healing method, no more Battle method
// other codes
}
This design has indeed solved certain code duplication problems, but the code duplication of common logic, like Sleep, is still duplicated in each class. The state management will still be an issue in this case if code design is using the interface. The Abstract classes allow the use of protected members, enabling shared logic while giving derived classes flexibility to modify or extend that behavior without exposing it externally—something interfaces can't do.
In my opinion, in this scenario, Abstract Class is a better choice than Interface.
Scenario 2. The interface does work, but Abstract Class doesn’t
In most cases, Interface design favors the Abstract class, which is about multiple inheritance.
Let's consider that we are building a drone; a drone can either fly, swim, or both. So, we create two interfaces.
interface IFlyable
{
void Fly();//fly feature
}
interface ISwimmable
{
void Swim();//swim feature
}
We have the FlyingDrone class that can fly. Therefore, it implements IFlyable, the same for SwimingDrone, which implements ISwimmable.
class FlyingDrone: IFlyable
{
public void Fly() => Console.WriteLine("Drone is flying");
}
class SwimingDrone : ISwimmable
{
public void Swim() => Console.WriteLine("Drone is swimming");
}
So, the main program.
FlyingDrone flyD = new FlyingDrone();
flyD.Fly();
SwimingDrone swmD = new SwimingDrone();
swmD.Swim();
//output:
//Drone is flying
//Drone is swimming
Suppose now we have a new generation of multi-purpose drones that can fly and swim at the same time, so we can apply the benefit of the interface that supports multiple inheritance, something like this.
class MultipurposeDrone : IFlyable, ISwimmable
{
public void Fly()
{
Console.WriteLine("Multi purpose Drone is flying");
}
public void Swim()
{
Console.WriteLine("Multi purpose Drone is swimming");
}
}
// Main program
MultipurposeDrone multiD = new MultipurposeDrone();
multiD.Fly();
multiD.Swim();
// Output
// Multi purpose Drone is flying
// Multi purpose Drone is swimming
As we can see, using multiple inheritances, I can control a different class of drone FlyingDrone, SwimingDrone, MultipurposeDrone or that has its own specific features.
The abstract class does not support multiple inheritance; however, i can write something like this, and the output is still exactly the same.
public abstract class AbstractDrone
{
public virtual void Fly() { }
public virtual void Swim() { }
}
public class AbstractFlyingDrone : AbstractDrone
{
public override void Fly()
{
Console.WriteLine("Drone is flying");
}
}
public class AbstractSwimmingDrone : AbstractDrone
{
public override void Swim()
{
Console.WriteLine("Drone is swimming");
}
}
public class AbstractMultiPurposeDrone : AbstractDrone
{
public override void Fly()
{
Console.WriteLine("Multi-purpose Drone is flying");
}
public override void Swim()
{
Console.WriteLine("Multi-purpose Drone is swimming");
}
}
// Main program
AbstractFlyingDrone abstractflyingdrone = new AbstractFlyingDrone();
abstractflyingdrone.Fly();
AbstractSwimmingDrone abstractswimmingdrone = new AbstractSwimmingDrone();
abstractswimmingdrone.Swim();
AbstractMultiPurposeDrone abstractmultipurposedrone = new AbstractMultiPurposeDrone();
abstractmultipurposedrone.Fly();
abstractmultipurposedrone.Swim();
Why choose interface in this scenario?
First is the separation of features. Although the code is working and the output is the same by using abstract class, the following line if I use abstract class also works.
abstractflyingdrone.Swim();//this will work
abstractswimmingdrone.Fly();//this will also work
This is not what we want since we might expose other features that we do not wish to expose, like enabling the swimming feature in a flying drone or enabling that flying feature in a swimming drone.
Another reason is that we can only inherit from one base using an abstract class. Let's say after 1 year of production. We need to create a new version of the drone that not only can fly and swim but also can speak!
If using the interface, I can easily create another Interface to serve our goal.
interface ISpeakable
{
void Speak();//speaking feature
}
And implement it to our new drone class, the code are neat and simple.
class MultiPurposeDrone : IFlyable, ISwimmable, ISpeakable
{
public void Fly() => Console.WriteLine("Multi purpose Drone is flying");
public void Swim() => Console.WriteLine("Multi purpose Drone is swimming");
public void Speak() => Console.WriteLine("Hello world I can speak now!");
}
I cannot do this in the abstract class since it does not support multiple inheritance.
public class AbstractMultiPurposeDrone : AbstractDrone, ISpeakable
{//invalid, abstract class do not support multiple inheritance
}
However, for the sake of argument, I can do something like this, which technically can work.
public abstract class AbstractDrone
{
public virtual void Fly() { }
public virtual void Swim() { }
public virtual void Speak() { } //new line
}
// new class
public class NewAbstractMultiPurposeDroneThatCanSpeak : AbstractMultiPurposeDrone
{
// fly method logic
// swim method logic
// speak method logic
}
This is a very bad practice although it can work as desired, this is a violation of the Interface Segregation Principle (from the ‘I’ of the SOLID principle). It makes clients depend on methods they do not use. In this case, the Fly() and Swim() methods might not be relevant for all drones, forcing all drones to implement them.
We also increase the inheritance hierarchy and code complexity, which leads to more bugs and maintenance in the future.
C# 8 new feature in Interface
The C# 8 has a new feature in Interface, which now also supports default implementation.
Go back to our IFlyable interface, I can have a new method CarryMissle with default implementation.
interface IFlyable
{
void Fly();//fly feature
void CarryMissle() => Console.WriteLine($"Drone is firing a missle!");
}
However, it is not compulsory to implement the derived class, like my FlyingDrone class, which inherits IFlyable. It can work well without implementing the CarryMisslemethod.
class FlyingDrone: IFlyable
{
public void Fly() => Console.WriteLine("Drone is flying");
//Doesn't need implement CarryMissle()
//since this version drone is civilian use
}
Or I can implement it if necessary.
class MilitaryDrone : IFlyable
{
public void Fly() => Console.WriteLine("Military drone is flying");
public void CarryMissle() => Console.WriteLine($"Military drone is firing air to ground missle");
}
Conclusion
I hope this article gives you some hindsight about interfaces and abstract classes, helping you make more informed decisions when designing your system architecture. In real life, where things can get very complex, interfaces and abstract classes are often used together in real-world projects to take advantage of their distinct strengths, especially in complex, scalable systems.
The sample source code of this article can be found on my Github.