Skillversitas

Composition over Inheritance

Composition builds an object from other objects it owns, instead of extending a class it inherits. Where inheritance says "a SavingsAccount is a BankAccount," composition says "a Report has a DataSource and a Formatter." The has-a relation is the more common real-world shape, and it avoids the two costs of inheritance: the rigid is-a commitment (a subclass can override but not remove what it inherits) and the coupling to a parent's internals. This chapter builds the pattern - delegation, the strategy object, and the facade - and gives the decision rule for when to reach for which.

Delegation: The Core Move

class Logger:
    def __init__(self, prefix: str):
        self.prefix = prefix
        self.lines: list[str] = []

    def write(self, msg: str) -> None:
        self.lines.append(f"[{self.prefix}] {msg}")

class Service:
    """Composes a Logger; does not inherit it."""

    def __init__(self, name: str):
        self.name = name
        self._log = Logger(name)          # has-a: an owned component

    def start(self) -> None:
        self._log.write("started")

    def stop(self) -> None:
        self._log.write("stopped")

    def log_lines(self) -> list[str]:
        return self._log.lines            # delegate the query outward

s = Service("billing")
s.start()
s.stop()
print(s.log_lines())
# ['[billing] started', '[billing] stopped']

The Service owns a Logger as a plain attribute and forwards the calls it wants to expose. The contrast with inheritance is sharp: inheriting a LoggingMixin would flatten the logger's methods into Service's namespace and bind self to the service (so the mixin's self.lines becomes the service's lines); composition keeps the logger a distinct object with its own state, and the service chooses exactly which calls to forward. The forwarding methods are the delegation - the service is the public face, the logger is the mechanism. The payoff is swap-ability: change the logger's class (to one that writes to a file) and the service is untouched, because it depends on the interface it calls, not on a base class it cannot shed.

The Strategy Object: Behavior as a Component

class PricingStrategy:
    def price(self, base: float, qty: int) -> float:
        raise NotImplementedError

class BulkDiscount(PricingStrategy):
    def price(self, base: float, qty: int) -> float:
        return base * qty * (0.9 if qty >= 100 else 1.0)

class LadderPrice(PricingStrategy):
    def price(self, base: float, qty: int) -> float:
        return base * qty * max(0.5, 1.0 - 0.01 * (qty // 50))

class Order:
    def __init__(self, strategy: PricingStrategy):
        self._strategy = strategy         # the behavior is a component

    def total(self, base: float, qty: int) -> float:
        return self._strategy.price(base, qty)

# the SAME Order, two different behaviors - no subclassing
a = Order(BulkDiscount())
b = Order(LadderPrice())
print(a.total(10, 150))     # 1350.0
print(b.total(10, 150))     # 700.0

The strategy pattern is composition's flagship: the variable behavior (pricing) is its own object, and the host (Order) holds one and delegates to it. Switching strategies is a construction-time choice - or a runtime one if you make strategy settable - with no class hierarchy and no if isinstance ladder. The contrast with the inheritance alternative is the design test: "pricing" is not a kind of Order; an Order that is a BulkDiscountOrder and a LadderOrder is two subclasses differing in one method, which is the exact shape that should be a strategy. The duck-typing pays off - Order does not need to name PricingStrategy; anything with a price(base, qty) works, and a typing.Protocol from the polymorphism chapter makes that contract explicit.

The Facade: One Object, Many Components

class Connection:
    def open(self) -> None: pass
    def close(self) -> None: pass

class QueryBuilder:
    def build(self, table: str, where: str) -> str:
        return f"SELECT * FROM {table} WHERE {where}"

class Cache:
    def __init__(self) -> None:
        self._store: dict[str, str] = {}

    def get(self, k: str) -> str | None:
        return self._store.get(k)

    def set(self, k: str, v: str) -> None:
        self._store[k] = v

class Repo:
    """A facade: composes connection + builder + cache behind one API."""

    def __init__(self) -> None:
        self._conn = Connection()
        self._qb = QueryBuilder()
        self._cache = Cache()

    def fetch(self, table: str, where: str) -> str:
        key = f"{table}:{where}"
        hit = self._cache.get(key)
        if hit is not None:
            return hit
        self._conn.open()
        sql = self._qb.build(table, where)
        result = f"rows({sql})"
        self._conn.close()
        self._cache.set(key, result)
        return result

r = Repo()
print(r.fetch("users", "active=1"))    # rows(SELECT ...)
print(r.fetch("users", "active=1"))     # served from the cache

The facade composes several collaborators and exposes a simpler surface than their sum. The components keep their own state and responsibilities; the facade orchestrates (open, build, query, cache) and hides the wiring from callers. This is the shape of most real "service" classes - they are not a hierarchy, they are a graph of owned components with one public entry point. The discipline: the facade owns the lifecycle (it opens and closes the connection, so callers cannot forget), and it does not leak the components' types into its public API unless a caller genuinely needs them. Composition scales to this many-part graph trivially; the inheritance alternative - a class that is a Connection and a QueryBuilder and a Cache - is the multiple-inheritance mess the MRO chapter warned about.

The Decision Rule

SignalReach for
"A child is genuinely a kind of the parent," and consumers use it as suchinheritance
"This object needs a capability it does not itself define"composition (delegate to a component)
"The behavior varies, and it is not a type distinction"strategy object (composition with a delegate)
"Many collaborators, one simple entry point"facade (composition as orchestration)
"I need to share a few methods, not a whole type"mixin or a helper object, not a base class

The one-sentence version: inherit when the relation is a true is-a that the type system should express; compose when the relation is a has-a or a uses-a. The pragmatic tiebreaker - when in doubt, compose - works because composition is always available (you can always own a component) while inheritance is a commitment (you inherit everything, and the parent's future changes reach you). The cost of composition is a few forwarding lines and a slightly longer object graph; the cost of a wrong inheritance is a hierarchy you spend quarters unwinding.

Summary

Composition builds from owned components: self._log = Logger(...) plus forwarding methods is delegation, a strategy attribute whose methods the host calls is the strategy pattern (variable behavior as a swappable component), and a facade is composition used as orchestration - one public object over many collaborators, owning their lifecycle. The contrast with inheritance is the design: has-a relations are swappable and decoupled (change the component's class, the host is untouched), while is-a is a permanent commitment to a parent's full namespace. Use inheritance for genuine kinds the type system should name; use composition for capabilities, variable behavior, and multi-part objects - and when the two feel equally plausible, compose, because the forwarding lines are cheap and the wrong base class is not.