// SOLID & Principles · 10
Composition over Inheritance
Build behaviour by combining objects, not by extending classes.
How it fits together
- 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 readExpandCollapse
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.