The 5 Most Common Design Patterns Explained in 6 Minutes
In today's article, we're going to talk about design patterns.
In today's article, we're going to talk about design patterns.
High-level programming languages have been around since the 1950s, and since then, programmers have been using them to solve all sorts of different problems.
Over time, programmers realized that even though the problems are unique, there are often similarities in the solutions.
They aren't exactly the same, so you can't create a library or use a standard algorithm to solve them.
Instead, developers would reuse the core ideas of a solution for the different problems they came across.
Eventually, names were given to these typical solutions, and that is how design patterns were born.
What is a Design Pattern?
If you're struggling to work out the difference between a design pattern and an algorithm, perhaps this analogy will help.
Let's say you want to bake a birthday cake.
To do this, you might follow a recipe, which provides step-by-step instructions on how to bake that cake.
The recipe, in this case, is an algorithm. It gives you a repeatable set of instructions to produce the same result.
Sure, you might switch out some ingredients to produce different cakes, but these are just different input variables.
Now, let's also say you want to have a birthday party.
If you think about a birthday party, you're probably imagining balloons, banners, and a cake with candles.
However, everyone's idea of a birthday party is different. There are no set steps to follow, no recipe like you have with a cake.
A birthday party will be different for every person, much like the problems we face in software engineering.
But if you could see what everyone else is thinking, even though their ideas of a party are very different, you would still recognize it as a "birthday party."
The birthday party, in this case, is a design pattern. It's the concept of a solution that can be reused, even if the implementation details are slightly different.
The Gang of Four and the 23 Patterns
In 1994, four authors wrote down 23 design patterns in the book Design Patterns: Elements of Reusable Object-Oriented Software.
That's a bit of a mouthful, so most people call it the "Gang of Four" book.
If you've been programming for a while, chances are you've already used some of these design patterns without even realizing it.
Most of them are common sense. These 23 design patterns are split into three main groups:
Creational Patterns: These relate to creating objects in your code.
Structural Patterns: These define how objects are structured, especially in large systems.
Behavioral Patterns: These cover the interaction between objects and their responsibilities.
No one expects you to memorize all 23 patterns.
However, it's worth being familiar with them.
When you encounter a problem, you might think, "Ah, that sounds similar to this design pattern," and then you can look it up to see if it applies.
Out of all these patterns, there are five that many developers find themselves using over and over again. Let's have a look at those:
1. The Strategy Pattern
Let's go back to the cake-baking analogy.
Imagine you love cake so much that you decide to write an application that provides cake recipes.
You start with a birthday cake recipe, but later you want to add a red velvet cake, then a carrot cake, a coffee cake, and a lemon cake.
You could use if
statements or switch
statements to handle all the different cakes, but your code would quickly become messy spaghetti.
The code in your application shouldn't need to care too much about which specific cake you pick.
This is where the Strategy Pattern comes in. You can split each cake recipe into its own class, with each class implementing the same methods, for example:
getIngredients()
getMethod()
This is incredibly useful if you have multiple ways of achieving the same thing.
For instance, in a maps application, you might want to provide navigation for walking, driving, or cycling.
They all serve the same function (navigation), but the implementation is different for each. This is a pattern worth having in your toolbox.
2. The Decorator Pattern
If you need to extend an object's functionality without changing its original implementation, the Decorator Pattern is the way to go.
This pattern is a key way to make your code open to extension but closed for modification (the "O" in SOLID principles).
You've likely used this pattern without realizing it!
Whenever you wrap a class in another class, you're effectively using the Decorator pattern.
The typical way to do this is to have the decorator implement the same interface as the component you want to extend.
Then, in the decorator's constructor, you pass in the original component as an argument.
You can then implement the interface, call the original component's method, and add your new functionality either before or after that call.
3. The Observer Pattern
You're probably already familiar with the concept behind the Observer Pattern.
It's used whenever you want to notify interested parties that something has happened.
Think of a newsletter subscription. Only those who have subscribed to the newsletter receive an email.
This is how the Observer pattern works. You have a Publisher that implements subscribe()
, unsubscribe()
, and notifySubscribers()
methods.
You then have a Subscriber interface that all subscribers must implement.
When a subscriber subscribes to a publisher, they are added to a list (like an array). When the publisher has an update, it loops through that list and calls the update()
method on each subscriber.
4. The Singleton Pattern
When we think of singletons, we think of creating just one instance of something.
While you can use global variables, they can be changed from anywhere, which can cause numerous problems.
However, there are cases where you genuinely need a single, globally accessible object, such as a database connection.
You don't want a new connection every time something needs to access the database.
The typical way to implement a Singleton is to create a sealed class with a private constructor.
The private constructor means nothing outside the class can create a new instance of it.
You then create a separate method or property that handles the instance creation.
Here is a basic implementation:
// NOTE: This version is NOT thread-safe
public sealed class Singleton
{
private static Singleton instance = null;
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
The problem with the code above is that it's not thread-safe!
Two threads could call the Instance
property at the same time, both pass the null
check, and end up creating two separate instances.
To get around this, we need a locking mechanism:
// Thread-safe version
public sealed class Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null) // Initial check
{
lock (padlock) // Lock the critical section
{
if (instance == null) // Double-check
{
instance = new Singleton();
}
}
}
return instance;
}
}
}
Another, much simpler way to do this in C# (.NET 4 and later) is with the Lazy<T>
type, which is thread-safe by default and uses less code:
// Thread-safe version using Lazy<T>
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy =
new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance { get { return lazy.Value; } }
private Singleton() { }
}
5. The Facade Pattern
The Facade Pattern is essential in software development.
It's all about simplicity. We constantly use different libraries and frameworks that, while saving us time, can make our code messy and complicated.
Often, these libraries do far more than we actually need.
Let's say you're using a fictional, poorly designed logging library.
This library can send logs to a file, the console, or even a Slack message.
The problem is, it only has one method, log()
, and for every call, you have to specify the log type and destination.
Rather than cluttering your code with these implementation details, you can create your own logger interface (a "facade") with simple methods like logInfo()
, logError()
, etc.
All the messy code from the bad library is then restricted to a single class that implements your clean interface.
If you ever decide to change the logging library, you only have one place to make changes, and the rest of your code is unaffected.
Conclusion
These are five of the most common design patterns that many developers use, but there are over 18 other patterns from the original book.
It's worth looking at them to become familiar!
Having them in the back of your mind will help you identify solutions when you come across complex problems in the future.
There are many great online resources that cover all 23 design patterns with examples of when to use them.