A design pattern is a common, reusable solution to a recurring problem encountered during software development. Design patterns enable us to create reusable, flexible, scalable, and efficient software systems. They are considered the best practices in software engineering that a programmer can use while developing applications. A natural question that comes to mind after reading about design patterns is when/how to use them?
Everyone uses library functions, instead of creating their own, to calculate the square root of a number as their efficiency and correctness have been tested over the years. For similar reasons, we use design patterns to solve recurring design problems. Just like the way we don’t set out to solve a problem with the intention to use the square root function, but use it only if the situation arises (like you might encounter the need to calculate square root while creating a real estate cost estimator), in the same way, we should not set out with the intention of using design patterns while designing a system. Doing so, generally results in an over-engineered and complex software. Instead, we should use design patterns when we face a particular design problem and identify how a specific design pattern is suitable for the problem at hand. Thus, design patterns should act as refactoring tools to be used once we are able to identify that our code/system has a complexity that could be simplified using a certain design pattern.
However, one difference between a library method and a design pattern is that while a library function (like the square root function) provides a definite ready-made solution to a given problem, design patterns provide us with a generalized template which needs to be specifically adapted and then implemented to solve our specific use-case.
Design patterns can be divided into the below three categories:
- Creational Design Patterns: Patterns under this category solve the problems related to the instantiation of objects of a class. Some common Creational Design Patterns are:
a. Abstract Factory Pattern
b. Builder Pattern
c. Dependency Injection Pattern
d. Singleton Pattern - Structural Design Patterns: Structural Design Patterns provide ways in which we can better organize the structure for our classes. For instance, a better organizational structure (like Flyweight pattern) might help improve space or/and time complexity of our solution. Some commonly encountered structural design patterns are:
a. Composite Pattern
b. Flyweight Pattern
c. Decorator Pattern
d. Adapter Pattern - Behavioral Design Patterns: Behavioral Design Patterns define ways in which objects in our code should interact with each other. Examples of such patterns are:
a. Chain of Responsibility Pattern
b. Command Pattern
c. Iterator Pattern
d. Publish Subscribe Pattern
To better understand the patterns, let’s look at some examples:
- Abstract Factory Pattern: Before understanding this pattern, we should define the term factory. A factory is nothing but an object or a method that returns an object. Thus, a factory hides the details of the creation of objects and just returns the object to the caller (client).
This pattern is used when we need instances of objects that come under a common theme. Assume we are creating a framework that allows clients to create apps for both iOS and Android in the same way. Now let us focus on the module required to draw GUI elements on the screen. To create this module, we will create a Button interface which will specify methods like draw(), getShape(), setShape(), etc. Classes IOSButton and AndroidButton will implement the Button interface and define their own implementations for the required methods. Similarly, we will create a TextBox interface and its implementations IOSTextBox and AndroidTextBox.
There can be many other GUI elements which will have a common interface but different implementations for iOS and Android platforms. To simplify the design of such a software, we can use Abstract Factory Pattern. To implement this, we will create an abstract factory class — GUIElementFactory. This abstract class (or interface) will provide a generic interface with methods like getButton(), getTextBox(), etc. We will also define concrete implementations of this factory class, namely, AndroidElementFactory and IOSElementFactory. Clients will create objects of concrete implementations of GUIElementFactory and get the GUI elements based on whether they are creating an iOS app or Android app. Below code will help understand the concept better.
public interface GUIElementFactory {
Button getbutton();
TextBox getTextBox();
// methods to create objects for other GUI elements
}public class IOSElementFactory implements GUIElementFactory {
public IOSButton getbutton() {
// implementation to create button for iOS device.
}
public IOSTextBox getTextBox() {
// implementation to create text box for iOS device.
}
// methods to create objects for other GUI elements
}public class AndroidElementFactory implements GUIElementFactory {
public AndroidButton getbutton() {
// implementation to create button for Android device.
}
public AndroidTextBox getTextBox() {
// implementation to create text box for Android device.
}
// methods to create objects for other GUI elements
}public class ClientClass {
public static void main(String args[]) {
String platform = args[0];
GUIElementFactory factory;
if(platform.equals("ios")) {
factory = new IOSElementFactory();
}
else if(platform.equals("android")) {
factory = new AndroidElementFactory();
}
else {
throw new IllegalArgumentException("Invalid Platform supplied in command line arguments");
}
// We have abstracted away the implementation details. Now the factory object will provide us with the correct
// GUI elements. From this point, we can focus on creating our application without the worrying about the
// platform.
Button button = factory.getbutton();
TextBox textBox = factory.getTextBox();
}
}
As we can see, this pattern simplifies the creation of objects representing GUI elements like button and text box by providing an appropriate factory object. Thus, this pattern comes under the category of Creational Design Pattern.
2. Flyweight Pattern: Flyweight pattern is a design pattern that can be used when we need to create a large number of objects and those objects have data that can be shared between them. The data that can be shared is placed in a separate object, reference of which is stored in individual objects. For instance, characters in a word processor like MS Word are represented by objects. If character ‘e’ occurs 1000 times in a document then we will have to create 1000 objects, one for each occurrence. This will require a huge amount of memory for just a single character. Imagine the memory usage for all the characters in the document. To prevent this, we can create 1000 light weight objects for character ‘e’. Each light weight object can store properties, like position of the character in the document, which are unique for each occurrence while keeping the common properties, like font style, in a separate shared object.
Another common example of flyweight pattern is string interning. In string interning, a single copy of each string is stored in a string pool. Thus, when a string literal is encountered whose content is the same as an already existing string in the pool, the reference to the already existing string is returned. This prevents the creation of duplicate string objects having the same content.