I’ll be taking a break from writing during the summer, so these weekly articles may contain slightly less deep content for a few weeks. When I’m back I have big plans for this newsletter, so make sure you’re subscribed so you don’t miss anything!
This week I thought I’d take a somewhat more detailed look into the 4 Rules of Simple Design. I’ve mentioned them quite a few times in my early articles, so let’s look a little more closely.
To recap the 4 rules, code that is “simple” (ie. habitable) must satisfy the following criteria:
Passes all its tests
Expresses our intention
Says everything once and only once
Has nothing superfluous
The first thing to note is that many people list rules 2 and 3 in the opposite order than I have. I actually don’t think there’s a difference — I’d say that rules 2 and 3, in practical terms, feed off each other so much that they may as well be parts of a single rule. I’ll come back to this point a bit later, but first I want to talk about what the ordering of the rules means.
In an ideal world, all of our code would satisfy all four rules simultaneously. In which case their order doesn’t matter.
The order of the rules comes into play when we have code that doesn’t already satisfy them — code that, by this definition at least, isn’t habitable. In this case, the order of the four rules tells us how to behave.
First, they say, get the code working and tested. Nothing else matters. If the code doesn’t work, nobody cares how habitable it is; the other rules don’t come into focus. (Actually, in practical terms that’s not quite true. If the code used to satisfy the 4 rules, and then we broke a test, that code will likely be much easier to fix than if we have a broken test of code that is a tangled mess of unreadable, duplicated logic.) Nevertheless, regardless of the prior state of the code, the 4 rules say that our first priority is the get the code working and tested again. And we’re allowed to break the other rules, if necessary, in order to get back to working and tested.
This mirrors the rules of test-driven development: After the RED step we have a failing test, so we want the GREEN step to be completed quickly. In getting back to GREEN we’re actively encouraged to Do The Simplest Thing That Could Possibly Work. Don’t spend time thinking about code quality or readability; don’t worry about introducing duplication or coupling; and don’t worry about leaving unused code in your wake. Just do what you have to do in order to get that test passing again.
It’s worth mentioning that it’s no accident that test-driven development’s RED-GREEN-REFACTOR cycle starts the same way as the 4 rules of simple design. That’s because they are the same rules. It’s just that the 4 rules give us a more helpful guide as to how to direct our refactoring effort. Which brings us neatly to rules 2, 3, and 4.
After our code is working and tested, TDD tells us to “refactor”, but stops there, before telling us how to go about it. And this is where rules 2, 3, and 4 step in.
First, make the code understandable by anyone who understands the domain. And if you need to introduce duplication in order to do that, that’s fine. The 4 rules say “make it readable first”; then fix the duplication; and only then shoot for minimalism. But you’re not allowed to remove duplication if doing so would make the code unreadable. And you’re not allowed to minimise the code if that would render it unreadable or introduce duplication.
So what do “readable” and “understandable” mean? We want to have the code express the Product Owner’s and the developer’s intentions. In all of the programming languages I can think of, the only tools we have for expressing ideas are the names we give to things. So we need to ensure that every code construct that we create has a meaningful, domain-related name. But we also need to ensure that we choose the “right” things to make nameable. As we break down our algorithm into functions, parameters, variables, classes, modules etc it’s important that those entities map onto the domain, so that the story told by the code makes sense in terms of the major concepts of that domain. I want my code to “tell the story” that my Product Owner told me when explaining their intention for this feature.
Next, on to rule 3. Once we have working tested code that tells the Product Owner’s story, we should ensure that every idea in that story is represented in the code exactly once and no more. And my working hypothesis — the question I’m exploring through these articles — is that this is best accomplished by looking for implicit coupling and making it explicit. My mission here is to see if we can develop a clearer sense of how to apply the Once And Only Once rule in practical day-to-day programming.
As I mentioned above, many (perhaps most) authors prioritise rules 2 and 3 the opposite way around from my list here. The reason I prefer to fix expressiveness first is that I’ve seen huge amounts of unreadable code produced in the name of removing duplication. And since we read each line of code much more often than we change it, I think there’s quite a productivity payoff in having expressiveness prioritised more highly than duplication.
And in practice rules 2 and 3 usually play off each other. Most of the time when we remove some duplication, that activity involves introducing one or more new nameable components into the code. So we have to go back around to rule 2 to ensure that the new names — and the new nameable things — tell the story at least as well as before. So rules 2 and 3 form what Joe Rainsberger has called a “dynamo”; they feed off each other and influence each other. And so it may be that their order doesn’t really matter if we’re being thorough. Be that as it may, I’m sticking with the order above; I want to ensure that if, say, I run out of time, my code a readable first and foremost.
Finally, there’s the fourth rule: be a minimalist. If something becomes unused while you’re following the other rules, just delete it. If there’s a simpler way of achieving something, do it that way — but make sure the code still tells the story at least as well as before.
So, that’s a whistle-stop tour of the 4 rules of simple design, including a little insight into where I think this newsletter fits in the grand scheme of things. Following these rules keeps your code habitable, and therefore also keeps the cost of change lower. But they aren’t easy — and rule 2 in particular is somewhat subjective. Expressing intention is hard!
Things to try
Can you completely let go of your “sense of smell” when you’re writing the code to get that test to pass?
Can your Product Owner read and understand your code?
In particular, if your Product Owner can’t read and understand your tests, you’re not done refactoring.
Please leave a comment if you just tried this approach for the first time: How did it feel? Did it make a difference to your code? What happened when a non-developer read your test code?
If your Product Owner can't read and understand your tests, you're not done refactoring.
Better yet: write your tests with your Product Owner in the ensemble. They don't have to program, but they can do everything else a member of the ensemble would do.
A well-written test -- indeed habitable code in general -- should primarily use language from the domain, and should be structured so that it tells the same story that your Product Owner would tell.
Can your Product Owner read and understand your tests?
When was the last time your team did multi-disciplinary test-driven development?
Hi Kevin, very nice article! I never thought of the 4 rules this way, from the pov of what to do on a codebase that’s not up to spec; but of course that’s the situation we’re in whenever we get to “refactor” in the tdd cycle. I will use this framing in my upcoming training!