Architecture & Patterns110 min total · 26 parts
ACID, SOLID, and Design Patterns: A Complete Software Design Reference
Part 24 of 26 · ~3 min
Composition Over Inheritance
Look back across the last four patterns and a single move keeps repeating: hand behavior to a small object supplied at runtime, rather than locking a decision into a class hierarchy that can only be changed by editing and recompiling. That repetition isn't a coincidence, and it's worth unpacking why the runtime version tends to hold up better over time.
Building a hierarchy means guessing every variation the future will ask for and carving each one into its own branch today — a bet that goes bad quickly once a piece of behavior needs to flex along two separate dimensions at the same time. A volunteer needs both how they're reminded (text, push notification, or nothing at all) and how they check in (kiosk scan, mobile tap, or a coordinator marking it manually) to vary independently. Try to express both through one inheritance tree and it sprawls into TextKioskVolunteer, TextMobileVolunteer, PushKioskVolunteer, and on — a fresh subclass per pairing, with the shared bits either copy-pasted between them or awkwardly hoisted into some intermediate class that exists for no reason other than to avoid repeating itself:
// Rigid — every combination of reminder and check-in style needs its own
// subclass, and a volunteer can't switch styles after being constructed.
public abstract class Volunteer
{
public abstract void Remind();
public abstract void CheckIn();
}
public class TextKioskVolunteer : Volunteer
{
public override void Remind() => Console.WriteLine("Texted");
public override void CheckIn() => Console.WriteLine("Scanned at kiosk");
}
public class SilentMobileVolunteer : Volunteer
{
public override void Remind() { /* deliberately does nothing — still forced to implement it */ }
public override void CheckIn() => Console.WriteLine("Tapped on phone");
}
// Composed from interchangeable behavior objects, reusing the same shape
// as IShiftNotifier from earlier — new combinations don't need new classes,
// and a volunteer can switch behaviors after being constructed.
public interface IReminderBehavior { void Remind(); }
public interface ICheckInBehavior { void CheckIn(); }
public class TextReminder : IReminderBehavior { public void Remind() => Console.WriteLine("Texted"); }
public class NoReminder : IReminderBehavior { public void Remind() { } }
public class KioskCheckIn : ICheckInBehavior { public void CheckIn() => Console.WriteLine("Scanned at kiosk"); }
public class MobileCheckIn : ICheckInBehavior { public void CheckIn() => Console.WriteLine("Tapped on phone"); }
public class Volunteer
{
private IReminderBehavior _reminderBehavior;
private readonly ICheckInBehavior _checkInBehavior;
public Volunteer(IReminderBehavior reminderBehavior, ICheckInBehavior checkInBehavior)
{
_reminderBehavior = reminderBehavior;
_checkInBehavior = checkInBehavior;
}
public void Remind() => _reminderBehavior.Remind();
public void CheckIn() => _checkInBehavior.CheckIn();
public void SwitchToTextReminders() => _reminderBehavior = new TextReminder(); // swappable at runtime
}
var v = new Volunteer(new NoReminder(), new MobileCheckIn());
This isn't an argument that inheritance has no place — when a relationship really is IS-A, and the contract behind it is genuinely stable (RegularShift and LockedShift sharing IShiftInfo, from a few chapters back, is a fair example), an interface or a base class is still the right tool. Composition earns its keep in the narrower, extremely common case where a class needs to vary along two or more axes independently, or where the behavior has to be changeable after the object is already alive and in use.