The healthy road to patterns was never designing them up front: it is arriving at them from simple code when the pain appears — we've been repeating it since the scale and turned it into a method in 05-01. This lesson teaches the missing half: the mechanics. Refactoring means changing the structure of the code without changing its behavior, and doing it toward a pattern on live code — code that ships, that bills, that has users — demands a safety net, small steps that always compile, and the judgment to know when the effort doesn't pay off. We'll work through the catalog of typical transformations, each with its before/after in PideYa.
Contents
- What refactoring is (and what it isn't)
- The non-negotiable precondition: tests
- The discipline: small steps that always compile
- Catalog of refactorings toward patterns
- When NOT to refactor
- Technical debt: the language of cost/benefit
- Exercises and conclusion
What refactoring is (and what it isn't)
Martin Fowler's definition (Refactoring, 1999; 2nd ed. 2018): changing the internal structure of the code without changing its observable behavior. Both halves matter:
- If you change behavior (fix a bug, add a feature), you are not refactoring: you are developing. Mixing both in the same commit is the recipe for "no idea what broke this".
- If there is no net verifying that behavior is preserved, you are not refactoring: you are rewriting and crossing your fingers.
Joshua Kerievsky (Refactoring to Patterns, 2004) added the piece that ties this course together: patterns are not just a design destination, they are refactoring destinations — each one has sequences of safe steps for getting to it (and sometimes away from it, when it's surplus: we'll see that in anti-patterns). Both books are annotated in the course bibliography.
The non-negotiable precondition: tests
Before moving a single line, you need to be able to answer "does it still work?" in seconds. Three scenarios:
- There are tests covering the area: go ahead.
- There are no tests, but the code is testable: first write characterization tests — tests that document what the code does today, quirks included. They don't judge whether it is correct; they photograph the current behavior so any deviation gets detected.
- There are no tests and the code is an untestable knot (static dependencies, database, singletons...): use a golden master — run the code with a wide batch of inputs, save the outputs as the "master", and after every step compare byte for byte.
// Characterization test over PideYa's legacy commission calculation.
// CAREFUL: we do not assert that 4.15 is CORRECT — we assert it is what it does TODAY.
@Test
void characterization_standardRestaurantCommission() {
var calculator = new LegacyCommissionCalculator();
// Values obtained by RUNNING the current code, not from the specification:
assertEquals(new BigDecimal("4.15"), calculator.calculate(orderOf("27.90"), "STANDARD"));
assertEquals(new BigDecimal("0.00"), calculator.calculate(orderOf("0.00"), "STANDARD"));
assertEquals(new BigDecimal("2.50"), calculator.calculate(orderOf("27.90"), "PREMIUM"));
}If while writing them you discover behavior that looks like a bug: note it down and preserve it. It gets fixed later, in its own commit; during the refactoring, the reproduced bug is part of the contract ("same behavior" includes the defects).
The discipline: small steps that always compile
The golden rule: between one commit and the next, the code compiles and the tests pass. There is never a "broken intermediate state I'll fix at the end" — because that end sometimes never comes (an urgent issue arrives, and the branch dies). The cycle:
flowchart LR
A[Tests green] --> B[ONE small step:<br/>extract, move, introduce]
B --> C{Compiles and<br/>tests green?}
C -- Yes --> D[Commit]
C -- No --> E[Revert the step<br/>never debug on red]
E --> B
D --> F{Did we reach<br/>the pattern?}
F -- No --> B
F -- Yes --> G[Final cleanup + commit]
The steps are Fowler's atomic moves, which your IDE also automates (and an automated IDE refactoring is safer than a manual one): Extract Method, Extract Class/Interface, Move Method, Introduce Parameter Object, Replace Constructor with Factory Method, Replace Conditional with Polymorphism. A pattern is reached by chaining half a dozen of these moves — never in one leap.
Catalog of refactorings toward patterns
| Smell (symptom) | Refactoring | Destination pattern |
|---|---|---|
A switch/if-else over a "type", repeated across several methods |
Replace Conditional with Polymorphism | Strategy or State |
| A conditional deciding which class to instantiate, duplicated | Replace Constructor/Conditional with Factory | Factory Method |
| Telescoping constructor (ever-growing overloads) | Introduce Builder | Builder |
| A god class that orchestrates and executes everything | Extract Class + facade over what was extracted | Facade + collaborators |
| Nearly identical methods that differ in steps | Form Template Method | Template Method |
Type switch → Strategy (or State)
The most common smell. In PideYa, before lesson 04-10, the delivery fee calculation was:
// BEFORE: the same switch also appeared in estimateTime() and in feeDescription()
public BigDecimal calculateDeliveryFee(Cart cart, Address address) {
switch (feeType) {
case DISTANCE: return baseFee.add(pricePerKm.multiply(distance(address)));
case FLAT_RATE: return new BigDecimal("2.99");
case FREE: return BigDecimal.ZERO;
default: throw new IllegalStateException();
}
}The safe sequence (characterization tests first, commit after each step):
- Extract Method for each branch:
calculateByDistance(),calculateFlatRate()... The switch becomes a trivial dispatcher. Compiles, green, commit. - Extract Interface
DeliveryFeeCalculationwithcalculate(cart, address)and one implementation per branch, moving each extracted method into its class. The switch now chooses the class instead of executing the logic. Green, commit. - Replace the switch with the object: the context receives a
DeliveryFeeCalculation(injected or resolved once) and delegates. The switches in the other methods fall one by one the same way. Green, commit. - Cleanup: remove the enum if nothing uses it anymore, or keep it purely as a configuration key → strategy.
What about State? The exact same mechanics when the switch's "type" is a lifecycle stage with transitions (the OrderState of 04-09 was born this way from an enum with four switches). Which one applies is decided by the head-to-head in 04-13; the refactoring is the same.
Creation conditionals → Factory
When if (market.equals("ES")) new RedsysGateway() else new ConektaGateway() shows up for the second time: Extract Method on the conditional into a createGateway(market), then Move Method into a factory class (or a map of Suppliers like the NotifierRegistry), then replace each duplicate with the call. Three steps, three commits — and the new → simple factory → Factory Method evolution from 02-03 walked on real code. If the conditionals were creating coordinated families, the same road leads to Abstract Factory.
Telescoping constructor → Builder
Order(customer), Order(customer, coupon), Order(customer, coupon, notes, scheduled)... The road that breaks nobody:
- Create the
Builderalongside the existing constructors, delegating to the most complete one. Green, commit. - Migrate the call sites to the builder one by one (each migration compiles on its own). Commits.
@Deprecatedon the telescoping constructors; when the last usage disappears, delete them and move the validations intobuild(). Green, final commit.
Step 1 is the general technique for APIs with many consumers: build the destination in parallel, migrate gradually, demolish the old at the end — never a big bang.
God class → Facade + extractions
The legendary 800-line OrderManager (it validated, calculated, charged, notified, printed). It doesn't get "turned into a facade": it gets emptied. Sequence: Extract Class for each cohesive responsibility (OrderValidator, AmountCalculator, PaymentService...), leaving only coordination in OrderManager — which by the end is a legitimate Facade, often renamed to CheckoutFacade so the name tells the truth. The difference from the original god class: it no longer executes, it orchestrates; each extracted piece is testable on its own.
Format if/else → Template Method
The closing reports from 04-11: three nearly cloned methods (generateCsv, generatePdf, generateHtml) that loaded and aggregated identically but formatted differently. Form Template Method: make the three take the same shape (same steps, same order), Extract Method for each step, pull the shared ones up into an abstract superclass, leave the variable ones abstract. If the variable steps later need to combine at runtime, the destination evolves into Strategy — the boundary of the inheritance/composition head-to-head.
When NOT to refactor
Refactoring has a cost (time, risk, review) and only pays off if the code is going to change. Don't refactor:
- Stable code nobody touches: if the accounting export module hasn't changed in three years and no change is planned, its ugly switch hurts nobody. Ugliness is not debt if nobody is paying interest.
- Code that is going to die: if the integration gets replaced next quarter, polishing it is paying off a car on its way to the scrapyard.
- With no safety net and no way to build one right now: note the debt and wait for a better moment — refactoring blind turns "ugly code that works" into "pretty code that maybe doesn't".
- In the middle of something else: the "opportunistic refactoring" that bloats a bug-fix commit with 40 renamed files. Practical rule: refactor and feature in separate commits (ideally separate PRs).
Technical debt: the language of cost/benefit
Ward Cunningham's metaphor provides the language both for deciding and for explaining it to the business: suboptimal design is a loan (ship earlier in exchange for worse structure) and its interest is the surcharge on every future change in that area. Hence the management rules:
- Interest is only paid on code that changes. That is why refactoring is prioritized by change frequency × pain per change, not by ugliness. A
git log --since="6 months ago" --name-onlytells you where the hot spots are; crossing it with "where we suffer" points to the best-invested euro. - Pay it down with the boy scout rule: leave the code a little better than you found it, in the area you are already touching. Small, continuous, no permission needed.
- Large refactorings are paid for as projects: with a goal ("be able to add a new market in days instead of weeks"), not as "cleaning up code" — the business buys capability, not aesthetics.
Common Mistakes and Tips
- Refactoring without a net ("it's a small change, what could go wrong"). Characterization tests are written in an afternoon; the production bug is paid for in weeks of lost trust.
- The big bang: a three-week
refactor-everythingbranch that doesn't compile until day 15 and dies in merge conflicts. Small steps, continuous integration, always green. - Sneaking in behavior changes ("while I'm at it, I'll fix this"). It breaks the characterization contract and muddies the diff. Separate bug, separate commit.
- Refactoring toward the wrong pattern by skipping the diagnosis: the method from 05-01 comes before this lesson's mechanics. The sequence is symptom → diagnosis → destination → steps.
- Overshooting the destination: the smell called for Strategy and you ended up with Strategy + Factory + Observer "while we're here". Every extra pattern needs its own symptom — otherwise you have just manufactured the material for the next lesson.
- Tip: use the IDE's automated refactorings (Rename, Extract Method/Interface, Move) whenever they exist: they are verified transformations that don't break references.
- Tip: name the commits of the sequence ("step 2/5: extract DeliveryFeeCalculation interface") — your reviewer will thank you and you'll be able to revert surgically.
Exercises
Exercise 1: planning the sequence
PideYa's generateKitchenTicket(Order o, String format) method has an if-else over format ("THERMAL_58", "THERMAL_80", "SCREEN") with 70% of the code duplicated across branches (identical header and footer, different body). There are two tests covering "THERMAL_58" and nothing else. Write the complete plan: (a) which safety net you build first and how, (b) the destination pattern with its diagnosis, (c) the sequence of steps with their commit points.
Exercise 2: refactor or not?
Decide and justify with cost/benefit:
- The parser for a file format that a single provider stopped emitting; it is kept "just in case" and hasn't been touched since 2024.
- The
Checkoutclass has been modified in 14 of the last 20 sprints; every payment-method change forces touching 5 methods with conditionals overpaymentType, and there have already been two regressions. - A colleague proposes migrating all the project's getters/setters to records "to modernize", 300 classes, with no functional change planned for most of them.
Exercise 3: the big-step trap
During the migration of Restaurant's telescoping constructor to the Builder, a colleague proposes: "I'll delete the four old constructors right now and fix the 60 compilation errors this afternoon". Explain (a) the two concrete risks of that plan compared with the gradual migration, (b) which signal of the lesson's cycle it violates, (c) how to redirect it while keeping their goal of finishing this week.
Solutions
Solution 1: (a) Characterization tests for "THERMAL_80" and "SCREEN" (the missing ones): run the current code with representative orders (with extras, with notes, with empty notes) and pin the exact outputs as expected — if the ticket is multi-line text, a per-file golden master compared line by line is even more convenient. (b) Diagnosis: a fixed flow (header → body → footer) with per-format variable steps and no need for runtime switching → Template Method (via the head-to-head with Strategy). (c) Sequence: 1. make the three branches take the same shape (same steps in the same order) — green, commit; 2. Extract Method per step (printHeader, printBody, printFooter) — green, commit; 3. create an abstract KitchenTicket with the template method generate() and one subclass per format, moving the bodies — green, commit; 4. replace the if-else with subclass selection (probably a mini-factory, a second symptom, its own pattern) — green, commit; 5. delete the original method — green, final commit.
Solution 2: (1) No: frozen code with no planned changes — debt with no interest doesn't get paid down; at most, document it as a deletion candidate. (2) Yes, and with priority: extremely high change frequency × proven pain (5 methods, 2 regressions) — it is the exact hot spot the metric hunts for; the likely destination is Strategy for paymentType (plus perhaps a Factory for its creation), with prior characterization of the payment flows. (3) Not as a project: a massive change with no planned functional change = cost and risk today, speculative benefit; redirect it to the boy scout rule — migrate to records the classes already being touched for another reason, and only where the record doesn't alter the contract (equals/hashCode change!).
Solution 3: (a) Risk 1: those "60-error afternoons" don't compile until the last fix — if something urgent comes up halfway through, the branch is left for dead or a half-baked merge gets forced; risk 2: 60 mechanical fixes under pressure are 60 opportunities to subtly change semantics (did that constructor apply a default the builder doesn't?) without the compiler noticing. (b) It violates "between one commit and the next, the code compiles and the tests pass" — the prolonged broken state is exactly what the cycle forbids. (c) Same goal, different mechanics: a builder delegating to the complete constructor today (green within an hour), migrate the 60 usages in small batches over the week (each batch compiles and commits on its own, and can be split across the team), @Deprecated on Wednesday, delete on Friday once the IDE confirms zero usages.
Conclusion
Refactoring toward patterns is a complete discipline: a net of tests (characterization or golden master when there are none), atomic steps that always compile, the catalog of sequences — switch to Strategy/State, creation conditional to Factory, telescoping constructor to Builder, god class to Facade, format clones to Template Method — and the economic judgment of technical debt for choosing where to and where not to. Fowler provides the moves, Kerievsky the destinations, and the method from 05-01 the prior diagnosis. But this road runs in both directions: sometimes the problem isn't reaching the pattern but that someone overshot it — patterns planted without a symptom, Singletons hiding global state, factories of a single thing. Recognizing those excesses, and knowing how to undo them, closes the module: Anti-Patterns: When Patterns Become a Problem.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
