Abstract Class vs Interface
Abstract classes and interfaces both say “subclasses must provide X”. The difference: an abstract class is a shared parent; an interface is a capability any class can add.
On this page · 6 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
In simple words#
An abstract class is a family business you join (one parent, shared assets). An interface is a skill certificate (you can hold many). Choose by asking: “is-a” family, or “can-do” skill?
- Abstract class
- One base, can share code and state.
- Interface
- Many allowed, contract only (mostly).
- Rule of thumb
- Interface for capability, abstract class for shared base.
Learning objectives#
- List the key differences
- Use the is-a vs can-do rule to choose
- Combine both in one design
- Answer the interview question with an example
Explanation#
Abstract class: can have fields, constructors and implemented methods. A class can extend only ONE. Use it for a family of closely related types that share code — “is-a”.
Interface: a contract of members, with no state. A class can implement MANY. Use it for capabilities that unrelated types can share — “can-do”.
Rule of thumb: Car is a Vehicle (abstract class). Car can be insured, a House can be insured (interface IInsurable).
They work together: class Car : Vehicle, IInsurable. Vehicle provides shared driving logic; IInsurable adds a capability.
Since C# 8, interfaces can include default method bodies, which blurs the line a little — but they still cannot hold instance fields, and the is-a / can-do rule still guides the choice.
Syntax#
public abstract class Vehicle(string regNo) // is-a: shared state + code
{
public string RegNo => regNo;
public int Km { get; private set; }
public void Drive(int km) => Km += km;
public abstract int ServiceInterval { get; }
public bool ServiceDue => Km >= ServiceInterval;
}
public interface IInsurable // can-do: a capability
{
decimal InsuranceValue();
}
public class Car(string regNo) : Vehicle(regNo), IInsurable
{
public override int ServiceInterval => 10000;
public decimal InsuranceValue() => 800000m;
}
public class House : IInsurable // unrelated, same capability
{
public decimal InsuranceValue() => 5000000m;
}
Code example: Vehicles share code; insurable things share a capability#
// Abstract class: shared state and code for a family of related types
class Vehicle {
constructor(regNo) {
if (new.target === Vehicle) throw new Error('Vehicle is abstract');
this.regNo = regNo;
this.km = 0;
}
drive(km) { this.km += km; return this; }
serviceDue() { return this.km >= this.serviceInterval(); }
serviceInterval() { throw new Error('abstract'); }
}
class Car extends Vehicle {
serviceInterval() { return 10000; }
insuranceValue() { return 800000; } // also "implements" Insurable
}
class Truck extends Vehicle {
serviceInterval() { return 5000; }
}
// Interface: a capability unrelated types can share
class House {
insuranceValue() { return 5000000; }
}
const isInsurable = obj => typeof obj.insuranceValue === 'function';
const car = new Car('MH12AB1234').drive(6000);
const truck = new Truck('KA01XY9999').drive(6000);
console.log(car.serviceDue(), truck.serviceDue());
const things = [car, truck, new House()];
const insurable = things.filter(isInsurable);
console.log(insurable.length, insurable.reduce((sum, t) => sum + t.insuranceValue(), 0));
false true 2 5800000
Step-by-step explanation#
- Vehicle is the abstract parent: Car and Truck share km, drive() and serviceDue().
- Each vehicle fills in its own serviceInterval().
- insuranceValue() is a capability: Car has it, Truck does not, and House — not a vehicle at all — has it too.
- The insurance code filters by capability, not by class, so it works across unrelated types.
Real-world use case#
In .NET, Stream is an abstract class (FileStream, MemoryStream share buffering code), while IDisposable is an interface implemented by streams, database connections and HTTP clients alike.
Common mistakes#
If there is no shared code, you are spending your one base-class slot for nothing. Use an interface.
If five classes repeat the same logic, that logic belongs in a shared base class (or a helper they use).
Give a concrete example: Vehicle (abstract) vs IInsurable (interface).
Best practices#
- Default to interfaces for dependencies and capabilities
- Use abstract classes for related types with shared code
- Combine both: one base class, several interfaces
- Keep both small
Remember this#
Abstract class = IS-A + shared code. Only one.
Interface = CAN-DO capability. As many as you like.
Car : Vehicle, IInsurable — use both together.
Practice exercise#
Your task: Model a Bird (abstract class with name and a shared describe()) and a Flyer capability (fly()). Create Sparrow (bird + flyer), Penguin (bird only) and Drone (flyer only, not a bird). Log which of the three can fly.
Need a hint?
Show solution
class Bird {
constructor(name) { this.name = name; }
describe() { return `${this.name} is a bird`; }
}
class Sparrow extends Bird { fly() { return 'flap flap'; } }
class Penguin extends Bird {}
class Drone { constructor() { this.name = 'Drone'; } fly() { return 'whirr'; } }
const all = [new Sparrow('Sparrow'), new Penguin('Penguin'), new Drone()];
console.log(all.filter(x => typeof x.fly === 'function').map(x => x.name));
// [ 'Sparrow', 'Drone' ]
Mini quiz#
Which can hold instance fields (state)?
Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions
My notes#
Saved in this browser.