Chapter 16: Refactoring SerialDate

clean-code serialdate case-study refactoring legacy-code

Status: Notes complete
Difficulty: Hard
Time to complete: ~50 min read


Overview

Chapter 16 is the most ambitious case study in the book. Martin takes SerialDate — a real, production Java class from the JCommon library (written by David Gilbert) — and reviews it in full. At roughly 1,000 lines, it is more than an order of magnitude larger than the ComparisonCompactor reviewed in Chapter 15. The class is also more structurally flawed: its design decisions (an abstract class used for constants, int constants instead of enums, undocumented magic numbers in date arithmetic) reflect patterns that were common in Java pre-2004 but are now recognized as anti-patterns.

The chapter’s two-phase structure:

  1. First, make it pass — write characterization tests to establish what the code actually does; fix bugs discovered in the process
  2. Then, make it clean — apply all applicable principles to produce a class that is correct, readable, and appropriately typed

Before: An abstract class called SerialDate with int constants for months and days, physical constants leaked into the abstract layer, and several untested edge cases harboring bugs.

After: A class called DayDate backed by a clean interface, with Month and Day enums replacing integer constants, and all bugs fixed.

The central lessons:

  • Tests before refactoring — always. They catch bugs and create a safety net.
  • Open-source code is not clean code. It can be reviewed and improved.
  • Enums convey type safety that int constants cannot.
  • An abstract class should have abstract behavior, not just be a container for constants.
  • Renaming a class is often the first and most important refactoring step.

The System Under Study

SerialDate is a date class in the JCommon library, used by JFreeChart (a popular Java charting library). Its explicit design goal is to represent a date without a time component — a “calendar day” — independent of time zones, suitable for financial calculations (business days, quarter starts, fiscal calendars).

It was designed to replace java.util.Date for use cases where the time-of-day component of Date caused off-by-one errors across time zones. In finance, “January 15” is an absolute calendar day, not a moment in time.

Key characteristics of the original:

  • About 1,000 lines of Java
  • abstract class SerialDate — but nearly all behavior is concrete; only one or two factory methods are abstract
  • Month constants defined as int: JANUARY = 1, FEBRUARY = 2, …, DECEMBER = 12
  • Day-of-week constants: MONDAY = 2, TUESDAY = 3, …, SUNDAY = 1 (matching java.util.Calendar)
  • Range constants: SERIAL_LOWER_BOUND = 2, SERIAL_UPPER_BOUND = 2958465
  • The “serial” in the name refers to the internal representation: the number of days since a fixed epoch (December 30, 1899 — matching Excel’s date serial numbering)

The class is mature, production-used code from a respected open-source author. That is precisely why it makes such a compelling case study.


First, Make It Pass

Before touching a line of production code, Martin writes tests. This is not optional — it is the prerequisite for safe refactoring.

Why Write Tests Before Refactoring?

  • Safety net: Tests confirm that your refactoring did not change behavior. Without them, you cannot distinguish a behavioral bug you introduced from a behavioral bug that was always there.
  • Discovery tool: Writing tests for untested code forces you to understand every branch and edge case. This is where bugs hide.
  • Characterization: A characterization test does not test what the code should do — it tests what the code does do. If the code has a bug, the characterization test captures the buggy behavior so you can fix it deliberately rather than accidentally.

Bugs Found While Writing Tests

Martin writes unit tests for each method and discovers several bugs in the process:

Bug 1: monthCodeToQuarter() throws for January

The method was supposed to return the fiscal quarter (1–4) for a given month code. The implementation used a switch statement that did not handle JANUARY (month code 1), falling through to a default that threw InvalidParameterException. The fix: add a case for month 1.

// ORIGINAL — BAD (crashes on January)
public static int monthCodeToQuarter(int code) {
    switch (code) {
        case FEBRUARY: case MARCH: return 1;       // Q1
        case APRIL: case MAY: case JUNE: return 2;  // Q2
        // ... missing JANUARY
        default: throw new InvalidParameterException("Invalid month code: " + code);
    }
}
 
// FIXED
public static int monthCodeToQuarter(int code) {
    switch (code) {
        case JANUARY: case FEBRUARY: case MARCH: return 1;
        case APRIL: case MAY: case JUNE: return 2;
        case JULY: case AUGUST: case SEPTEMBER: return 3;
        case OCTOBER: case NOVEMBER: case DECEMBER: return 4;
        default: throw new InvalidParameterException("Invalid month code: " + code);
    }
}

