🏗️

Inheritance Vs Composition

Object-Oriented Programming Intermediate 3 min read 400 words

Inheritance vs Composition in C#

Introduction

“Favor composition over inheritance” is one of the most important design principles in object-oriented programming. This guide explores when to use each approach, with practical C# examples.


Table of Contents


Understanding the Difference

Inheritance: “Is-A” Relationship

// Dog IS-A Animal
public class Animal
{
    public string Name { get; set; }
    public void Breathe() => Console.WriteLine("Breathing...");
}

public class Dog : Animal
{
    public void Bark() => Console.WriteLine("Woof!");
}

Composition: “Has-A” Relationship

// Car HAS-A Engine
public class Engine
{
    public void Start() => Console.WriteLine("Engine starting...");
    public void Stop() => Console.WriteLine("Engine stopping...");
}

public class Car
{
    private readonly Engine _engine;

    public Car(Engine engine)
    {
        _engine = engine;
    }

    public void Start() => _engine.Start();
}

Quick Comparison

Aspect Inheritance Composition
Relationship Is-A Has-A
Coupling Tight Loose
Flexibility Static (compile-time) Dynamic (runtime)
Reusability Limited by hierarchy High
Testing Harder to isolate Easy to mock
Changes Ripple through hierarchy Localized

When Inheritance is Appropriate

True “Is-A” Relationships

// ✅ Genuine specialization
public abstract class Shape
{
    public abstract double CalculateArea();
    public abstract double CalculatePerimeter();
}

public class Circle : Shape
{
    public double Radius { get; }

    public Circle(double radius) => Radius = radius;

    public override double CalculateArea() => Math.PI * Radius * Radius;
    public override double CalculatePerimeter() => 2 * Math.PI * Radius;
}

public class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double CalculateArea() => Width * Height;
    public override double CalculatePerimeter() => 2 * (Width + Height);
}

Framework Extension Points

// ✅ Extending framework classes (designed for inheritance)
public class CustomException : Exception
{
    public string ErrorCode { get; }

    public CustomException(string message, string errorCode)
        : base(message)
    {
        ErrorCode = errorCode;
    }
}

// ✅ ASP.NET Controller inheritance
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok(_products);
}

Template Method Pattern

// ✅ When you want to define the skeleton of an algorithm
public abstract class DataExporter
{
    // Template method - defines the algorithm
    public void Export()
    {
        var data = FetchData();
        var formatted = FormatData(data);
        WriteOutput(formatted);
        Cleanup();
    }

    protected abstract IEnumerable<object> FetchData();
    protected abstract string FormatData(IEnumerable<object> data);

    // Hook - can be overridden
    protected virtual void WriteOutput(string content)
    {
        Console.WriteLine(content);
    }

    protected virtual void Cleanup() { }
}

public class CsvExporter : DataExporter
{
    protected override IEnumerable<object> FetchData()
        => _repository.GetAll();

    protected override string FormatData(IEnumerable<object> data)
        => string.Join("\n", data.Select(ToCsv));
}

public class JsonExporter : DataExporter
{
    protected override IEnumerable<object> FetchData()
        => _repository.GetAll();

    protected override string FormatData(IEnumerable<object> data)
        => JsonSerializer.Serialize(data);
}

When to Use Composition

1. Sharing Behavior Across Unrelated Classes

// ❌ Inheritance: Forces unnatural hierarchy
public class Logger { }
public class UserService : Logger { }      // UserService IS-A Logger? No!
public class OrderService : Logger { }     // OrderService IS-A Logger? No!

// ✅ Composition: Natural relationship
public class Logger
{
    public void Log(string message) => Console.WriteLine($"[{DateTime.Now}] {message}");
}

public class UserService
{
    private readonly Logger _logger;

    public UserService(Logger logger) => _logger = logger;

    public void CreateUser(User user)
    {
        // Use logger
        _logger.Log($"Creating user: {user.Name}");
    }
}

public class OrderService
{
    private readonly Logger _logger;

    public OrderService(Logger logger) => _logger = logger;

    public void PlaceOrder(Order order)
    {
        _logger.Log($"Placing order: {order.Id}");
    }
}

2. Multiple Variations Along Different Dimensions

// ❌ Inheritance explosion
public class Document { }
public class PdfDocument : Document { }
public class WordDocument : Document { }
public class EncryptedPdfDocument : PdfDocument { }
public class EncryptedWordDocument : WordDocument { }
public class CompressedPdfDocument : PdfDocument { }
public class CompressedWordDocument : WordDocument { }
public class EncryptedCompressedPdfDocument : ???? // Where to inherit from?

