Lecture Slides

Chapter 11: Refactoring and Legacy Code

SE506: Object Oriented Development and Agile Practices

Dr. Polla Abdulhamid Fattah
Department of Computer Science & Artificial Intelligence
University of Kurdistan Hewlêr (UKH)

Chapter Roadmap (1 of 3)

  • 11.1 Working with unfamiliar code
    • Observe, seam, pin, reshape, then change behaviour.
  • 11.2 Smells: prompts to investigate, not verdicts
    • A smell is only worth acting on when it blocks a change.
  • 11.3 Pinning what the code does: characterisation tests
    • Record what the code does, even when it looks wrong.
  • 11.4 Seams: getting hidden dependencies out of the way
    • Clock, locale and standard output.

Chapter Roadmap (2 of 3)

  • 11.5 Reshaping the report: Extract, Move and Rename
    • Four stages of one overdue report.
  • 11.6 Small steps, and how to step back
    • One step, one check, one commit.
  • 11.7 Technical debt: say it with evidence
  • 11.8 Reviewing an AI refactoring patch review a refactoring patch.

Chapter Roadmap (3 of 3)

  • 11.9 Refactoring habits that mislead
  • 11.10 Try it yourself: pin, seam and refactor the report
    • The book’s project B1 to B7; your team’s project T1 to T4.
  • 11.11 Questions to think with, transfer challenge and Quiz 2 revision
    • An ML-serving analysis script.
    • Quiz 2 revision pointers (Chapters 6 to 10).
  • What to take away, and what comes next
    • Chapter 12: measuring progress and flow.

Learning Outcomes (1 of 2)

By the end of this chapter, you should be able to:

  1. Diagnose a smell from a concrete difficulty of change, instead of treating it as a verdict.
  2. Distinguish a refactoring from a behaviour change, and state what must stay the same.
  3. Write characterisation tests that record current behaviour, including behaviour that looks wrong.

Learning Outcomes (2 of 2)

  1. Create a seam that removes a hidden dependency, so that code can be tested.
  2. Apply small refactorings (Extract Method, Extract Class, Move Method, Rename) with a check after each step and a way back.
  3. Record uncertainties, suspected defects and technical debt separately, with evidence.

Where This Chapter Fits

  • Supports LO3 and LO4. Readings: the refactoring chapter of Martin; Feathers on legacy code is the recommended companion. The Week 10 lab is refactoring untested legacy code with regression tests.
  • Quiz 2 is held in Week 10 and covers dependency injection, TDD and test doubles, design patterns and layered architecture (Weeks 5 to 9). The legacy lab is separate from that scope.
  • You start with: Chapter 10’s project and package structure. This chapter adds one isolated package, legacy, standing for the old records system’s overdue report. Production code must not refer to it, and the dependency test now checks that.

How to Use This Chapter

  • Core reading: Sections 11.1 to 11.6.
  • Working knowledge: 11.7 (technical debt and evidence): describe debt with evidence and separate defects from preferences. You do not manage a debt register for a real project.
  • Minimum lab: B1 to B3 (the book’s project: extend the supplied tests, take the first refactoring steps) and T1 to T4 (your team’s project: characterise one untested method and refactor it in small commits).
  • If you want more: B4 to B7, the review questions, the transfer challenge.

Opening Scenario: An Old Overdue Report

  • It lists every unreturned loan past its due date and adds a fine.
  • A laptop two days late shows a fine of 0.00 yet still counts as overdue.
  • A colleague on another machine sees amounts printed as 0,50 where everyone else sees 0.50.
  • The office asks for a small change: staff should get seven days of grace instead of five.

You Open the Code

  • One method of about forty lines.
  • The staff rule in one branch, the student rule in another, and a third branch for anyone else.
  • The grace number, five, appears twice in the staff branch; changing only one is a bug. The same literal 5 also stands for the cap, so a search-and-replace would change the wrong thing too.
  • It reads today’s date from the system clock: its output changes every day.
  • Nobody wrote tests, and the author has left.

A Cautionary Tale

  • A colleague once tidied a report like this one. The new version was shorter. It also listed fewer loans.
  • Nobody could say whether a defect had been repaired or a promise broken: nobody had recorded what the old report promised.
  • So what do you change first? Not the staff grace period.
  • Before changing a rule, know what the code does today, so you can tell afterwards that you changed only what you meant to.

The Method of This Chapter

  1. Observe.
  2. Pin what you observe.
  3. Reshape the structure in small checked steps.
  4. Keep every correction of behaviour separate from every improvement of structure.
  • Changing code you do not yet understand, without tests that protect it.

11.1 Working with Unfamiliar Code

Legacy means no tests

What Makes Code “Legacy”

  • Feathers: legacy code is code without tests. Age does not matter.
  • Code written last month becomes legacy when you must change it, cannot tell what a change will break, and nothing will tell you.
  • The report is old, but the habits that produced it (global state, hidden dependencies, one long method) are not.

