
A pull request labeled "cleanup" renames functions, changes price rounding and replaces response fields. Tests pass because their expectations were updated alongside the implementation. When a customer notices a difference, the team must reconstruct which behavior it intended to preserve. The review has lost essential information: where reorganization ended and functional change began.
I recommend Refactoring for developing that discipline. Small moves allow us to improve a structure while retaining a reference for judging the result. The aim is to make the next change easier without hiding contract changes inside an apparently cosmetic diff. That requires choosing what to observe and acknowledging the limits of the available evidence.
This review covers Refactoring: Improving the Design of Existing Code, second edition, published in 2018. Fowler's own book page credits it to "Martin Fowler, with Kent Beck" and describes its organization: an opening example, discussion of fundamentals, signs of design problems and a catalog of transformations with motivation, mechanics and examples.
Second-edition examples use JavaScript, as the official refactoring site explains. The example in this review is original Python code. My recommendation does not depend on using the book's language: the benefit is in following a controlled sequence, identifying the reason for each move and adapting the reasoning to your own environment.
The catalog format also shapes how I suggest reading. Understand the process first, then consult a transformation connected to an actual problem. Learning every technique's name offers less benefit than practicing one while preserving a contract you can explain. That is where a reference book starts to improve daily work.
The following teaching scenario calculates a price with a membership benefit. Amounts are integer cents. Members pay 90% of the amount, using integer division. Negative amounts raise an error. The first version contains everything; the second version extracts only the member-price calculation. None of this code comes from the book.
Use Python 3 with no external dependencies. Save the block as example.py and run python3 example.py. Expected output is 7 value cases and 2 error checks passed.
def price_before(cents, member):
if cents < 0:
raise ValueError("negative amount")
return cents * 90 // 100 if member else cents
def member_price(cents):
return cents * 90 // 100
def price_after(cents, member):
if cents < 0:
raise ValueError("negative amount")
return member_price(cents) if member else cents
cases = [
(0, False, 0), (0, True, 0), (1, True, 0),
(99, True, 89), (100, True, 90),
(1001, True, 900), (1001, False, 1001),
]
for cents, member, expected in cases:
assert price_before(cents, member) == expected
assert price_after(cents, member) == expected
for function in (price_before, price_after):
try:
function(-1, True)
except ValueError as error:
assert str(error) == "negative amount"
else:
raise AssertionError("negative amount was accepted")
print("7 value cases and 2 error checks passed")
The one-cent case is deliberate: the resulting price is zero. That may prompt an important business discussion, but extraction should preserve the existing decision. Switching to another rounding rule would be a functional change. Even a desirable correction needs its own justification and examples expressing the new outcome.
Fixed expectations matter too. Comparing only the two versions could allow both to carry the same mistake unquestioned. The seven chosen outcomes provide an independent reference for this narrow scope. They do not establish equivalence for every possible input or validate every type Python permits callers to pass into these functions.
Free and paid products to turn ideas into a foundation you can actually ship.
13 products availableContinue exploring similar topics

Learn from Kent Beck how to turn requirements into small feedback steps without equating passing tests with the right product.

Use Michael Feathers' approach to begin changes in untested code, identify dependencies and capture existing behavior.

