Chapter 14: Successive Refinement

clean-code successive-refinement refactoring case-study open-closed-principle

Status: Notes complete
Difficulty: Medium
Time to complete: ~40 min read


Overview

This chapter is a case study — not a collection of principles, but a walkthrough of a real refactoring process applied to a specific program. The program is an Args class that parses command-line arguments. The lesson is not about argument parsing; it is about how clean code comes to exist.

The central claim: clean code is not written in one shot. It starts as a messy first draft that works, and then it is iteratively refined into something clean. The alternative — trying to write it perfectly from the start — produces either paralysis or a second mess. The discipline is: make it work, then make it right.

Cross-references: ch10-classes, ch11-systems, ch03-functions, ch09-unit-tests, ch17-smells-and-heuristics


The Problem: What Bad Code Looks Like

The pattern that leads to a messy first draft is familiar: you start with a simple requirement, write code that handles it, then a new requirement arrives, and you extend the code without stepping back to redesign. The code grows organically. Each addition makes the next addition harder. Eventually you have something that works but is embarrassing — tangled, repetitive, and impossible to extend cleanly.

The cost of stopping here (the “works, ship it” decision) is that every future reader and every future modification pays a tax — the interest on the technical debt.


The Args Problem

The Args class parses a command-line argument schema and an array of argument strings:

Schema: "l,p#,d*"
Args:   ["-l", "-p", "8080", "-d", "/home/bob"]

Schema characters mean:

  • l — boolean flag
  • p# — integer argument
  • d* — string argument

Usage:

Args arg = new Args("l,p#,d*", args);
boolean logging = arg.getBoolean('l');    // true
int port = arg.getInt('p');               // 8080
String directory = arg.getString('d');   // "/home/bob"

The problem grows when you add new types: ## for double, [*] for string array. Each new type tests whether the design can absorb change cleanly.


The First Draft — Making It Work

The first draft is a class that handles the schema and parses booleans, integers, and strings — but everything is mixed together in one monolithic implementation.

// FIRST DRAFT — BAD: works, but messy
public class Args {
    private String schema;
    private String[] args;
    private boolean valid = true;
    private Set<Character> unexpectedArguments = new TreeSet<>();
    private Map<Character, Boolean> booleanArgs = new HashMap<>();
    private Map<Character, String> stringArgs = new HashMap<>();
    private Map<Character, Integer> intArgs = new HashMap<>();
    private Set<Character> argsFound = new HashSet<>();
    private int currentArgument;
    private char errorArgumentId = '\0';
    private String errorParameter = "TILT";
    private ErrorCode errorCode = ErrorCode.OK;
 
    private enum ErrorCode {
        OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT
    }
 
    public Args(String schema, String[] args) throws ParseException {
        this.schema = schema;
        this.args = args;
        valid = parse();
    }
 
    private boolean parse() throws ParseException {
        if (schema.length() == 0 && args.length == 0)
            return true;
        parseSchema();
        try {
            parseArguments();
        } catch (ArgsException e) {
        }
        return valid;
    }
 
    private boolean parseSchema() throws ParseException {
        for (String element : schema.split(",")) {
            if (element.length() > 0) {
                String trimmedElement = element.trim();
                parseSchemaElement(trimmedElement);
            }
        }
        return true;
    }
 
    private void parseSchemaElement(String element) throws ParseException {
        char elementId = element.charAt(0);
        String elementTail = element.substring(1);
        validateSchemaElementId(elementId);
        if (isBooleanSchemaElement(elementTail))
            parseBooleanSchemaElement(elementId);
        else if (isStringSchemaElement(elementTail))
            parseStringSchemaElement(elementId);
        else if (isIntegerSchemaElement(elementTail))
            parseIntegerSchemaElement(elementId);
        else
            throw new ParseException(
                String.format("Argument: %c has invalid format: %s.", elementId, elementTail), 0);
    }
 
    private void validateSchemaElementId(char elementId) throws ParseException {
        if (!Character.isLetter(elementId))
            throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
    }
 
