Don’t Get Obsessed With Design Patterns
Design patterns can be our best ally when used correctly.
However, when used in the wrong place, they can do more harm than good. It's imperative that we learn how to use them correctly.
Since a design pattern is a predefined solution to a specific kind of problem, proven over time and known by the software community, you may be tempted to apply it as soon as you have a chance.
When you learn a new technique, it's good to try it out in the wild as soon as you can. However, having the mindset of “looking for the slightest opportunity to introduce a pattern into the code” seems like a bad idea to me.
How can I know when it's best to use a design pattern? As we’ll see below, assessing the likelihood of changes in different parts of our code and understanding the intent of the pattern we’re using are crucial to making the appropriate decision.
I’ve come to this conclusion by considering three things. Let's take a look at them.
If you know and apply object-oriented design principles, design patterns will come out naturally
I believe that knowing object-oriented design principles and applying best practices like SOLID, KISS and YAGNI are far more important than design patterns themselves. If you apply these principles, patterns will come out naturally.
Let's see it with an example. Suppose that we're working on a small app that displays a music sheet. For the sake of simplicity let's assume that, for now, we'll just draw half and quarter notes.
Among the different classes of our system, we have this one:
As you can see, there's a clear duplication here. The only difference is in the parameter applied to the drawNoteHead method.
After some consideration, you’ll likely come to the conclusion that a good way to get rid of the duplication is to do the following:
- Create a generic drawNote method and copy the body from one of the existing functions.
- Transform the parameter of the drawNoteHead method into a new method. Then, this method can be overridden by children classes, so the father class holds the basic logic and the children classes hold the differences.
- Replace all callings to drawHalfNote and drawQuarterNote with the new drawNote method of the appropriate child class.
All right, let's do it:
There might be other solutions, but we got what we wanted: we eliminated the duplication of the code.
Well, guess what?
You just applied the Template pattern.
Just like that. Without thinking about it. That's the beauty of object-oriented design principles.
You may not know all the design patterns in the world, but as long as you know good object-oriented design principles, you don't have to.
Of course, if you knew the Template pattern, you would have seen it coming right away. You would say “this is the perfect scenario for the Template pattern” and apply it. That's the right way of applying patterns: when you clearly see that they fit and they make your life easier.
This brings me to my second point.
You might not need it at all
Sometimes we look at a piece of code, we cringe at how ugly it is, and we notice instantly that a pattern may make it better.
But what if it works and you don't need to change it?
For example, let's go back to our music sheet displaying app, but with a slightly different scenario. Imagine that we want to create one class for each type of clef. Our app is going to support all clefs based on the 3 basic ones: the C, the F and the G clef.
We have a function in our code that returns the correct instance of our clef. It looks like this:
Now that's some ugly code. You're probably feeling the urge to refactor it, right? Perhaps you're thinking of applying the Factory Method pattern, and, yes, it would fit and definitely make the code cleaner.
But… what if I tell you that this code is not going to change… ever?
I'm not a musician, so I might be wrong, but it seems unrealistic to me that new notes are going to be invented in the future. If there are no notes added, then there is no need to change this code. It's so unlikely that it's simply not worth it to touch this code at all.
No matter how ugly a piece of code is, if it works and you don't need to change it, refactoring the code (with a design pattern or any other technique) has a needless cost.
This brings me to my third point.
The cost of adding a design pattern
There are many different design patterns, but most of them have something in common: when you apply them, you need to lay out some structure. In other words, you need to add classes and/or interfaces to the code.
In the first example, this structure consists of an abstract class extended by two children classes. What's more, in order for the old code to use these new classes, you also need to make some updates not directly related to the design pattern itself.
The moral of the story is: if you're thinking about applying a design pattern, consider the cost of doing so and the potential benefit. Doing it only for the sake of doing it will make your code more complex than it needs to be.
How can I know when it’s best to use a design pattern?
That's the million dollar question. It depends on two things: how likely a piece of code is going to change in the future, and the intent of the pattern.
The likelihood of changes in different parts of our code
The examples that we saw in this post showcase the importance of considering changes in order to decide whether to include a design pattern or not.
In the first example it was absolutely necessary to include a pattern. After all, we're not going to settle with music sheets that only have half and quarter notes, are we? Since adding more types of notes would make the duplication problem worse, a pattern is needed in order to avoid that.
In the second example it's exactly the opposite. Since we can safely assume that no clefs or notes will be added, this piece of code doesn't need to be changed and therefore, there's no need to include a pattern.
The intent of the pattern
It's not enough to know how a pattern is implemented; we also need to know the reason behind the existence of the pattern.
In other words: we need to know what the pattern is made for.
We need to perfectly understand the kind of problem that it solves. If we know this, it will be easier for us to know the proper time to apply a pattern.
On making these types of decisions, two books come to my mind:
- Head First Design Patterns: if you've never heard of design patterns before, or you're just starting out, this book is a must. It explains the most frequently used design patterns in a very friendly and easy way.
- Design Patterns: Elements of Reusable Object-Oriented Software: this is a classic and a must-read as well. It's written as a catalog of the most common design patterns, which are organized into three main categories: creational, structural, and behavioral.
Unfortunately, there's yet another problem we must face: recognizing and applying patterns in the wild.
When you're writing new code from scratch, it's fairly easy to recognize the need for a pattern as you go along. However, taking old code and making it use a design pattern is more challenging.
You need to be sure that you fully understand what the code does before touching it. Figuring this out can be the easiest or the most painful experience, depending on how convoluted the code is.
Luckily for us, there's another book that can help with this problem: Refactoring to Patterns. This book tells you how to actually take existing code and integrate design patterns into it.
These books, along with many other ones, are featured in John’s ultimate list of programming books, which I highly recommend.
Design patterns are not the holy grail of programming. In fact, I don't think such a thing exists.
They are simply a mechanism that can make our code much cleaner and easier to understand and maintain… when applied correctly.
So, when do you think it's appropriate to use a design pattern, and when it's not? Tell us in the comments below.
Do check out our Simple Programmer course — How to Market Yourself as a Software Developer for tips and tricks on progressing your career.