My name is Yurii Serdiuk, and I work as an automation QA engineer at the P2H international company. I have been working in QA since 2019 and have progressed from manual to automation testing. In this article, I will discuss the importance of design patterns in programming, their definition, and their classification. Due to the vastness of the topic, I will focus on a few examples to illustrate the concepts.
A design pattern refers to a typical approach to solving programming problems. It is not a pre-built function or library that can be directly inserted into the code. Instead, it is a general problem-solving principle that needs to be adapted to the program’s specific needs.
Design patterns and algorithms are often mistakenly assumed to be the same but they have distinct characteristics. An algorithm refers to a specific set of instructions to perform a task, while a design pattern is a general description of a solution that can be implemented in various programming languages.
Using established solutions can save significant time and effort, as it eliminates the need to reinvent the wheel whenever a problem occurs. Standardizing solutions can help to prevent design errors and promote code uniformity. Using common pattern terminology improves communication and understanding among team members, allowing for the efficient implementation of project design.
Established solutions can save a lot of time, and while some may seem obvious, others may surprise with their effectiveness and usefulness. Utilizing standardized solutions allows you to avoid design errors, as all potential issues have been identified and resolved. This promotes code standardization and enhances comprehension of the project by all team members. Common pattern terminology enables programmers to communicate and understand each other more efficiently.
Classification of patterns:
- Creational patterns are concerned with the flexible creation of objects without introducing unnecessary dependencies into the program.
- Structural patterns demonstrate different ways of constructing relationships between objects.
- Behavioral patterns are concerned with effective communication between objects.
The list of patterns we will review as examples include:
- Decorator
- Facade
- Composite
- Factory method
- Builder
- Singleton
- Chain of Responsibility
A decorator is a structural design pattern that allows you to dynamically add new functionality to objects by wrapping them in useful “wrappers”. It allows new responsibilities to be added to objects on the fly, invisible to the code that uses those objects, and is particularly useful when you cannot extend an object’s responsibilities through inheritance.
Example:
When we create company attributes, we have methods that generate or retrieve, for example, a location. Only the admin has access to the locations, so first, we log in as an administrator. And to avoid doing extra functions inside, we just use this decorator, which makes our job much easier.
A facade is a structural design pattern that provides a simple interface to a complex system of classes, libraries, or frameworks. It allows you to provide a simple or simplified interface to a complex subsystem and to decompose the subsystem into separate levels.
Example:
We are creating a company, and it happens that the tests are in one project, and the creation of all objects is in another. Or in the same, but with the possibility of access from only one point. Therefore, we create a company through an API call, and we do not know what is happening “under the hood” but only get the result.
Composer is a structural design pattern that allows you to group several objects into a tree-like structure and work with it as a single object. It is used when you need to present a complex tree-like structure of objects and interact with them in the same way.
Example:
We have a company that has certain attributes. For example, an employee is a separate class that has subclasses. Therefore, we create a tree-like structure, which will make our work much easier in the future. And in this case, we do not need to pass a bunch of parameters. Only one is enough, and we continue to work with it.
The factory method is a generative design pattern that defines a common interface for creating objects. It is useful when the types and dependencies of the objects that your code must work with are not known in advance, when you want to allow users to extend parts of your framework or library, or when you want to use system resources efficiently by reusing already created objects, instead of creating new ones.
Example:
The CompanyFactory class contains the create_company method, which creates company objects depending on the passed department parameter. Now we can use CompanyFactory to create different types of companies. With the help of the factory method, we can ensure the creation of different types of objects, depending on the needs of our program, and further transfer not a bunch of different parameters but one object – company options, and work with it where we need. This is useful for UI and API tests.
Builder is a generative design pattern that allows you to create complex objects step by step. The builder allows you to use the same building code to obtain different representations of objects. It is used when the code must create different representations of the same object and provides an assembly of complex objects.
Example:
In the application, you can see that with the help of this pattern, we create the class and then use it in various places to create complex objects.
Singleton is a generative design pattern that ensures that there is only one instance of a class and provides a global access point to it. It is used when an application needs to have a single instance of a particular class available to all clients (for example, to share database access from different parts of the application) or when more control over global variables is desired.
Example:
In this code, we see the use of the Singleton pattern using the singleton decorator defined by the class. The Singleton pattern aims to ensure that only one object is created from a single class and that object can be accessed from anywhere in the program. The ReportManager class is a singleton because it has only one instance of the class, and we can access it from anywhere in our program.
The @singleton decorator ensures that only one object of the ReportManager class will be created. That is, when we create an instance of this class, it is created only once and saved for future use. This is very useful when we have several places in the program that need to interact with the same instance of the class.
So, by using the Singleton pattern, we ensure that only one instance of the ReportManager class is created, and we can easily access it from anywhere in the application.
A chain of responsibilities is a behavioral design pattern that allows requests to be passed sequentially through a chain of handlers. Each subsequent handler decides whether it can process the request on its own or whether it should be passed down the chain. It is used when the program must handle various requests in different ways, but it is not known in advance exactly which requests will arrive and which handlers will be needed for them. It is also used if it is important to ensure a strict order of execution of handlers.
Example:
We have companies with three different statuses. When we pass the company in the last status, it enters this method, but before that, it calls the previous status, which, in turn, also calls the previous one, and thus, the chain moves.
This is quite often used in situations where, for example, we open a browser to run UI tests, and if the browser is available, the test is executed, and if it is not available, the execution continues.
Design patterns are time-tested solutions to common problems that arise when designing software applications. These patterns provide a general framework that can be adapted to the specific requirements of a project, making it easier to develop high-quality, maintainable code. By using design patterns, developers can save time, reduce the risk of errors, and promote code standardization. Additionally, having a common pattern terminology makes communication between team members more efficient.
Having knowledge of design patterns is a valuable asset for developers seeking effective solutions to complex problems in their projects. Whether you’re building a simple web application or a large-scale enterprise system, understanding design patterns can help you create well-structured, flexible code that can be easily maintained and extended. I hope that the information in the article was useful for you and that you were able to find answers to your questions or understand the topic in more detail.