Apply Michael Nygard's approach to timeouts, isolation and recovery through a controlled failure exercise.
A 47-point checklist to find bugs, security risks, and performance issues before launch.
Production-tested templates trusted by developers. Save weeks of setup on your next project.
Fowler demonstrates reorganizing code before adding a capability in his preparatory refactoring example. My application here would be to extract member_price, keep the results stable and inspect the diff. If the business then requests another benefit tier, add its new cases and change the policy in an identifiable step.
That separation helps a reviewer. During extraction, the questions concern preserved behavior and whether the name helps. During the new tier, the question is whether the requested rule was implemented. Mixing those intentions forces reviewers to reconstruct both at once while the structure itself is moving.
This does not require opening a pull request for every moved line. Small steps can happen during development, supported by frequent tests, and appear as commits or reviewable differences according to context. The aim is to retain understandable reference points and avoid spending long periods with several unfinished transformations. The sequence should help the team locate a mistake, not create ceremonial work.
The example checks return values and a specific error. An application may expose other properties: event ordering, the number of writes, response fields and when an exception is raised. Extracting an expression that reads a clock or consumes an iterator requires care to preserve how often it is evaluated.
Operational commitments matter too. A transformation passing result tests can increase database queries or memory consumption. If latency or resource use belongs to the conditions of use, choose representative checks for those properties. Functional tests and performance measurements answer different questions; neither should be presented as a universal substitute for the other.
I would avoid updating expectations solely to turn a suite green during a structural change. A failed expectation requires investigation: did behavior regress, did the test depend on an internal detail, or was the contract misunderstood? Those diagnoses imply different decisions. A fragile test may need adjustment, but the adjustment still needs an explanation.
The reference boundary should also be chosen carefully. Testing every newly extracted helper can couple the suite to the decomposition you intend to keep improving. Prefer observable outcomes where they provide sufficient protection, and test a helper independently when its contract genuinely warrants it. Testing strategy affects how affordable the next refactoring will be.
A long function, a repeated rule and a vague name are useful starting points for review. My interpretation of the book's heuristics is to treat them as questions about change costs. A long function might express a coherent sequence; repeated text might belong to independently evolving rules. Appearance creates a suspicion without establishing which transformation will help.
In the example, extraction is defensible if member pricing becomes a concept we need to explain or modify independently. Without that need, the gain may be small. Adding helpers that merely restate operators would increase navigation without introducing a better idea. A technique catalog needs the accompanying ability to decide when to leave code alone.
Practiced critically, the method lets you assess each move before proceeding. If the new structure makes a rule harder to find, you can reconsider before also adding a feature. Reversing the step is easier to explain because it had a bounded intention. That matters when the first attempt at a better structure turns out to be the wrong one.
Renaming a private function and renaming a field consumed by other systems have different consequences. In the latter case, updating every known reference in the repository does not establish that every client has migrated. Documentation, outside integrations and persisted data may still carry the old contract.
I would use a gradual transition: introduce the new entry point while retaining the old one, delegate both to the same implementation, migrate consumers and retire compatibility in an agreed stage. This expand-then-contract approach adds work and needs a time limit to avoid preserving two interfaces indefinitely. It does not make a public break a simple internal reorganization.
Databases and external effects need similar care. After publishing a format or irreversibly writing data, reverting code may be insufficient. Review must account for mixed states, compatibility and recovery procedures. The small extraction shown here supplies no evidence for those situations; it teaches the method within a controlled context.
I would not begin broad reorganization during an incident without understanding how it helps stabilize the service. I would also postpone improvements to code about to be removed when the likely benefit does not justify the cost. When behavior is unknown and tests are absent, the first job may be establishing a dependable observation, rather than immediately applying a preferred transformation.
A catalog cannot set those priorities for a team. It supplies moves and motivations, but does not know your delivery window, external consumers or operational exposure. My recommendation requires justifying the investment through a change the structure is obstructing. Calling code ugly is insufficient authorization for a large migration.
Readers who can follow functions, conditionals and basic tests have enough foundation to begin. Existing code and a concrete intended change make the reading more useful. Someone still learning syntax can practice smaller examples before chaining several techniques. Experienced developers can use the catalog to improve the precision of reviews and mentoring.
Within this series, I would prioritize Working Effectively with Legacy Code when the obstacle is establishing protection around hard-to-test code. Test-Driven Development: By Example provides another practice for developing the feedback cycle. Clean Code helps frame readability discussions; Refactoring adds a way to conduct a transformation while preserving a reference.
Start with the book's opening example and reproduce one transformation at a time. Then choose one of your own functions, write down its observable contracts and select a small move. Record the test, result and navigation cost before and after. Introduce a new rule only once the reorganization is understood and checked.
My verdict is favorable because the book makes the process of change teachable and open to scrutiny. Its value depends less on reaching an ideal structure than on retaining confidence along the way. Small steps cannot remove every risk, but make it clearer what was preserved, what changed intentionally and which evidence supports that claim.