The Computing Series

How It Works

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.

Read in the book →