Scott McCloud, in Understanding Comics, uses a simple image to explain how people employ assumptions when reading comics:
I may have drawn an axe being raised in this example, but I'm not the one who let it drop or decided how hard the blow or who screamed or why. That, dear reader, was your special crime, each of you committing it in your own style.
I argue that the same is true when reading code. The difference, however, is that with executables we can check those assumptions against our invented reality.
Allow me to illustrate. In code this panel might look something like:
Person Jim = new Person();
Person John = new Person();
Axe axe = new Axe();
Jim.wield(axe);
Jim.attack(John);
Which might print to standard out:
Jim: Now You Die!
John: No! No!
EEYAA!
Or perhaps:
Population: 150,000
Population: 150,000
Population: 149,999
Population: 149,999
Population: 149,999
...
If we care about Population, we want to be able to query Population without having to care about each individual life. If we care about noise level in the neighborhood we could use the Observer pattern to listen for random screams. When reading this method, though, we probably don't care about the result of the attack at all. All that is important to this panel is that Jim attacked John with an axe. The resolution is encapsulated elsewhere; in specific objects or functions or whatever paradigm you are using.
Reading this method, like reading that comic panel, we make assumptions based on the information provided. A Person represents some abstraction of a human individual. Apparently such a human can wield some objects including an Axe, which I'd presume means future actions involving that Person may involve whatever is being wielded. The Person is able to attack a Person, and presumably that has some effect on at least one of them. When I run the code, I'm going to assume that the behavior of the program are caused by the code I've invoked and not because, say, some unmentioned Bobcat screams every Tuesday at 3pm.
Even if the implementation details are important to the programmer at some point, when we are reading one panel's worth of code we don't care any more than the reader of the comic cares what precisely the acceleration due to gravity is or what brand the axe is. Unlike the comic book, if reader does care this code points us to the places where those questions are answered. Code functions as a hypertext document; method signatures, debuggers, tests and IDE code navigation* let us find details we may be curious about. We can always drill down and find the specific arc through space traced by the axe, the acceleration provided by Jim's strength, or the details of the coordinate system employed; there is no need to mention them here.
This also starts illustrating the limitations of assumptions in code, and the role unit tests plays in illustrating intended behavior. If a Person can attack a Person, can it attack itself? Does wielding something new change the effect of attack()? Would the outcome be the same if the Person wielded a Fish? What about a Minnow? Sarcasm? Nothing? What if the Person is-a Infant? Or Superman?
If I am debugging, I might care about the answers to all of these. Then again, probably not; I'm likely to only care about one or two of them at any given time. Maybe all I want to know is why the position of axe changed, and this method probably gives me sufficient information to leap to the correct conclusion.
We can imagine what this code would look like if we made many fewer assumptions:
// Skipping over the definition of a bunch of constants
printf('John: Now You Die!')
printf('Jim: No! No!')
int currentX = START_POSITION_OF_AXE_X;
int currentY = START_POSITION_OF_AXE_Y;
int currentZ = START_POSITION_OF_AXE_Z;
while(currentZ > GROUND_LEVEL) {
if(currentZ > GROUND_LEVEL &&
currentZ < JOHN_HEIGHT &&
currentX > JOHN_X &&
currentX < (JOHN_X+JOHN_WIDTH_X) &&
currentY > JOHN_Y &&
currentY < (JOHN_Y+JOHN_WIDTH_Y)) {
printf("EEYAA!");
// Whatever else we do if someone gets hit by an axe
break;
} else {
// The calculus involved in swinging the axe
...
And so on and so forth. Sure it's long, but that's not the worst part: even with meaningful variable names it makes basically no sense. I can't imagine someone looking at this code and thinking "oh yeah, I'm looking at a version of that panel". This version expects the reader to wade through a great deal of irrelevant information to determining what is going on. It is also less flexible than the version dependent on assumptions: if we moved from Cartesian coordinates to geometric algebra much of the code would have to change. Even this example involves a lot of assumptions; I should probably have named all my variables AX, CY, JX and so forth and used assembly language. Instead I leave that as an exercise for the reader.
Assumptions are how humans handle complexity. Without assumptions, code is just math, instead of communication.
This has some practical implications:
- Method, object and variable names can convey connotations to the reader, making their job easier. These are potentially specific to the culture, business domain, company or even team you are working in. These assumptions are also impossible for the compiler to enforce. Human-readable unit tests can serve that purpose at the behavior level, but the primarily responsibility for choosing communicative names falls to the programmer.
- Debugging code that is behaving differently than desired largely consists of identifying and questioning assumptions until we find the one that was either undesirable or was unfulfilled by the implementation.
- Communicating about code is frequently about coming to a shared set of assumptions. This is why design patterns can be powerful. It is also why having a cohesive metaphor leads to more maintainable code: it makes it much easier to synchronize assumptions, especially when introducing a code base to new coders.
- Whatever code you are about to write, I, as the reader, don't care about the vast majority of it. If I read a piece of code, I am asking to be told how the method achieves what its name implies it will achieve. Thus, the level of a piece of code is specified by the block name. Anything that I wouldn't care about when I'm trying to understand how this method does what it says it does I want to put somewhere else. Anything that is specifically relevant to that promise should be right here. In order to understand this method I shouldn't need to mentally gloss a chunk of code or go somewhere else in the code (as long as I understand the method's role in the larger system.)
When writing a method, I always ask, "do I care about this right now?" If the answer is "no", I want to put that work somewhere else. This leads to code where each method has a single responsibility and there are many small methods. In real life I relax this principle when I think my readers will be annoyed by a huge numbers of small methods. I sometimes compromise by signposting with comments, though that has the unfortunate side effect of teaching programmers to skim over the actual code.
* Sure, you could also use documentation, but anything not enforced by the compiler is merely someone else's opinion. Documentation is approximately as useful as your first best guess.