    private void parseBooleanSchemaElement(char elementId) {
        booleanArgs.put(elementId, false);
    }
    private void parseIntegerSchemaElement(char elementId) {
        intArgs.put(elementId, 0);
    }
    private void parseStringSchemaElement(char elementId) {
        stringArgs.put(elementId, "");
    }
    private boolean isStringSchemaElement(String elementTail) { return elementTail.equals("*"); }
    private boolean isBooleanSchemaElement(String elementTail) { return elementTail.length() == 0; }
    private boolean isIntegerSchemaElement(String elementTail) { return elementTail.equals("#"); }
 
    private boolean parseArguments() throws ArgsException {
        for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
            String arg = args[currentArgument];
            parseArgument(arg);
        }
        return true;
    }
 
    private void parseArgument(String arg) throws ArgsException {
        if (arg.startsWith("-"))
            parseElements(arg);
    }
 
    private void parseElements(String arg) throws ArgsException {
        for (int i = 1; i < arg.length(); i++)
            parseElement(arg.charAt(i));
    }
 
    private void parseElement(char argChar) throws ArgsException {
        if (setArgument(argChar))
            argsFound.add(argChar);
        else {
            unexpectedArguments.add(argChar);
            errorCode = ErrorCode.UNEXPECTED_ARGUMENT;
            valid = false;
        }
    }
 
    private boolean setArgument(char argChar) throws ArgsException {
        if (isBooleanArg(argChar))
            setBooleanArg(argChar, true);
        else if (isStringArg(argChar))
            setStringArg(argChar);
        else if (isIntArg(argChar))
            setIntArg(argChar);
        else
            return false;
        return true;
    }
 
    private boolean isIntArg(char argChar)     { return intArgs.containsKey(argChar); }
    private boolean isStringArg(char argChar)  { return stringArgs.containsKey(argChar); }
    private boolean isBooleanArg(char argChar) { return booleanArgs.containsKey(argChar); }
 
    private void setBooleanArg(char argChar, boolean value) {
        booleanArgs.put(argChar, value);
    }
 
    private void setStringArg(char argChar) throws ArgsException {
        currentArgument++;
        try {
            stringArgs.put(argChar, args[currentArgument]);
        } catch (ArrayIndexOutOfBoundsException e) {
            valid = false;
            errorArgumentId = argChar;
            errorCode = ErrorCode.MISSING_STRING;
            throw new ArgsException();
        }
    }
 
    private void setIntArg(char argChar) throws ArgsException {
        currentArgument++;
        String parameter = null;
        try {
            parameter = args[currentArgument];
            intArgs.put(argChar, new Integer(parameter));
        } catch (ArrayIndexOutOfBoundsException e) {
            valid = false;
            errorArgumentId = argChar;
            errorCode = ErrorCode.MISSING_INTEGER;
            throw new ArgsException();
        } catch (NumberFormatException e) {
            valid = false;
            errorArgumentId = argChar;
            errorParameter = parameter;
            errorCode = ErrorCode.INVALID_INTEGER;
            throw new ArgsException();
        }
    }
 
    public int cardinality() { return argsFound.size(); }
    public String usage()    { return schema.length() > 0 ? "-[" + schema + "]" : ""; }
    public boolean isValid() { return valid; }
    public boolean has(char arg) { return argsFound.contains(arg); }
    public boolean getBoolean(char arg) { return falseIfNull(booleanArgs.get(arg)); }
    public String getString(char arg)   { return blankIfNull(stringArgs.get(arg)); }
    public int getInt(char arg)         { return 0; /* omitted for brevity */ }
}

This code works. But look at what it contains:

  • Three separate maps (booleanArgs, stringArgs, intArgs) — one per type
  • Three sets of isBooleanArg/setBooleanArg/parseBooleanSchemaElement — repeated per type
  • Error handling tangled directly into the parsing logic
  • valid, errorCode, errorArgumentId, errorParameter — multiple fields just to track what went wrong
  • Adding a new type (double, String[]) requires changes in five or six places

The code is not unreadable, but it is not clean. The second developer to touch it will struggle. The third will fear it.


Why the First Draft Is Problematic — Code Smells

Smell 1: Parallel data structures that must always be kept in sync

booleanArgs, stringArgs, and intArgs are three separate maps. Every type-specific operation requires the same three-part pattern. If you add a type, you add a fourth map. If you forget to add the corresponding isXxxArg, setXxxArg, and parseXxxSchemaElement, you have a silent bug.

