Card 06/ 08

GotchaDifficulty: Advanced2 min

Child Methods on the Component Interface

Put add() on Component and a leaf must refuse it; leave it off and the client must know which it holds.

The Composite pattern cannot make leaves and composites fully interchangeable, because only a composite can hold children. Where add is declared decides who pays for that difference, and the Gang of Four name the two choices transparency and safety. This card shows both, the error each one allows, and how to choose.

Two places to declare add

Transparencyadd on Component

Every node has add, so a client can fill any node without checking its type. A leaf must refuse at run time.

py
class Component:
    def add(self, child):
        raise TypeError("cannot hold children")

The mistake reaches production as an exception.

Safetyadd on Composite only

Only a composite has add, so a type checker refuses leaf.add(...) before the code runs.

py
class Composite(Component):
    def add(self, child):
        self._children.append(child)

A client that builds the tree must know, or check, which kind of node it holds.

py
class Component:
    def size(self) -> int:
        raise NotImplementedError

    def add(self, child: "Component") -> None:
        raise TypeError(f"{type(self).__name__} cannot hold children")


class File(Component):
    def __init__(self, size_kb: int) -> None:
        self._size_kb = size_kb

    def size(self) -> int:
        return self._size_kb


class Folder(Component):
    def __init__(self) -> None:
        self._children: list[Component] = []

    def add(self, child: Component) -> None:
        self._children.append(child)

    def size(self) -> int:
        return sum(child.size() for child in self._children)


def fill(component: Component) -> None:
    component.add(File(4))
    print(f"added, size now {component.size()} KB")


fill(Folder())
try:
    fill(File(2))
except TypeError as error:
    print(f"TypeError: {error}")
OutputRan with python:3.12-slim (Python 3.12.13)
added, size now 4 KB
TypeError: File cannot hold children
`fill` accepts any component; the file fails only when `add` is called on it.

The transparent version lets fill take any component, and the folder accepts the file. The file refuses, and the refusal is a TypeError that only a test or a user will find. Swing chose transparency: JComponent extends Container, so a JButton is a container and button.add(panel) compiles, because the method is inherited.

Choose safety when the tree is built in a few known places and read in many, which is the usual case: the builder knows what it is creating, and every reader uses only the shared operations. Choose transparency when generic code must build trees from nodes it cannot see, such as a parser filling a tree from a file, and make the leaf's refusal loud and specific.