// ✅ Composition: Features are composable
public interface IDocumentFormat
{
    byte[] Encode(string content);
    string Decode(byte[] data);
}

public interface IEncryption
{
    byte[] Encrypt(byte[] data);
    byte[] Decrypt(byte[] data);
}

public interface ICompression
{
    byte[] Compress(byte[] data);
    byte[] Decompress(byte[] data);
}

public class Document
{
    private readonly IDocumentFormat _format;
    private readonly IEncryption? _encryption;
    private readonly ICompression? _compression;

    public Document(
        IDocumentFormat format,
        IEncryption? encryption = null,
        ICompression? compression = null)
    {
        _format = format;
        _encryption = encryption;
        _compression = compression;
    }

    public byte[] Save(string content)
    {
        var data = _format.Encode(content);

        if (_compression != null)
            data = _compression.Compress(data);

        if (_encryption != null)
            data = _encryption.Encrypt(data);

        return data;
    }
}

// Now any combination is possible
var secureCompressedPdf = new Document(
    new PdfFormat(),
    new AesEncryption(),
    new GzipCompression());

3. Runtime Behavior Changes

// Strategy pattern via composition
public interface ISortStrategy
{
    IEnumerable<T> Sort<T>(IEnumerable<T> items) where T : IComparable<T>;
}

public class QuickSortStrategy : ISortStrategy
{
    public IEnumerable<T> Sort<T>(IEnumerable<T> items) where T : IComparable<T>
    {
        // Quick sort implementation
        return items.OrderBy(x => x);
    }
}

public class MergeSortStrategy : ISortStrategy
{
    public IEnumerable<T> Sort<T>(IEnumerable<T> items) where T : IComparable<T>
    {
        // Merge sort implementation
        return items.OrderBy(x => x);
    }
}

public class Sorter
{
    private ISortStrategy _strategy;

    public Sorter(ISortStrategy strategy)
    {
        _strategy = strategy;
    }

    // Can change strategy at runtime!
    public void SetStrategy(ISortStrategy strategy)
    {
        _strategy = strategy;
    }

    public IEnumerable<T> Sort<T>(IEnumerable<T> items) where T : IComparable<T>
    {
        return _strategy.Sort(items);
    }
}

// Usage
var sorter = new Sorter(new QuickSortStrategy());
var sorted = sorter.Sort(numbers);

// Change at runtime based on data size
if (numbers.Count > 10000)
    sorter.SetStrategy(new MergeSortStrategy());

Composition Patterns

Delegation Pattern

// Delegate behavior to composed object
public class Stack<T>
{
    // Delegate to List instead of inheriting from it
    private readonly List<T> _items = new();

    public void Push(T item) => _items.Add(item);

    public T Pop()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Stack empty");

        var item = _items[^1];
        _items.RemoveAt(_items.Count - 1);
        return item;
    }

    public T Peek() => _items[^1];

    public int Count => _items.Count;

    // We DON'T expose List operations like Insert, RemoveAt, etc.
    // This maintains stack semantics
}

Decorator Pattern

// Add behavior through composition
public interface INotifier
{
    void Send(string message);
}

public class EmailNotifier : INotifier
{
    public void Send(string message)
        => Console.WriteLine($"Email: {message}");
}

// Decorators add behavior
public abstract class NotifierDecorator : INotifier
{
    protected readonly INotifier _notifier;

    protected NotifierDecorator(INotifier notifier)
        => _notifier = notifier;

    public virtual void Send(string message)
        => _notifier.Send(message);
}

public class SmsNotifier : NotifierDecorator
{
    public SmsNotifier(INotifier notifier) : base(notifier) { }

    public override void Send(string message)
    {
        base.Send(message);
        Console.WriteLine($"SMS: {message}");
    }
}

public class SlackNotifier : NotifierDecorator
{
    public SlackNotifier(INotifier notifier) : base(notifier) { }

    public override void Send(string message)
    {
        base.Send(message);
        Console.WriteLine($"Slack: {message}");
    }
}

// Compose at runtime
INotifier notifier = new EmailNotifier();
notifier = new SmsNotifier(notifier);      // Add SMS
notifier = new SlackNotifier(notifier);    // Add Slack

notifier.Send("Hello!");  // Sends via Email, SMS, and Slack

Aggregation vs Composition

// Aggregation: Part can exist independently
public class Department
{
    public string Name { get; set; }
}

public class Employee
{
    // Employee references Department, but Department exists independently
    public Department Department { get; set; }
}

// Composition: Part cannot exist without whole
public class Order
{
    private readonly List<OrderLine> _lines = new();

