This is the first post in a series about the Four Rules of Simple Design. Click a link in the list below to navigate to a post about the linked rule.
In the first edition of the book Extreme Programming Explained, Kent Beck introduced four rules of simple design. Martin Fowler has a succinct summary of them:
- Passes the tests
- Reveals intention (this post)
- No duplication
- Fewest elements
Let’s discuss all of them in a series of posts, skipping over “passes the tests” for the moment; we’ll look at it at the end in light of the other rules. In this first post we’ll look at “reveals intention”.
Why reveal intention
The rule “reveal intention” indicates that code should help make it clear to the reader of the code what it’s intended to do.
Harold Abelson, the author of Structure and Interpretation of Computer Programs, famously wrote “programs must be written for people to read, and only incidentally for machines to execute.” This might or might not be a dramatic exaggeration, but it counteracts the common assumption programmers can tend to make, which is “programs are written for machines to execute, and only incidentally for people to read.”
Why does it matter how readable a program is by people? When a programmer is first writing the code, presumably they understand it at the time (although this is not always the case!). But when another programmer needs to make changes to that code, they might have a much harder time understanding it because they don’t have the same context as the original programmer. And sometimes the other programmer is the author in six months, when they’ve forgotten what they were thinking at the time.
Difficult-to-understand code causes a lot of problems in development: it takes more time to get ready to make changes to it, it’s harder to figure out what change will accomplish your current goal, and it’s more likely you’ll break something unintentionally.
How to reveal intention
So what contributes to code revealing intention? Here are some of the most common ways I’ve found it’s helpful to do so.
Format the code in a consistent way, ideally with an automatic formatter if there is one available for the language. Really bad formatting can lead to missing things, but even slightly different formatting throughout a project slows down the reader.
Avoid nonessential differences. If two bits of code do the same thing, they should do it in the same way. If two bits of code do something in a different way, there should be a reason for that difference. Differences take cognitive effort to process, and so they should always be used for a reason. Doing the same thing in the same way helps patterns emerge that can help with removing duplication, the next rule of simple design.
Choose names for clarity. Avoid abbreviations unless they’re well-known concepts in general or at the company (but remember that new people do join the company who don’t know the acronyms). Avoid single-letter variable names except in contexts where it’s very clear what they mean: maybe loop variables and single-line closure functions passed into another function. Make sure variable names aren’t too general (count
may often be) or too specific (if a button is used for both create and editing, don’t call it createButton
). Remember that you don’t need to name a function argument the same as the variable you’re passing into it—for example, if you’re using a button to submit a form, you might pass a handleSubmit
function to it. But if that button is used for other uses in addition to submitting forms, it doesn’t know or care whether it’s being used for submitting a form or not, so don’t name that argument handleSubmit
or onSubmit
—name it onClick
, because that is what the button knows about.
Instead of adding comments, make the code more clear when you can. This isn’t an absolute rule (“never use comments”), it’s a priority: add comments only when you’ve made the code as clear as you can, and there is still more to write. The reason to do this is that comments can be ignored and the code can drift so that it no longer matches what the functions say, which is at best time-wasting and at worst can lead to misunderstandings and bugs. If a comment clarifies the purpose of a function or variable, see if you can rename the function or variable to make it clear on its own. Requirements that every function and property on an object must have a comment can sound good on paper, but when the functions and properties are named well they often devolve into a repeat of the function/property name. Don’t be embarrassed about long function and variable names—clarity is the goal, and if a forty-letter-long variable name makes it clearer, name it that way. React has a few great examples of this: the names of its dangerouslySetInnerHTML
and UNSAFE_componentWillReceiveProps
APIs make sure that any developer reading them has a warning that these should be used carefully, and often lead to good conversations during code review that catch potential issues. One case where comments can be the best option is when you need to record why code is written a certain way, which is often hard to make clear in the code itself.
Splitting code into smaller pieces (smaller functions, classes, and files) is another way to help reveal the intention of the code. If you have a 500-line-long function with lots of loops and conditionals, you have to read the whole function and keep it in your head to understand what it’s doing. Sometimes developers will put a comment at the top of different sections in the function to describe what each is doing. Instead, for each section, create a function whose name matches the comment you would use to describe that section, move the code for that section into that function, and call that function instead. That way, the reader can read that function to get a high-level overview of what it does, and can dig into individual step functions only when they want the details for that step. If you are concerned about extra function calls hindering the performance of your program, the increased understandability of the code is probably more important than any minuscule performance hit unless you’re working on embedded systems.
Work at the highest level of abstraction that enables you to solve the problem. For example, decades ago the most common way to iterate over a list was to create a counter variable and repeatedly increment it by 1 in a for
loop. But now many language have built-in collection libraries that allow you to forEach
or map
over a list, or even dedicated syntax to loop over each element of a list in turn. This means that the reader of your code doesn’t have to process the incrementing of the counter itself (and think about the possibility of a bug in that code); they can just focus on what you are doing at each step. Another example is the reduce
function: although it’s a collection function that automatically handles incrementing, it’s lower-level than map
and filter
and so is harder to understand at a glance. If you are just transforming each list element, use map
instead of reduce
; if you are just removing some elements from the list, use filter
instead of reduce
. Only reach for the extra complexity of reduce
when a higher-level abstraction won’t meet your need.
Use the closest-fit function for what you’re trying to accomplish. For example, the purpose of map
is to return a new list that has transformed versions of the list elements. If you want to loop over a list for the sake of side effects, not for the sake of transforming the list elements, don’t use map
: that’s what forEach
and similar language constructs are for. Using map
is misleading because it suggests to the reader of the code that it’s being used for something that it isn’t really used for.
Limit the public interface for a unit of code. Make it clear to users of that code how it’s intended to be used. In object-oriented languages this often means only marking object methods public if you intend them to be used externally, and otherwise using a more restrictive visibility like private. In JavaScript modules this means only exporting the functions and variables you intend to be called from elsewhere.
Limit the scope of variables to the narrowest scope needed. If a variable is only needed in the body of a function, make it local to that function. If the variable is only needed in the body of a conditional or loop, and the language supports block-scoped variables, make the variable local to that block. This lets readers of the code know at a glance that that variable is only used in that narrow scope, and they don’t need to think about the effects of that variable elsewhere in the code. It also prevents a future developer from using the variable in a context that it wasn’t designed to be used. Avoid global variables: they have the widest scope possible and so have the highest chance of having unintended effects.
When a constant value is hard-coded in the code but its purpose is not clear, consider giving it an explaining variable name: assign it to a variable so you can name that variable over what it’s intended to be used for. For example, if you have a three-column layout, instead of using the number 3
throughout the code, consider assigning it to a numColumns
variable. That will help the reader of the code know why a 3 is being used in a given case. It also handles the possibility of the same number being used for reasons other than the number of columns—which could lead to bugs if you need to update the number of columns in the future and change a 3 that should not have been changed.
Conclusion
Focusing on revealing intention as you write code is helpful, but the proof is how that code does in code review. A reviewer may not be able to figure out what code is doing; if so, the author should not only explain it to them, but also try to update the code so that it explains itself. Or a reviewer may think they know what the code is doing but misunderstand; this is another occasion to update the code to reveal intention more clearly. Even if a reviewer is able to figure out what the code means, if it took them some effort or they see a way the code could be more clear, they should suggest it, and the author should make changes accordingly. As an author getting your code reviewed, err on the side of making changes in response to review comments instead of leaving it as-is. Whether you make the exact change requested, or whether you come up with a third option that satisfies both you and the reviewer, either works.
If you apply these principles to make your code more intention-revealing, you’ll see a significant benefit in your ability to make changes to the codebase quickly and reliably. You’ll also get a feeling of increased confidence in your ability to work in the codebase. You’ll likely think of other principles that help the code reveal intention as well: maybe principles specific to your language, framework, or project.