The topic of design patterns is covered well enough. There are plenty of good resources for it: starting from the famous book “Design Patterns” by the Gang of Four and finishing with various online resources.
Design patterns are an essential topic for students in the software engineering course. Understanding and effectively applying them can make the software reliable and maintainable.
But do we need all of them? How should you apply more than 20 design patterns in your solutions? The correct answer is: you shouldn’t.
There are 23 design patterns across three categories: Creational Design Patterns, Structural Design Patterns, and Behavioral Design Patterns. However, only a few of them are used in almost any solution. Which are those?
Only a Single Instance
Singleton is a well-known creational design pattern. Its main purpose is to guarantee only a single instance of the entity across the entire application.
When would you use it?
The pattern Singleton can be helpful when you operate with a file session during runtime. Imagine you want a single instance of the logger in your application. Otherwise, it can lead to concurrency issues – multiple class instances will try to write to the file. Some of them will need to wait; in certain cases, this will lead to defects in your code.
How would you implement it?
This is a challenging question because the implementation can dramatically differ based on the programming language. Each language has specifics and constraints that allow the implementation of this pattern.
On top of that, there are multiple versions of this pattern: thread-safe and thread-unsafe. For example, if you want to support the initial object creation in the concurrent environment.
Python offers a few options for implementing the Singleton pattern because of its flexibility. Here is what one of the basic implementations of Python looks like.
class Singleton:
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "instance"):
cls.instance = super().__new__(cls)
return cls.instance
The method __new__
is a special static one. It is always called to create a new instance of the class. So we check if the instance was already created. If it was not, we create an instance of the class Singleton
and store it in the variable instance
.
Every time we try to instantiate a new object by calling Singleton()
we will always receive the same instance of the class Singleton
.
Produce Like a Manufactory
There are multiple implementations of this design pattern, as well as different names. You could hear about Factory Method, Factory, and Abstract Factory. Each has some details in the implementation, but all have a common purpose – to make a specific instance of the class.
The design pattern Factory resolves the problem when you work with a few classes following the same interface. As a user of the interface, you are not interested in the implementation details of a certain class. Your goal is to have an instance that serves your needs.
For example, consider various sign-in options on your website. The project already contains the logic to authenticate via Facebook or Google. Each authenticator has implementation specifics, but what is more important, each of them has a function login()
.
from typing import Protocol
class Authenticator(Protocol):
def login(self): ...
class FacebookAuthenticator:
def login(self):
print("Sign-in via Facebook")
class GoogleAuthenticator:
def login(self):
print("Sign-in via Google")
How the design pattern Factory Method can help?
We explore one of the possible options of the pattern in Python.
def make_authenticator(authenticator_name: str) -> Authenticator:
authenticators = {
"Facebook": FacebookAuthenticator(),
"Google": GoogleAuthenticator(),
}
authenticator = authenticators.get(authenticator_name)
if authenticator is None:
raise Exception("Authenticator not implemented")
return authenticator
authenticator = make_authenticator("Facebook")
authenticator.login()
The factory method make_authenticator()
accepts a string as a parameter. The parameter authenticator_name
can also be an Integer or an Enum. Then we try to create a requested authenticator based on the parameter.
Pay attention to the return type of the factory method. It is a protocol Authenticator
. We don’t know the actual instance type, and this allows us to hide the implementation details from the users of the factory.
The design pattern Factory or Factory Method is a common one, and so far, I’ve seen it in every single project. It is easy to implement, and it brings much value.
Notify Me When It Happens
The first time I discovered this pattern was in 2010, working on the project for OS Symbian. Nokia made the design pattern Observer as a core of their operating system.
The pattern Observer is a necessary step towards supporting asynchronous actions.
To better understand the asynchronous support, we make an analogy with the real world. When you turn on a kettle to boil water for tea, you want to know when the water is boiled. You don’t want to stare at the kettle all the time. Instead, you wish to receive a notification that the work is ready. A “smart” kettle would send you a push notification about that.
The Observer pattern works the same way.
First, we need to define an interface for the observer.
from typing import Protocol
class Listener(Protocol):
def notify(self): ...
class PushNotificationListener:
def notify(self):
print("PushNotificationListener is notified")
class SmsListener:
def notify(self):
print("SmsListener is notified")
We have a few observers declared now, and we can pass them where we want to use them.
from typing import List
class Kettle:
def boil_water(self, listeners: List[Listener]):
print("Water is cooked")
for l in listeners:
l.notify()
The method boil_water()
receives a list of listeners that need to be notified. When the action is completed, we loop over the listeners and call the method in each of them.
The results of the execution can be seen below.
listeners = [SmsListener(), PushNotificationListener()]
kettle = Kettle()
kettle.boil_water(listeners)
>>> Water is cooked
>>> SmsListener is notified
>>> PushNotificationListener is notified
The biggest advantage of the pattern Observer is that it allows a decoupling of system components. In our example, the class Kettle
doesn’t know anything about SMS or push notifications. It receives a generic interface and calls a method notify()
when needed. Every entity in this example remains well separated from the other.
The Pattern Observer is slightly more complex than the others. But the benefits it brings are not comparable.
We can tell the same about the design patterns. It is not a universal approach that will solve all the problems. But it is a tool that should be used when necessary.
“When all you’ve got is a hammer, every problem looks like a nail”. — Abraham Maslow
Make sure you master at least the ones in this article; they will significantly improve your architecture.
Do you want to know how to grow as a software developer?
What are the essential principles of a successful engineer?
Are you curious about how to achieve the next level in your career?
My book Unlock the Code offers a comprehensive list of steps to boost
your professional life. Get your copy now!