Bug 2: LAST_DAY_OF_MONTH array has a wrong value at index 0

The array was indexed 1–12 (one entry per month) but declared with size 13, using index 0 as a throw-away slot:

static final int[] LAST_DAY_OF_MONTH =
    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

The value at index 0 was 0 — correct only if nothing ever accessed it. But some methods were written with an off-by-one in the indexing logic that could access index 0. The fix involves either documenting the dummy slot clearly or switching to a proper 0-indexed structure.

Bug 3: isLeapYear returned wrong results for some edge cases

The test suite exposed that the method mishandled year-boundary values. The fix was to adjust the comparison operators.

The Characterization Test Approach

// Characterization test — captures existing behavior
@Test
public void testMonthCodeToQuarterForAllMonths() {
    assertEquals(1, SerialDate.monthCodeToQuarter(SerialDate.JANUARY));   // was: throws
    assertEquals(1, SerialDate.monthCodeToQuarter(SerialDate.FEBRUARY));
    assertEquals(1, SerialDate.monthCodeToQuarter(SerialDate.MARCH));
    assertEquals(2, SerialDate.monthCodeToQuarter(SerialDate.APRIL));
    // ... all 12 months
}

The discipline: write the test, watch it fail (revealing the bug), fix the production code, watch the test pass. Only then proceed to structural refactoring.


Problems Found (Code Smells)

Smell 1 — The Class Name Leaks an Implementation Detail

Category: Names (N1 — Choose Descriptive Names)
Where: The class name SerialDate
Why it’s a problem: “Serial” refers to the internal representation — the number of days since a fixed epoch, like Excel’s serial date. This is an implementation detail. Users of the class do not interact with serial numbers; they interact with calendar days. The class represents a day — calling it SerialDate is like naming a String class ArrayCharDate because it internally uses a char array.
The fix: Rename to DayDate. The name communicates the abstraction (a day), not the mechanism (a serial integer).


Smell 2 — Inappropriate Use of Abstract Class

Category: General (G17 — Misplaced Responsibility)
Where: abstract class SerialDate
Why it’s a problem: SerialDate is declared abstract but has almost no abstract methods. It functions as a concrete class with one factory method that returns a concrete subclass. The abstract modifier was used to prevent direct instantiation (forcing use of the factory), but this is the wrong tool. An abstract class signals: “this class is incomplete — subclasses provide the concrete behavior.” Using it merely to enforce instantiation through a factory misleads readers into expecting polymorphic subclasses with overridden behavior.

More critically, the abstract class contains physical constants (SERIAL_LOWER_BOUND, SERIAL_UPPER_BOUND) that belong in the concrete implementation, not the abstraction.

The fix: Either:

  • Extract a DayDate interface defining the contract (preferred — separates what from how)
  • Or use a concrete DayDate class with a private constructor and static factory methods

The concrete implementation becomes SpreadsheetDate (because the serial representation matches spreadsheet software).

// BAD — abstract class misused as interface + implementation hybrid
public abstract class SerialDate implements Comparable, Serializable {
    public static final int SERIAL_LOWER_BOUND = 2;      // implementation constant in abstraction
    public static final int SERIAL_UPPER_BOUND = 2958465; // implementation constant in abstraction
    public static final int JANUARY = 1;
    // ... hundreds of lines of constants and concrete methods
}
 
// GOOD — clean separation
public abstract class DayDate implements Comparable<DayDate>, Serializable {
    // Only abstract methods, pure interface behavior
    public abstract int getOrdinalDay();
    public abstract int getYear();
    public abstract Month getMonth();
    public abstract int getDayOfMonth();
    // ... factory
    public static DayDate createInstance(int day, Month month, int year) {
        return new SpreadsheetDate(day, month, year);
    }
}
 
// Physical constants live in the concrete class
class SpreadsheetDate extends DayDate {
    private static final int SERIAL_LOWER_BOUND = 2;
    private static final int SERIAL_UPPER_BOUND = 2958465;
    // ...
}

Smell 3 — int Constants Instead of Enums

Category: General (G27 — Structure Over Convention)
Where: Month constants (JANUARY = 1, FEBRUARY = 2, …) and day-of-week constants (MONDAY = 2, TUESDAY = 3, …)
Why it’s a problem: int constants provide no type safety. A method declared as public boolean isInRange(SerialDate d1, SerialDate d2, int include) can accept any integer — the compiler cannot prevent you from passing JANUARY where include is expected. The enum check: passing Month.JANUARY where a Month parameter is required causes a compile error if you accidentally pass a Day. Type errors become compile errors, not runtime bugs.