The Dilemma

  • Refactoring changes internal structure while external behaviour stays the same.
  • To know behaviour stayed the same you need tests.
  • But the code is hard to test because of its structure, so you want to refactor first.
  • The way out is a short sequence.

The Way Out: Five Steps

  1. Observe: run the code and read it with a specific question in mind. You need not understand all of it.
  2. Make the smallest edit that lets you test it: a seam. Keep it mechanical, so the compiler and a quick read can check it.
  3. Pin: characterisation tests that record what the code does now.
  4. Reshape: refactor in small steps, tests after each.
  5. Only then change behaviour, in a separate step with its own tests.

Figure 11.1: The Order of Work

Observerun and readSeamsmallestmechanical editPincharacterisationtestsReshapesmall refactorings,test after eachChange behavioura separate step,new testsRoll back the stepa test failsredo itDebug backwards, not forwards:undo the last step and redo it.

The Code as Found (1 of 3)

public static String build(List<LoanRow> rows) {
    String out = "";
    double total = 0;
    int count = 0;
    LocalDate today = LocalDate.now();
    out += "OVERDUE REPORT " + today + "\n";
    for (int i = 0; i < rows.size(); i++) {
        LoanRow r = rows.get(i);
        if (r.returned) {
            continue;
        }
        long late = ChronoUnit.DAYS.between(r.due, today);
  • Read it once and write down what you notice before reading on.

The Code as Found (2 of 3)

        if (late > 0) {
            double fine = 0;
            if (r.kind.equals("S")) {
                if (late > 2) {
                    fine = (late - 2) * 0.50;
                    if (fine > 10) {
                        fine = 10;
                    }
                }
            } else if (r.kind.equals("T")) {
                if (late > 5) {
                    fine = (late - 5) * 0.25;
                    if (fine > 5) { fine = 5; }
                }
            } else {
                fine = 1.00 * late;
            }

The Code as Found (3 of 3)

            out += r.item + " " + r.borrower + " " + late
                    + " day(s) late, fine " + String.format("%.2f", fine) + "\n";
            total += fine;
            count++;
        }
    }
    out += count + " overdue, total fines " + String.format("%.2f", total) + "\n";
    return out;
}
  • LoanRow is the old system’s export format: item, borrower, kind ("S" student, "T" staff), due date, returned flag, all public mutable fields. Other programs read it, so it stays as it is.

Separate What You Know From What You Guess

ObservationHow you knowStatus
Returned loans are skippedRead: continue on returnedKnown
Fines start after a grace period and are cappedRead: two nested comparisons per kindKnown
Neither "S" nor "T" pays 1.00 a day, no grace, no capRead: the final elseKnown what, unknown whether intended

Separate What You Know From What You Guess (2)

ObservationHow you knowStatus
The output depends on the date the program runsRead: LocalDate.now()Known
Amounts print with a comma in some localesString.format without a locale; %f localises the separatorLikely; confirm by running
A loan two days late is listed with a fine of 0.00 and countedThe count is incremented inside late > 0Likely; confirm by running
  • “Unknown whether intended” is an honest status. It is not your job yet to decide it is wrong.

11.2 Smells

Prompts to investigate, not verdicts

What a Smell Is

  • A surface indication that usually corresponds to a deeper problem: not always a problem.
  • Its use here: a prompt to investigate.
  • Only worth acting on when you can name a concrete difficulty of change that it causes.

Smells in the Report (1 of 3)

SmellThe difficulty it causes
Duplicated branches (student and staff blocks have the same shape)A change to the rule shape must be made in every branch; the grace number appears twice in each, so editing one of the two gives a wrong result
Long methodTo change the fine you must read the loop, the formatting and the totals too

Smells in the Report (2 of 3)

SmellThe difficulty it causes
Hidden dependency on the system clockThe output differs every day: no test can compare it with a fixed answer
Hidden dependency on the default localeThe same data prints differently on two machines
Magic numbers (2, 0.50, 10, 5, 0.25)A reader cannot tell a grace period from a cap

Smells in the Report (3 of 3)

SmellThe difficulty it causes
A string kind compared with equalsAn unexpected kind falls into the final branch without anyone deciding that
String += in a loopNot a difficulty of change here; a possible slowness for very long reports
  • The last row is a deliberate counter-example.

Not Every Smell Needs Repair

  • A smell in code nobody changes costs very little. Its repair value depends on how often the code changes and how much the difficulty costs.
  • Decide where to spend effort by asking which changes are coming.
  • The office has just asked for a grace-period change: the duplicated branches and magic numbers are worth fixing now.
  • The String += loop is not.

11.3 Pinning What the Code Does

Characterisation tests

Two Different Promises

  • A regression test protects behaviour that everyone agrees is correct.
  • A characterisation test records what the code does today, whatever that is, so you notice if a change alters it.
  • They look the same in the test framework.
  • A passing characterisation test means “unchanged”. It does not mean “correct”.

How to Write One (1 of 2)

  1. Isolate the code so it can run in a test (often the hardest part).
  2. Write a test with a placeholder expectation, a value you know to be wrong.
  3. Run it and read the actual result from the failure message.

