Over the past couple of months I’ve been working with several clients struggling with doing incremental architecture and design. As an organization moves away from a typical waterfall method – with lots of time for analysis and design – they have to find ways to still build scalable, sustainable systems in an incremental fashion.
I just finished up an article for the Norwegian Developers Conference Magazine around iterative design. In talking through the article points with the ever wonderful Corey Haines and Pat Maddox, Corey brought up the notion that he tends to shy away from design patterns discussions with beginners because it tends to lead to large, bloated designs.
It’s unfortunate that the application of design patterns far too often does lead to large, bloated, unmaintainable designs. It’s a very common misconception that the patterns are “recipes” we apply to a solution to build it into something recognizable. But, in fact, patterns are actually inherent in the problem itself, and present options for understanding different approaches towards the design of a solution. It’s when we blindly apply that pattern or sets of a pattern as the solution that we get in trouble.
I have some larger examples in the article, but I thought it would be fun to apply the thinking to Conway’s Game of Life – a canonical example in many of the Code Retreats and other design and emergent code exercises. It’s a simple game with simple rules that produces some amazingly complex behavior.
To follow along, there are two key elements to understand. The first is the guidelines the authors of the “Gang of Four” (GoF) Design Patterns book laid out for the application of patterns. These are:
- Design to Interfaces – This is commonly construed as meaning we should design all interactions to be through interface types. Instead the guidance is saying that we should design based on the interfaces and interactions between the objects – something we call Programming by Intention
- Favor Object Delegation over Inheritance – Solving specialization through inheritance works OK – until we hit a second specialization case. So we should favor delegating work to other classes over creating specialized subclasses whenever possible
- Encapsulate the Concept that Varies – When things vary – whether they are implementations, designs, relationships or anything else – we need to find ways make it appear to the calling classes that the varying issue isnÊ¼t actually varying
The second is the rules of the Game of Life. The board is laid out with cells, and each cell has up to 8 neighbors. At each tick in time, the following transitions happen simultaneously (from the Wikipedia article):
- Any live cell with fewer than two live neighbors dies, as if caused by under-population
- Any live cell with two or three live neighbors lives on to the next generation
- Any live cell with more than three live neighbors dies, as if by overcrowding
- Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction
Normally we can start implementing this exercise using a test-first method. But what happens if we step back and look at the problem and see what patterns are inherent in it? We know that we have a cell, and that it has two variations – Alive or Dead. Each variation has different behavior. However – and this is key – the two variations don’t produce different versions of the cell. This matters, because we could implement a cell using subclassing for the states:
|Cell| |AliveCell| |DeadCell|
But it would make our system more difficult because we’d have to swap cell instances every time the state changes. But it is a variation, and looking at the third GoF guideline above, we should encapsulate that variation, which does happen above. But, we violate the second guideline, which is to favor delegation over inheritance.
So, if we keep looking at this, a
Cell doesn’t actually care what state it’s in – all it cares about is that it has a state. A
Cell also doesn’t care about what happens because of that state – it only cares that it needs to see if its state should be updated. So, we could think about the problem with this design:
|Cell| <------- |Status| |Alive| |Dead|
And now the job of the cell becomes to respond to a time tick and find out if it needs a new status. With that thinking, we can apply a Strategy based on the state to determine the next state to move into, or run the
Cell through a Chain of Responsibility to determine the next state (each step would return the correct Status object, with the default to return the existing one).
Is this overkill? Having a Cell with a separate Status object which gets selected based on a Strategy or Chain of Responsibility? No! Because at this point, we aren’t talking about solutions but about the patterns inherent in the problem. Certainly if we implemented a
Cell with a
Status object with an
AliveStatus class and a
DeadStatus class someone, somewhere, should slap us upside the head. But if we just use patterns thinking as an initial vector for our approach, we can think through some of the different scenarios, and then begin implementing the code test-first following the 4 Rules of Simple Design and probably end up with a
Cell class that has a
Boolean is_alive field.
So, when you are tackling a problem, step back and look at what the problem is trying to tell you. More than likely there is some type of variation which needs to be encapsulate, and several patterns inherent in that problem. Just don’t blindly continue on with implementing the patterns based on a book. Patterns aren’t the code you write. They are the ideas in the problem you are solving – and you should implement those ideas in the simplest way you can.