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
- When Inheritance is Appropriate
- When to Use Composition
- Composition Patterns
- Refactoring from Inheritance to Composition
- Performance Considerations
- Interview Questions
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
- Default to composition - Use inheritance only when there’s a true “is-a” relationship
- Inheritance creates tight coupling - Base class changes affect all derived classes
- Composition is more flexible - Behaviors can be added/removed at runtime
- Use interfaces for contracts - Composition works best with interfaces
- Inheritance for framework extension points - When the framework is designed for it