by Zoran Horvat
Sep 22, 2015
Design patterns are a great idea, but they don't work that well - that is the sentence I have heard several times from developers. Indeed, the way design patterns are applied in so many cases is far from ideal. Is it the patterns or something else that causes undesired consequences? That is the question we will try to answer in this article by covering a few of the most prominent misunderstandings.
Many authors make clear distinction between design patterns and ready-made solutions. The famous quote of Christopher Alexander's seminal book on design patterns in architecture (architecture in building towns, not building software) tells precisely that:
"Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice." [Christopher Alexander et al., A Pattern Language: Towns, Buildings, Construction, Oxford University Press, New York, 1977, p. x]
The key part in this sentence is "without ever doing it the same way twice." Apply the same pattern to a similar, but not quite identical problem as before, and its implementation will most likely be at least a bit different.
Some patterns have found their place in programming languages. In C# we have the foreach keyword which backs the Iterator pattern, or the event keyword for the Observer pattern. Scala supports mixin-based inheritance out of the box. There are more examples in other languages, especially dynamically typed ones. This may add to the confusion so let’s try to explain the effects.
The foreach keyword in C# is just half of the solution. It requires an IEnumerable implementation but doesn’t say who implements it. Is it the underlying object, or that object exposes a member that returns it? That is a tough question sometimes, but unfortunately not considered by many programmers who just take the shortcut, oblivious of the fact that built-in Iterator implementation is far from complete.
The event keyword offers a complete implementation of the Observer pattern, though. Yet many Domain-Driven Design practitioners avoid it altogether. If you have played with event sourcing in DDD, you have probably learned that custom Observer implementation is more appropriate than the one sitting behind the event keyword [Vaughn Vernon, Implementing Domain-Driven Design, Addision-Wesley, Boston, 2013, p. 296].
This confirms that the design pattern is not a recipe, even when the programming language offers a helping hand.
Many programmers tend to read requirements in terms of design patterns. Too easily developers fall into a trap of seeing some pattern almost like a destiny to their design. We could build the patterns into the initial design and be happy with it for a while. But what when the requirements change?
The Gang of Four seminal book warns us of this problem: “Consider what should be variable in your design. [...] The focus here is on encapsulating the concept that varies, a theme of many design patterns.” [Erich Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Boston, 1994, p. 29]
Design patterns are adding value to the design if they let us wrap the variation into a separate class. But how can we identify the varying element in the original requirements? If the customers knew what is going to vary, they would vary the requirements already and you would be faced with a different set of them to implement!
It takes a working system and the changing mind to see what parts of the system really vary in practice. Or, as Martin Fowler had put it: “The big frustration, of course, is that all this flexibility is not needed. Some of it is, but it's impossible to predict which pieces those are.” [Martin Fowler, Refactoring: Improving the Design of Existing Code, Addison-Wesley, Boston, 1999, p. 57]
Trying to figure that from initial requirements often turns to be a premature decision. We recognize this syndrome after becoming unable to capture the variation because the target part was needlessly fixed in some larger picture and now cannot escape it.
Take Remote Proxy pattern as an example: Designs fail for introducing remote proxies too early, when in the end nothing really gets remote. And by having a remote proxy in the heart of the implementation, it is impossible to introduce transactions over a subsystem – the requirement that suddenly appeared much later in the process.
In almost all cases, if we recognize a design pattern in requirements, that is just because we didn't read the requirements well. They never spoke in terms of design patterns. It could only be that we have projected our desire to see a certain solution.
So we come to the question of how or, more precisely, when to apply a design pattern if we wish to be successful at it. After years of experience my practices have converged to a very small set.
In the beginning of the design I tend to ignore design patterns. The goal is to produce a design which follows requirements to the letter. At that stage pulling abstractions and giving generic names to objects is a trap. It's too early to generalize or to capture variation.
Later on, we naturally begin to feel discomfort about particular segments of code. That is where and when we start applying design patterns. They solve actual issues in the design. By applying a pattern we step from one solution that works to a better solution that still works.
Bottom line is that design patterns perform much better if used in context of refactoring. When added to the initial design, we are at great risk of forcing an inappropriate solution. In cases like that, we mostly end up dealing with the complexity of a design pattern, without enjoying the benefits it brings.
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented development style and clean coding practices and techniques that improve longevity of complex business applications.