Understanding the Four Rules of Simple Design and other lessons from watching thousands of pairs work on Conway's Game of Life (2014)
OTHER GOOD STUFF
This section contains material either not directly related to the focus of the book, or is supplementary to the content outlined in the book.
· Other Design Guidelines
· Examples of Session Constraints
· Some Thoughts on Pair-Programming Styles
· Further Reading
Other Design Guidelines
While the 4 rules of simple design are fundamental concepts in building an adaptable codebase, there are a lot of other design guidelines out there. It is worth studying these, as well.
Just like Design Patterns are best used as descriptions of designs, rather than prescriptions of how to build systems, design guidelines, especially higher-level ideas like SOLID, can be best used when describing where you are, and why it is appropriate. Along with that, looking back at a design, it can be useful to explain why a piece of code does NOT abide by this or that design guideline. It can be difficult, though, at the time of writing, to really apply most of these principles. In the end, most design guidelines are best internalized and applied subconsciously. Once this happens, these guidelines can move into the realm of descriptive usage.
The 4 rules, on the other hand, are simple enough to apply consciously. Thinking about good naming to express intent and looking for duplication of knowledge are two techniques that can have real effects on the code you write at the time of writing.
Over the years, I’ve come to see the 4 rules of simple design as the most useful concrete, coding-time principles to keep in mind. While the examples in this book primarily use the 4 rules to guide the code, let’s look at how they lead us naturally to code that satisfies these other principles.
The SOLID Principles
The SOLID principles were originally codified by Robert “Uncle Bob” Martin, bringing a set of existing design principles together in a more easily-understandable format. Like the 4 rules, they focus on making systems flexible and adaptable when changes are required. As we’ll see, focusing on the 4 rules of simple design can lead us to satisfying these principles. Let’s look at these principles, and how they can relate to flexible designs.
Single Responsibility (SRP)
“A class (component) should have one, and only one, reason to change”
The Single Responsibility Principle is by far one of the most popular, while simultaneously one of the least understood. Because of the name, discussions around this consist of defining what is meant by “responsibility” of a component, while the definition talks only about change. “Change” makes it a bit more concrete, but still leaves a lot open for discussion: what level of change do we look at?
Systems that satisfy the SRP are flexible with isolated behaviors contained in small, cohesive packages. This allows us to safely make changes to functionality. At its core, SRP is another way of maximizing cohesion. After vigorously eliminating duplication and making sure that our pieces are named appropriately and expressively, we generally find that our code satisfies the SRP.
“A system should be open for extension, but closed for modification”
Changing code is dangerous; once we have it written and tested, we want to minimize the chance for bugs to be introduced. By introducing or altering behavior only through extension, we benefit from the stability of small, stable core pieces that won’t change out from under us.
There is a danger when focusing too much on the OCP. If we plan for extensibility, our systems become riddled with unnecessary and unwieldy extension points and extensibility mechanisms. To counter this, we should focus on isolating knowledge, naturally building only the extension points that truly represent the pieces of our system that will change.
Liskov Substitution (LSP)
“Derived types should be substitutable for their base types”
Polymorphism is a key part of a flexible design. Being able to substitute a more specific type when a general type is expected allows us to provide different behaviors without having complex branching. There is a danger, though, if the specialized type significantly changes fundamental expectations of the more general type’s behavior. Derived types should enhance any base behaviors, rather than change it.
A healthy focus on the names we give our types can help in abiding by LSP. A specialized type’s name should reflect that it is an enhancement of the base, not a change.
“Interfaces should be small, focused on a specific use case”
The surface area of a class has a direct influence on how easy it is to use. Although a class might have several different ways to use it, any specific client should see only those behaviors specific to its needs.
When we focus on effectively grouping and naming the behaviors of our class, we naturally build small interfaces that provide a clear, cohesive view of what our class does. If it is difficult to name, that is feedback that our class is getting too large.
“Depend on abstractions, rather than concrete implementations”
One of the most dangerous parts when changing a system is having your changes unexpectedly influence other, unrelated parts of your system. We want to guard against the situation where a change ripples through the whole system, causing waves and possible bugs throughout. By depending on abstractions, decoupling ourselves from concrete implementations, we can set up walls between behaviors. Abstractions better move us into standardized communication methods between components, making it easier to independently replace or change things.
Law of Demeter
Contrary to popular belief, the Law of Demeter (Lod) was not originally described by a person named Demeter. Nor was it a reference to the ancient Greek god, Demeter (patron god of measurement, of course). Instead, it was developed during the Demeter project as a simple guideline for the code there.
The original statement can sound a bit cryptic to ears raised on current languages, but in a simple form, you can think of it as:
A method can access either locally-instantiated variables, parameters passed in, or instance variables.
A much simpler way to think about it is:
Only one dot per statement.
At its heart, the LoD is about encapsulation. We don’t want to reach inside an object and manipulate its insides; that’s just mean. Instead, we want to ask objects to perform some action for us. Let the object deal with its collaborators.
The LoD can also be thought of in terms of knowledge duplication. By exposing the internals of an object, we are spreading structural knowledge through our code. Both the object and the outside collaborator know about its internals.
Personally, I find the LoD to be an extremely simple, incredibly powerful mechanism for helping ensure proper encapsulation and decoupling of behaviors across an object graph. The best way, of course, to get a sense of its power is to read the Original Paper.
Some constraints that I’ve used:
Lines of code per method <= 3
We all have seen methods we consider too large. But what size is reasonable for a method? Let’s go to the extreme and say three lines is the maximum.
No in-method branching statements
Branching statements, such as the notorious if, are a form of procedural polymorphism. Let’s work on improving our object skills by finding other mechanisms to get the same effect. Prefer type-based polymorphism or lookup tables.
No primitives across method boundaries (input or output)
The only types that can be passed across method boundaries (inputs and outputs) are ones that we have defined. No data primitives, such as booleans, integers or strings. You also are not allowed to use primitive data structures such as Lists, Arrays or Enumerables. Focus instead on understanding what the types represent and building types for those concepts.
Mute ping-pong pairing
One member of the pair writes the unit tests, the other member writes the code to turn those tests green. You can think of the roles as “test redder” and “test greener.” This is standard. However, the only communication allowed between partners is through the tests and the code. And no cheating by putting a bunch of comments!
Find the loophole
Generally coupled with ping-pong pairing, one pair writes the tests, the other pair tries to get those tests passing. The catch is that the pair working to get the tests passing writes the wrong code. How long can you go before the tests force you into a “correct” algorithm. Here’s the catch, though: you must write production-level code, think of it as code you would show a prospective employer.
No return values
Similar to the principle of “tell, don’t ask” this constraint takes away your ability to return values from a function. Focus entirely on sending messages to objects.
Program like it’s 1969
Compilation is expensive and not real-time. You can only run your code twice during the session: first at the 30-minute mark, the second at the 44-minute mark. Better pay attention to syntax!
Practicing different aspects of object-oriented design is great, but what happens when you put everything together in an extreme situation? Object Calisthenics provides a set of rules that really work out your OO understanding.
Some Thoughts On Pair-Programming Styles
Coderetreat workshops encourage pair-programming as a form of sharing and learning together. While the majority of this book consists of concrete examples of code-level design decisions, this section addresses some patterns I’ve seen around pair-programming.
Traditionally, pair-programming has been introduced via the Driver-Navigator form. In this form, one member has the keyboard and control of the input. Their job is to type and focus on the minute-to-minute coding. The other member is the navigator. Their job is to pay attention to the code being written, but keep the larger picture in mind, guiding the driver in the right direction.The pair should swap roles frequently.
Unfortunately, too often this form of pair-programming leads to what I call the “Driver-Twitterer” style of collaboration. In this mode, the person with the keyboard is writing code while the other person watches intently for a short time. Then, after a bit, the navigator starts to lose interest. Perhaps the driver isn’t talking, perhaps the navigator doesn’t want to disturb them. Sometimes I’ve seen where the driver says “just a second, I’ve got an idea,” and then proceeds to code in silence for minutes on end. This can have the effect of boring the navigator. So, what do they do? Naturally, they check twitter. Or email. Or some other non-code-focused task.
As with every aspect of development, communication is key here. But, without practice, driver-navigator level of communication is lacking. In order for this style to work, the pair needs to have good communication habits, constantly keeping the other abreast of what thoughts are going through their head. Unfortunately, this level of communication isn’t necessarily built-in to a new pair. Because of this intense communication requirement, I generally consider the driver-navigator style of pair-programming to be a more intermediate level style.
It is quite common for a coderetreat workshop to be a person’s first time pair-programming, their introduction to the practice of writing code as a team. Because of this, I like to introduce a style that has the necessary level of communication built-in to the practice. The style I introduce is called “Ping-Pong Pairing.”
There are two basic forms of ping-pong, but they both share on very important aspect: both members are writing code frequently. Because of this, I stress the importance of having two sets of live input devices, one for each participant. So, there would be two keyboards and two mouses, all live. I find that having this setup minimizes the context shift when switching who is typing. Having two live input devices isn’t a requirement, of course, but it definitely smooths over some inherent friction in having to pass the keyboard back and forth.
The first style of ping-pong is where one member takes on the role of test writer, and the other takes on the role of getting the tests to pass. I like to call the test writer the “test redder” and the one getting them to pass the “test greener.” The table below illustrates the flow of writing.
Ping-Pong Form 1
make test green
make test green
The second style of ping-pong is where the role of “test redder” passes between participants. This is done by having the first member write a test, then control is passed to the other member. That person gets the test to pass, to turn green, then they are responsible for writing the next test. The table below illustrates the flow of writing.
Ping-Pong Form 2
make test green
write next test
make test green
write next test
make test green
write next test
The primary difference between these two is that in the first form, the role is stable, but control is passed. In the second, the role is passed along with control. Both are effective and great ways to introduce people to pair-programming.
Which Style Should You Choose?
If you are an experienced pair, or at least both members are experienced at this style of collaborative code writing, then it doesn’t matter which style you use. In fact, it is common to see all styles used through a pairing session. With experience, participants generally have developed the level of communication necessary for working in whatever form is useful at the moment.
As I mentioned above, though, I consider driver-navigator a more intermediate style. So, if one, or both, of the participants are new to pair-programming, then ping-pong can be a fantastic way to introduce the concepts. I generally recommend a specific ping-pong style based on the level of testing experience of the members.
Only one member has experience writing tests: Form 1
Having the experienced person writing most of the tests is the most effective. Over time, the less-experienced person can start picking up test writing. By watching the tests being written, though, they can learn the thought process behind test-driven development.
Both members have experience writing tests: Form 2
Since the tests guide the design, it can be useful to have both members influencing that aspect. Passing the test-writing role back and forth can help keep both members interested.
Pairing is a fantastic way to develop software. I’ve written some of my best code, my best systems, when two people’s hands were on the keyboard. At the end of a coderetreat, it is very common to get the feedback from first-timers that they didn’t expect working in a pair to be so productive. Pair-programming is listed frequently in the closing questions as both surprising and what they will take with them going forward.
4 Rules of Simple Design
Here are some significant places to read more about the 4 rules of simple design.
There is some interesting discussion on the c2 wiki page for XP Simplicity Rules. And, of course the c2 wiki will have links to a plethora of discussions around related principles.
For example, there is also information and discussion on the c2 wiki about the DRY Principle.
Conversations with Joe Rainsberger have had a significant impact on my thoughts around applying the 4 rules. He has a blog post titled “The Four Elements of Simple Design”. He also has a wonderful post that explains the apparent discrepancy in how different people order rules 2 and 3, titled “Putting an Age-Old Battle to Rest”.
And, of course, these are just a start. I highly recommend that you do a search for more.
Here are some suggestions for books and papers to read on the general topic of design.
Practical Object-Oriented Design in Ruby by Sandi Metz
This is a fantastic book that covers ideas and guidelines for building maintainable systems. Sandi is an experienced developer with tons of object-oriented design experience. She definitely knows her stuff and how to explain it in an understandable fashion. Although this book has Ruby in the title, most of the lessons in it are applicable across all object languages.
On the criteria to be used in decomposing systems into modules by David Parnas
Wow, what can I say about this paper? It is from 1971. It covers an investigation into two types of modularization and the effect they have on maintainability. You know it is going to be good by the first line of the abstract, “This paper discusses modularization as a mechanism for improving the flexibility and comprehensibility of a system while allowing the shortening of its development time.” Read it!
Law of Demeter
There is a whole section on this in the other design guidelines section, so I won’t say too much. Go read this paper, though. You won’t be sorry.
Clean Code by Robert Martin
This book lays out in fine detail some general rules around how to write robust, maintainable code. Through concrete examples and guidelines, the book is a wonderful tour through techniques that can make a codebase stand the test of time.
Growing Object-Oriented Software Guided By Tests by Nat Pryce and Steve Freeman
Walk through building a system using “London-style” test-driven development by the guys who literally invented the style of heavy isolation through mocks. The examples are in Java, but the techniques can easily be translated over to other languages.
Mock Roles, not Objects by Steve Freeman, Nat Pryce, Tim Mackinnon, Joe Walnes
A classic paper about effective use of test doubles when doing unit testing.
Test-Driven Development Screencast by Kent Beck
This is a fabulous short screencast series where Kent Beck goes through building a component using TDD. All the while, he outlines his thought process. Rare chance to watch and listen to the father of test-driven development use the technique.
Other Things You Probably Should Most Definitely Read
The Pragmatic Programmer by Dave Thomas and Andy Hunt
This book literally changed my life. Filled with tips and ideas about how to be effective as a software developer, including a section on the DRY principle.
1. Breeder Pattern picture by wikipedia user HyperDeath. For more information on licensing, see the picture’s wikipedia page.↩
2. I had originally used the term factory method here. Thanks to Ian Whitney for pointing out that it could be confusing, as this is really closer to the builder pattern.↩
3. Components should be open for extension, but closed for modification. See the Other Design Guidelines section in the back of this book.↩