Chapter 5 — Bend, or Break

Topics 28–32 | Pages 226–288


Core Idea

To survive change, code must be flexible. Coupling is the enemy of flexibility. This chapter covers the major sources of coupling and how to eliminate them.


Topic 28 — Decoupling (p228)

Tip 44: Decoupled Code Is Easier to Change

Coupling is transitive: if A→B→C, then A is coupled to everything in B and C’s dependency chains. The more coupling, the larger the blast radius of any change.

Symptoms of a coupled system:

  • “Simple” changes break unrelated modules
  • Every change requires a meeting because no one knows what it will affect
  • Developers afraid to touch code
  • Wacky dependencies between unrelated modules

Train Wrecks

Method chains like customer.orders.find(id).getTotals().grandTotal traverse five abstraction levels and lock in all the implementation details of each layer. Business logic (e.g., “discount ≤ 40%”) can’t be enforced in one place.

Tip 45: Tell, Don’t Ask
Don’t fetch an object’s state, make a decision, then update it. Let the object make the decision itself. Delegate discounting to the totals object; delegate searching to the customer object.

Tip 46: Don’t Chain Method Calls
Try not to have more than one ”.” when accessing something (including via intermediate variables). Exception: chaining over stable things like standard library functions.

Globalization

Global data is an implicit extra parameter added to every function in the system. It couples all code that touches it. Test setup becomes a nightmare (must recreate global environment). Singletons that expose mutable state are just globals with longer names.

Tip 47: Avoid Global Data
Tip 48: If It’s Important Enough to Be Global, Wrap It in an API

Wrapping global/external state behind an API gives you control, allows change, and enables testing.


Topic 29 — Juggling the Real World (p239)

Modern software must react to events rather than dictate control flow. Four strategies for event-driven design:

1. Finite State Machines (FSM)

An FSM is a specification of how to handle events. It has:

  • A set of states, one of which is current
  • For each state: a mapping of events → new states (transition table)
  • Optionally: actions triggered on transitions

FSMs can be expressed as pure data (a transition table). Surprisingly few lines of code; vastly underused by developers. Great for protocols, parsers, workflows.

2. Observer Pattern

An observable object maintains a list of observers (callbacks). When an event occurs, it iterates the list and calls each callback. Simple to implement without a library. Limitation: observers must register with the observable (coupling), and callbacks run synchronously (performance bottleneck).

3. Publish/Subscribe (Pub/Sub)

Generalizes the observer pattern. Publishers write to named channels; subscribers register interest in channels. The channel infrastructure decouples publishers from subscribers entirely — they don’t need to know about each other. Async-capable. Downside: harder to trace what’s consuming which events.

4. Reactive Programming and Streams

Streams treat events as collections that grow over time. All collection operations (map, filter, combine, zip) apply to event streams. Enables composing complex event-driven logic in a declarative style. RxJS, RxJava, ReactiveX are common implementations. Unifies sync and async processing behind a common API.


Topic 30 — Transforming Programming (p255)

Tip 49: Programming Is About Code, But Programs Are About Data

Reframe programs as pipelines of transformations: data flows in one end, transformed data comes out the other. This is how Unix shell pipelines work; it’s how functional languages think.

Instead of: objects owning data and chattering state changes between each other
→ Think: data flowing through a series of functions, each transforming it

Tip 50: Don’t Hoard State; Pass It Around

Benefits:

  • Functions become small and focused (each does one transformation)
  • Code reads like prose (pipeline = problem statement)
  • Coupling drops because functions don’t share state
  • Functions become reusable wherever their input/output types match

Error handling in pipelines: wrap values in a result type ({:ok, value} / {:error, reason}). When an error occurs, it flows through the rest of the pipeline unchanged. No need for explicit error checking at every step.

Languages without native pipeline operators (|>) can simulate this with a series of assignments:

const content = File.read(file_name);
const lines   = find_matching_lines(content, pattern);
const result  = truncate_lines(lines);

Topic 31 — Inheritance Tax (p272)

Tip 51: Don’t Pay Inheritance Tax

Inheritance creates deep coupling: the child is coupled to the parent, the parent’s parent, and so on. Code using the child is coupled to all ancestors. Changing the parent’s API or internals silently breaks children and their callers.

Why inheritance exists (historical):

  • Simula: for combining types (mixing in capabilities like linked lists)
  • Smalltalk: for “differential programming” (this is like that except…)

Modern reasons developers use inheritance:

  1. Laziness (avoid typing by inheriting base class functionality)
  2. Type expression (Car is-a Vehicle hierarchies)

Both are problems. Deep type hierarchies become unmaintainable. Laziness leads to objects carrying unwanted APIs.

Three better alternatives:

TechniqueWhen to use
Interfaces / ProtocolsPolymorphism without inheritance — share type information, not implementation
DelegationUse Has-A instead of Is-A; object holds a reference to a service and delegates
Mixins / TraitsShare reusable behavior across classes without creating type relationships

Tip 52: Prefer Interfaces to Express Polymorphism
Tip 53: Delegate to Services: Has-A Trumps Is-A
Tip 54: Use Mixins to Share Functionality


Topic 32 — Configuration (p284)

Tip 55: Parameterize Your App Using External Configuration

Values that change between environments, customers, or deployments should live outside the code:

  • External service credentials
  • Logging levels and destinations
  • Ports, IPs, cluster names
  • Environment-specific validation parameters
  • Tax rates, license keys, site-specific formatting

Two approaches:

  1. Static configuration (YAML/JSON files, database tables) — read at startup
  2. Configuration-as-a-service (dynamic, behind an API) — multiple apps share it; changes take effect without restart; components subscribe to update notifications

Regardless of approach: wrap configuration behind an API, not a raw global struct. This decouples your code from the configuration representation.

Cautions:

  • Don’t make every field configurable — it creates a maintenance nightmare
  • Don’t push decisions to config out of laziness — make the call, get feedback

Key Takeaways

  1. Coupling is transitive and accumulates — the more dependencies you create, the harder change becomes.
  2. Tell objects what to do; don’t fetch, inspect, and update their internals.
  3. Don’t chain method calls deeper than one level; avoid global data; wrap externals behind APIs.
  4. FSMs, observer/pub-sub, and reactive streams are tools for event-driven design — choose based on coupling needs.
  5. Think of code as data transformations: pipelines are cleaner, more composable, and less coupled than OO chattering.
  6. Inheritance creates deep coupling; prefer interfaces, delegation, and mixins.
  7. Keep environment-specific values outside code; wrap them behind an API for maximum flexibility.