Smell 2: Parsing mixed with state mutation

The setIntArg method advances currentArgument (parsing) while also writing into intArgs (state mutation) while also setting errorCode, errorArgumentId, and errorParameter (error tracking) — three concerns in one method.

Smell 3: Error handling entangled with business logic

The valid flag, errorCode, errorArgumentId, and errorParameter fields exist only to accumulate error information across the parsing process. They are not part of the parsing logic — they are a side channel for error reporting. They make every method harder to read because you must track their state.

Smell 4: Violates OCP (Open/Closed Principle)

Adding a new argument type requires modifying existing code in multiple places: add a new map, add isXxxArg, add setXxxArg, add parseXxxSchemaElement, add a branch in setArgument, add a getter. The class is not closed for modification.


Refactoring — Making It Right

The refactoring centers on one insight: the type-specific behavior should be encapsulated in type-specific objects. Instead of three parallel maps, use one map from char to an ArgumentMarshaler object. The marshaler knows how to parse its type and how to return its value.

Step 1: Extract the ArgumentMarshaler Interface

// The interface all marshalers implement
public interface ArgumentMarshaler {
    void set(Iterator<String> currentArgument) throws ArgsException;
}

Step 2: Create Type-Specific Marshalers

// BooleanArgumentMarshaler — GOOD
public class BooleanArgumentMarshaler implements ArgumentMarshaler {
    private boolean booleanValue = false;
 
    public void set(Iterator<String> currentArgument) throws ArgsException {
        booleanValue = true; // presence of the flag is enough
    }
 
    public static boolean getValue(ArgumentMarshaler am) {
        if (am instanceof BooleanArgumentMarshaler)
            return ((BooleanArgumentMarshaler) am).booleanValue;
        return false;
    }
}
 
// StringArgumentMarshaler — GOOD
public 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);
        }
    }
 
    public static String getValue(ArgumentMarshaler am) {
        if (am instanceof StringArgumentMarshaler)
            return ((StringArgumentMarshaler) am).stringValue;
        return "";
    }
}
 
// IntegerArgumentMarshaler — GOOD
public class IntegerArgumentMarshaler implements ArgumentMarshaler {
    private int intValue = 0;
 
    public void set(Iterator<String> currentArgument) throws ArgsException {
        String parameter = null;
        try {
            parameter = currentArgument.next();
            intValue = Integer.parseInt(parameter);
        } catch (NoSuchElementException e) {
            throw new ArgsException(MISSING_INTEGER);
        } catch (NumberFormatException e) {
            throw new ArgsException(INVALID_INTEGER, parameter);
        }
    }
 
    public static int getValue(ArgumentMarshaler am) {
        if (am instanceof IntegerArgumentMarshaler)
            return ((IntegerArgumentMarshaler) am).intValue;
        return 0;
    }
}

Step 3: Replace Three Maps with One

// BAD — three parallel maps
private Map<Character, Boolean>  booleanArgs = new HashMap<>();
private Map<Character, String>   stringArgs  = new HashMap<>();
private Map<Character, Integer>  intArgs     = new HashMap<>();
 
// GOOD — one map of marshalers
private Map<Character, ArgumentMarshaler> marshalers = new HashMap<>();

Step 4: Clean Up the Parse Loop

The schema parsing registers the right marshaler for each schema element:

// GOOD — clean schema parser that registers marshalers
private void parseSchemaElement(String element) throws ArgsException {
    char elementId = element.charAt(0);
    String elementTail = element.substring(1);
    validateSchemaElementId(elementId);
    if (elementTail.length() == 0)
        marshalers.put(elementId, new BooleanArgumentMarshaler());
    else if (elementTail.equals("*"))
        marshalers.put(elementId, new StringArgumentMarshaler());
    else if (elementTail.equals("#"))
        marshalers.put(elementId, new IntegerArgumentMarshaler());
    else if (elementTail.equals("##"))
        marshalers.put(elementId, new DoubleArgumentMarshaler());
    else
        throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail);
}

The argument parsing loop simply delegates to the marshaler for each argument character:

// GOOD — clean parse loop that delegates to marshalers
private void parseArgumentCharacters(String argChars) throws ArgsException {
    for (int i = 0; i < argChars.length(); i++)
        parseArgumentCharacter(argChars.charAt(i));
}
 
