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 flexibilityYou have a true “is-a” relationship
Behaviors can be mixed and matchedThe hierarchy is shallow and stable
You’re writing unit testsYou’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#?

‘“Composition over inheritance” is an object-oriented design principle where you build classes by combining simple, reusable components (composition) instead of relying on deep inheritance hierarchies. This approach leads to more flexible, maintainable, and testable code. In C#, you typically achieve this by injecting dependencies or using interfaces and delegation.’

Why is composition often preferred over inheritance?

Composition is preferred because it avoids the rigidity and complexity of deep inheritance trees. It allows behaviors to be mixed and matched, supports better code reuse, and makes it easier to adapt to changing requirements. Composition also reduces coupling and makes unit testing simpler.

When should you use inheritance instead of composition?

Use inheritance when there is a clear “is-a” relationship and the hierarchy is shallow and stable. Inheritance is suitable for modeling domain concepts that naturally extend each other, such as Bird inheriting from Animal. For most other cases, especially when flexibility is needed, prefer composition.

How does composition improve testability in C#?

Composition improves testability by allowing you to inject mock or stub implementations of dependencies. For example, you can pass a mock service or strategy to a class via its constructor, making it easy to isolate and test the class’s behavior without relying on complex inheritance setups.

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?

Deep inheritance hierarchies make code harder to understand, maintain, and extend. They can lead to fragile base class problems, tight coupling, and difficulties when requirements change. Refactoring or adding new features often requires changes across multiple classes.

How does composition support the SOLID principles?

Composition supports SOLID principles, especially the Single Responsibility and Open/Closed principles. By composing behaviors, each class has a focused responsibility and can be extended with new functionality without modifying existing code. This leads to more robust and adaptable designs.

How can you refactor an inheritance-based design to use composition?

Identify behaviors that vary across subclasses and extract them into interfaces or separate classes. Inject these behaviors into your main class via constructor or property injection. Replace inheritance with delegation, so the main class delegates work to the composed objects.

What is the difference between aggregation and composition in OOP?

Both are forms of association, but composition implies a strong ownership where the child cannot exist without the parent, while aggregation is a weaker relationship where the child can exist independently. In C#, composition is often implemented by having one class instantiate and manage the lifecycle of another.

How does composition help with runtime flexibility?

Composition allows you to change or swap behaviors at runtime by injecting different implementations. For example, you can switch logging strategies or payment processors without changing the main class’s code, making your application more adaptable to new requirements.
See other oops posts