    public void AddLine(string product, int quantity, decimal price)
    {
        // OrderLine is created and owned by Order
        _lines.Add(new OrderLine(product, quantity, price));
    }

    // OrderLine lifecycle tied to Order
    public class OrderLine  // Nested class emphasizes ownership
    {
        public string Product { get; }
        public int Quantity { get; }
        public decimal Price { get; }

        internal OrderLine(string product, int quantity, decimal price)
        {
            Product = product;
            Quantity = quantity;
            Price = price;
        }
    }
}

Mixins via Interfaces + Default Methods (C# 8+)

// Simulate mixins with default interface methods
public interface ITimestamped
{
    DateTime CreatedAt { get; set; }
    DateTime? UpdatedAt { get; set; }

    // Default implementation
    void Touch()
    {
        if (CreatedAt == default)
            CreatedAt = DateTime.UtcNow;
        else
            UpdatedAt = DateTime.UtcNow;
    }
}

public interface IAuditable
{
    string CreatedBy { get; set; }
    string? UpdatedBy { get; set; }

    void SetAuditInfo(string user)
    {
        if (string.IsNullOrEmpty(CreatedBy))
            CreatedBy = user;
        else
            UpdatedBy = user;
    }
}

// Classes can "mix in" behaviors
public class Document : ITimestamped, IAuditable
{
    public string Title { get; set; }
    public string Content { get; set; }

    // ITimestamped
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }

    // IAuditable
    public string CreatedBy { get; set; }
    public string? UpdatedBy { get; set; }
}

// Usage
var doc = new Document { Title = "Report" };
((ITimestamped)doc).Touch();
((IAuditable)doc).SetAuditInfo("john.doe");

Refactoring from Inheritance to Composition

Before: Deep Inheritance Hierarchy

// ❌ Problematic hierarchy
public class Employee
{
    public string Name { get; set; }
    public decimal BaseSalary { get; set; }

    public virtual decimal CalculatePay() => BaseSalary;
}

public class Manager : Employee
{
    public decimal Bonus { get; set; }

    public override decimal CalculatePay() => BaseSalary + Bonus;
}

public class SalesManager : Manager
{
    public decimal Commission { get; set; }

    public override decimal CalculatePay() => base.CalculatePay() + Commission;
}

public class RegionalSalesManager : SalesManager
{
    public decimal RegionalBonus { get; set; }

    public override decimal CalculatePay() => base.CalculatePay() + RegionalBonus;
}

// Problems:
// 1. What if a regular employee can also earn commission?
// 2. Deep hierarchy is hard to understand
// 3. Changes to base classes ripple down

After: Composition-Based Design

// ✅ Flexible composition
public interface IPayComponent
{
    decimal Calculate(Employee employee);
}

public class BaseSalary : IPayComponent
{
    public decimal Calculate(Employee employee) => employee.BaseSalary;
}

public class BonusComponent : IPayComponent
{
    private readonly decimal _bonusPercentage;

    public BonusComponent(decimal bonusPercentage)
    {
        _bonusPercentage = bonusPercentage;
    }

    public decimal Calculate(Employee employee)
        => employee.BaseSalary * _bonusPercentage;
}

public class CommissionComponent : IPayComponent
{
    private readonly decimal _commissionRate;

    public CommissionComponent(decimal commissionRate)
    {
        _commissionRate = commissionRate;
    }

    public decimal Calculate(Employee employee)
        => employee.SalesAmount * _commissionRate;
}

public class RegionalBonusComponent : IPayComponent
{
    private readonly decimal _regionMultiplier;

    public RegionalBonusComponent(decimal regionMultiplier)
    {
        _regionMultiplier = regionMultiplier;
    }

    public decimal Calculate(Employee employee)
        => employee.BaseSalary * _regionMultiplier;
}

public class Employee
{
    public string Name { get; set; }
    public decimal BaseSalary { get; set; }
    public decimal SalesAmount { get; set; }

    private readonly List<IPayComponent> _payComponents = new();

    public void AddPayComponent(IPayComponent component)
    {
        _payComponents.Add(component);
    }

    public decimal CalculatePay()
    {
        return _payComponents.Sum(c => c.Calculate(this));
    }
}

// Usage - create any combination
var salesPerson = new Employee { Name = "Alice", BaseSalary = 50000, SalesAmount = 100000 };
salesPerson.AddPayComponent(new BaseSalary());
salesPerson.AddPayComponent(new CommissionComponent(0.05m));

var manager = new Employee { Name = "Bob", BaseSalary = 80000 };
manager.AddPayComponent(new BaseSalary());
manager.AddPayComponent(new BonusComponent(0.2m));