private void parseArgumentCharacter(char argChar) throws ArgsException {
    ArgumentMarshaler m = marshalers.get(argChar);
    if (m == null) {
        throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null);
    } else {
        argsFound.add(argChar);
        try {
            m.set(currentArgument); // delegate entirely to the marshaler
        } catch (ArgsException e) {
            e.setErrorArgumentId(argChar);
            throw e;
        }
    }
}

Step 5: Clean Up the Getters

// GOOD — getters delegate to static getValue methods on marshalers
public boolean getBoolean(char arg) {
    return BooleanArgumentMarshaler.getValue(marshalers.get(arg));
}
public String getString(char arg) {
    return StringArgumentMarshaler.getValue(marshalers.get(arg));
}
public int getInt(char arg) {
    return IntegerArgumentMarshaler.getValue(marshalers.get(arg));
}

Before and After — The Full Constructor

// BAD — first draft Args constructor
public Args(String schema, String[] args) throws ParseException {
    this.schema = schema;
    this.args = args;
    valid = parse();
    // ...10 fields initialized above this
}
 
// GOOD — clean Args constructor
public Args(String schema, String[] args) throws ArgsException {
    this.marshalers = new HashMap<>();
    this.argsFound = new HashSet<>();
    parseSchema(schema);
    parseArgumentStrings(Arrays.asList(args));
}

Adding New Argument Types — The Payoff of OCP

After the refactoring, adding a double argument type requires exactly two things:

// Step 1: Add a new marshaler class — GOOD
public class DoubleArgumentMarshaler implements ArgumentMarshaler {
    private double doubleValue = 0;
 
    public void set(Iterator<String> currentArgument) throws ArgsException {
        String parameter = null;
        try {
            parameter = currentArgument.next();
            doubleValue = Double.parseDouble(parameter);
        } catch (NoSuchElementException e) {
            throw new ArgsException(MISSING_DOUBLE);
        } catch (NumberFormatException e) {
            throw new ArgsException(INVALID_DOUBLE, parameter);
        }
    }
 
    public static double getValue(ArgumentMarshaler am) {
        if (am instanceof DoubleArgumentMarshaler)
            return ((DoubleArgumentMarshaler) am).doubleValue;
        return 0.0;
    }
}
 
// Step 2: Register it in parseSchemaElement — one new line
else if (elementTail.equals("##"))
    marshalers.put(elementId, new DoubleArgumentMarshaler());

No changes to the parsing loop, no new maps, no new isXxxArg method, no new branch in setArgument. The core class is closed for modification and open for extension — that is the Open/Closed Principle achieved through successive refinement.


Python and C++ Equivalents

Python — Clean Args Parser (Handwritten, Not argparse)

# GOOD — Python equivalent using marshaler pattern
from abc import ABC, abstractmethod
from typing import Iterator
 
class ArgumentMarshaler(ABC):
    @abstractmethod
    def set(self, args_iter: Iterator[str]) -> None:
        pass
 
class BooleanArgumentMarshaler(ArgumentMarshaler):
    def __init__(self):
        self.value = False
 
    def set(self, args_iter: Iterator[str]) -> None:
        self.value = True
 
class StringArgumentMarshaler(ArgumentMarshaler):
    def __init__(self):
        self.value = ""
 
    def set(self, args_iter: Iterator[str]) -> None:
        try:
            self.value = next(args_iter)
        except StopIteration:
            raise ValueError("Missing string parameter")
 
class IntegerArgumentMarshaler(ArgumentMarshaler):
    def __init__(self):
        self.value = 0
 
    def set(self, args_iter: Iterator[str]) -> None:
        try:
            raw = next(args_iter)
            self.value = int(raw)
        except StopIteration:
            raise ValueError("Missing integer parameter")
        except ValueError:
            raise ValueError(f"Invalid integer: {raw}")
 
