CodeOath
← All posts
Architecture & Patterns80 min total · 26 parts

ACID, SOLID, and Common Design Patterns: A Software Design Reference

Contents — Part 24 of 26: Composition Over Inheritance
Part 24 of 26 · ~2 min

Composition Over Inheritance

Strategy, Decorator, Observer, and Visitor all share one underlying idea: prefer plugging in small, focused objects at runtime over committing to a rigid inheritance hierarchy fixed at compile time. It's worth stating explicitly why this tends to age better.

Inheritance forces you to anticipate every future variation as a class in a hierarchy, decided up front — and hierarchies get awkward fast once a variation cuts across more than one axis. A Duck class hierarchy that needs both "flies or doesn't" and "quacks or doesn't" independently can't cleanly express every combination through single inheritance alone without duplicating code or creating oddly-named intermediate classes like FlyingQuackingDuck.

// Rigid — every new combination of behaviors needs its own subclass,
// and behavior can't change after construction.
public abstract class Duck
{
    public abstract void Fly();
    public abstract void Quack();
}

public class MallardDuck : Duck
{
    public override void Fly() => Console.WriteLine("Flying");
    public override void Quack() => Console.WriteLine("Quack");
}

public class RubberDuck : Duck
{
    public override void Fly() { /* can't fly — but still forced to implement this method */ }
    public override void Quack() => Console.WriteLine("Squeak");
}
// Composed from interchangeable behavior objects — new combinations don't
// need new classes, and behavior can even be swapped at runtime.
public interface IFlyBehavior { void Fly(); }
public interface IQuackBehavior { void Quack(); }

public class FliesNormally : IFlyBehavior { public void Fly() => Console.WriteLine("Flying"); }
public class CannotFly : IFlyBehavior { public void Fly() => Console.WriteLine("..."); }
public class NormalQuack : IQuackBehavior { public void Quack() => Console.WriteLine("Quack"); }
public class Squeak : IQuackBehavior { public void Quack() => Console.WriteLine("Squeak"); }

public class Duck
{
    private IFlyBehavior _flyBehavior;
    private IQuackBehavior _quackBehavior;

    public Duck(IFlyBehavior flyBehavior, IQuackBehavior quackBehavior)
    {
        _flyBehavior = flyBehavior;
        _quackBehavior = quackBehavior;
    }

    public void Fly() => _flyBehavior.Fly();
    public void Quack() => _quackBehavior.Quack();
    public void SetFlyBehavior(IFlyBehavior newBehavior) => _flyBehavior = newBehavior; // swappable at runtime
}

var rubberDuck = new Duck(new CannotFly(), new Squeak());

This isn't "inheritance is always wrong" — a genuine IS-A relationship with a stable, shared contract (the Rectangle/IShape example earlier) is exactly what inheritance and interfaces are for. It's specifically that behavior varying independently along multiple axes, or needing to change after an object is already constructed, is where composition consistently wins.