Skip to main content

Successive Refinement

Most chapters in this book show a "before" and an "after" snippet in isolation. This one instead follows a single piece of code - a command-line argument parser called Args - through several real rounds of change, to make an argument the rest of the book only implies: nobody writes clean code in one pass. The point of studying this case isn't the argument parser itself; it's watching what "successive refinement" actually looks like in practice; a working first draft, followed by small, safe, test-covered steps that keep the design honest as new requirements land.

The starting point: a working first draft

The Args class begins with a genuinely reasonable job: given a schema describing which command-line flags are booleans, which take a string, and which take an integer, parse a raw array of argument strings into usable values. It didn't arrive at three flag types all at once, either - the very first version handled booleans only, and it was small and clean. Adding string support made it a little messier. Adding integer support, with its own parsing failures to report, is what tipped it into a real mess: one long parsing method with a growing conditional chain, three parallel maps (one per type) threaded through every caller, and a pile of instance variables and half-named error fields at the top of the class.

The pain points that show up as it grows

At three flag types the class still passed its tests and was "not that bad," but the seams were already visible. The method was doing at least three separate jobs at once - deciding which flag a token names, deciding how to consume that flag's arguments, and deciding how to store the parsed value for retrieval later - and none of those jobs were separated from each other. Looking ahead to two more flag types that were actually needed next (a double, and a string array), it was clear that bulldozing forward would produce a mess too large to fix later. So the author stopped adding features and started refactoring instead, while the design was still salvageable.

The refactoring: extracting a marshaler per type

Three parallel maps, each requiring the same parse/store/retrieve trio of behavior per type - that's a class trying to happen. The fix pulls the part that varies - "how do I parse and store this flag type" - out from behind a single name, an ArgumentMarshaler, that every flag type implements the same way. Each concrete marshaler (boolean, string, integer, and so on) knows how to consume its own tokens from the argument list and how to hand back its own parsed value.

None of this happened as one big rewrite. The refactor proceeded as dozens of individually tiny, test-verified steps: a do-nothing ArgumentMarshaler skeleton was appended first, with nothing using it yet; then, one type at a time, its storage moved into a marshaler instance. The type-specific parsing logic originally read two instance variables - the argument array and a current-index counter - which blocked pushing it into a marshaler with a single clean argument. Switching the parser to hand out a cursor over the remaining tokens, instead of an array plus an index, cleared that blocker, and only then could the if/elif chain that checked each argument's type before dispatching to it be deleted, replaced by one polymorphic call. Adding a brand-new type afterward still costs one line recognizing its schema symbol (## for a double, say) - refactoring removed the type-dispatch branch, not every branch in the class.

# One giant conditional doing type-specific parsing inline.
# Every new flag type means editing this method again.
class Args:
def parse(self, schema, args):
booleans, strings, ints = {}, {}, {}
i = 0
while i < len(args):
flag, flag_type = self._lookup(schema, args[i])
if flag_type == "boolean":
booleans[flag] = True
i += 1
elif flag_type == "string":
strings[flag] = args[i + 1]
i += 2
elif flag_type == "integer":
ints[flag] = int(args[i + 1])
i += 2
else:
raise ValueError(f"Unknown flag type for {args[i]}")
return booleans, strings, ints
 
# Adding a "float" flag means finding this method and adding
# a fourth elif branch, plus a fourth dict to thread through
# every caller that wants the results.
# Each flag type owns its own parsing and storage behind one interface.
class BooleanArgumentMarshaler:
def set(self, args, i):
self.value = True
return i + 1
def get(self):
return self.value
 
class StringArgumentMarshaler:
def set(self, args, i):
self.value = args[i + 1]
return i + 2
def get(self):
return self.value
 
class IntegerArgumentMarshaler:
def set(self, args, i):
self.value = int(args[i + 1])
return i + 2
def get(self):
return self.value
 
class Args:
def __init__(self):
self._marshalers = {
"boolean": BooleanArgumentMarshaler,
"string": StringArgumentMarshaler,
"integer": IntegerArgumentMarshaler,
}
 
def parse(self, schema, args):
results = {}
i = 0
while i < len(args):
flag, flag_type = self._lookup(schema, args[i])
marshaler = self._marshalers[flag_type]()
i = marshaler.set(args, i)
results[flag] = marshaler
return results
 
# Adding "float" is one new class, one map entry, and one line teaching
# _lookup() to recognize its schema symbol. The dispatch loop above -
# the part that used to grow an elif per type - never changes again.
«interface»ArgumentMarshaler+ set(args, i)+ get()Args- marshalers: map<type, Marshaler>+ parse(schema, args)SCHEMA-DRIVEN,BooleanArgumentMarshaler+ set(args, i)+ get()StringArgumentMarshaler+ set(args, i)+ get()IntegerArgumentMarshaler+ set(args, i)+ get()
implementsaggregation
The type-dispatch chain becomes a lookup; a new flag type is a new class, not a new branch.

What actually made this safe

A suite of existing unit and acceptance tests is what let each of these tiny steps happen with confidence - the discipline, drawn from test-driven development, was to never let a change break the system, so every step got verified before the next one began. That also meant some steps looked like undoing the previous one: code removed in one step sometimes had to be put back two steps later once a different piece of state moved. That's normal - refactoring in small, test-covered steps is closer to solving a Rubik's cube than writing prose, where a lot of individually small moves compound toward one large goal that no single move could safely reach on its own.

The real lesson

The Args class itself is not the point. The point is that professional code is rarely clean on the first attempt, and it doesn't need to be. What matters is the discipline of getting something working, then continuously improving its structure in small, test-covered steps as you understand the problem better and as new requirements arrive - rather than either freezing the first draft in place forever, or waiting for a rewrite that never comes. Clean code is something you refine your way into, not something you're expected to produce in one sitting.