Additionally, int constants do not enumerate their valid range. You cannot iterate over months using the constants. With an enum, Month.values() gives you all months in order.

// BAD — int constants, no type safety
public static final int JANUARY  = 1;
public static final int FEBRUARY = 2;
// ...
public static SerialDate getFollowingDayOfWeek(int targetDOW, SerialDate base) {
    // targetDOW is an int — nothing stops caller from passing JANUARY here
}
 
// GOOD — enum, type-safe
public enum Month {
    JANUARY(1), FEBRUARY(2), MARCH(3), APRIL(4),
    MAY(5), JUNE(6), JULY(7), AUGUST(8),
    SEPTEMBER(9), OCTOBER(10), NOVEMBER(11), DECEMBER(12);
 
    private final int index;
    Month(int index) { this.index = index; }
    public int toInt() { return index; }
 
    public static Month fromInt(int monthIndex) {
        for (Month m : values()) {
            if (m.index == monthIndex) return m;
        }
        throw new IllegalArgumentException("Invalid month index: " + monthIndex);
    }
}
 
public enum Day {
    MONDAY(2), TUESDAY(3), WEDNESDAY(4),
    THURSDAY(5), FRIDAY(6), SATURDAY(7), SUNDAY(1);
 
    private final int index;
    Day(int index) { this.index = index; }
    public int toInt() { return index; }
}

Smell 4 — Wrong or Misleading Javadoc

Category: Comments (C2 — Obsolete Comment)
Where: Javadoc on the factory method
Why it’s a problem: The Javadoc described behavior that was different from the actual implementation. Incorrect comments are worse than no comments — they actively mislead readers and waste time during debugging.
The fix: Delete incorrect comments. Rewrite with accurate descriptions after verifying against the implementation and tests. Do not write comments that describe intent that differs from execution.


Smell 5 — Magic Numbers in Date Arithmetic

Category: General (G25 — Replace Magic Numbers with Named Constants)
Where: The internal date arithmetic formulas
Why it’s a problem: Expressions like (y / 4) - (y / 100) + (y / 400) appear without explanation. The constants 4, 100, and 400 are the Gregorian calendar leap-year correction factors, but nothing in the code says so. A reader must either know the Gregorian calendar rules or go to Wikipedia.
The fix: Extract named constants or helper methods with descriptive names:

