Recently, I was working on implementing a piece of software starting from scratch. While setting up the code base, I tried to ensure the best practices were followed at all times (like unit test coverage, code formatting, logging etc) by using various libraries like Jacocco, Checkstyle, find bugs and Lombok etc. At the same time, I was reminded of best practices which can't directly be enforced via such libraries as the "design patterns". So I thought of covering this topic as I tried to enforce them in my code base! And this led to this series of common design patterns which I will be covering in the parts going forward. Readers can refer to these patterns in detail separately as well. I personally liked a few posts like this and this.
But before we begin, what is a design pattern and how many board types of design patterns are we going to talk about? In software engineering, a design pattern is a general reusable solution to a recurring problem in the context of system design. These solutions are not end solutions in themselves but serve as a "patch" to improve the design and solve a "patch" problem. These are generally derived over time based on experience and improvisations. There is no "absolute" classification of these solutions but they can be grouped into three major subheadings:
- Creational solutions: Provide solutions to the creation of objects in the most optimized and reusable manner in the cleanest way.
- Structural solutions: They provide efficient solutions and standards for class compositions and object structures. The concept of inheritance is used to compose interfaces and define ways to compose objects to obtain new functionalities.
- Behavioural solutions: The behaviour pattern deals with communication between class objects. They are used to detect the presence of communication patterns already present and can manipulate these patterns. These design patterns are specifically related to communication between objects.
In this first part of the post, I will try to cover the first type of solution, i.e the creational solutions. In the later posts, I will try to cover the remaining ones. So when it comes to creational patterns, I will try to focus on the most common and important ones :
- Singleton
- Factory
- Abstract Factory
- Builder
- Prototype
Let's take a look at each of them one by one.
The singleton pattern simply ensures that for a given class, only one instance of the object is made. You can't have more than one object of a class and everywhere you need to use the object, you will have to use that one object only. An obvious question is why!! There are two main reasons. Firstly, some classes do not actually need to produce more than one object and rather need single-point access. For eg, let's say you have a logger class which you use to log events/logs. If you use a logger in ten files, you should not allow the creation of ten objects. Instead, you should create one object and pass it to all ten places as a best practice (most likely via dependency injection). The second reason for using this pattern is to minimize the permissions to create objects in different classes. To make a class singleton, we:
- Make the default constructor private.
- Create a static creation method that acts as a constructor. Under the hood, this method calls the private constructor to create an object and saves it in a static field. All following calls to this method return the cached object.
This pattern is used to create the "correct" implementation object for a given interface. Let's say you have a "Bird" interface and three classes "Duck", "Parrot" and "Eagle" implement this interface. Now, say you want to create an object based on user input. If the user wants a "Duck" type bird, you create a "Duck" object as Bird bird = new Duck() and so on. One way is to have an if-else condition and check if the input is Duck, create a Duck object and so on. And the other way is to use a factory pattern which is essentially a class dedicated to creating objects based on the input. Say you create a class which returns an object based on the input passed to that class. You can then create an object directly by using this code: Bird bird = birdFactory("Duck"); and so on. This pattern is called a factory pattern. This ensures a single point of "manufacture" of objects. This will help you add new implementations-based objects only in one place. This will help you ease out maintenance in the long run.
Abstract Factory pattern is just one level above the Factory pattern. Abstract factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes. Let's say we have a furniture shop and we have two "types" of furniture: modern and old. Each type has three types of furniture chair, table and bed. Now, we would not want to create a "modern chair", "modern table" or "old bed". To make sure we enforce that we always have a set of modern and old furniture and they do not "mix". We can declare an abstract factory which produces two objects "modern" and "old" which then initialize and create a "modern chair", "modern table" and "modern bed" and so on so that they don't "mix". This is a typical abstract factory pattern at a high level.
Builder pattern helps create "complex" objects step by step by "customizing" the fields. Let's say we have a "User" class with variables name, roll, address, and phone. We can create a User object with only name and roll, or we may create an object with just name and phone and such permutations. One way is to create a constructor for such initialization. A better way is to use a builder pattern (which is also available off the shelf in libraries like Lombok etc.). If we use builder pattern, we may use it to "construct" the object like this : User user = User.builder().name("abc").roll(123).build(); Or User user = User.builder().roll(123).phone(998765431).build(); and so on. This gives us the ease to create the object in a "customized" way. The remaining fields are set to defaults. To make this happen, we move the constructors out of the main class. I personally prefer using the @Builder annotations etc available in libraries like Lombok for Java.
Say you want to create a clone of an object. One of the approaches is to create a new object and then iterate over each field and copy the value. This is not the best way as you might not have access to each field (some fields may be private) and also you may have to have a dependency on the original class which is not a good practice. To solve this problem, we came up with the prototype pattern. To achieve this, we add a clone() method in the interface of the original class and implement it to return a new object with the same fields. Then you can create a new clone as User user2 = user1.clone(); This seemingly simple idea is very helpful in the long run!
So these are the basic (and most used) "creational" design patterns. As you can see, they serve the purpose to solve "frequent" issues in the implementation of programmes. They should be frequently used so that the code and system are very well maintained in the long run and the system is easy to "extend" but tough to "modify"! That is all for this post! Feedback and suggestions are most welcome!
-Amrit Raj
Comments