How to Write One (2 of 2)

  1. Record that result as the expectation, after you have looked at it.
  2. Rename the test to say what you now understand the code does.
  3. Keep exploring with further inputs, especially boundaries and odd cases.
  • Step 4 holds the discipline: do not paste the result blindly. Look at each one and classify it.

Classify Every Result

ClassMeaningWhat you do
ExpectedMatches the rule you were toldKeep the test
SurprisingYou did not expect it, but it may be intendedKeep the test; ask the owner
Suspected defectIt looks wrongKeep the test, mark it clearly, record the question; do not fix it here
UnknownYou cannot tellKeep the test and record the question

What the Report Does: Fines by Days Late

0.005.0010.0020.0030.00051015202530days latestudentstaffother kindscap at 22 dayscap at 25 daysno grace, no cap(suspected defect)

Observed Fines (1 of 2)

KindDays lateFineComment
S1, 20.00Listed and counted with no fine (grace)
S30.50First fine
S104.00
S22, 23, 10010.00The cap is reached at 22 days and holds
T1 to 50.00Listed and counted with no fine (grace)
T60.25First fine
T25, 26, 1005.00The cap is reached at 25 days and holds

Observed Fines (2 of 2)

KindDays lateFineComment
X (unknown)11.00Suspected defect: no grace
X (unknown)100100.00Suspected defect: no cap
s (lower case)33.00Suspected defect: a typo in the data costs six times the student fine
blank22.00Same branch
  • Run on the supplied code with today fixed at 20 October 2026.

Four More Observations

  • A loan due today or later is not listed.
  • A returned loan is ignored.
  • A null kind throws NullPointerException (and only for a loan that is actually overdue).
  • Under a German locale the amounts print with a comma.
  • One of each class: the grace listing and the comma are surprising; the unknown kinds are suspected defects; the null-kind failure is unknown.

One Test, in the Style

// SUSPECTED DEFECT, pinned on purpose: any other kind pays 1.00 a day
// with no grace and no cap.
@ParameterizedTest
@CsvSource({"X,1,1.00", "X,100,100.00", "s,3,3.00"})
void otherKindsPayTheFullRateWithNoGraceOrCap(String kind, int daysLate, String fine) {
    assertEquals(oneLoan(daysLate, fine), OverdueReport.build(List.of(open(kind, daysLate)), TODAY));
}
  • The comment is part of the test. In six months someone sees it fail during an unrelated change: the comment tells them whether to fix their change or ask a question.

A Second Net: A Differential Check

  • Keep a copy of the original code as an oracle, generate thousands of inputs with a fixed seed, compare old and new output.
  • The companion’s test runs 3,000 generated lists.
  • Stronger than the examples for the kinds of input it generates. It says nothing about inputs outside them.
  • Neither net proves equivalence: they show a chosen set of behaviours is unchanged. Add more where you are about to change more.

11.4 Seams

Getting hidden dependencies out of the way

What a Seam Is

  • A seam is a place where you can change what the program does without editing the code at that place.
  • To use it you need an enabling point: somewhere you can choose the other behaviour.
  • The report has three hidden dependencies, each needing a different answer.

Three Hidden Dependencies

Hidden dependencyWhere it isHow to isolate it
The system clockLocalDate.now() inside buildA parameter seam: add build(rows, today); the old method calls it
The default localeString.format("%.2f", ...)Pin the locale in the test and restore it afterwards
Standard outputprint writes to System.outCapture System.out in one test and restore it afterwards

The Clock Seam

public static String build(List<LoanRow> rows) {
    return build(rows, LocalDate.now());
}

