Understanding the Four Rules of Simple Design and other lessons from watching thousands of pairs work on Conway's Game of Life (2014)
WHERE DO THESE THOUGHTS COME FROM?
As developers, we often enjoy discussing what makes a good design. These conversations are useful and important, but I think they are best done after-hours, in a relaxed atmosphere. The idea of “Good Design” can often lead to a feeling that there is a pinnacle, that there is an Aristotelian ideal for the design of a system. Unfortunately, this just isn’t true. That is why discussions about this ideal are best done away from the stresses of day-to-day work, the pressures that cause us to want to “just get it done.”
If you ask a room of developers what makes a “Good Design,” you’ll most likely get as many answers as there are respondents. I think this variety makes these conversations valuable. Comparing thoughts and ideas on this topic can sometimes yield insight into techniques for improving a codebase, especially if the discussion centers around a concrete piece of code. However, they are ultimately fruitless when trying to reach some ideal of “Good Design.”
Instead, I prefer to talk about “Better Design.” This takes us to a more concrete footing, allowing us to entertain the idea that perhaps there are more than one design that works, depending on the situation. It also removes the conflict inherent in “your design is bad, because it isn’t ‘good’” when talking. If we can look at things from a comparison point of view, perhaps we can find some fundamental ideas about “better.” When talking about fundamental ideas, it can be important to talk about what, if anything, we truly know about software development.
The one constant that we know for sure in software development is that things are going to change. Whether it is our personal projects or a system for a multi-national corporation, the desired functionality will change over time. And these changes to the system require changes to the underlying codebase. If we don’t pay attention, our code rots, calcifying into a hardened mass that resists change, pushing back on us whenever we try to add something new.
Simple design, though, is one that is easy to change. Striving for a simple design — one that is adaptable to changing needs — is the key to a “better design.” Whenever we have a choice to make, look for the choice which would be easier to change.
It is important to keep in mind that this does not mean you should strive for huge, xml-configuration-based systems, making everything configurable. Quite the opposite. When we plan and build explicit extension- and configurability points, we are going against the idea of simple design. There is a second constant that we know to be true in software development: we don’t know exactly what is going to need to change. Every configuration and extensibility point you explicitly plan and build is a belief about the evolution of the system. A concrete statement “this is going to change in the future, so it is worth my investment right now.” But, as the old saying goes, “we’ll never be more ignorant than we are at this moment.” Rather than planning for change points, we build systems, by applying simple design principles, that can change easily at ANY point.
Sandi Metz had a tweet that captures this well.
As time goes on, and we learn more about what places are candidates for frequent change, we then move, based on that knowledge, to make those parts of the system even easier to change. As more changes are applied to a simple codebase, it often naturally exudes the right extension mechanism.
So, what are these “simple design principles?”
As a developer learns more about design, they start to find out about different design principles and guidelines. Things like the SOLID principles, Law of Demeter, Design Patterns, and so forth. All of these are important at different levels of the development lifecycle, but they can often be a bit too abstract. They also seem to fall out naturally when applying some basic principles. These principles are called the “4 Rules of Simple Design.” And this is what we’ll focus on primarily in this book: applying these principles on small sample bits of code, but large enough to really see the thought process during refactoring.
Generally, when we are developing, we have a goal of putting code into production. No matter if we are getting paid, or if we are working on some side project, we generally have a feeling that we want to get it done. Of course, this adds pressure to do things the way in which we are most familiar and comfortable. In general, we don’t get paid for trying new things, we get paid for building things for production.
We might occasionally pick up a side project to learn something new. However, even these projects tend to have a goal of “getting it done.” Also, when learning, we have to live with the mistakes we make at the beginning, during our initial learning phase. If you ask someone to delete the past week’s worth of learning, you’ll receive a pretty big resistance; even though most people would agree that the second (or third) time you write something, it is done faster and in a better way.
What if there were a day where you were encouraged to try new things? Not just new things, things that you’ve never even thought of? And, you didn’t have to live with the mistakes you’ve made during learning, you can just throw it away. That’s coderetreat.
Coderetreat is a day-long workshop focused on analyzing and practicing the decisions we make when writing code. With a set format, evolved over a couple years of learning, the exercises are focused on practicing and studying the small minute-by-minute decisions we make when writing code. While there is value in larger design activities, the small steps of refactoring are equally as important.
The format of a standard coderetreat is simple:
· Full day (5 to 6 sessions)
· Participants write code in pairs (pair programming)
· 45-minute sessions
· Conway’s Game of Life is the problem
· Code is deleted after each session
· New pairs each session
· At the end of the day, we do a short retrospective where everyone answers the following questions
o What, if anything, did you learn today?
o What, if anything, surprised you today?
o What, if anything, will you do differently moving forward?
For each session, a set of constraints is given. These constraints are generally a bit extreme. They have the goal of breaking the participants out of their usual way of thinking. Most people will begin working on a problem in the way they are comfortable. The constraints are there to remove the ability to code in a familiar, comfortable way. You can find some examples of session constraints in the appendix.
The urge to rush to finishing is strong. One of the goals of the morning sessions is to break this feeling, allowing people to relax into the idea of not finishing. It emphasises enjoying the feeling of explicitly not thinking about the end, but paying attention to the minute-by-minute coding, living for the moment-to-moment decisions when writing and refactoring. Often the resistance comes from a feeling of ownership: “this is my code, its existence represents me.” Or, sometimes it is a sense of value: “this code is valuable, due to the time I’ve spent on it.”
All these attitudes are learned habits and can be transcended by practice. In the context of coderetreat, the practice is repeatedly deleting the code you’ve written. Being interrupted and asked to delete it, starting over.
Pretty rapidly, most people gain a previously unrealized perspective about code ownership. In fact, after deleting and starting again enough times, I’ve often heard people say they have a feeling of freedom they’ve not experienced before. The separation of identity from code frees them to experiment with new ideas. When value isn’t tied to amount (or quality) of code, they can more readily accept that an attempt isn’t working and discard it.
Conway’s Game of Life
At coderetreats we traditionally work on Conway’s Game of Life. This application is very simple and easily understandable, yet the underlying domain and structure can hold a lot of subtle lessons in low-level design. When we couple this problem with a time limit and constraints to pull ourselves out of our comfort zone, it becomes even more rich.
So, what is the game?
Conway’s Game of Life (GoL) is what is known as a zero-player game. Sounds fun, right? It actually is a fascinating system called a cellular automaton. We set up an initial pattern on a board, start the program running, and the system evolves the board through a series of generations.
The game is played on an infinite two-dimensional grid. Each cell in this grid is considered either alive or dead.
A Beehive of Living Cells
A Block of Living Cells
When we run the game, the program goes over each cell and calculates whether it will be alive or dead in the next generation. The determination is based on four simple rules that take into account the number of living neighbors. It is worth noting that there are eight neighbors to a cell, diagonals count.
1. If a living cell has less than two living neighbors, it is dead in the next generation, as if by underpopulation.
2. If a living cell has two or three living neighbors, it stays alive in the next generation.
3. If a living cell has more than three living neighbors, it is dead in the next generation, as if by overcrowding.
4. If a dead cell has exactly three living neighbors, it comes to life in the next generation.
Each tick of the game calculates the next generation based on these four simple rules. The beauty comes out when you see some of the fantastically complex structures that arise from such simplicity1.
An Active Breeder Pattern
This system is the backdrop for the examples in this book. I would challenge you to spend a little bit of time thinking about how you would build this system, how you would design it. Assume that changes are coming, but you don’t know what they are. But, don’t just settle with one idea, see if you can come up with several different approaches. Take some time and think about it now. The rest of the book can wait. Check out the wikipedia entry for more information. Once you are done, feel free to check out the sample list of coderetreat session constraints and ask yourself how they impact your proposed solution(s).
4 Rules of Simple Design
So, what are these 4 Rules of Simple Design?
Originally codified by Kent Beck in the late 90’s, these rules outline some fundamental concepts around software design. The two core rules can guide us as we make our small, code-level refactorings.
Here they are in a simplified form.
1. Tests Pass
2. Expresses Intent
3. No Duplication (DRY)
Let’s look at these in order and see what they mean.
1. Tests Pass
It makes sense that this would be the first one. After all, if you can’t verify that your system works, then it doesn’t really matter how great your design is, does it? With the modern tools that exist, we generally mean that these tests are automated. But, notice that the rule doesn’t say “Automated Tests Pass,” just “Tests Pass.” It is about correctness and verification. Looking at this from the point of view of “easier to change,” though, you can see that the length of time it takes to make sure your “Tests Pass” can be a significant factor in making changes. If you can type a command and have your system verified in a matter of seconds, or less, then you can change your system more readily than if you have to wait hours, or even days. So, when looking at your testing strategy, tend towards automated, and tend towards making them fast(er). I have a saying that I like to use:
“If you have to ask how fast your test suite should be, it should be faster.”
2. Expresses Intent
How often have you went looking at a piece of code and found a method with name like process_transaction, but after looking more closely, you realize it neither processes nor has anything to do with transactions? This is an extreme case, but highlights an important problem when we are writing, and especially when you are updating, code: it is easy for the names we give things to stray from what they represent.
One of the most important qualities of a codebase, when it comes time to change, is how quickly you can find the part that should be changed. The first step is identifying the code related to the functionality we are addressing. Paying attention to the names and how our code expresses itself is the key to making our lives easy when we come back to it.
Also, over time, as we change the functionality of our system, classes and methods can become filled with unrelated behaviors. This makes it difficult to have the name effectively express their intent. As we start to see structures getting large, the difficulty in finding an expressive name is a red flag that it is doing too much and should be refactored.
3. No Duplication (DRY)
This is the most subtle of the rules. We tend to think of duplication at a code level — a mechanical “this looks like that, so duplication!” level. However, this rule isn’t about code duplication; it is about knowledge duplication.
A lot of people are introduced to this idea through the DRY principle, or Don’t Repeat Yourself. This was established in the book, The Pragmatic Programmer, by Dave Thomas and Andy Hunt.
The DRY principle states “Every piece of knowledge should have one and only one representation.” This rule also has been expressed as “Once and Only Once.”
Instead of looking for code duplication, always ask yourself whether or not the duplication you see is an example of core knowledge in the system.
Once we’ve applied the above rules, it is important to look back and make sure that you don’t have any extraneous pieces. Some questions I like to ask myself when I take a step back after writing some code.
· Do I have any vestigial code that is no longer used?
This is an easy one. Sometimes, as we are working through our system, we build things that aren’t used in the final product. Maybe they seemed like a good idea at the time, but the capability never came to fruition. If so, no questions asked, just delete that.
· Do I have any duplicate abstractions?
In the course of refactoring, we often end up extracting abstractions, whether they be methods or new types. While we strive to keep duplication down, per the DRY principle, sometimes we find that we missed something. Perhaps the duplication is far apart in the codebase. Perhaps it is was hard to see the similarity when focused on the small. Take a moment to see if you notice anything now. If so, combine them.
Sometimes, though, it isn’t that the full abstractions are duplicate, but just that they have some similar characteristics, perhaps a behavior, or two. If so, then we might be missing another common abstraction that they can rely on. Don’t wait, extract it.
· Have I extracted too far?
In the course of writing, we can sometimes over-extract. A common case of this is when we extract a method for readability, to better express our intent. However, once we are done with the rest of our cleanup, we can inline the extracted method. This is a great example of the fluidity of a codebase’s expressiveness over time.
An important thing to realize about these rules is that they iterate over each other. Frequently, fixing a naming issue will uncover some duplication. Eliminating that duplication will then reveal some expressiveness that can be improved. Joe Rainsberger wrote a great blog post about thisiterative nature of the 4 rules.
There are many very interesting articles on the internet about the 4 rules of simple design. You can find them in the further reading section.