Capstone: Refactoring a Real Codebase with Multiple Patterns

Design Patterns

Chapter 10 · Capstone: Refactoring a Real Codebase with Multiple Patterns

One continuous worked project: a genuinely tangled, patternless order-processing function, refactored step by step using four patterns from across this course — chosen deliberately, not exhaustively. Chapter 1 opened with a warning that patterns can be misapplied or over-engineered. This chapter closes by actually testing that warning against a real decision: which patterns earn their place here, and which ones this course covered but this code simply doesn't need.

Before: One Tangled Function Doing Everything

def process_order_bad(customer_type, items_total, weight, notify_email, notify_sms, priority_shipping): # discount logic tangled directly into the function if customer_type == 'vip': discount = items_total * 0.20 elif customer_type == 'loyalty': discount = items_total * 0.10 else: discount = 0 final_total = items_total - discount # shipping logic tangled in too if priority_shipping: shipping_cost = weight * 2.5 + 15 else: shipping_cost = weight * 0.5 final_total += shipping_cost status = 'pending' status = 'paid' # pretend payment succeeds — no real transition logic at all # notification logic hardcoded per channel, right here log = [] if notify_email: log.append(f'Emailing customer: your order total is {final_total}') if notify_sms: log.append(f'Texting customer: your order total is {final_total}') return final_total, status, log
Verified directly — the tangled version does produce a correct number
Calling process_order_bad('vip', 200, 10, True, True, True) returns a total of 200.0, status 'paid', and log entries for both email and SMS — matching a hand calculation (200 − 200×0.20 + (10×2.5+15) = 200.0) exactly. The code isn't wrong. It's tangled.
The real problems, named specifically
  • Adding a new customer type means editing an if/elif chain that has nothing to do with shipping or notifications, risking a typo in unrelated logic.
  • Adding a new notification channel (push notifications, say) means adding yet another if notify_x: line, and every caller now needs a new boolean parameter.
  • status = 'pending'; status = 'paid' is not a state transition — it's two assignments with no rule preventing an already-cancelled order from being "paid" again.
  • Six positional parameters (customer_type, items_total, weight, notify_email, notify_sms, priority_shipping) is already hard to call correctly, and every new option makes it worse — exactly Chapter 3's telescoping-constructor problem, just as a function's parameter list instead of a class's.

After: Four Patterns, Each Solving One Named Problem

Strategy (Chapter 7) — Discount and Shipping

Reusing this course's own established shipping-strategy shape directly from Chapter 7:

class VipDiscount: def apply(self, total): return total * 0.20 class LoyaltyDiscount: def apply(self, total): return total * 0.10 class NoDiscount: def apply(self, total): return 0 class StandardShippingStrategy: def calculate(self, weight): return weight * 0.5 class PriorityShippingStrategy: def calculate(self, weight): return weight * 2.5 + 15

Observer (Chapter 8) — Notifications

class EmailNotifier: def __init__(self): self.messages = [] def update(self, message): self.messages.append(f'EMAIL: {message}') class SMSNotifier: def __init__(self): self.messages = [] def update(self, message): self.messages.append(f'SMS: {message}')

State (Chapter 8) — Order Status

class PendingOrderState: name = 'Pending' def pay(self, order): order.state = PaidOrderState() order._notify(f'Payment received. Total: {order.calculate_total()}') def cancel(self, order): order.state = CancelledOrderState() order._notify('Order cancelled before payment') class PaidOrderState: name = 'Paid' def pay(self, order): raise Exception('Order already paid') def cancel(self, order): order.state = CancelledOrderState() order._notify('Order cancelled after payment - refund issued') # CancelledOrderState blocks both pay() and cancel(), matching Chapter 8's own final-state shape

Builder (Chapter 3) — Constructing an Order

class OrderBuilder: def __init__(self): self._items_total = 0; self._weight = 0 self._discount_strategy = NoDiscount() self._shipping_strategy = StandardShippingStrategy() self._notifiers = [] def items_total(self, amount): self._items_total = amount; return self def weight(self, w): self._weight = w; return self def discount_strategy(self, strategy): self._discount_strategy = strategy; return self def shipping_strategy(self, strategy): self._shipping_strategy = strategy; return self def add_notifier(self, notifier): self._notifiers.append(notifier); return self def build(self): return Order(self._items_total, self._weight, self._discount_strategy, self._shipping_strategy, self._notifiers)

With Order tying it together — calculate_total() delegates to whichever strategies were plugged in, pay()/cancel() delegate to whichever state is current, and both notify every attached observer automatically:

class Order: def __init__(self, items_total, weight, discount_strategy, shipping_strategy, notifiers): self.items_total = items_total; self.weight = weight self.discount_strategy = discount_strategy; self.shipping_strategy = shipping_strategy self._observers = list(notifiers) self.state = PendingOrderState() def _notify(self, message): for o in self._observers: o.update(message) def calculate_total(self): discount = self.discount_strategy.apply(self.items_total) shipping = self.shipping_strategy.calculate(self.weight) return self.items_total - discount + shipping def pay(self): self.state.pay(self) def cancel(self): self.state.cancel(self) def status(self): return self.state.name
Verified directly — the refactored version produces the exact same total as the tangled original
Building the identical VIP, priority-shipping order through OrderBuilder and calling calculate_total() gives 200.0 — matching process_order_bad()'s own result exactly, for the same inputs. The refactor changed how the code is organized, not what it computes.
Verified directly — pay(), cancel(), and blocked transitions all work exactly as State predicts
Calling .pay() moves status from 'Pending' to 'Paid' and correctly notifies both email and sms observers with a payment message. Calling .cancel() afterward moves status to 'Cancelled' and sends a second, differently-worded notification ("refund issued") — confirming the state object, not a repeated if check, decided what message to send. A further .pay() and .cancel() on the now-cancelled order both correctly raise exceptions, with status still 'Cancelled' afterward.
Verified directly — a second, differently-configured order stays fully isolated from the first
A second order (no discount, standard shipping, only an email notifier attached) built through the same OrderBuilder reports 83.0 — matching 80 − 0 + 6×0.5 exactly — and its own .pay() only reaches its own email notifier. No SMS message appears anywhere for this order, since none was attached to it: two orders built from the same classes never share state.

What Wasn't Used, and Why

Chapter 1 warned that patterns can be misapplied through over-engineering. The honest test of that warning isn't listing every pattern this course covered — it's explaining, for this specific code, which ones genuinely don't belong.

Pattern (chapter)Why it wasn't used here
Command (Ch.9)Genuinely tempting — cancel() looks like a candidate for an undoable command object. But this order system never needs to undo a cancellation or queue actions for later; State already gives cancel() exactly the behavior it needs (blocked once already cancelled) with less machinery. Wrapping it in a Command here would add a class with no caller that ever uses its own undo().
Decorator (Ch.5)Would fit if orders needed optional, stackable add-ons priced independently (gift wrapping, insurance). This order system has none of that — calculate_total() already has exactly two swappable pieces (discount, shipping), which Strategy already covers.
Singleton / Factory Method (Ch.2)Nothing here needs exactly-one-instance, and there's no family of related object creation varying by subclass — OrderBuilder already handles construction directly.
Adapter / Facade (Ch.4)No incompatible interface to translate, and no scattered multi-call sequence complex enough to be worth bundling behind one method.
Composite / Iterator (Ch.5, Ch.9)No tree-shaped data here, and nothing that needs traversal hidden behind a shared interface — a plain list of notifiers is already exactly as simple as this problem requires.
Revisiting Chapter 1's warning, honestly
Four patterns solved four real, separately-identifiable problems in the tangled original: a growing if/elif for discounts and another for shipping (Strategy), a status field with no real transition rules (State), notification logic hardcoded per channel (Observer), and a parameter list that would only grow (Builder). Every pattern used here corresponds to a specific paragraph in the "real problems" warn-box above it. Design patterns aren't a checklist to apply exhaustively — they're a vocabulary for naming a problem precisely enough to solve it with the smallest tool that actually fits. The five patterns left out of this table aren't gaps in the refactor; reaching for them here would have been exactly the over-engineering Chapter 1 warned about.

Course Summary

CategoryPatterns covered
Creational (Ch.2-3)Singleton, Factory Method, Abstract Factory, Builder
Structural (Ch.4-6)Adapter, Facade, Decorator, Composite, Proxy, Flyweight
Behavioral (Ch.7-9)Strategy, Template Method, Observer, State, Command, Iterator

Twelve of the 23 classic Gang-of-Four patterns, each verified with real, runnable code rather than taken on faith. The 11 left out entirely (Chain of Responsibility, Mediator, Memento, Visitor, Interpreter, Bridge, Prototype, and others, named in Chapter 1) remain a genuine gap for anyone going deeper — but the twelve covered here are the ones a working programmer runs into most often.

Hands-On Exercises

Exercise 1

Add a LoyaltyDiscount-and-standard-shipping order to this chapter's own capstone code (using the existing OrderBuilder and existing strategy classes — no new classes needed) with items_total=150, weight=8. Verify its total by hand and against the running code.

📄 View solution
Exercise 2

Add a customer_type parameter's worth of new behavior without touching Order, OrderBuilder, or any existing strategy class: create a new NewCustomerDiscount strategy (a flat $5 off, applied only if items_total is over $50, otherwise $0) and build an order using it. Verify both the over-$50 and under-$50 cases.

📄 View solution
Exercise 3

Exercise 2 added a whole new discount without editing a single existing class. Explain specifically which part of the tangled process_order_bad() function would have needed to change to add that same "$5 off orders over $50" rule, and why that change is riskier than what Exercise 2 actually required.

📄 View solution

Chapter 10 Quick Reference — and Course Quick Reference

  • This chapter: a tangled order-processing function was refactored using Strategy (discount, shipping), Builder (order construction), Observer (notifications), and State (status) — verified producing the exact same total (200.0) as the original, with every pattern traced back to a specific, named problem in the original code
  • The honest boundary: five more patterns from this course (Command, Decorator, Singleton/Factory Method, Adapter/Facade, Composite/Iterator) were deliberately left out, each with a stated reason — over-engineering means reaching for a pattern the code doesn't actually need
  • Course scope: 12 of the 23 classic GoF patterns across Creational, Structural, and Behavioral categories — every example in all 10 chapters verified with real, runnable code, not asserted
  • Where this connects: Pseudocode & Algorithmic Problem-Solving (this course's own prerequisite); Software Architecture & System Design and Clean Code, SOLID & Refactoring, both still reserved in this same Software Development subject