A function that exhibits control coupling:
function processOrder(order, mode):
if mode == "strict":
validate_aggressively(order)
charge_immediately(order)
elif mode == "deferred":
validate_minimally(order)
queue_charge(order)
elif mode == "test":
skip_validation(order)
log_only(order)
The caller must know which modes exist and what they mean. When a new
mode is needed, the caller learns internal logic of
processOrder. This is the failure signature of control
coupling: the interface exports branching knowledge.
The correction replaces the flag with a strategy (covered in Chapter 8):
interface OrderProcessor:
method process(order: Order) -> Result
class StrictOrderProcessor implements OrderProcessor:
method process(order):
validate_aggressively(order)
charge_immediately(order)
class DeferredOrderProcessor implements OrderProcessor:
method process(order):
validate_minimally(order)
queue_charge(order)
Callers depend on the interface. The choice of processor is a deployment decision, not a runtime flag. No caller needs to know the branching logic.
Common coupling example — the global config anti-pattern:
# Module level
PAYMENT_CONFIG = {
"timeout": 30,
"retry_count": 3,
"processor": "stripe"
}
# In payment_service.py
function charge(amount):
timeout = PAYMENT_CONFIG["timeout"]
...
# In notification_service.py
function notify_payment(result):
processor = PAYMENT_CONFIG["processor"]
...
Payment and notification services are now coupled through
PAYMENT_CONFIG. A test that modifies
PAYMENT_CONFIG["timeout"] affects both services.
Parallelising tests that touch this config produces race conditions.