class Args:
    _MARSHALERS = {'': BooleanArgumentMarshaler, '*': StringArgumentMarshaler, '#': IntegerArgumentMarshaler}
 
    def __init__(self, schema: str, args: list[str]):
        self._marshalers: dict[str, ArgumentMarshaler] = {}
        self._args_found: set[str] = set()
        self._parse_schema(schema)
        self._parse_arguments(args)
 
    def _parse_schema(self, schema: str):
        for element in schema.split(','):
            element = element.strip()
            if element:
                key = element[0]
                suffix = element[1:]
                if suffix not in self._MARSHALERS:
                    raise ValueError(f"Unknown schema suffix: '{suffix}' for arg '{key}'")
                self._marshalers[key] = self._MARSHALERS[suffix]()
 
    def _parse_arguments(self, args: list[str]):
        args_iter = iter(args)
        for arg in args_iter:
            if arg.startswith('-'):
                for char in arg[1:]:
                    marshaler = self._marshalers.get(char)
                    if marshaler is None:
                        raise ValueError(f"Unexpected argument: -{char}")
                    self._args_found.add(char)
                    marshaler.set(args_iter)
 
    def get_boolean(self, key: str) -> bool:
        m = self._marshalers.get(key)
        return m.value if isinstance(m, BooleanArgumentMarshaler) else False
 
    def get_string(self, key: str) -> str:
        m = self._marshalers.get(key)
        return m.value if isinstance(m, StringArgumentMarshaler) else ""
 
    def get_int(self, key: str) -> int:
        m = self._marshalers.get(key)
        return m.value if isinstance(m, IntegerArgumentMarshaler) else 0
 
# Usage:
# arg = Args("l,p#,d*", ["-l", "-p", "8080", "-d", "/home/bob"])
# print(arg.get_boolean('l'))   # True
# print(arg.get_int('p'))       # 8080
# print(arg.get_string('d'))    # /home/bob

C++ — Template Marshaling

// C++ — type-safe marshaling with a template base approach
#include <string>
#include <unordered_map>
#include <vector>
#include <stdexcept>
#include <sstream>
 
class ArgumentMarshaler {
public:
    virtual ~ArgumentMarshaler() = default;
    virtual void set(std::vector<std::string>::const_iterator& it,
                     const std::vector<std::string>::const_iterator& end) = 0;
};
 
class BooleanArgumentMarshaler : public ArgumentMarshaler {
public:
    bool value = false;
    void set(std::vector<std::string>::const_iterator&,
             const std::vector<std::string>::const_iterator&) override {
        value = true;
    }
};
 
class StringArgumentMarshaler : public ArgumentMarshaler {
public:
    std::string value;
    void set(std::vector<std::string>::const_iterator& it,
             const std::vector<std::string>::const_iterator& end) override {
        if (++it == end) throw std::runtime_error("Missing string parameter");
        value = *it;
    }
};
 
class IntegerArgumentMarshaler : public ArgumentMarshaler {
public:
    int value = 0;
    void set(std::vector<std::string>::const_iterator& it,
             const std::vector<std::string>::const_iterator& end) override {
        if (++it == end) throw std::runtime_error("Missing integer parameter");
        try { value = std::stoi(*it); }
        catch (...) { throw std::runtime_error("Invalid integer: " + *it); }
    }
};
 
class Args {
    std::unordered_map<char, std::unique_ptr<ArgumentMarshaler>> marshalers;
public:
    Args(const std::string& schema, const std::vector<std::string>& args) {
        parseSchema(schema);
        parseArgs(args);
    }
 
    bool getBoolean(char c) const {
        auto it = marshalers.find(c);
        if (it == marshalers.end()) return false;
        auto* bm = dynamic_cast<BooleanArgumentMarshaler*>(it->second.get());
        return bm ? bm->value : false;
    }
    // getString, getInt follow the same pattern
};

The Lesson: Bad Code Cannot Be Fixed All at Once

Martin’s central point is not that you should write perfect code on the first try — that is impossible. The lesson is that you should expect to refine it, and you should do so continuously and incrementally.

The danger of a “big rewrite” is that it produces a new mess, because you still lack the full understanding of the problem that emerges only from working with the messy code. The discipline is:

Make it work. Then make it right. Then make it fast.
— Kent Beck

Make it work: get a complete, passing test suite. Do not refactor broken code.
Make it right: apply design principles (SRP, OCP, cohesion) through small, surgical changes, keeping tests green at every step.
Make it fast: only after it is correct and clean, measure and optimize the slow parts.


The Cost of Stopping Too Soon

