Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,25 @@ python-design-patterns-mcp --http --host 127.0.0.1 --port 8734
| Tool | What it does |
|---|---|
| `list_patterns(group?, verdict?)` | Catalog listing, filterable |
| `get_pattern(pattern_id, variant?)` | Full prose (+ legacy variant source files: `naive.py` / `pythonic.py` / `real_world.py`, or `all`) |
| `get_pattern(pattern_id, variant?)` | Full prose and metadata (`variant` served only pre-module-shape units; the current catalog has none) |
| `search_patterns(query, limit?)` | BM25 full-text search over names, aliases, problems, symptoms, prose |
| `get_pattern_docs(pattern_id, doc)` | A migrated pattern's teaching doc: `fundamentals`, `implementation`, or `examples` |
| `list_examples(pattern_id)` | A migrated pattern's runnable mini-projects |
| `run_example(pattern_id, variant?/example?)` | Executes a vendored example (legacy `variant` or migrated `example`) in a sandboxed subprocess; returns real stdout |
| `read_source(pattern_id)` | A migrated pattern's own implementation (`pattern/` package) |
| `get_pattern_docs(pattern_id, doc)` | A pattern's teaching doc: `fundamentals`, `implementation`, or `examples` |
| `list_examples(pattern_id)` | A pattern's runnable mini-projects |
| `run_example(pattern_id, example=...)` | Executes a mini-project in a sandboxed subprocess; returns real stdout (`variant=` remains for pre-module-shape units only) |
| `read_source(pattern_id)` | A pattern's own implementation (`pattern/` package) |
| `recommend_pattern(problem_statement, limit?)` | Ranked candidates with caveats; `prefer-alternative` verdicts tell you what to write instead |

Migrated (module-shape) patterns follow three access levels: scan docs
Every pattern follows three access levels: scan docs
(`get_pattern_docs`) → run a use case (`list_examples` + `run_example`) →
read the source (`read_source`).
read the source (`read_source`). Units predating the module shape would
expose flat variant files instead; the current catalog has none.

## Resources

- `catalog://index` — the whole catalog as JSON
- `pattern://<group>/<slug>` — one pattern's prose
- `pattern://<group>/<slug>/<variant>` — one legacy example's source
- `pattern://<group>/<slug>/docs/<doc>` — one migrated pattern's teaching doc
- `pattern://<group>/<slug>/docs/<doc>` — one pattern's teaching doc
- `pattern://<group>/<slug>/<variant>` — a pre-module-shape unit's variant source (none in the current catalog)

## Prompts

Expand Down
42 changes: 14 additions & 28 deletions patterns/principle/composition_over_inheritance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,17 @@ stdlib_sightings: [logging.Logger, logging.Handler, logging.Filter]

# Composition Over Inheritance

## Problem

A logger can filter messages and can write to a file or a socket. With
inheritance, every combination costs a class: `FilteredLogger`,
`SocketLogger`, `FilteredSocketLogger`… M filters × N destinations = M×N
classes. This is the guide's opening case study.

## Naive solution

`naive.py` builds exactly that explosion, three classes deep, so you can
watch the combinatorics happen.

## Pythonic solution

Split each axis into its own object — filters decide, handlers write — and
*compose* them in one logger. M + N small classes cover all M × N behaviors,
and new combinations are constructor arguments, not new classes.

## In the wild

The stdlib `logging` module is this principle shipped at scale: `Logger`
composes `Handler`s, `Filter`s, and `Formatter`s, and no class named
`FilteredRotatingSyslogLogger` needs to exist.

## Verdict

**Pythonic** — and the single most load-bearing idea behind the other
patterns in this catalog.
One small piece per axis of variation, composed at a single point: M + N
pieces instead of M × N subclasses. **Verdict: pythonic** — the most
load-bearing idea behind the rest of this catalog.

| Where | What |
|---|---|
| [`pattern/`](pattern/) | The importable code: `Pipeline` (the composition point), `Filter`/`Transform`/`Sink` axis aliases, and `Logger` built on it |
| [`docs/`](docs/) | [Fundamentals](docs/fundamentals.md) · [Implementation guide](docs/implementation.md) · [External examples](docs/examples.md) |
| [`examples/notification_router/`](examples/notification_router/) | Mini-project: alerts through filter × format × deliver pieces, zero combination subclasses |
| [`tests/`](tests/) | Behavioral tests for the pattern and the mini-project |

```bash
uv run python -m patterns.principle.composition_over_inheritance.examples.notification_router
```
16 changes: 15 additions & 1 deletion patterns/principle/composition_over_inheritance/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
"""Composition over inheritance: objects per axis, not classes per combination."""
"""Composition Over Inheritance — public API.

>>> from patterns.principle.composition_over_inheritance import Pipeline, Logger
"""

