Skip to content

// SOLID & Principles · 10

Composition over Inheritance

Deep class hierarchyleads toSmall parts, combined

Build behaviour by combining objects, not by extending classes.

How it fits together

Composition over Inheritance structureBeforeAfterPdfEmailReportFormat()CachedPdfEmailReportCache()«abstract»ReportBaseFormat()Send()Cache()CsvEmailReportFormat()CachingFormatterFormat(data)cache hit, else innerReportRun(data)no subclasses«interface»ReportFormatterFormat(data)«interface»ReportSenderSend(bytes)formattersender
extendsimplementswraps / holds
  • PdfEmailReport extends ReportBase
  • CsvEmailReport extends ReportBase
  • CachedPdfEmailReport extends PdfEmailReport
  • CachingFormatter implements ReportFormatter
  • Report wraps / holds ReportFormatter (formatter)
  • Report wraps / holds ReportSender (sender)

Spot it

  • A base class everyone fears to change
  • One subclass per combination of features
  • Overrides that call base, then patch it

It costs

More wiring and small objects; genuine is-a hierarchies read better as inheritance.

In code

public interface IReportFormatter{    byte[] Format(ReportData data);}​public sealed class CachingFormatter(IReportFormatter inner) : IReportFormatter{    private readonly Dictionary<Guid, byte[]> cache = [];​    public byte[] Format(ReportData data)    {        if (!cache.TryGetValue(data.Id, out var bytes))        {            bytes = inner.Format(data);            cache[data.Id] = bytes;        }​        return bytes;    }}​public sealed class Report(IReportFormatter formatter, IReportSender sender){    public Task RunAsync(ReportData data, CancellationToken ct) =>        sender.SendAsync(formatter.Format(data), ct);}
Go deeperThe full explanation and what to ask for · 1 min readExpand

From the Gang of Four: favour object composition over class inheritance. Inheritance fixes behaviour at compile time and exposes a parent's internals to every child; composition swaps parts at run time behind interfaces.

The fragile base class problem is the cost: a harmless-looking change in a parent can break subclasses that relied on its internal call order.

It is "over", not "instead of". Inheritance still fits genuine is-a relationships, framework extension points and shallow abstract bases. Reach for composition when variation comes in independent dimensions.

Ask for it

  • has-a vs is-a
  • delegation
  • fragile base class
  • decorator
  • strategy

Replace the ReportBase inheritance tree with composition: extract formatting and delivery into ReportFormatter and ReportSender, pass them into a Report class that cannot be subclassed, and wrap cross-cutting behaviour like caching in decorators, not subclasses.