How to think about patterns
What is a design pattern? Sometimes it's an evanescent concept to explain, so I put together this list of roles a pattern fulfills in software development to get a concrete feel about why we are codifying solutions as Composite or Data Mappers.
What patterns are
A solution to a recurring problem: patterns are harvested from real-world code, and face a typical problem of the object-oriented paradigm giving you the experience of all the programmers that have encountered it before. Functional languages in fact have different patterns (such as Currying), as their list of common problems is different too.
A superstructure for your object-oriented code: patterns are a way to document and communicate design decisions that spans from one to 5-6 classes or interfaces. When I read Visitor, I already know how these few classes work together, without reading their implementation (but only method signatures). Even if the pattern is an obvious solution (like the Strategy one), naming it is the first step in talking about it and comparing it quickly with other solutions.
Proper nouns: I talk about Decorator and Strategy, not decorator and strategy. The capitalization resembling that of a person's name helps to distinguish them while reading or just skimming text.
But most of all they are...
A solution in a particular context, shaped by external forces: if you want to be closed against change A and B, apply pattern X while you will then spend more time if this other change C is needed (a trade-off).
It follows from the context sensitivity that there is no a priori right choice of a pattern without external forces to take into consideration. This is one of the reason why katas usually have to be directed (I want to experiment with the Composite pattern or with my editor or with TDD at the acceptance level), since they do not get a particular direction giving the lack of a larger context with likely axis of change and unlikely ones.
Consider this simple example: design an object-oriented structure to apply operations to a tree.
One of the patterns that come to mind is the Composite:
- create an interface Tree
- implement it with a single class Leaf
- implement it with another class Branch that composes a number of other Trees (that can in turn either be a Leaf or a Branch... or else).
With the application of this pattern, you get a graph (actually a tree, but calling it like that would be confusing) of objects, instances of the classes Leaf and Branch.
This pattern closes against the change of other kinds of Trees: you can create a DeadLeaf class, or if you have a binary branch you can create a TripleBranch that can compose three Trees instead of two. These changes are local in the object graph and easy to perform, so we can say the Composite pattern is a solution that works well in the presence of forces that multiply the kinds of Trees you're dealing with.
If instead we need to add a new operation to the Tree, such as calculating its height, we need to add a method to all the classes involved. The change is not local and more invasive; the Composite pattern cannot deal with the forces of adding operations to the Tree type (while for example the Visitor or functional approaches can).
What patterns are not
Something that should be forced onto your code. I've never encountered an application that required more than half, let alone all, of the 17 original patterns of the Gang of Four book.
Some of the patterns of that book are even surpassed (Singleton) or never useful (Chain of Responsibility?), while more modern books like Patterns of Enterprise Application Architecture present alternative patterns for the same problem, and as such it is impossible to implement many of them as the same time as they are incompatible (think about Active Record and Data Mapper).
A recipe to follow to the letter. It's hard to misimplement a pattern: there are many variations admitted, and if a new one solves your problem, you're free to adapt the pattern. Just be aware of the forces the original pattern dealt with (addition or removal of operations? Of new types? Of new implementations of a type? Of logic in predefined hooks?) so that you can explain why you needed to depart.
The title of this post is an homage to How to think about OO.