When a team ships the first draft as-is:

  • Reading cost: Every future reader must decipher the tangled logic instead of reading clean, expressive code.
  • Change cost: Every new requirement touches five places instead of one.
  • Bug cost: The tangled error-handling logic produces subtle bugs that are hard to reproduce and harder to fix.
  • Morale cost: Good engineers dread touching code that they cannot understand quickly.

The team that says “it works, we’ll clean it up later” almost never does. The debt compounds. The cost is paid not once but on every interaction with the code, forever.


Refactoring Steps Summary

StepWhat ChangedWhy
1Extract ArgumentMarshaler interfaceSRP — separate parsing concern from type-conversion concern
2Create BooleanArgumentMarshaler, StringArgumentMarshaler, IntegerArgumentMarshalerCohesion — each type owns its own conversion and validation logic
3Replace Map<Character, Boolean> + Map<Character, String> + Map<Character, Integer> with Map<Character, ArgumentMarshaler>DRY — no type-specific maps; one map for all types; no casting in getters
4Clean up the parse loop to delegate entirely to m.set(currentArgument)SRP — parse loop only dispatches; it no longer knows anything about types
5Move error handling into ArgsException with typed error codesSeparation of concerns — error tracking no longer entangled in parsing logic
6Add DoubleArgumentMarshaler without touching existing codeOCP — new types added by extension, not modification

When to Apply / Common Exceptions

Apply successive refinement whenever:

  • You are unsure of the full requirements at the start (almost always true)
  • You are building something incrementally with new types or behaviors being added
  • The first draft worked but reviewing it makes you uncomfortable

Successive refinement does NOT mean:

  • Rewriting the entire class from scratch whenever it feels messy (that wastes working tests)
  • Refactoring without a test suite (you will break things silently)
  • Refactoring all at once in one giant commit (small, testable steps only)

When to stop:

  • When the next person can read the code and understand it without asking questions
  • When adding a new type or behavior requires touching only one place
  • When the test suite runs green and the code expresses its intent clearly

Checklist

  • Each argument type has its own marshaler class (not a growing conditional)
  • The parse loop does not contain type-specific branches — it delegates to marshalers
  • Error handling is in dedicated exception/error classes, not entangled in parsing logic
  • Adding a new argument type requires only: (a) a new marshaler class + (b) one registration line
  • Every refactoring step leaves all tests passing
  • The code reads top-to-bottom like a description of what it does, not how it does it
  • No parallel data structures that must be kept in sync manually

Key Takeaways

  1. Clean code is not written in one shot — it starts as a working mess and is iteratively refined into something clean. The discipline is in the refinement, not the initial draft.
  2. Make it work, make it right, make it fast — in that order, never backwards. Refactoring broken code is dangerous; optimizing unclean code is premature.
  3. The marshaler pattern eliminates parallel data structures — instead of N maps (one per type), use one map of type objects that each know how to handle their type. This is OCP in practice.
  4. Adding a new type should touch only one place — if adding double support requires changes in 5 methods, the design is closed for extension. The refactored Args proves that OCP is achievable through successive refinement, not initial genius.
  5. Refactor in small, safe steps — each commit should be a minimal change with all tests still green. A large refactoring that breaks tests is a rewrite, not a refinement.
  6. The cost of stopping at the first draft is compounding — every future reader and modifier pays the tax. The engineer who says “it works, ship it” imposes that tax on the entire team, forever.
  7. Separation of concerns emerges from refactoring — the first draft mixed parsing, type-conversion, and error-handling in each method. The clean version separates them into distinct classes with distinct responsibilities.

  • ch10-classes — SRP for classes: each class should have one reason to change; BooleanArgumentMarshaler and IntegerArgumentMarshaler each have exactly one
  • ch03-functions — Small functions that do one thing; the clean parse loop is the result of applying this principle
  • ch09-unit-tests — You cannot safely refactor without a test suite; tests are the safety net that makes successive refinement possible
  • ch11-systems — Separation of construction from use; the Args constructor sets up marshalers, then the parse loop uses them
  • ch17-smells-and-heuristics — Several heuristics are illustrated by the Args first draft: G13 (artificial coupling), G14 (feature envy), G30 (functions should do one thing)

Last Updated: 2026-04-14