TL;DR
- Prefer composition over inheritance for flexible, maintainable C# code.
- Use interfaces and dependency injection to compose behaviors instead of deep class hierarchies.
- Composition makes code easier to test, extend, and adapt to changing requirements.
- Use inheritance only for clear “is-a” relationships with shallow, stable hierarchies.
- Refactor rigid inheritance trees by extracting behaviors into separate classes or interfaces.
Think about building a car. You could create a CarWithElectricEngine
class, then inherit from it to make SportyCarWithElectricEngine
, then inherit again for LuxurySportyCarWithElectricEngine
. But what happens when you need a hybrid engine? Or a diesel? You end up with an inheritance nightmare.
This is exactly what happens in our C# code when we rely too heavily on inheritance. Let’s break down why composition often beats inheritance and how to write more flexible code.
The Inheritance Trap We’ve All Fallen Into
You’ve probably seen this pattern before:
public abstract class Employee
{
public string Name { get; set; }
public abstract decimal CalculatePay();
}
public class SalariedEmployee : Employee
{
public decimal Salary { get; set; }
public override decimal CalculatePay() => Salary / 12;
}
public class HourlyEmployee : Employee
{
public decimal HourlyRate { get; set; }
public int HoursWorked { get; set; }
public override decimal CalculatePay() => HourlyRate * HoursWorked;
}
This looks clean at first, but here’s where it gets messy. What if you need a contractor who gets paid hourly but also receives bonuses? Or a salaried employee with overtime? You either end up with multiple inheritance (which C# doesn’t support) or a deep, rigid hierarchy that’s painful to change.
Composition: Building with LEGO Blocks
Instead of saying “a salaried employee is a employee,” composition says “an employee has a pay calculator.” Here’s the same example using composition:
// Define behavior contracts
public interface IPayCalculator
{
decimal CalculatePay();
}
// Implement specific behaviors
public class SalaryCalculator : IPayCalculator
{
private readonly decimal _monthlySalary;
public SalaryCalculator(decimal monthlySalary) => _monthlySalary = monthlySalary;
public decimal CalculatePay() => _monthlySalary;
}
public class HourlyCalculator : IPayCalculator
{
private readonly decimal _hourlyRate;
private readonly int _hoursWorked;
public HourlyCalculator(decimal hourlyRate, int hoursWorked)
{
_hourlyRate = hourlyRate;
_hoursWorked = hoursWorked;
}
public decimal CalculatePay() => _hourlyRate * _hoursWorked;
}
// Compose the employee with the behavior it needs
public class Employee
{
public string Name { get; set; }
private readonly IPayCalculator _payCalculator;
public Employee(string name, IPayCalculator payCalculator)
{
Name = name;
_payCalculator = payCalculator;
}
public decimal CalculatePay() => _payCalculator.CalculatePay();
}
Now creating employees is straightforward:
var salariedEmployee = new Employee("John", new SalaryCalculator(5000));
var hourlyEmployee = new Employee("Jane", new HourlyCalculator(25, 160));
Why This Makes Your Life Easier
Testing becomes simple. You can mock IPayCalculator
and test employee logic independently. No need to set up complex inheritance hierarchies in your tests.
Adding new pay types is trivial. Need commission-based pay? Create CommissionCalculator
without touching existing code.
Runtime flexibility. You can even swap pay calculators at runtime if business rules change.
No coupling headaches. Your Employee
class doesn’t care how pay is calculated, it just knows something will handle it.
When to Choose What
Use Composition When… | Use Inheritance When… |
---|---|
You need runtime flexibility | You have a true “is-a” relationship |
Behaviors can be mixed and matched | The hierarchy is shallow and stable |
You’re writing unit tests | You’re modeling domain concepts that naturally inherit |
Requirements change frequently |
The key insight? Favor composition over inheritance by default. Your future self will thank you when requirements change and you can adapt without rewriting half your codebase.
Start small, next time you reach for inheritance, ask yourself: “Could I inject this behavior instead?” Your code will be more testable, flexible, and easier to change.
Frequently Asked Questions
What is “composition over inheritance” in C#?
Why is composition often preferred over inheritance?
When should you use inheritance instead of composition?
Bird
inheriting from Animal
. For most other cases, especially when flexibility is needed, prefer composition.How does composition improve testability in C#?
Can you give a simple example of composition in C#?
Yes. Instead of subclassing, you can inject a behavior using an interface-
public interface ILogger { void Log(string message); }
public class FileLogger : ILogger { public void Log(string message) { /*...*/ } }
public class Service { private readonly ILogger _logger; public Service(ILogger logger) { _logger = logger; } }
Here, Service
can use any ILogger
implementation, demonstrating composition.
What are the drawbacks of deep inheritance hierarchies?
How does composition support the SOLID principles?
How can you refactor an inheritance-based design to use composition?
What is the difference between aggregation and composition in OOP?
How does composition help with runtime flexibility?
See other oops posts
- How Does Composition Support the SOLID Principles? (C# Examples & Best Practices)
- DIP vs DI vs IoC: Understanding Key Software Design Concepts
- Cohesion vs Coupling in Object-Oriented Programming: A Complete Guide
- Encapsulation and Information Hiding in C#: Best Practices and Real-World Examples
- Object-Oriented Programming: Core Principles and C# Implementation
- How Polymorphism Makes C# Code Flexible: Real-World Examples and Best Practices
- SOLID Principles in C#: A Practical Guide with Real‑World Examples