var regionalSalesManager = new Employee { Name = "Carol", BaseSalary = 90000, SalesAmount = 200000 };
regionalSalesManager.AddPayComponent(new BaseSalary());
regionalSalesManager.AddPayComponent(new BonusComponent(0.15m));
regionalSalesManager.AddPayComponent(new CommissionComponent(0.03m));
regionalSalesManager.AddPayComponent(new RegionalBonusComponent(0.1m));

Performance Considerations

Virtual Method Dispatch vs Composition

// Inheritance: Virtual method dispatch has small overhead
public class Base
{
    public virtual void DoWork() { }
}

public class Derived : Base
{
    public override void DoWork() { }
}

// Composition: Interface dispatch similar overhead
public interface IWorker
{
    void DoWork();
}

public class Worker : IWorker
{
    public void DoWork() { }
}

// In practice, the difference is negligible for most applications
// Focus on design quality over micro-optimization

Memory Considerations

// Inheritance: Single object
public class InheritedLogger : BaseLogger  // One allocation
{
    // Additional fields here
}

// Composition: Multiple objects
public class ComposedLogger  // Multiple allocations
{
    private readonly ILogFormatter _formatter;  // Separate object
    private readonly ILogWriter _writer;        // Separate object
}

// Trade-off: Slightly more memory for much better flexibility
// Only matters in extreme performance scenarios (games, HFT)

Sealed Classes for Performance

// 'sealed' allows JIT to devirtualize calls
public sealed class FastService : IService
{
    public void Process() { }  // Can be inlined by JIT
}

Interview Questions

1. Why do we say “favor composition over inheritance”?

Answer: Composition provides:

  • Loose coupling: Components can change independently
  • Flexibility: Can swap implementations at runtime
  • Testability: Easy to mock composed dependencies
  • Avoids fragile base class problem: Changes to base don’t break derived classes
  • Multiple “inheritance”: Can compose multiple behaviors (C# doesn’t allow multiple inheritance)

Use inheritance only for true “is-a” relationships where polymorphism is needed.


2. What is the fragile base class problem?

Answer: When changes to a base class break derived classes unexpectedly.

// Base class
public class List
{
    public virtual void Add(object item) { /* ... */ }
    public virtual void AddRange(IEnumerable items)
    {
        foreach (var item in items)
            Add(item);  // Calls virtual Add
    }
}

// Derived class
public class CountingList : List
{
    public int AddCount { get; private set; }

    public override void Add(object item)
    {
        AddCount++;
        base.Add(item);
    }
}

// Problem: AddRange calls Add 3 times, so count is 3, not 1
var list = new CountingList();
list.AddRange(new[] { 1, 2, 3 });  // AddCount = 3, not 1!

With composition, this wouldn’t happen because the internal implementation is hidden.


3. Can you give an example where inheritance is better than composition?

Answer: When you need polymorphism with shared state and behavior:

// Inheritance makes sense for UI controls
public abstract class Control
{
    protected int X { get; set; }
    protected int Y { get; set; }
    protected int Width { get; set; }
    protected int Height { get; set; }

    public void Move(int x, int y) { X = x; Y = y; }
    public abstract void Draw();
}

public class Button : Control
{
    public string Text { get; set; }
    public override void Draw() { /* Draw button */ }
}

public class TextBox : Control
{
    public string Value { get; set; }
    public override void Draw() { /* Draw textbox */ }
}

// Controls can be treated uniformly
List<Control> controls = new() { new Button(), new TextBox() };
foreach (var control in controls)
    control.Draw();  // Polymorphism

4. How would you refactor a class that inherits from List?

Answer:

// ❌ Before: Inheritance
public class StudentCollection : List<Student>
{
    public double AverageGpa => this.Average(s => s.Gpa);
}

// ✅ After: Composition
public class StudentCollection
{
    private readonly List<Student> _students = new();

    public void Add(Student student) => _students.Add(student);
    public void Remove(Student student) => _students.Remove(student);
    public int Count => _students.Count;

    public double AverageGpa => _students.Average(s => s.Gpa);

    public IEnumerable<Student> GetByGpaAbove(double threshold)
        => _students.Where(s => s.Gpa > threshold);
}

Benefits: Can’t misuse List methods, can add validation, implementation hidden.


Key Takeaways

  1. Default to composition - Use inheritance only when there’s a true “is-a” relationship
  2. Inheritance creates tight coupling - Base class changes affect all derived classes
  3. Composition is more flexible - Behaviors can be added/removed at runtime
  4. Use interfaces for contracts - Composition works best with interfaces
  5. Inheritance for framework extension points - When the framework is designed for it

Further Reading

📚 Related Articles