As software engineers, one of our main responsibilities is to map the real world through a software that addresses its issues.
Since most applications stay alive after their creation, flexibility is a need. Otherwise, new requirements now or then will be quite a challenge and a cumbersome task.
Design patterns came to help you out by fulfilling those needs.
What is a Design Pattern
Every design problem, in a specific application context, tends to have its identity pattern. Such a flaw in a system might compromise a great solution, turning it into a non-flexible, expensive, and difficult-to-maintain solution. Usually, new requirements will break other functionalities.
A design pattern is known for highlighting and working around such design flaws. It also describes how entities within a system should interact and be exposed. We can look at it as blueprints, which you can customize to solve a recurring design problem in your code.
Patterns sometimes might get misunderstood as a piece of code that you can easily copy and paste into your codebase. But those should be known as theoretical concepts for solving a particular problem.
Design patterns can be split into four different categories:
- A name that describes its purposes;
- A problem waiting to be solved;
- A solution that describes how the components should relate or be composed;
- A collection of consequences that a design pattern might bring;
It is always a matter of trade-offs when choosing one design pattern over the other.
Types of Design Patterns
Design patterns can be of three different categories:
These categories differ in terms of complexity, level of detail, and scalability when applied to the entire system.
These patterns are technology agnostic, and you can easily apply them to any programming language or framework.
Such design pattern type provides several creational mechanisms, increasing code flexibility, and reusability. It abstracts the instantiation process, separating how objects should be created, composed, and represented from the code that relies on them.
- Factory Method → Relies on an interface for creating entities in a parent class. However, it allows changing which type of entities a subclass can create.
- Abstract Factory → Enables one to produce families of related objects without specifying any concrete type.
- Builder → Enables one to create object types and representations step by step.
- Prototype → One can copy existing objects without directly depending on their classes.
- Singleton → Ensures that a given class has only one instance and access point.
It describes how entities should be assembled into larger structures while keeping them flexible and efficient.
- Adapter → It enables entities with incompatible interfaces to collaborate.
- Bridge → It enables one to segregate an entity, or a set of related ones, into different hierarchies, abstractions, and implementation. Such segregation allows working both independently.
- Composite → Describes a set of entities that should be handled the same way. It composes entities into tree structures, where you can request each node to perform a given task.
- Decorator → Allows one to introduce new behaviors by placing the entity within a wrapper.
- Facade → Provides an interface and simplifies the usage of a given library or a set of complex frameworks.
- Flyweight → This design pattern is about sharing resources, allowing one to fit more objects into the available amount of RAM. Such a goal gets achieved by sharing different object states across the system instead of keeping it within each object.
- Proxy → One can introduce additional behavior to get performed before or after the request gets through the original entity.
Such design patterns identify common interaction patterns and enhance flexibility in carrying out this communication. Doing so allows those entities to easily talk to each other while maintaining the loose coupling between components.
- Chain of Responsibility → A given set of requests is assigned to specific handlers. Either a given handler processes the request, or it just passes the request to the following one.
- Memento → Allows one to save the current or previous state of a given entity.
- Template Method → It defines an algorithm skeleton to be executed by others. Such steps within this skeleton must be overridden and defined by its subclasses.
- Command → Turns a given request into an entity containing all the information about such a request. Doing so will allow one to move requests as method arguments throughout layers or other methods.
- Observer → It defines a subscription ready to be subscribed by interested entities that want to get notified whenever a given event occurs.
- Visitor → Used whenever we have to operate on a group of entities of the same kind. It allows moving operational logic from a given entity to another.
- Iterator → It provides a standard way to iterate through a set of entities.
- State → One can change the current entity's behavior whenever its internal state gets changed.
- Mediator → One can reduce the number of dependencies between entities. Entities are forced to interact throughout a mediator entity.
- Strategy → One can change the current entity's behavior during run time. Based upon a given context, a given strategy takes place.
From theory to practice
After introducing the theoretical concepts, let me give you now a real example of the application of design patterns, by telling you about a problem I had to face at work.
Once I had a ticket where the goal was to refactor our data file extractor service. Visually it would reflect on having to upload a file, such as a PDF or an excel. After the upload, we needed to extract all the file data and display it through a pretty UI. We all knew that another new file type was heading our way in a future sprint, reinforcing the need for such refactoring.
Diagram showing the structure before applying any solution.
We’ve noticed throughout the development that a lot of similar code was being created, making the task of new additions more cumbersome and less flexible. A change to the specification, like a constraint change, would now require to be redone equally in two separate components. In terms of readability, if you had seven different types of files to upload, the probability of missing some crucial steps could be high.
Luckily, design patterns describe several problems that other engineers faced during their careers. Most of the reasoning it's done, leaving you with the task of recognising the issue and applying the corresponding solution.
While investigating several design patterns, the Template Method Pattern seemed to be the solution for the type of challenge we were facing.
It suggests the existence of a skeleton shared through other subclasses. Inside this skeleton, we can define several steps, which behavior might differ based on the context. In those cases, a subclass should override the step and introduce its specifics without changing the overall algorithm structure.
Perfect, that’s what we needed!
Following this approach allow us to:
- Easily add constraints that automatically get applied to all supported types;
- Follow the DRY (Don’t Repeat Yourself) principle;
- Add a new file type to our functionality that would resume creating a new class and override a few methods. Fast, flexible, and pretty straightforward.
Technically, by the end it looked like this:
The final structure.
As you can see, there’s a template method that we used as a starting point. Besides this template method, which shouldn’t be changed by subclasses, we have several steps that subclasses can override if needed.
To implement this, we have followed the below blueprint:
- Analyze the duplicated algorithm for common and unique steps;
- Create an abstract class where the template method contains all common steps. For the unique ones, define their corresponding abstract methods;
- Create a subclass for each variation of the algorithm;
- Override the abstract methods with the specific logic.
Design patterns 101: final thoughts
There are plenty of solutions for multiple problems that one might have, and each solution has its pros and cons. It is always up to you and your team to decide if the former covers the latter so you can follow such a path.
Identifying the problem and matching it to a given pattern is one of the most challenging things that one engineer will face during their career. It’s all about experience and experience.
I know folks with more than ten years of engineering experience that still struggle to identify some of those. So don’t worry, because time and code experience will help you with it.