Refactoring Capstone

Learning Objectives

Refactor tightly coupled order/payment/notification code incrementally.

Compare dependencies before and after and justify each abstraction.

Prerequisites: Modules 19–22, 52–53.

Intuition and Definition

The starting point is a typical "it works, don't touch it" service: one function places an order, charges a card, writes to the database and sends an email — with concrete classes created inside, hidden globals, and no tests.

Pin behaviour with a characterization test (requires seams for I/O).

Introduce seams: interfaces for payment, repository and notifier (DIP).

Inject them (DI); the test uses fakes.

Extract the pricing rule (SRP) and replace the payment type switch with strategies only if more providers are coming.

Compare the dependency graph; remove anything that doesn't pay for itself.

Why It Matters

This is the most common refactoring task in industry: making untestable business code testable without a rewrite.

Syntax and Language Notes

Language-neutral; shown in C++ with fakes. The same steps apply with Mockito (Java), Moq/NSubstitute (C#), unittest.mock (Python) or Jest/Vitest (TypeScript).

Complete Example

#include <cassert>
#include <iostream>
#include <string>
#include <vector>

// ===== BEFORE (shown for reference; untestable: talks to "real" systems directly) =====
// void placeOrder(std::string customer, double amount, std::string card) {
//     double total = amount > 100 ? amount * 0.95 : amount;      // pricing rule buried here
//     if (!PaymentGatewaySingleton::get().charge(card, total)) throw ...;
//     MySqlDb db("prod"); db.exec("INSERT ...");                  // concrete database
//     SmtpMailer().send(customer, "Order confirmed");             // concrete email
// }

// ===== AFTER =====
struct Order { std::string customer; double total; };

struct Payment    { virtual ~Payment() = default;    virtual bool charge(const std::string& card, double amount) = 0; };
struct OrderStore { virtual ~OrderStore() = default; virtual int save(const Order&) = 0; };
struct Notifier   { virtual ~Notifier() = default;   virtual void send(const std::string& to, const std::string& text) = 0; };

class Pricing {                                     // extracted rule: one reason to change
public:
    double total(double amount) const { return amount > 100 ? amount * 0.95 : amount; }
};

c
…
ana@example.com: Order 1001 confirmed: 190.00
raj@example.com: Payment declined
capstone tests passed

Step-by-Step Walkthrough

Seams: Payment, OrderStore and Notifier interfaces replace the singleton, the concrete database and the mailer.

Injection: OrderService receives them in its constructor; production wiring passes real adapters, tests pass fakes.

SRP: the discount rule moved into Pricing, testable alone.

Tests now run in milliseconds and cover the declined-payment path, which was impossible to test before.

**What we did *not* add:** no strategy hierarchy for payment providers (only one exists), no repository generics, no event bus. Each abstraction above exists because a test needed a seam or a rule needed a home.

flowchart LR
  subgraph Before
    P1[placeOrder] --> G[PaymentGatewaySingleton]
    P1 --> D[MySqlDb]
    P1 --> M[SmtpMailer]
  end
  subgraph After
    S[OrderService] --> PI[Payment interface]
    S --> SI[OrderStore interface]
    S --> NI[Notifier interface]
    S --> PR[Pricing]
  end

Common Mistakes

Mocking everything, including value objects and pure functions — tests then mirror implementation details.

Introducing every pattern from this stage "while we're at it".

Changing behaviour during the refactoring (e.g. fixing the discount rounding) without a separate commit and test.

Side effects inside assert(...) — assert(svc.place(...) == Placed) silently stops placing the order in builds with NDEBUG defined (most release builds). Call first, assert on the stored result.

Trade-offs and Alternatives

The refactored version has more types but each is small, named and testable. If the code were a one-off script, the "before" version might be acceptable — refactor where change and risk justify it.

Real-World Use

Legacy modernization projects, preparing a monolith for extraction of services (seams first), and making business code testable before adding features.

Practice

Q1. (Easy) What is a seam?

Answer: A place where behaviour can be changed without editing the code there — typically an interface or injected dependency.

Q2. (Medium) Why test the declined-payment path?

Answer: It's an important alternative flow that real payment systems rarely trigger in development; with a fake it is trivial and fast to cover.

Q3. (Advanced) A second payment provider arrives. What changes now?

Answer: Add a second Payment implementation (adapter for the new provider) and choose between them at the composition root (or via a small strategy/factory keyed by customer settings). OrderService doesn't change.

Interview Question

Q1. "Walk me through making an untestable service class testable."

Answer: Identify hidden dependencies (singletons, new of infrastructure, static calls, time, randomness). Introduce interfaces for them at the service's level of abstraction (DIP), inject them through the constructor, and provide production adapters. Add a characterization test with fakes first if possible (or via a temporary seam such as an overridable factory method). Extract pure business rules into their own classes. Keep each step small and behaviour-preserving, and stop adding abstractions once tests and current requirements are served.

Quick Quiz

Q1. First step in the capstone?

Answer: Pin current behaviour with tests (after creating seams for I/O).

Q2. Which principle drives the interfaces here?

Answer: Dependency Inversion.

Q3. Should you add a payment strategy hierarchy for one provider?

Answer: No (YAGNI) — add it when a second provider is real.

Summary

Seams → injection → extracted rules → tests; stop when abstractions stop paying off.

Related: Modules 19–22, 52–53, 64–65.