Skip to main content
CodeOath
← All posts

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.