The most satisfying problems in software engineering are those that no one has solved before. Cracking a unique problem is something that you can use in job interviews and talk about in conferences. But the reality is that the majority of challenges you face will have already been solved. You can use those solutions to better your own software.
Software design patterns are typical solutions for the reoccurring design problems in software engineering. They're like the best practices employed by many experienced software developers. You can use design patterns to make your application scalable and flexible.
In this article, you'll discover what design patterns are and how you can apply them to develop better software applications, either from the start or through refactoring your existing code.
Note: Before learning design patterns, you should have a basic understanding of object-oriented programming.
What are design patterns?
Design patterns are solutions to commonly occurring design problems in developing flexible software using object-oriented programming. Design patterns typically use classes and objects, but you can also implement some of them using functional programming. They define how classes should be structured and how they should communicate with one another in order to solve specific problems.
Some beginners may mix up design patterns and algorithms. While an algorithm is a well-defined set of instructions, a design pattern is a higher-level description of a solution. You can implement a design pattern in various ways, whereas you must follow the specific instructions in an algorithm. They don’t solve the problem; they solve the design of the solution.
Design patterns are not blocks of code you can copy and paste to implement. They are like frameworks of solutions with which one can solve a specific problem.
Classification of design patterns
The book, Design Patterns- Elements of Reusable Object-Oriented Software written by the Gang of Four (Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm) introduced the idea of design patterns in software development. The book contains 23 design patterns to solve a variety of object-oriented design problems. These patterns are a toolbox of tried and tested solutions for various common problems that you may encounter while developing software applications.
Design patterns vary according to their complexity, level of detail, and scope of applicability for the whole system. They can be classified into three groups based on their purpose:
- Creational patterns describe various methods for creating objects to increase code flexibility and reuse.
- Structural patterns describe relations between objects and classes in making them into complex structures while keeping them flexible and efficient.
- Behavioral patterns define how objects should communicate and interact with one another.
Why should you use design patterns?
You can be a professional software developer even if you don't know a single design pattern. You may be using some design patterns without even knowing them. But knowing design patterns and how to use them will give you an idea of solving a particular problem using the best design principles of object-oriented programming. You can refactor complex objects into simpler code segments that are easy to implement, modify, test, and reuse. You don’t need to confine yourself to one specific programming language; you can implement design patterns in any programming language. They represent the idea, not the implementation.
Design patterns are all about the code. They make you follow the best design principles of software development, such as the open/closed principle (objects should be open for extension but closed for modification) and the single responsibility principle (A class should have only one reason to change). This article discusses design principles in greater detail.
You can make your application more flexible by using design patterns that break it into reusable code segments. You can add new features to your application without breaking the existing code at any time. Design patterns also enhance the readability of code; if someone wants to extend your application, they will understand the code with little difficulty.
What are useful design patterns?
Every design pattern solves a specific problem. You can use it in that particular situation. When you use design patterns in the wrong context, your code appears complex, with many classes and objects. The following are some examples of the most commonly used design patterns.
Singleton design pattern
Object oriented code has a bad reputation for being cluttered. How can you avoid creating large numbers of unnecessary objects? How can you limit the number of instances of a class? And how can a class control its instantiation?
Using a singleton pattern solves these problems. It’s a creational design pattern that describes how to define classes with only a single instance that will be accessed globally. To implement the singleton pattern, you should make the constructor of the main class private so that it is only accessible to members of the class and create a static method (getInstance) for object creation that acts as a constructor.
Here’s the implementation of the singleton pattern in Python.
# Implementation of the Singleton Pattern
class Singleton(object):
_instance = None
def __init__(self):
raise RuntimeError('Call getInstance() instead')
@classmethod
def getInstance(cls):
if cls._instance is None:
print('Creating the object')
cls._instance = super().__new__(cls)
return cls._instance
The above code is the traditional way to implement the singleton pattern, but you can make it easier by using __new__ or creating a metaclass).
You should use this design pattern only when you are 100% certain that your application requires only a single instance of the main class. Singleton pattern has several drawbacks compared to other design patterns:
- You should not define something in the global scope but singleton pattern provides globally accessible instance.
- It violates the Single-responsibility principle.
Check out some more drawbacks of using a singleton pattern.
Decorator design pattern
If you’re following SOLID principles (and in general, you should), you’ll want to create objects or entities that are open for extension but closed for modification. How can you extend the functionality of an object at run-time? How can you extend an object’s behavior without affecting the other existing objects? You might consider using inheritance to extend the behavior of an existing object. However, inheritance is static. You can’t modify an object at runtime. Alternatively, you can use the decorator pattern to add additional functionality to objects (subclasses) at runtime without changing the parent class. The decorator pattern (also known as a wrapper) is a structural design pattern that lets you cover an existing class with multiple wrappers.
For wrappers, it employs abstract classes or interfaces through composition (instead of inheritance). In composition, one object contains an instance of other classes that implement the desired functionality rather than inheriting from the parent class. Many design patterns, including the decorator, are based on the principle of composition. Check out why you should use composition over inheritance.
# Implementing decorator pattern
class Component():
def operation(self):
pass
class ConcreteComponent(Component):
def operation(self):
return 'ConcreteComponent'
class Decorator(Component):
_component: Component = None
def __init__(self, component: Component):
self._component = component
@property
def component(self):
return self._component
def operation(self):
return self._component.operation()
class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({self.component.operation()})"
class ConcreteDecoratorB(Decorator):
def operation(self):
return f"ConcreteDecoratorB({self.component.operation()})"
simpleComponent = ConcreteComponent()
print(simpleComponent.operation())
# decorators can wrap simple components as well as the other decorators also.
decorator1 = ConcreteDecoratorA(simple)
print(decorator1.operation())
decorator2 = ConcreteDecoratorB(decorator1)
print(decorator2.operation())
The above code is the classic way of implementing the decorator pattern. You can also implement it using functions.
The decorator pattern implements the single-responsibility principle. You can split large classes into several small classes, each implementing a specific behavior and extend them afterward. Wrapping the decorators with other decorators increases the complexity of code with multiple layers. Also, it is difficult to remove a specific wrapper from the wrappers' stack.
Strategy design pattern
How can you change the algorithm at the run-time? You might tend to use conditional statements. But if you have many variants of algorithms, using conditionals makes our main class verbose. How can you refactor these algorithms to be less verbose?
The strategy pattern allows you to change algorithms at runtime. You can avoid using conditional statements inside the main class and refactor the code into separate strategy classes. In the strategy pattern, you should define a family of algorithms, encapsulate each one and make them interchangeable at runtime.
You can easily implement the strategy pattern by creating separate classes for algorithms. You can also implement different strategies as functions instead of using classes.
Here’s a typical implementation of the strategy pattern:
# Implementing strategy pattern
from abc import ABC, abstractmethod
class Strategy(ABC):
@abstractmethod
def execute(self):
pass
class ConcreteStrategyA(Strategy):
def execute(self):
return "ConcreteStrategy A"
class ConcreteStrategyB(Strategy):
def execute(self):
return "ConcreteStrategy B"
class Default(Strategy):
def execute(self):
return "Default"
class Context:
strategy: Strategy
def setStrategy(self, strategy: Strategy = None):
if strategy is not None:
self.strategy = strategy
else:
self.strategy = Default()
def executeStrategy(self):
print(self.strategy.execute())
## Example application
appA = Context()
appB = Context()
appC = Context()
appA.setStrategy(ConcreteStrategyA())
appB.setStrategy(ConcreteStrategyB())
appC.setStrategy()
appA.executeStrategy()
appB.executeStrategy()
appC.executeStrategy()
In the above code snippet, the client code is simple and straightforward. But in real-world application, the context changes depend on user actions, like when they click a button or change the level of the game. For example, in a chess application, the computer uses different strategy when you select the level of difficulty.
It follows the single-responsibility principle as the massive content main (context) class is divided into different strategy classes. You can add as many additional strategies as you want while keeping the main class unchanged (open/closed principle). It increases the flexibility of our application. It would be best to use this pattern when your main class has many conditional statements that switch between different variants of the same algorithm. However, if your code contains only a few algorithms, there is no need to use a strategy pattern. It just makes your code look complicated with all of the classes and objects.
State design pattern
Object oriented programming in particular has to deal with the state that the application is currently in. How can you change an object’s behavior based on its internal state? What is the best way to define state-specific behavior?
The state pattern is a behavioral design pattern. It provides an alternative approach to using massive conditional blocks for implementing state-dependent behavior in your main class. Your application behaves differently depending on its internal state, which a user can change at runtime. You can design finite state machines using the state pattern. In the state pattern, you should define separate classes for each state and add transitions between them.
# implementing state pattern
from __future__ import annotations
from abc import ABC, abstractmethod
class Context:
_state = None
def __init__(self, state: State):
self.setState(state)
def setState(self, state: State):
print(f"Context: Transitioning to {type(state).__name__}")
self._state = state
self._state.context = self
def doSomething(self):
self._state.doSomething()
class State(ABC):
@property
def context(self):
return self._context
@context.setter
def context(self, context: Context):
self._context = context
@abstractmethod
def doSomething(self):
pass
class ConcreteStateA(State):
def doSomething(self):
print("The context is in the state of ConcreteStateA.")
print("ConcreteStateA now changes the state of the context.")
self.context.setState(ConcreteStateB())
class ConcreteStateB(State):
def doSomething(self):
print("The context is in the state of ConcreteStateB.")
print("ConcreteStateB wants to change the state of the context.")
self.context.setState(ConcreteStateA())
# example application
context = Context(ConcreteStateA())
context.doSomething()
context.doSomething()
State pattern follows both the single-responsibility principle as well as the open/closed principle. You can add as many states and transitions as you want without changing the main class. The state pattern is very similar to the strategy pattern, but a strategy is unaware of other strategies, whereas a state is aware of other states and can switch between them. If your class (or state machine) has a few states or rarely changes, you should avoid using the state pattern.
Command design pattern
The command pattern is a behavioral design pattern that encapsulates all the information about a request into a separate command object. Using the command pattern, you can store multiple commands in a class to use them over and over. It lets you parameterize methods with different requests, delay or queue a request’s execution, and support undoable operations. It increases the flexibility of your application.
A command pattern implements the single-responsibility principle, as you have divided the request into separate classes such as invokers, commands, and receivers. It also follows the open/closed principle. You can add new command objects without changing the previous commands.
Suppose you want to implement reversible operations (like undo/redo) using a command pattern. In that case, you should maintain a command history: a stack containing all executed command objects and the application’s state. It consumes a lot of RAM, and sometimes it is impossible to implement an efficient solution. You should use the command pattern if you have many commands to execute; otherwise, the code may become more complicated since you’re adding a separate layer of commands between senders and receivers.
Conclusion
According to most software design principles including the well-established SOLID principles, you should write reusable code and extendable applications. Design patterns allow you to develop flexible, scalable, and maintainable object-oriented software using best practices and design principles. All the design patterns are tried and tested solutions for various recurring problems. Even if you don't use them right away, knowing about them will give you a better understanding of how to solve different types of problems in object-oriented design. You can implement the design patterns in any programming language as they are just the description of the solution, not the implementation.
If you’re going to build large-scale applications, you should consider using design patterns because they provide a better way of developing software. If you’re interested in getting to know these patterns better, consider implementing each design pattern in your favorite programming language.