Writing good code is all about making it fit for human consumption. Any idiot can write code a computer can understand, it takes care to write code another human can understand. But what does it mean to make code easy to understand? Programming is a literate task – writing well requires experience of reading code and in particular reading well written code. But how do we read code? Do we start at the beginning, read line by line until the end? Hardly.
Let me start by asking a different question: how do you write code? You probably write a test, write a small amount of code to make the test pass, then refactor to improve the readability, design etc of your code. Little by little the functionality accumulates; little by little the design emerges from a sequence of decisions and refactoring steps. TDD is fundamentally a design activity. Although you may have an idea of what your design will look like, the actual design will emerge from a sequence of small, interdependent activities.
Now six months later I’m reading this same code. What do I do? Well, I might start by reading the tests – well written tests should help me understand the intent of the code in question and, if they are acceptance tests, tell me what the customer thinks is important. In practice, I find this a painful way of figuring out the important things about a system. If TDD has been followed religiously you’ll have approximately 3.6 billion tests, grouped in a number of different ways making it difficult to keep enough context in my head at once to make any sense of the system.
Instead, I probably have something specific I’m trying to do. I have a change request or a bug to track down. So I probably dive in and make some guesses about where to start. I can’t remember the code so it’s a needle in a haystack. I probably miss and have to wend my way through the code trying to figure out how it fits together. I need to get some kind of big picture – at least for the part of the code I care about, as soon as I work out what that means.
What I’m trying to figure out is the design of the software. How do these 200 classes relate to each other? What are the bigger patterns that will help me figure out where I need to look. Now, if I had documentation, I could look at that. But 1. it’s probably out of date or missing 2. even if it’s there, I wouldn’t trust it anyway.
So instead I find myself scribbling in a notebook – drawing class diagrams and other doodles with boxes, arrows and lines going all over. But how do I infer the design? I’m trying to understand how some parts of the system interact with each other. I look for references to interesting methods, chasing up the call stack to see what’s interesting. I find interesting classes involved along the way and I click through into interesting method invocations to see if anything fun’s happening. All the time I’m bouncing up and down call hierarchies trying to fit the system together.
As my notes start to coalesce I’ve got some idea about the key classes I care about and their roles in the system – I might find one or two key classes and scan through the whole thing, to see what other responsibilities it has. All the time following interesting method calls and looking for references.
Reading code is frankly nothing like writing code. But if writing great code means writing code that is easy to read, it’s a damn shame that the task of writing is so fundamentally different from the task of writing. It’s not as if after I’ve written a class I can pretend to have forgotten how it works and try and infer it. The best I can do is make sure that the code itself looks superficially clean. Is the style right? Is it formatted neatly? Insanely superficial stuff that frankly won’t make any difference in 6 months when I’m cursing the idiot that decided 16 levels of method calls with similar names was a sensible design approach (me).
On a superficial level I wish we could stop dealing with code as text and instead work on the syntax tree, with automated formatting following my preferences – not yours. Then we can all have our own idiosyncratic way of having code presented without having to argue for the billionth time whether spaces are preferable to tabs (of course they are) or whether curlies should be on a new line (of course not, heathen!).
The craftsmanship ethic of writing clean, well factored code is a good step. It doesn’t tackle the fundamental problem that reading code is different from writing it – but reminds us to address some of the symptoms: a comment is a whole new method just dying to be extracted. A 100 hundred line method will be at least twice as easy to understand refactored to two 50 line methods. Make things clear and simple and maybe the code will be easy to understand. Of course, I can still write a clear and simple mess.
It’d be nice if our IDEs were less damned text based. Maybe then we could have IDEs that give us a better, visual language to describe relationships between classes in. But it’s difficult to see how that would work, or how it would avoid degenerating into all the lousy CASE tools that already litter architects desks.
Ideally writing code would be more like reading code. We would be able to describe relationships natively, at a higher level than naming methods and classes. But what does that even mean?! How can I describe a set of classes without scribbling boxes and arrows? And more importantly, how do I make that a part of the implementation language so it never gets out of sync?