Widgets (or services) updating each other directly: the country dropdown pokes the shipping selector, which pokes payment, which pokes the total — and adding one field means auditing every other field's handlers.
- Inventory the cross-component rules — write each as a sentence ("cash-on-delivery is only offered on express"). These sentences become one method's body, so their number tells you the mediator's size.
- Dumb the components down to value + change notification.
Fieldis that reduced form; subclassFormand create each one withadd_field, so every change notifies the one mediator. - Write one
recheckthat re-derives everything from current values: recompute options, reset invalidated selections, update totals, gate submission. Deriving the whole state each time is what makes cascades (country → shipping → payment) fall out for free. - Keep the rules as data where they are data. Tables like
SHIPPING_BY_COUNTRYstay dicts the mediator consults — don't encode them as conditionals. - Test the mediator headlessly. The rules never needed a UI: set values, assert derived state — including the cascade paths.
form = CheckoutForm(cart_cents=5000)
form.country.set("CA")
form.shipping.set("express")
assert form.payment_options == ("card", "cod")- The notify wire is just a bound method (
add_fieldwires eachFieldtoself.recheck) — no observer framework, no signals library. - Recompute-everything beats surgical updates until profiling says otherwise: correctness first, the rules stay declarative.
- Components that are values-with-validation can be dataclasses; the
mediator subclass composes its fields in
__init__viaadd_fieldand ends with one initialrecheck()so derived state starts coherent.
- God-object drift — the mediator's budget is interaction rules; the moment domain logic (pricing, tax) moves in, split it: mediator coordinates, domain objects compute.
- Notification loops.
recheckwritingfield.valuedirectly (not viaset) is deliberate here — callingsetfrom inside the mediator would re-enter it. Keep one direction: components notify in, mediator writes out. - Hidden ordering dependencies between rules in
recheck— derive facts in dependency order (options before validity before gating) and test the cascade explicitly. - A queue would do. If your "rules" are only "pass work along",
queue.Queueis the whole mediator.
examples/checkout_form/ applies every step —
country/shipping/payment with cascading resets and submit gating:
uv run python -m patterns.behavioral.mediator.examples.checkout_form.main