Chapter 14: Successive Refinement
The Process of Getting Clean
This chapter is a long, detailed case study — Martin walks through building an Args command-line argument parser in Java. It demonstrates one of the most important lessons in the book: clean code is not written in one pass. It is refined iteratively.
Rather than reproducing all the code, this chapter explains the key lessons and the progression of the refactoring.
The Starting Point: A Working but Growing Mess
Martin begins by showing a working first draft of the Args parser. The initial version is functional — it handles simple cases. But as requirements grow (support for booleans, integers, strings, etc.), the code accumulates more and more special cases.
The code starts to show the classic signs of rot:
- Long, intertwined methods
- Repeated patterns with subtle differences
- Boolean flags controlling behavior for different data types
- Multiple responsibilities crammed into single methods
"I could tell you that I once had the ability to write clean code from scratch, but that would be a lie. I did not write clean code from scratch. I wrote it in successively cleaner drafts."
Stopping Before It Gets Worse
Martin makes a deliberate decision: stop adding features and clean up first.
This is a critical professional judgment. When a module starts to feel messy, the instinct is to push through — "I'll clean it up later." Martin shows that this delay is costly. The longer you wait, the more entangled the mess becomes.
"So I stopped adding features and worked to improve the code."
The test suite protects against regressions during refactoring. This is why clean tests are so important — they're the safety net that makes iterative refinement possible.
The Refactoring Journey
Step 1: Extract to Separate Classes
The original code has boolean/integer/string parsing logic all mixed together in one large class. The first refactoring extracts each type into its own ArgumentMarshaler:
// Before — all logic in one class with parallel conditionals
private boolean isBooleanArg(char argChar) { return booleanArgs.containsKey(argChar); }
private boolean isIntArg(char argChar) { return intArgs.containsKey(argChar); }
private boolean isStringArg(char argChar) { return stringArgs.containsKey(argChar); }
// After — polymorphic behavior
private interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
}
private class BooleanArgumentMarshaler implements ArgumentMarshaler {
private boolean booleanValue = false;
public void set(Iterator<String> currentArgument) { booleanValue = true; }
}
private class StringArgumentMarshaler implements ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {
try { stringValue = currentArgument.next(); }
catch (NoSuchElementException e) { throw new ArgsException(MISSING_STRING); }
}
}
Now adding a new type (e.g., double) is simply adding a new ArgumentMarshaler implementation — no modification to existing code (Open/Closed Principle).
Step 2: Replace Switch/If Chains with Maps
Instead of a chain of if statements to pick the right marshaler, use a Map<Character, ArgumentMarshaler>:
// Before — parallel if statements for each type
private void setArgument(char argChar) throws ArgsException {
if (isBooleanArg(argChar)) setBooleanArg(argChar);
else if (isStringArg(argChar)) setStringArg(argChar);
else if (isIntArg(argChar)) setIntArg(argChar);
}
// After — polymorphism via map lookup
private void setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null) throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null);
try { m.set(currentArgument); }
catch (ArgsException e) { e.setErrorArgumentId(argChar); throw e; }
}
Adding a new type no longer requires changing setArgument() at all.
Step 3: Clean Up Error Handling
Error handling is extracted into an ArgsException class with typed error codes:
public class ArgsException extends Exception {
public enum ErrorCode {
OK,
MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER,
UNEXPECTED_ARGUMENT, MISSING_DOUBLE, INVALID_DOUBLE
}
private char errorArgumentId = '\0';
private String errorParameter = null;
private ErrorCode errorCode = ErrorCode.OK;
public String errorMessage() {
switch (errorCode) {
case MISSING_STRING:
return String.format("Could not find string parameter for -%c.", errorArgumentId);
case INVALID_INTEGER:
return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
// ... etc
}
}
}
Error messages are now in one place, easy to update and localize.
The Final Design
The final Args class is clean:
// Usage — reads like the domain
Args arg = new Args("l,p#,d*", args); // l=boolean, p=int, d=string
boolean logging = arg.getBoolean('l');
int port = arg.getInt('p');
String directory = arg.getString('d');
The parser is:
- Easy to understand
- Easy to extend (add a new type by adding a new
ArgumentMarshaler) - Easy to test (each marshaler is tested independently)
- Under 200 lines in the
Argsclass itself
The Key Lessons
1. First Make It Work, Then Make It Clean
The working-but-messy version came first. That's fine. The discipline is not to ship the messy version — to refactor before moving on.
2. Tests Make Refactoring Safe
Every step of the refactoring was covered by tests. Without tests, each refactoring step would carry the risk of silent regression.
3. Small Steps, Always Green
Each refactoring was done in small increments, running tests after every change. If a step breaks a test, you know immediately what caused it. Large refactorings done in one big bang are risky.
4. Don't Let the Mess Grow
"It is not enough to write the code well. The code has to be kept clean over time. We've all seen code degrade. And so, too, we must prevent that degradation. The longer you wait, the harder it is to clean."
5. The Boy Scout Rule in Action
Every time you work in a module, leave it better than you found it. The Args parser was incrementally improved over many sessions — not cleaned in a single heroic effort.
Practical Takeaway
When you encounter a growing module:
- Stop adding features — even temporarily
- Ensure you have test coverage — you can't safely refactor without it
- Pick the most egregious problem and fix it
- Run tests — green? Move to the next problem
- Repeat — each small improvement builds on the last
This is successive refinement: the professional practice of continuously improving code as you work with it.