Simple Design

Rewiring my brain to write simple, maintainable code

Jessica Chung
6 min readApr 25, 2019
Photo by Fabian Fauth on Unsplash

Recently, I finished reading Understanding the 4 Rules of Simple Design by Corey Haines. I’ve been working on refactoring my code accordingly which has been a process. Mostly because my usual coding process has been something like this:

Simple Design is all about creating code that is easy to change. Making a habit of being conscious of the 4 rules will pay off by making changes easier down the road.

This is something I can relate to; I often feel wary of making changes in my codebase if I know it’ll be an undertaking, if not annoying. It turns out that was a sign that my code should be refactored to a better design. So I’d like to be a pal and do my future self a favor by writing code that will be easier for her to change (and anyone else for that matter who may inherit my code).

After all, the only constant in life is change.

Being intentional about simple design has higher stakes when it comes to maintaining a larger application. Making changes in a system takes time and can be costly, but a simple design should minimize those costs(faster changes) and maximize benefits(people can jump into a codebase quickly). I haven’t had experience maintaining a larger application yet, but this should be good preparation for that.

So what are the 4 rules predictive of successful design? Haines simplified it to the following:

  1. Tests Pass
  2. Expresses Intent
  3. No Duplication (DRY)
  4. Small

Sure sounds simple enough, right? You might have already encountered these rules before, and they sound like common sense. It was only after I read some examples of how they’re used in creating mindful design that I saw how powerful their application can be in my own design process.

Another thing to note is that these rules aren’t silos, but work in parallel with each other. For example, removing duplication and writing good tests can simultaneously express intent.

Here’s a deeper look into each rule and some key takeaways I had from Understanding the 4 Rules of Simple Design.

Rule #1 Tests Pass — Meets Specifications; IT WORKS!

Basically, the code does what it’s created to do, which can’t be verified if we don’t have tests! Tests act as specifications, documentation, and the first virtual user of your code.

Coupled with learning to be disciplined in Test Driven Development (TDD), I am learning to appreciate writing good tests. Before, I scrambled to write tests just because the project required them, rather than treating tests as the requirements for the project themselves. I’ve found that writing tests really comes in handy when you are refactoring and have to delete a whole chunk of code. Even if you decide to delete all of your code, your specifications will still be there to help you piece your code back together in a more meaningful way.

Good design can emerge from writing good tests. Starting your code by writing tests first in TDD fashion will help shape the API of your code. Something that Haines touched on that has helped me write more focused tests is testing behavior vs. testing state.

When I test for the sake of writing tests, I tend to focus on the state of the structures I already have. When I write tests first, I can test for the kind of behavior I want the system to have, build out my system accordingly, and have a set of focused, useful tests.

Rule #2 Expresses Intent — The code reveals why it exists

This rule covers giving appropriate and descriptive names to classes, methods, and variables, but it can also have its subtleties.

Haines notes that difficulty in finding a placement for code can also be a sign that there is a lack of expression.

“If we are working on a new behavior, but are not sure where to place it — what object it belongs to — this might be an indication that we have a concept that isn’t expressed well in our system.”

Excerpt From: Corey Haines. “Understanding the Four Rules of Simple Design.”

I think we’ve all been guilty of creating a “God” class at one time or other. If a class is doing too much, it might be hard to place a new behavior where it makes sense. Breaking it up into smaller classes that make conceptual sense will allow for placement to come naturally.

Rule #3 No Duplication — DRY: No KNOWLEDGE Duplication

I think the biggest thing I took away from this rule was that no duplication doesn’t necessarily mean not to be repetitive in your code, but in knowledge, which can be much more subtle. Repetition in code does not always mean that there is duplication in knowledge.

“A good way to detect knowledge duplication is to ask what happens if we want to change something. What effort is required? How many places will we need to look at and change? ”

Excerpt From: Corey Haines. “Understanding the Four Rules of Simple Design.”

As I mentioned, I often find myself wary of changing something in my existing codebase if I know that half the work will be figuring out where the changes should be made. If a hard coded detail is continuously showing up in classes and methods, it will have to be manually changed in all its locations. If the detail is promoted to an abstraction, any change can be made in that one place.

Rule #4 Small — No Needless Complexity

Sometimes a reminder to keep the system small may be about remembering to rid the system of unused methods. Other times, it may mean being aware of when more abstraction only leads to more complexity. Often in the name of creating “better design”, I end up really just adding needless complexity. Oops.

For example, Haines mentions that inheritance may seem like a good way of removing duplication of knowledge, but it may just be creating a needless parent class. Often if the same dependency runs across multiple classes, a good technique is to reverse the dependency.

Putting it all together

Here’s an example of some of these rules in action. This is a unit test I wrote early in my coding journey for a game of Scrabble in Ruby.

it "Must return that there are no tiles left if we draw 98 tiles" do
t2 = Scrabble::TileBag.new
t2.draw_tiles(98)
expect(t2.tiles.values.inject(:+)).must_equal(nil)
end

This test checks to make sure the tile bag is emptied of its tiles. It is testing the state of the tile bag, but this check is necessary for the system to properly end the game.

The test’s descriptor isn’t really indicative of what’s actually happening in the test. There isn’t yet a concept that represents an empty tile bag; the test is finding its own way of looking into the tile bag object and adding up the number tiles. If the system ever wanted to check if the tile bag was empty, there is no set method in place to make that happen.

Below is a more expressive refactoring of this test. I’ve created the concept of an empty tilebag, the method of which can now be fleshed out in the actual code. Using more expressive variable names also makes the test easier to understand and read.

it "drawing all 98 tiles will empty tile bag" do
tilebag = Scrabble::TileBag.new
tilebag.draw_tiles(98)
expect(tilebag.empty?).must_equal(true)
end

There are a lot of other principles out there to create good design, but following these 4 rules is a good way to start!

--

--