public static String build(List<LoanRow> rows, LocalDate today) {
    String out = "";
    double total = 0;
    int count = 0;
    out += "OVERDUE REPORT " + today + "\n";
    // ... the rest of the method is unchanged

Why This Edit Is Safe Before Any Test

  • It is mechanical: one expression moves from the body to a call, one line is deleted.
  • It is checked by the compiler: if anything else changed, the code would not compile.
  • It is visible to a reviewer: a few lines, readable in a minute.
  • Existing callers see no difference. This is Chapter 7’s injected Clock at the smallest scale: a parameter suffices for a static method.
  • A bigger seam (injected Clock, non-static class) is better design but an untested edit: the very risk the chapter is about.

Check the Enabling Edit by Running It

  • The companion’s probe contains the original report with only its class name changed (OriginalUnseamed) and a program comparing the original with the seamed report through their public one-argument entry points.
  • It compares five rows (a late student, a late staff member, a row due today, an unknown kind, a returned loan) and an empty list, and checks one literal result.
  • It refuses to run if the date changes while it executes.
  • Expected: Original and seamed reports agree on 5 rows and on an empty list.

The Probe Is Bounded

  • It examines a few rows, not every input, and cannot tell you the report is correct.
  • What it adds to the compiler and your eyes: a direct comparison of the code as found with the code after the only untested edit.
  • Keep it small, run it once, then put the real protection in the characterisation tests.

Who Chooses the Zone?

  • The report now takes a date, so the caller decides which calendar date “today” is.
  • A Clock carries a zone; LocalDate.now(clock) converts its instant using it.
  • At 22:30 UTC on 21 October it is already 22 October in Baghdad. A loan due on 21 October is not late in UTC and one day late in Baghdad.
  • The seam did not remove the question of which zone is right. It moved it to the one place that has to answer it.

The Locale and the Output Stream

  • Left alone, because changing them would be a bigger edit than the tests justify. The tests deal with them instead:
    • a setup sets the default locale to Locale.ROOT before each test and restores it;
    • one test sets Locale.GERMANY and records the comma;
    • one test replaces System.out and restores it in a finally block.
  • Both are global, process-wide state. Such tests cannot safely run in parallel, and one that forgets to restore breaks an unrelated test far away.

Other Seams Worth Knowing

  • Subclass and override: make a class non-final; a test subclass replaces one method.
  • Extract interface: put a dependency behind an interface (Chapter 6’s move).
  • Both change more code than a parameter does: choose the smallest seam that gets the code under test.
  • A better design (a PrintStream parameter on print, a locale argument) is worth adding when you have a reason to change those lines.

11.5 Reshaping the Report

Extract, Move and Rename

Refactorings Have Names

  • So a team can say what it did and a reviewer can check it.
  • Fowler’s catalogue: Extract Function, Move Function; the first edition called them Extract Method and Move Method. Used interchangeably.
  • Each stage below was a separate step; the characterisation tests and the differential test passed after each.

Stage 2: Extract Method

double fine = fineFor(r.kind, late);
out += r.item + " " + r.borrower + " " + late + " day(s) late, fine " + money(fine) + "\n";
...
private static double fineFor(String kind, long late) { /* the three branches, moved as they were */ }
private static String money(double amount) { return String.format("%.2f", amount); }
  • The behaviour is the same because the moved code is the same.
  • The loop reads in one glance, and the formatting rule (and its locale dependency) lives in exactly one place.

Stage 3: A Missing Concept

  • The three branches differ only in three numbers: free days, a daily rate, a cap.
  • That is a missing concept, and it can be a small class.
record FinePolicy(int graceDays, double ratePerDay, double cap) {
    double fineFor(long daysLate) {
        if (daysLate <= graceDays) {
            return 0;
        }
        return Math.min((daysLate - graceDays) * ratePerDay, cap);
    }
}

Stage 3: Three Named Policies

private static final FinePolicy STUDENT = new FinePolicy(2, 0.50, 10.00);
private static final FinePolicy STAFF = new FinePolicy(5, 0.25, 5.00);
/** Suspected defect, preserved on purpose: any other kind pays 1.00 a day
 *  with no grace and no cap. */
private static final FinePolicy ANY_OTHER_KIND = new FinePolicy(0, 1.00, Double.MAX_VALUE);
  • fineFor moved into the new class (Move Method).
  • The odd third branch did not disappear: it now has a name, a comment and a policy of its own.

Is the New Code Equal to the Old?

Old codeFinePolicy.fineFor(daysLate) with the same numbers
if (late > 2) not taken: fine stays 0daysLate <= graceDays returns 0
fine = (late - 2) * 0.50; if (fine > 10) fine = 10;Math.min((daysLate - graceDays) * ratePerDay, cap)
Staff: the same with 5, 0.25 and 5The same with those values
Other kinds: fine = 1.00 * lateGrace 0, rate 1.00, cap Double.MAX_VALUE
  • Check it algebraically, then let the tests confirm the algebra for the inputs they cover.

Figure 11.2: After Stage 3

OverdueReport+build(List<LoanRow>) String+build(List<LoanRow>, LocalDate) String+print(List<LoanRow>) void-policyFor(String) FinePolicy-money(double) String«record»FinePolicyint graceDaysdouble ratePerDaydouble cap~fineFor(long) doubleLoanRow+String item+String borrower+String kind+LocalDate due+boolean returnedholds threereadsSTUDENT, STAFF,ANY_OTHER_KIND

What the Refactoring Did Not Do

  • The odd third branch is not hidden: hiding a suspected defect inside tidy code is the opposite of what refactoring is for.
  • LoanRow is unchanged; the signatures of build and print are unchanged, because other programs call them.
  • The solid arrow means OverdueReport holds three FinePolicy values; the dashed arrow means it only uses LoanRow.

Stage 4: Rename and Tidy

  • r becomes row, late becomes daysLate; a for-each loop; StringBuilder instead of String +=; count and total become overdueCount and totalFines.
  • None of this changes the output.
long daysLate = ChronoUnit.DAYS.between(row.due, today);
if (daysLate > 0) {
    double fine = policyFor(row.kind).fineFor(daysLate);
    report.append(row.item).append(' ').append(row.borrower).append(' ')
            .append(daysLate).append(" day(s) late, fine ").append(money(fine)).append('\n');
    totalFines += fine;
    overdueCount++;
}

The Request That Started the Chapter

  • The staff grace period is now a one-number change: new FinePolicy(7, 0.25, 5.00).
  • It is a functional change: it waits for its own commit and test change.
  • Run the suite after it: five of the 28 report invocations fail: the rows T,6 (0.25 becomes 0.00), T,25 (5.00 becomes 4.50), T,26 (5.00 becomes 4.75), the mixed-list test and the differential test.
  • Those failures are the expected evidence of the intended change. Update the expectations in the same commit as the rule.

11.6 Small Steps, and How to Step Back

One step, one check, one commit

The Commit Sequence

CommitKindWhat the reviewer checks
1. Add a date parameter to build (seam)Mechanical edit before testsThe old method delegates and nothing else moved
2. Add characterisation tests and the differential testTests onlyEvery expectation was run and reviewed; surprises commented
3. Extract fineFor and moneyRefactoringThe moved code is identical; all tests pass
4. Extract FinePolicyRefactoringThe algebra; all tests pass; the odd policy is visible
5. Rename and tidyRefactoringNo behavioural edits hide among the renames

Do Not Mix

  • Mixing a refactoring and a behaviour change in one commit is the most common way to lose control.
  • A failing test no longer says which of the two caused it.
  • Chapter 5 made the same point with its two-stage change.
  • The sixth commit in the companion (rejecting unknown kinds) is a different kind and reviewed as one: its message says what behaviour changed and why, and its test diff shows old expectations deliberately replaced.

Decide What a Difference Means

A check reportsa differenceWas this behaviourchange agreed?Inspect or undothe latest editSeparate thefunctional changeRecord the rule andaffected examplesRerun the unchangedexpectationsNoYes

Reading Figure 11.3

  • A check reports a difference: ask one question before touching anything: was this behaviour change agreed?
  • An agreement needs a specific rule: “clean up the report” is not one.
  • Until the records office agrees a rule for unknown kinds or for loans inside their grace period, a difference at those boundaries is an unintended change.

Roll Back, Do Not Debug Forward

  • After a step, a failing test gives you a precise failing change and a green state one step behind.
  • Undo the step (git restore for uncommitted work, git revert for a commit) and redo it more carefully.
  • Debugging forward from a half-changed structure is slower and riskier.
  • The failing test is evidence about what the step got wrong: keep its name and message in your notes.

Separate the Functional Correction

  • Suppose the records office confirms only "S" and "T" exist and anything else is a data error.
  • That is a functional change: its own branch and commit. policyFor throws IllegalArgumentException("Unknown borrower kind: ..."); ANY_OTHER_KIND disappears.
  • The tests that pinned the suspected defect are replaced deliberately in the same commit, and the message says so.
  • The differential test is restricted to the kinds that did not change.
  • The rule itself is illustrative: the records office decides it, not the developer.

11.7 Technical Debt

Say it with evidence

The Metaphor

  • “Technical debt”: attributed to Ward Cunningham, the accumulated cruft that makes a system harder to change.
  • The extra effort of every later change is the interest; cleaning up is paying down the principal.
  • It helps in conversations with people who do not read code. It misleads when it excuses neglect.
  • Fowler notes the cost cannot be measured objectively, only estimated.

Attach Evidence

  • “This code is messy” is an opinion.
  • “A change to the staff grace period needs two edits in one branch and breaks silently if one is missed, and there is no test” is a statement a colleague can check.
  • After the refactoring the same change is one number, protected by 27 characterisation tests.

A Register Entry

FieldEntry for the report
ItemOverdue report: fine rules duplicated, hidden clock and locale
EvidenceGrace number appears twice per branch; output varies by date and locale; no tests
Cost (interest)Each fine change needs careful edits in several places and a manual check
DecisionPaid down: seam, tests and refactoring in five commits
RemainingThree suspected defects, one unknown

Four Kinds of Statement

  • A known defect: confirmed wrong.
  • A suspected defect: looks wrong, unconfirmed.
  • Accepted behaviour: confirmed as intended, however odd.
  • A question: unknown.
  • Only the owner of the requirement can move an item between them. Mixing them is how a refactoring becomes an unreviewed behaviour change, and how a developer’s taste becomes a defect report.

The Pinned Behaviours and Questions for the Owner (1 of 2)

Pinned behaviourClassQuestion
A loan within grace is listed with fine 0.00 and countedSurprising (SD-11-2)Should it appear at all?
Student fines start day 3, 0.50 a day, stop at 10.00ExpectedNone
Staff fines start day 6, 0.25 a day, stop at 5.00ExpectedNone
Any other kind pays 1.00 a day, no grace, no capSuspected defect (SD-11-1)Do other kinds exist? Reject them?

The Pinned Behaviours and Questions for the Owner (2 of 2)

Pinned behaviourClassQuestion
A loan due today, due later, or returned is not listedExpectedNone
A null kind throws NullPointerException, only for an overdue unreturned loanUnknown (SD-11-3)Can the export contain a null kind?
Amounts use the default locale: 0,50 under Locale.GERMANYSurprising (SD-11-4)Should the separator follow the machine or the office?

11.8 Reviewing an AI Refactoring Patch

“Equivalent” is a claim to trace

Ground Rules for the Activity (1 of 2)

For this activity
Your goalDecide whether a proposed refactoring patch preserves behaviour, using evidence rather than the patch’s own claims
You start withThe refactored OverdueReport from 11.5, the characterisation suite and the differential test
What is in boundsEvaluate and, if worthwhile, repair the patch; do not change the tests to make it pass

Ground Rules for the Activity (2 of 2)

For this activity
How to checkTrace the boundary case by hand; apply each claim to a scratch copy; run the suite; read each failing test
How to decideA verdict on each claim, with the failing tests as evidence
No AI account?The ready-made example is enough

The Ready-Made Example

Author-written fictional AI output.

I tidied build. The overdue test is now !row.due.isAfter(today), which reads better and is equivalent to daysLate > 0. I also choose the fine policy before the check, so the loop has one path. Unknown kinds fall back to the student policy, since students are the only borrowers who should ever be fined. All formatting is unchanged, so behaviour is preserved.

The Patch

long daysLate = ChronoUnit.DAYS.between(row.due, today);
FinePolicy policy = row.kind.equals("T") ? STAFF : STUDENT;
if (!row.due.isAfter(today)) {
    double fine = policy.fineFor(daysLate);
    // ... append the line, add to the total and the count, as before
}
  • Trace the first claim before running anything. On the due date daysLate is 0, so the original skips the row. What does !row.due.isAfter(today) do for a row due today?

Verdicts (1 of 2)

ClaimFinding
!row.due.isAfter(today) is equivalent to daysLate > 0False on the due date: daysLate is 0 so the original skips the row, but isAfter is false so the patch includes it. 5 invocations fail
Choose the policy before the checkA second behaviour change: a row not yet due now has its kind inspected, so a null kind throws where it was ignored. 1 invocation fails: the only test that notices

Verdicts (2 of 2)

ClaimFinding
Unknown kinds fall back to the student policyA functional change presented as a tidy-up: X, s and blank paid 1.00 a day; now they follow student rules. 5 invocations fail
Formatting is unchanged, so behaviour is preservedTrue premise, false conclusion: formatting is not the only behaviour. 9 of the 38 report and bridge invocations fail
  • Reject it as a refactoring. The first two can be repaired; the third needs a decision from the records office.

Tracing and Running

  • The equality case can be settled by tracing the due date before any test runs.
  • The null-kind change is not visible from the patch text unless you ask what each line now does for a row that is not overdue.
  • Applying the claims one at a time is what lets each failing test point at one claim.
  • Do not use a generated test to approve a generated change.

11.9 Refactoring Habits That Mislead

Eleven to avoid

Mistakes 1 to 4

MistakeConsequenceBetter action
Fix a suspected defect during a refactoringOne commit cannot be reviewed as “no behaviour change”Pin it, record the question, fix it later in its own commit
Paste actual output into tests without reading itThe suite blesses bugs invisiblyClassify each result and comment the odd ones
Test only the happy path of a legacy methodOdd branches change unnoticedProbe boundaries, unknown inputs, nulls, hidden dependencies
Make a large edit to create a seamThe seam edit is itself untestedThe smallest mechanical edit, checked by eye

Mistakes 5 to 8

MistakeConsequenceBetter action
Leave a test changing the locale or System.out unrestoredUnrelated tests fail far awayRestore global state in finally or @AfterEach
Refactor several things per commitFailures cannot be attributedOne refactoring per commit, tests between
Read “all tests pass” as “behaviour identical”Untested behaviour was never protectedState what the tests cover and what they do not
Refactor everything that smellsTime spent where nothing will changePick smells that block a change you must make

Mistakes 9 to 11

MistakeConsequenceBetter action
Change the shape of a public type other programs useHidden callers breakKeep signatures and formats others depend on
Trust the seam edit because it compiledThe one untested edit could still have changed behaviourRun the original beside the seamed version on a few rows, then pin
Accept “equivalent” for a boundary rewriteisAfter and a day count differ exactly on the due dateTrace the equality case by hand, then run the tests

11.10 Try It Yourself

Pin, seam and refactor the report

Input and Demonstrations

  • You start with: the Chapter 10 version plus the legacy package (LoanRow, the stage 1 report, the characterisation suite, the differential test, the probe) and your team’s repository.
  • Demonstration: run the legacy report on one day and then another (or two dates through the seam) to show why the clock must be isolated. Run the probe. Pin one surprising result live, with its comment.
  • Guided practice: classify five pinned results from 11.3 and say what you would ask the records office about each.

B1 and B2

  • B1. Observe and characterise. Run the probe, then the supplied suite against stage 1. Add three characterisation tests of your own (many loans for one borrower; two loans for the same item; a due date far in the past). For each: record the actual result, classify it, comment if surprising.
  • B2. Refactor in small steps. From stage 1, apply Extract Method and Extract Class (stages 2 and 3), one commit each, running the suite after each. If a test fails, roll back and redo.

B1: Three Example Answers

InputObservedClass
Two rows for the same item, both overdueTwo lines, in input order, fines addedExpected
A student exactly 22 days late, then 2310.00 for both; the cap is reached at 22Expected (a boundary worth pinning)
A row with a blank item nameA line that starts with a spaceSurprising: can blank names occur?

B3 and Your Team’s Project Steps

  • B3. Report your findings. What changed structurally, what you checked after each step, what did not change on purpose (and why), and which questions remain. Add one suspected-defect register entry.
  • T1. Choose one method in your starter codebase with a smell and no tests, that you have a reason to change. Reading note: observation, how you know, status.
  • T2. Pin. The smallest enabling edit; run original beside edited; at least six characterisation tests including a boundary and an odd case; classify each.
  • T3. Refactor. At least three small refactorings, each its own commit, tests before each.
  • T4. Rationale and a teammate’s review.

If You Want More: The Book’s Project

  • B4. Break the boundary on purpose: change daysLate > 0 to daysLate >= 0. Predict which tests fail before you run them, read the messages, restore.
  • B5. A separate functional change: unknown kinds are rejected. Replace pinned tests deliberately; restrict the differential test; write the commit message to say what changed and why.
  • B6. The AI activity.
  • B7. Connect the report to the live loans through the bootstrap bridge.

B7: The Bridge to Live Loans

  • LoanSnapshotRows joins the loan and reservation snapshots into report rows. A loan carries no due date, so it comes from its reservation: the last permitted day is the day before the exclusive end. [20 Oct, 22 Oct) is due back on 21 October; the report counts it late from 22 October.
  • OverdueReportService reads one date from a Clock and one snapshot per call. A source failure propagates: it is never reported as “0 overdue”.
  • A loan with no matching reservation, one that disagrees with its reservation, and a borrower with no kind are each an error, not a row.
  • It lives in bootstrap, the only place allowed to know both the lending layers and legacy.

What You Should End Up With, and When You Are Done

  • What you should end up with: B1 to B3 with the commits or stage files; for your team’s project, observation notes, characterisation tests, at least three refactoring commits, the rationale and the review.
  • Accept when: every characterisation test was run before its expectation was recorded; surprising and suspect results are commented; no refactoring commit changes behaviour; tests pass after every commit; the seam edit is mechanical and was checked against the original; questions are recorded, not silently fixed; signatures others depend on are unchanged.
  • A passing suite is evidence for the behaviours it covers, not a proof of equivalence.

Do the Tests Detect Faults? (1 of 4)

AreaFaults triedResult
Student rulesGrace 2 to 3; rate 0.50 to 0.60; cap 10 to 9All three caught
Staff rulesGrace 5 to 4; rate 0.25 to 0.30; cap 5 to 6All three caught
Other kindsOne grace day; a cap of 10; lower-case s accepted as a studentAll three caught

Do the Tests Detect Faults? (2 of 4)

AreaFaults triedResult
Policy arithmeticMath.max instead of Math.min; grace test < instead of <=max caught. The < change survives: an equivalent mutant, since at exactly the grace day the fine is (grace - grace) * rate = 0 either way
Listing rulesDue today counted as overdue; returned loans not skipped; count off by two; total not accumulatedAll four caught
Zero-fine patchesZero-fine loans skipped; fine computed first and zero-fine loans skippedBoth caught

Do the Tests Detect Faults? (3 of 4)

AreaFaults triedResult
The AI claims!isAfter; policy looked up first; unknown kinds as students; all three togetherAll four caught
Hidden dependenciesAmounts always formatted with a point; header uses the system date; print writes to standard error; short entry point uses yesterdayAll four caught
Date arithmeticDates subtracted the wrong way round; days late from Period.getDays()Both caught

Do the Tests Detect Faults? (4 of 4)

AreaFaults triedResult
BridgeThe due day is the exclusive end; returned flag inverted; mismatched loan not detected; null kind passed throughAll four caught
ServiceA source failure becomes an empty report; null snapshot as empty; system date instead of the clock; clock zone ignored; snapshot cached between reportsAll five caught
  • 36 faults; 35 caught; 1 survives (the equivalent mutant). First round: 22 of 24 caught; the test aNullKindOnALoanThatIsReturnedOrNotYetDueIsIgnored was added to close one gap.
  • Period.getDays() is a realistic assistant suggestion: it returns the days field only, so 100 days late would not count as 100.

11.11 Questions to Think with, Transfer Challenge and Quiz 2 Revision

Concept, diagnosis, transfer, Quiz 2

Concept Questions

  1. Why is a characterisation test not the same as a regression test, and what does a passing characterisation test tell you?
  2. What is a seam, and why is the date parameter a better first edit than rewriting the method?
  3. Why should a suspected defect be pinned rather than fixed during a refactoring?
  4. What can a differential test show that a table of examples cannot, and what can it not show?

Diagnosis Questions (1 of 2)

  1. A teammate refactors the report and says “all tests pass, so nothing changed”. Name two kinds of behaviour the suite does not cover, and say what you would do about each.
  2. A developer extracts FinePolicy and, in the same commit, changes the staff cap from 5.00 to 6.00 “because it looked low”. Explain what is wrong with the commit and what the test run would and would not show.

Diagnosis Questions (2 of 2)

  1. The report runs at 22:30 UTC on a server whose clock zone is Baghdad. A loan was due on 21 October. Which date does the report use, what does it print for that loan, and which one object chooses the zone?

Guidance: Questions 1 to 4

  1. A regression test protects agreed-correct behaviour; a characterisation test records whatever the code does today. A pass means unchanged, not right.
  2. A seam lets you change behaviour without editing there; the second parameter of build is the enabling point. It is mechanical, compiler-checked and small enough to review, so it can precede any test.
  3. A refactoring is defined by unchanged behaviour. Combining a fix with it makes a failing test ambiguous and review impossible. Pinning keeps the suspect behaviour visible and protected until the owner decides.
  4. It compares thousands of generated inputs with the original, finding differences nobody wrote an example for. It cannot test inputs the generator does not produce, and is only as good as the oracle.

Guidance: Questions 5 to 7

  1. Not covered: other time zones or midnight (the short entry point reads the clock); null items or borrowers (a null name prints as null); very large lists. Add a test for the case you are about to touch, or record it as a known gap.
  2. The commit cannot be reviewed as “no behaviour change”. The staff rows and the differential test fail and look like a refactoring error, or the developer edits the expectations and hides the change. Revert the cap, refactor, then change the cap in its own commit.
  3. At 22:30 UTC it is 01:30 on 22 October in Baghdad. A Baghdad clock uses 22 October: one day late, fine 0.00, still listed. A UTC clock uses 21 October: not listed. The Clock supplied to OverdueReportService chooses the zone.

Transfer Challenge: An ML-Serving Script

An analysis script in an ML model-serving tool formats prediction scores for a daily summary. It reads the current time, prints numbers in the default locale, and has three near-identical branches for different model types. No tests.

  • Describe how you would find its seams.
  • Which six or seven inputs would you pin first?
  • Which refactorings would you apply?
  • Name one behaviour you would record as a question instead of fixing.

Transfer Guidance

  • Seams: a date parameter on the entry point; pin the locale in the example data; capture standard output once.
  • Pin first: a score at each boundary of a model type’s rule; one at the maximum; an unknown model type; an empty list; a null; the locale case.
  • Refactor: Extract Method for the formatting; Extract Class for the three near-identical branches (a small value object holding what differs); Rename.
  • Question, not fix: whatever an unknown model type does today.

Quiz 2 Revision Pointers (1 of 3)

  • Quiz 2 is held in Week 10 and covers dependency injection, TDD and test doubles, design patterns and layered architecture (Weeks 5 to 9).
  • Revise Chapters 6 to 10. Do not treat this chapter’s legacy lab as quiz content.
ChapterRevision focus
6Interfaces and ISP, dependency direction, constructor injection, composition root, what a container automates
7Red-green-refactor, test anatomy, stubs, fakes, spies and mocks, controlled time, weak tests

Quiz 2 Revision Pointers (2 of 3)

ChapterRevision focus
8Factory Method, Abstract Factory, Builder, Adapter, Decorator, and choosing the simpler alternative
9Strategy, State, Observer and Command, partial failure, and choosing a pattern by the variation it handles
10Layers and dependency direction, ports and adapters, repositories, entities and value objects, package cohesion, coupling and stability

Quiz 2 Revision Pointers (3 of 3)

  • Use the module leader’s confirmed quiz guidance if it provides more detail.
  • These pointers add no assessment rules and predict no particular questions.

What to Take Away, and What Comes Next

What to carry forward

Summary

  • Changing code you do not understand is a sequence, not a leap: observe, make the smallest edit that lets you test it, pin behaviour, reshape in small steps with a check and a way back, and keep behaviour corrections apart from structure improvements.
  • Smells say where to look, debt claims need evidence, and a green suite is evidence for the behaviours it covers.

Where the Report Stands

  • A seam checked against the original.
  • A pinned suite.
  • A small policy class.
  • Four recorded oddities waiting for the records office.
  • Chapter 12 turns to measuring progress and flow, and introduces the visiting-researcher change request the rest of the project must absorb.

Sources and Further Reading

  1. Fowler: Definition of Refactoring; the Refactoring catalogue; Technical Debt; Code Smell; Legacy Seam.
  2. Feathers: Characterization Testing; Testing Effectively With Legacy Code; Working Effectively with Legacy Code (2004).
  3. Oracle: Java SE 17 Formatter, Clock, ChronoUnit and Period.
  4. JUnit: User Guide 5.13.4.
  5. Martin: Agile Software Development (2002).
  • Full references and links are in the chapter text.