// BAD — unexplained formula
boolean isLeap = (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
 
// GOOD — Gregorian rule made explicit
private static final int LEAP_YEAR_DIVISOR   = 4;
private static final int CENTURY_DIVISOR     = 100;
private static final int QUADCENTURY_DIVISOR = 400;
 
boolean isLeap = (year % LEAP_YEAR_DIVISOR == 0)
    && ((year % CENTURY_DIVISOR != 0) || (year % QUADCENTURY_DIVISOR == 0));
 
// Or extract a well-named method:
private static boolean isLeapYear(int year) {
    boolean divisibleBy4   = (year % 4 == 0);
    boolean divisibleBy100 = (year % 100 == 0);
    boolean divisibleBy400 = (year % 400 == 0);
    return divisibleBy4 && (!divisibleBy100 || divisibleBy400);
}

Smell 6 — Constants Exposed in the Wrong Layer

Category: General (G17 — Misplaced Responsibility)
Where: SERIAL_LOWER_BOUND and SERIAL_UPPER_BOUND in SerialDate
Why it’s a problem: These constants are specific to the spreadsheet-serial implementation (dates start at 2 because Excel had a famous leap-year bug in 1900). They are not part of the logical abstraction of “a calendar day.” Exposing them in the abstract class leaks implementation details into the interface.
The fix: Move to SpreadsheetDate. Users of DayDate have no need for these constants.


Key Refactoring Steps

Step 1 — Rename SerialDate to DayDate

This is the first and highest-leverage refactoring. The new name communicates the abstraction; “serial” can live in the concrete subclass name SpreadsheetDate.

Rename across the codebase: class name, file name, constructor references, factory calls, test class names. In a real IDE: Refactor → Rename (shift-F6 in IntelliJ) does this safely with one command.


Step 2 — Convert int Month Constants to Month Enum

Before:

// BAD
public static final int JANUARY = 1;
// ...
public boolean isInMonth(int month) { ... }

After:

// GOOD
public enum Month {
    JANUARY(1), FEBRUARY(2), /* ... */ DECEMBER(12);
    private final int index;
    Month(int i) { this.index = i; }
    public int toInt() { return index; }
}
// ...
public boolean isInMonth(Month month) { ... }

Update all call sites. The compiler flags every call site that passes an int where a Month is now required — these are all potential type errors that were previously invisible.


Step 3 — Convert Day-of-Week Constants to Day Enum

Same process as Step 2, applied to MONDAY, TUESDAY, …, SUNDAY. Note that java.util.Calendar uses SUNDAY = 1, but the enum can encode any internal representation:

public enum Day {
    SUNDAY(1), MONDAY(2), TUESDAY(3), WEDNESDAY(4),
    THURSDAY(5), FRIDAY(6), SATURDAY(7);
 
    private final int calendarConstant;
    Day(int c) { this.calendarConstant = c; }
 
    public static Day fromCalendar(int calendarDayValue) {
        for (Day d : values()) {
            if (d.calendarConstant == calendarDayValue) return d;
        }
        throw new IllegalArgumentException(
            "No Day for calendar value: " + calendarDayValue);
    }
}

Step 4 — Separate Interface from Implementation

  • DayDate becomes an abstract class (or interface) with only the conceptual API
  • SpreadsheetDate extends DayDate carries the serial-number implementation and the physical constants (SERIAL_LOWER_BOUND, SERIAL_UPPER_BOUND)
  • The factory method DayDate.createInstance() returns a SpreadsheetDate — callers never reference the concrete type

Step 5 — Fix All Bugs Found in Testing

Apply the fixes for monthCodeToQuarter(), the LAST_DAY_OF_MONTH array, and isLeapYear(). Each fix must be validated by the characterization test that found it.


Step 6 — Clean Up Javadoc and Remove Incorrect Comments

Delete comments that are wrong. Rewrite surviving comments to describe the why, not the what. The Javadoc for public methods should describe contract (what the method guarantees), not implementation (how it achieves it).


The Result

After all refactorings:

  • DayDate is a clean abstraction: a compact abstract class (or interface) with no physical constants
  • SpreadsheetDate is the concrete implementation: holds the serial representation, the physical constants, and the arithmetic
  • Month and Day enums replace all int constants, catching type errors at compile time
  • All tests pass, including tests that were written before refactoring began
  • Bugs that existed for years in production code are now documented and fixed

The class went from ~1000 lines with mixed concerns to a layered design where each layer has a clear responsibility.


Lessons Learned

1. Write tests before refactoring. Always.
You cannot safely refactor code you do not understand. Tests both document existing behavior and create a net that catches unintended changes. The act of writing tests reveals bugs. Without tests first, you are guessing.

2. Open-source code is not clean code.
Widely-used, mature libraries still have smells. SerialDate was used in production charts by thousands of developers. It had bugs. “It’s in production” does not mean “it’s clean” — it means “it works well enough that the bugs haven’t been noticed yet.” A patient review finds more.

3. Enums are always superior to int constants for named values.
int constants are type-unsafe, non-enumerable, and silent when misused. Java enum (and equivalents in every modern language) provide type checking, iteration, and a canonical set of values. There is no benefit to int constants that enums do not also provide. Convert them.

4. Abstract classes should have abstract behavior.
If a class is abstract, it signals that subclasses provide the missing implementation. Using abstract merely to prevent direct instantiation and then writing all the behavior in the abstract class is dishonest. It creates a structural lie that misleads every reader. Use an interface for the contract; use a concrete class with a private constructor and factory methods if direct instantiation needs to be prevented.

5. A class name is a contract.
SerialDate promises a serial-number-based date class. DayDate promises a class that represents a calendar day. The second name matches the user’s mental model; the first name exposes an internal mechanism. When the class name is wrong, change it — this single rename communicates more than a dozen comments.

6. Characterization tests are a tool, not a compromise.
Writing a test that captures buggy behavior is not the same as approving the bug. It is a stake in the ground: “Before I change anything, here is what this code does.” Once you have that stake, you can change behavior deliberately rather than accidentally.

7. Refactoring layered by concern produces layered architecture.
When you systematically move constants, concrete methods, and implementation details to the right layer, the architecture becomes visible. The abstract layer describes the domain (days, months); the concrete layer describes the mechanism (serial numbers, array lookups). No single step creates this — it emerges from repeated application of the principle.


Application to Other Languages

C++ — Replacing int Constants with enum class

The same int-constant anti-pattern appears throughout older C++ code. Modern C++11 enum class (scoped enums) are the idiomatic fix.

Before (C++ with int constants):

// BAD — C++ with int constants, no type safety
class Date {
public:
    static const int JANUARY  = 1;
    static const int FEBRUARY = 2;
    static const int MARCH    = 3;
    // ... etc
 
    static const int MONDAY  = 1;
    static const int TUESDAY = 2;
    // ... etc
 
    // Bug waiting to happen: caller can pass JANUARY where dayOfWeek is expected
    bool isWeekday(int dayOfWeek) const;
    int  getMonth() const;
};
 
// Caller — nothing stops this mistake:
date.isWeekday(Date::JANUARY);   // passes JANUARY (1) as day-of-week — silent bug

After (C++17 with enum class):

// GOOD — C++17 scoped enums, type-safe
enum class Month {
    January = 1, February, March, April,
    May, June, July, August,
    September, October, November, December
};
 
enum class Day {
    Sunday = 1, Monday, Tuesday, Wednesday,
    Thursday, Friday, Saturday
};
 
class DayDate {
public:
    virtual ~DayDate() = default;
    virtual Month       getMonth()      const = 0;
    virtual int         getDayOfMonth() const = 0;
    virtual int         getYear()       const = 0;
 
    bool isWeekday(Day dayOfWeek) const;  // Day, not int
 
    static std::unique_ptr<DayDate> create(int day, Month month, int year);
};
 
// Now this is a compile error:
// date->isWeekday(Month::January);   // error: cannot convert Month to Day

Using enum class over unscoped enum:

  • Scoped: Month::January instead of January — no name collisions
  • No implicit conversion to int — prevents passing Month where Day is expected
  • Can still convert explicitly: static_cast<int>(Month::January) == 1

Python — Replacing Integer Constants with enum.Enum

Python did not have enums until Python 3.4. Before that, integer constants at module scope were standard. The refactoring is direct.

Before (Python with module constants):

# BAD — Python with int constants, no type safety
JANUARY  = 1
FEBRUARY = 2
MARCH    = 3
# ...
MONDAY    = 1
TUESDAY   = 2
# ...
 
class DayDate:
    def __init__(self, day: int, month: int, year: int):
        self.day = day
        self.month = month    # int — could be MONDAY, no check
        self.year = year
 
    def is_weekday(self, day_of_week: int) -> bool:
        # day_of_week is int — caller can pass JANUARY with no error
        return self.get_day_of_week() == day_of_week

After (Python 3.10+ with enum.Enum):

# GOOD — Python with enums
from __future__ import annotations
from enum import Enum, auto
from dataclasses import dataclass
from datetime import date as stdlib_date
import calendar
 
 
class Month(Enum):
    JANUARY   = 1
    FEBRUARY  = 2
    MARCH     = 3
    APRIL     = 4
    MAY       = 5
    JUNE      = 6
    JULY      = 7
    AUGUST    = 8
    SEPTEMBER = 9
    OCTOBER   = 10
    NOVEMBER  = 11
    DECEMBER  = 12
 
    @property
    def last_day(self) -> int:
        """Return the number of days in this month (non-leap year)."""
        return calendar.monthrange(2001, self.value)[1]  # 2001 is non-leap
 
    @classmethod
    def from_int(cls, value: int) -> Month:
        try:
            return cls(value)
        except ValueError:
            raise ValueError(f"No month for value {value}")
 
 
class Day(Enum):
    MONDAY    = 0   # matches datetime.weekday() convention
    TUESDAY   = 1
    WEDNESDAY = 2
    THURSDAY  = 3
    FRIDAY    = 4
    SATURDAY  = 5
    SUNDAY    = 6
 
    @property
    def is_weekend(self) -> bool:
        return self in (Day.SATURDAY, Day.SUNDAY)
 
 
@dataclass(frozen=True)
class DayDate:
    """A calendar day, independent of time zone and time-of-day."""
    day: int
    month: Month   # Month, not int — type-safe
    year: int
 
    def __post_init__(self):
        # Validate at construction time
        if not (1 <= self.day <= self.month.last_day):
            raise ValueError(
                f"Day {self.day} is out of range for {self.month.name}")
 
    @classmethod
    def from_stdlib(cls, d: stdlib_date) -> DayDate:
        return cls(day=d.day, month=Month(d.month), year=d.year)
 
    def get_day_of_week(self) -> Day:
        return Day(stdlib_date(self.year, self.month.value, self.day).weekday())
 
    def is_weekday(self, day: Day) -> bool:   # Day, not int — type-safe
        return self.get_day_of_week() == day
 
    def add_days(self, n: int) -> DayDate:
        result = stdlib_date(self.year, self.month.value, self.day)
        from datetime import timedelta
        result += timedelta(days=n)
        return DayDate.from_stdlib(result)
 
 
# Usage — type-safe, readable:
d = DayDate(day=15, month=Month.JANUARY, year=2024)
print(d.is_weekday(Day.MONDAY))  # True or False
 
# This is now a type error caught by a type checker (mypy/pyright):
# d.is_weekday(Month.JANUARY)   # error: argument type "Month" incompatible with "Day"

Python-specific observations:

  • @dataclass(frozen=True) replaces a hand-written constructor and makes the object immutable — no need for fDay, fMonth style encoding
  • @property on the enum (Month.last_day) is idiomatic Python for computed attributes — no need for static utility methods
  • __post_init__ provides constructor-time validation cleanly
  • The real-world lesson: Python’s datetime.date is itself the correct solution for most use cases — but replicating the refactoring from int-based constants to enum.Enum illustrates the principle exactly

Checklist

  • Was a characterization test suite written before any refactoring began?
  • Were all bugs found during test-writing fixed and covered by tests?
  • Does the class name describe the abstraction (the domain concept), not the implementation?
  • Are month, day-of-week, and status constants replaced by enums (Java enum, C++ enum class, Python enum.Enum)?
  • Does the abstract class (if present) have actual abstract methods that subclasses implement?
  • Are implementation constants (bounds, epoch values) in the concrete class, not the abstraction?
  • Are all Javadoc / Doxygen / docstrings accurate — verified against tests?
  • Are magic numbers in date arithmetic replaced by named constants or extracted helper methods?
  • Can the factory method create instances without exposing the concrete class type to callers?
  • Does each test cover exactly one behavior, at the level of a single method or edge case?

Key Takeaways

  1. Write tests before refactoring. This is not optional. Characterization tests capture existing behavior, create a safety net, and reveal bugs. Skipping this step transforms refactoring into guessing.

  2. Open-source does not mean clean. Production code from respected authors has bugs and smells. The solution is review and improvement — not reverence.

  3. Enums are always better than int constants for named values. Enums provide type safety, iteration, and a canonical set. int constants provide none of these. There is no valid reason in modern Java, C++, or Python to use int constants for a fixed set of named values.

  4. Abstract classes should have abstract behavior. Using abstract to prevent direct instantiation while providing a complete concrete implementation is a structural lie. It misleads readers who expect polymorphic subclasses. Use a private constructor + static factory if instantiation control is the only goal.

  5. Class names communicate architecture. SerialDate names an implementation mechanism. DayDate names a domain concept. The class name is often the first thing a reader sees — make it describe the abstraction.

  6. Refactoring reveals bugs. The process of writing tests and tracing data flow through the code is how bugs in mature codebases are discovered. Tests are not just a safety net — they are an investigation tool.

  7. Type safety is not just convenience — it is a bug-prevention mechanism. A method that accepts Month month instead of int month cannot be called with a day-of-week by accident. Moving from int constants to enums converts a class of runtime bugs into compile-time errors.

  8. The Boy Scout Rule applies to legacy codebases. The SerialDateDayDate journey is long, but each individual step is small: rename this class, convert these constants, move this constant to the right layer. The resulting architecture is the sum of many small improvements.


  • ch17-smells-and-heuristics — Master catalog: G25 (magic numbers), G27 (structure over convention), G17 (misplaced responsibility), C2 (obsolete comments), N1 (descriptive names)
  • ch15-junit-internals — Companion case study: same discipline applied to a smaller class (ComparisonCompactor); good entry point before tackling SerialDate’s scale
  • ch09-unit-tests — The principles behind characterization tests: clean tests, FIRST properties, one concept per test
  • ch14-successive-refinement — The third case study chapter: iterative improvement of an Args parser; shows how starting clean avoids the SerialDate problem
  • ch02-meaningful-names — The naming principles behind SerialDateDayDate and enum naming conventions
  • ch10-classes — SRP and cohesion principles that motivate separating DayDate (abstract) from SpreadsheetDate (concrete)

Last Updated: 2026-04-14