from patterns.principle.composition_over_inheritance.pattern import (
Filter,
Logger,
Pipeline,
Sink,
Transform,
identity,
)

__all__ = ["Filter", "Logger", "Pipeline", "Sink", "Transform", "identity"]
31 changes: 31 additions & 0 deletions patterns/principle/composition_over_inheritance/docs/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Composition Over Inheritance — where it lives outside this repo

Cited, real implementations to study (or point an agent at) when designing or
reviewing class hierarchies.

## Python standard library

- **`logging`.** The guide's own worked example, shipped at scale: `Logger`
composes `Handler`s, `Filter`s, and `Formatter`s — three orthogonal axes,
no class per combination.
[docs.python.org/3/library/logging.html](https://docs.python.org/3/library/logging.html)
- **`socketserver`.** The honest contrast: the stdlib's mixin dodge
(`ThreadingMixIn` + `TCPServer` = `ThreadingTCPServer`), useful to study
as the alternative the principle warns will reconverge on the diamond.
[docs.python.org/3/library/socketserver.html](https://docs.python.org/3/library/socketserver.html)

## Major ecosystems

- **pytest's plugin architecture.** Behavior is added by composing plugins
registered with hook functions — not by subclassing the test runner.
[docs.pytest.org/en/stable/how-to/writing_plugins.html](https://docs.pytest.org/en/stable/how-to/writing_plugins.html)
- **The guide chapter** — the subclass-explosion case study and the taxonomy
of dodges (multiple inheritance, mixins, dynamically built classes).
[python-patterns.guide/gang-of-four/composition-over-inheritance](https://python-patterns.guide/gang-of-four/composition-over-inheritance/)

## What to notice across all of them

In every healthy example the *combination* is expressed at runtime — a
constructor call, a registration — while the *pieces* stay single-purpose.
When reviewing, count adjectives in class names: two or more is the explosion
starting.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Composition Over Inheritance — fundamentals

## Intent

Vary independent behaviors without one subclass per combination of them.
"Favor object composition over class inheritance" is the second principle in
the GoF introduction — the soil the structural and behavioral patterns grow
from. Each independent axis of variation becomes its own small object,
injected where needed; M + N pieces cover M × N behaviors. Source chapter:
[python-patterns.guide/gang-of-four/composition-over-inheritance](https://python-patterns.guide/gang-of-four/composition-over-inheritance/).

## Participants

| Role | What it is |
|---|---|
| Axes of variation | The independent behaviors (what to accept, how to shape, where to send) |
| One small piece per axis | A callable or tiny class per behavior — `Filter`/`Transform`/`Sink` in [`pattern/compose.py`](../pattern/compose.py) |
| The composition point | The one class that holds a piece per axis and wires them — `Pipeline` in [`pattern/compose.py`](../pattern/compose.py); `Logger` and the example's `Notifier` are `Pipeline` put to work |
| Clients | Pick pieces and construct; a new combination is a constructor call |

## Mechanism

1. Name the axes. If a class name wants two adjectives
(`FilteredSocketLogger`), there are at least two.
2. Give each axis its own minimal interface — in Python usually just a
callable signature.
3. Write one composition-point class holding one piece per axis; its methods
delegate in a fixed, readable order.
4. Combinations are now data: constructed, passed, and tested — never
subclassed into existence.

## The subclass explosion, and what composition replaces

The classic route bolts each behavior on by subclassing — and then needs a
class *per combination*:

```python
class Logger: ... # base


class FilteredLogger(Logger): ... # axis 1 bolted on


class UppercaseLogger(Logger): ... # axis 2 bolted on


class FilteredUppercaseLogger(FilteredLogger): # the explosion begins:
"""One class PER COMBINATION.""" # M x N x P classes coming
```

Two axes already cost four classes; each new filter or destination
*multiplies* the count. The dodges — multiple inheritance, mixins,
dynamically built classes — postpone the explosion rather than end it. The
composed form keeps one class per *piece* plus one composition point, and
the arithmetic turns from multiplication into addition.

## When to use it

- Two or more behaviors vary independently (filtering × formatting ×
destination; retry × serialization × transport).
- Subclass names are growing adjectives, or a mixin diamond is forming.

## When not to use it

- One axis, two variants, stable → a single `if` or a subclass is honest and
smaller; composition machinery would be ceremony.
- The "axes" are not independent (one behavior's output feeds another's
contract) → model the coupling explicitly; forced composition hides it.

## Verdict: pythonic

The single most load-bearing idea behind the rest of this catalog — the
stdlib's `logging` (Logger/Handler/Filter/Formatter) is this principle
shipped at scale, with no `FilteredRotatingSyslogLogger` in sight.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Composition Over Inheritance — putting it into a system

## The smell it fixes

Class names collecting adjectives, and a new requirement meaning a new
subclass:

```python
class JsonFileNotifier(FileNotifier): ...


class DedupJsonFileNotifier(JsonFileNotifier): ...


class DedupJsonWebhookNotifier(...): ... # and severity thresholds arrive Monday
```

## Steps

1. **List the axes.** Read the subclass names: each adjective is an axis
(dedup / json / webhook = decide / reshape / act).
2. **Define one minimal interface per axis.** In Python a callable signature
is usually enough — `Filter[T] = Callable[[T], bool]`,
`Transform[T, U] = Callable[[T], U]`, `Sink[T] = Callable[[T], None]`
(importable from this unit's `pattern/`).
3. **Extract each behavior into a piece.** Plain functions for stateless
pieces; a small callable class where state is real (`Dedup`); a closure
factory where only a parameter varies (`min_severity(4)`).
4. **Write the composition point** — one dataclass with one field per axis
and the delegation order spelled out in one method.
5. **Delete the subclass tree.** Every leaf class becomes a constructor call;
its tests become tests of a *combination*, which now read as configuration.

```python
from patterns.principle.composition_over_inheritance import Pipeline

# Notifier = Pipeline[Alert, str] — the domain names the composition point.
pager = Notifier(filters=(min_severity(4), Dedup()), transform=as_json, sink=webhook)
```

## Python idioms that keep it small

- **Callables are the interfaces.** No ABCs needed until an axis has multiple
methods; `Protocol` when it does.
- **Closure factories replace parameter subclasses**: `min_severity(4)` is
the whole class `SeverityAtLeastFourFilter`.
- **`functools.partial`** turns any configurable function into a piece.
- **Dataclasses as composition points** make the wiring visible in `repr`
and trivially testable.

## Pitfalls

- **Rebuilding inheritance inside composition** — a piece that reaches into
the composition point (or another piece) recreates the coupling with extra
steps. Pieces see only their own input.
- **The god-piece.** One "filter" that also formats and delivers has eaten
the axes; if a piece needs two verbs to describe, split it.
- **The mixin dodge.** `class DedupMixin: ...` feels cheaper today and
reconverges on the diamond tomorrow — the guide's taxonomy of dodges is
worth rereading when tempted.
- **Order left implicit.** The composition point owns the delegation order;
document and test it (filters run in order and short-circuit — a stateful
filter placed after a veto never records vetoed items).

## Worked example

[`examples/notification_router/`](../examples/notification_router/) routes
alerts through filter × format × deliver pieces — console plain-text at
severity ≥ 2, deduped JSON to a webhook at severity ≥ 4 — one `Notifier`
class, zero combination subclasses. Run it:

```bash
uv run python -m patterns.principle.composition_over_inheritance.examples.notification_router
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Mini-projects demonstrating Composition Over Inheritance in practice."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""An alert router built on Composition Over Inheritance.

Run it: ``uv run python -m patterns.principle.composition_over_inheritance\
.examples.notification_router``
"""

from patterns.principle.composition_over_inheritance.examples.notification_router.axes import (
Dedup,
FakeWebhook,
MemorySink,
as_json,
console,
min_severity,
plain_text,
)
from patterns.principle.composition_over_inheritance.examples.notification_router.models import (
Alert,
)
from patterns.principle.composition_over_inheritance.examples.notification_router.router import (
Notifier,
Router,
)

__all__ = [
"Alert",
"Dedup",
"FakeWebhook",
"MemorySink",
"Notifier",
"Router",
"as_json",
"console",
"min_severity",
"plain_text",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Demo: two very different notifiers from one class and a handful of pieces."""

from __future__ import annotations

from patterns.principle.composition_over_inheritance.examples.notification_router.axes import (
Dedup,
FakeWebhook,
as_json,
console,
min_severity,
plain_text,
)
from patterns.principle.composition_over_inheritance.examples.notification_router.models import (
Alert,
)
from patterns.principle.composition_over_inheritance.examples.notification_router.router import (
Notifier,
Router,
)


def main() -> None:
webhook = FakeWebhook("https://pager.example/hook")
router = Router(
notifiers=(
Notifier(filters=(min_severity(2),), transform=plain_text, sink=console),
Notifier(filters=(min_severity(4), Dedup()), transform=as_json, sink=webhook),
)
)
alerts = [
Alert("api", 2, "latency rising"),
Alert("db", 5, "primary down"),
Alert("db", 5, "primary down"), # duplicate: webhook dedups, console repeats
Alert("cron", 1, "nightly job finished"),
]
for alert in alerts:
router.broadcast(alert)
print(f"webhook received {len(webhook.posted)} page(s): {webhook.posted[0]}")


if __name__ == "__main__":
main()
Loading
Loading