Design patterns could make a difference in the way you write code.
The thing we all need to understand is that we will come across many people who could write code, but there will be very few who write the code in a way that is scalable, maintainable, free of smells and most importantly readable. And to make a shift, it is important to know and understand different design patterns.
Another thing to keep in mind, design patterns are there to make your code easy and maintainable. But sometimes we get lost in design patterns so much that we try to include them everywhere, even the places that don’t need them. So, as much as they make our lives easier it is our responsibility to use them responsibly.
That said, let’s start with the top 5 most used design patterns.
Table of Contents
Singleton
Singleton is a creational design pattern and it is used almost in every application.
If you have worked with the Spring framework, then you might know that all beans are singleton by default. There are mainly two reasons behind creating a singleton bean.
- Ensure Class Only Has Single Instance
This is needed to save the effort of creating the same object again and again. Creating new objects every time is wasteful. For example – in your application, you would like to access a database which a shared resource. Now the properties of the database are not gonna change throughout the lifecycle of the app. So would it make sense to create a new object every time that would take additional space (duplicate) when some code needs to access the database? Instead, a singleton class could be created for that. The role of the class is to provide the shared instance to all the callers, thus instead of creating a new object every time, you store the instance in memory and share that with everyone.
- Globally Accessible
This singleton class should be globally accessible to everyone. Therefore, you should be very careful as to not overwrite the content of this class with some other class. That would result in undesirable behaviour in your application. This would mean that you should provide a safe way to create the singleton object that would make sure that the object is not getting copied over.
This is not possible with the default constructor because the job of the default constructor is to create a new object in the memory. So instead of calling the constructor, you will have to provide a static method that is responsible for creating a new object if not created already and store it statically for future use.
Code – create a singleton class
class Singleton {
private static Singleton INSTANCE;
private Singleton() {
// initialization code can go here when the object is created for the first time
}
public static Singleton getInstance() {
if (Singleton.INSTANCE == null)
Singleton.INSTANCE = new Singleton();
return Singleton.INSTANCE;
}
}
The above class could be accessed from anywhere by calling Singleton.getInstance()
. The job of this method is to ensure that if the object is not present, that would mean INSTANCE
create a new object and assign it to
Decorator Pattern
This is a structural design pattern that is used to add additional information to the object. The basic premise of this pattern is to create a wrapper over another object and yet another wrapper to that object and so on… Each wrapper adds functionality to the already present object.
This is a bit hard to wrap your head around (you see what I did there :p) without a real-world example. So I will try to provide a real-world example for it.
This pattern is applicable in multiple scenarios if you could identify it. And this pattern makes it really easy to extend the behaviour of an existing object. I always found it hard to get PR approval with this pattern :p But I hope I will be able to explain it to you better so that you can start implementing it in your own projects.
Example
I will take an example of a Pizza. A pizza is something that is built in layers.
First, you have a dough. You have toppings. Now, these toppings could be multiple on the different doughs.
For example – you could order a mozzarella pizza with plain dough and extra sauce.
With each topping, you add some cost to your pizza and in the end, you get your delicious pizza to eat.
Let’s see how we can implement it with code.
First of all, we need a blueprint for a pizza. I will keep it simple with just ingredients and cost.
interface Pizza {
String ingredients();
double cost();
}
Now, a concrete implementation of this blueprint. Think of the basic pizza. Let’s call it PlainPizza
that implements Pizza
.
class PlainPizza implements Pizza {
public PlainPizza() {
System.out.println("Adding Thin dough");
}
@Override
public String ingredients() {
return "Thin dough";
}
@Override
public double cost() {
return 5.00;
}
}
Now comes, the time for different toppings. For different toppings, we will need a ToppingDecorator
.
This ToppingDecorator
holds the instance of the pizza for which the topping is going to be added. In our case, we will have two toppings Mozarella cheese and Tomato Sauce.
abstract class ToppingDecorator implements Pizza {
Pizza pizza;
public ToppingDecorator(Pizza newPizza) {
pizza = newPizza;
}
@Override
public String ingredients() {
return pizza.ingredients();
}
@Override
public double cost() {
return pizza.cost();
}
}
Now, it’s time to add toppings. Starting with Mozarella. Here we will add the Mozarella cheese to the ingredients and add the extra cost of 1.5 dollars. Yeah it’s a costly topping.
class MozzarellaPizza extends ToppingDecorator {
public MozzarellaPizza(Pizza newPizza) {
super(newPizza);
System.out.println("Adding Mozzarella Cheese");
}
@Override
public String ingredients() {
return super.ingredients() + ", Mozzarella Cheese";
}
@Override
public double cost() {
return super.cost() + 1.5;
}
}
Next, let’s add the tomato sauce. It will be similar to the Mozarella topping.
class TomatoSauce extends ToppingDecorator {
public TomatoSauce(Pizza newPizza) {
super(newPizza);
System.out.println("Adding Tomato Sauce");
}
@Override
public String ingredients() {
return super.ingredients() + ", Tomato Sauce";
}
@Override
public double cost() {
return super.cost() + 0.45;
}
}
Lastly, we just need a PizzaMaker to put all things into perspective and cook a delicious pizza with custom toppings.
class PizzaMaker {
public static void main(String[] args) {
// Step 1. Take a plain dough
var plainPizza = new PlainPizza();
// Step 2. Add a Mozarella Wrapper/decoration
var mozzarellaPizza = new MozzarellaPizza(plainPizza);
// Step 3. Add tomato sauce
var finalPizza = new TomatoSauce(mozzarellaPizza);
// Step 4. Final Pizza Ingredients and Cost
System.out.println("Ingredients: " + finalPizza.ingredients());
System.out.println("Total Prize: " + finalPizza.cost());
}
}
And at the end, we get the final ingredients and the cost of adding different toppings with Thin Pizza.
Adding Thin dough
Adding Mozzarella Cheese
Adding Tomato Sauce
Ingredients: Thin dough, Mozzarella Cheese, Tomato Sauce
Total Prize: 6.95
I hope this is useful. But I would like to add – use this where it actually makes sense. You will find many places where this seems to be an option, but don’t just use it because it could be used instead look for the actual resemblance.
Let me know your views in the comments below.
Strategy Design Pattern
Strategy is a behavioural design pattern that lets you design different algorithms and keep them segregated in different classes but still make them interchangeable using a common interface.
The main importance of a strategy pattern is when you have to dynamically swap algorithms based on the problem at hand. By using the parameters to identify which strategy is the best to apply in the situation.
A very good example of understanding strategy patterns is to design a calculator. A calculator that would take two numbers and perform addition, subtraction, multiplication and division. Each of these operations is a different algorithm in itself. So let’s try to create a very basic calculator and apply a strategy pattern to it. Later this could evolve to hold more complex operations like solving equations.
Common Interface
First and for most we need a common interface to which each algorithm (in our case operation) will implement.
interface Operation {
int execute(int a, int b);
}
Then we need 4 operation for add, subtract, multiple and divide,
class Add implements Operation {
@Override
public int execute(int a, int b) {
return a + b;
}
}
class Subtract implements Operation {
@Override
public int execute(int a, int b) {
return a - b;
}
}
class Multiply implements Operation {
@Override
public int execute(int a, int b) {
return a * b;
}
}
class Divide implements Operation {
@Override
public int execute(int a, int b) {
return a / b;
}
}
So far we have 4 different algorithms. Now, we need a calculator to actually use them.
This calculator will take string expressions and provide the output. Let’s see how to use these operations dynamically.
class Calculator {
private static final Map<String, Operation> operations = Map.of(
"+", new Add(),
"-", new Subtract(),
"*", new Multiply(),
"/", new Divide()
);
public int solve(String expression) {
Set<String> operationSet = this.operations.keySet();
// sanitize the input and parse the operators and operand
String[] tokens = expression.split(" ");
var operand = new ArrayDeque<Integer>();
var operator = new ArrayDeque<String>();
for (String token: tokens) {
if (operationSet.contains(token)) {
operator.offer(token);
} else {
operand.offer(Integer.parseInt(token));
}
}
// iterate and perform each operation and store the result to the front
while (!operator.isEmpty()) {
int a = operand.poll();
int b = operand.poll();
String op = operator.poll();
Operation algorithm = operations.get(op); // this is selecting algorithm dynamically
int c = algorithm.execute(a, b);
operand.offerFirst(c);
}
return operand.poll();
}
}
Here you see, based on the operator, an appropriate implementation will be picked at the runtime. This is the beauty of strategy patterns. Now, you have the ability to develop each of these operations independently. So if you are working on large scale projects with multiple developers. This pattern is very useful to segregate work into their own classes and pick one dynamically based on the input parameters. This also allows parallelization as you no longer are working on the same code with convoluted if-else ladders. This is a clean way of working and scaling horizontally without touching any different algorithms.
Now let’s see this in action.
public static void main(String[] args) {
// here the algo is pretty simple and I've not respected the BODMAS rule.
// So, all the operations will be applied from the left hand side to the right.
String expression = "10 + 40 - 50 + 100 * 2 / 100";
int ans = new Calculator().solve(expression);
System.out.println(ans);
}
2
If you have anything to share or have doubts of any kind, do let me know in the comments below.
Factory Design Pattern
This is a creational design pattern. It is without a doubt the most widely used pattern. You will see it everywhere. This pattern is used when you do not know which implementation to choose at runtime. In other words, a factory pattern comes into the picture when you do not know the object beforehand.
There are many use cases where you do not know the implementation beforehand and decide something based on the environment your app is running or based on some configuration that could change runtime. Therefore, factory as the name suggests provides you with a “factory” method to create “objects” at runtime.
This provides an abstraction between the creator and the object. Now the creator does not use the new keyword to create the object anymore but rather relies on the factory to pass the correct object at the runtime. At first, it doesn’t look like a big difference. It seems like delegating the creation of the method to some other class but that is very powerful. It provides the capability to create different objects dynamically.
Let’s take an example to understand where factory patterns can be used.
Let’s assume you have a transport company that offers various modes of transport say Truck, Ship, Car etc. Each transport type has its own base fare and cost per distance. But this transport type cannot be decided beforehand. It will be decided based on some logistics logic in the service based on which suitable type of object will be created. With such assumptions let’s try to implement a factory design pattern.
/**
* Common contract that every transport type should oblige to
*/
interface Transport {
String getType();
int fare(int distance);
}
Now, there will be concrete classes that will be contracted to Transport
interface.
- Truck
- Ship
- Car
class Truck implements Transport {
private static final int baseFare = 100;
@Override
public String getType() {
return "truck";
}
@Override
public int fare(int distance) {
return baseFare * distance;
}
}
class Ship implements Transport {
private static final int baseFare = 10;
@Override
public String getType() {
return "ship";
}
@Override
public int fare(int distance) {
return 10 * distance;
}
}
class Car implements Transport {
private static final int baseFare = 200;
@Override
public String getType() {
return "car";
}
@Override
public int fare(int distance) {
return baseFare * distance;
}
}
Now, there will be some configuration or logic to decide which type of transport to choose. And based on that logic, the factory method will be called. Here’s the factory class that would create different objects based on the type and return the common contract Transport
.
static class TransportFactory {
public static Transport createTransport(String transportType) {
Transport transport;
switch (transportType) {
case "truck":
transport = new Truck();
break;
case "ship":
transport = new Ship();
break;
case "car":
transport = new Car();
break;
default:
throw new IllegalArgumentException("Given transport type does not exist");
}
return transport;
}
}
This factory class will be called in the following manner.
public static void main(String[] args) {
int distance = 200;
Transport transport = TransportFactory.createTransport("car");
System.out.println("Transport type: " + transport.getType());
System.out.println("Transport cost: " + transport.fare(distance));
transport = TransportFactory.createTransport("truck");
System.out.println("Transport type: " + transport.getType());
System.out.println("Transport cost: " + transport.fare(distance));
transport = TransportFactory.createTransport("ship");
System.out.println("Transport type: " + transport.getType());
System.out.println("Transport cost: " + transport.fare(distance));
}
Usually, there will be a config step or logic that would return the type of transport based on the distance.
public static void main(String[] args) {
int distance = 200;
// the logic to decide which transport type to choose based on the distance
String type = findTransportType(distance);
Transport transport = TransportFactory.createTransport(type);
System.out.println("Transport type: " + transport.getType());
System.out.println("Transport cost: " + transport.fare(distance));
}
Here’s the output for your reference.
Transport type: car
Transport cost: 40000
Transport type: truck
Transport cost: 0
Transport type: ship
Transport cost: 2000
Observer Pattern
As the name suggests, someone will be the observer of something. And when that something changes, all their observers will be notified. This is a behavioural design pattern that lets you design a subscription mechanism to notify multiple objects of any event that happens to the object they are observing.
The best analogy that is used in every other example describing observer pattern is the subscription model. Let’s say you visit a website and you find a product that interests you. Instead of checking every hour of the day whether the product is available, a better solution is that the store will notify you whenever the product become available. And not just you, everyone showing similar interest will be notified once the product is available. That kind of pattern is called an observer pattern.
You are observing a product. The act of observing is creating a subscription. And once you are subscribed you will be notified when that product is available in the store.
Now, let’s talk about the components of an observer pattern.
Components of Observer Pattern
There are basically two main components of Observer Pattern,
- Publisher
- Subscriber
The publisher is responsible for notifying the subscribers of the new events. The new events could be any event that is produced by producers. There could be multiple producers that could use the same Publisher as long as they respect the contract.
Subscribers are also known as Listeners. They are the ones that will be notified of any new event. There could be any number of subscribers for a particular event type.
Here’s the basic interaction between different components.
Example Code
I tried coding a quick example that depicts the above interaction.
First I started with defining the interfaces as above,
interface Publisher {
void subscribe(String eventName, Subscriber subscriber);
void unsubscribe(String eventName, Subscriber subscriber);
void notify(Event event);
}
interface Subscriber {
void update(Event event);
}
Next, I started by creating the concrete implementation of these interfaces.
I created EventManager which would be the Publisher.
class EventManager implements Publisher {
Map<String, Set<Subscriber>> listenersByName;
public EventManager(String... events) {
this.listenersByName = new HashMap<>();
for (String event: events)
listenersByName.computeIfAbsent(event, x -> new HashSet<>());
}
@Override
public void subscribe(String eventName, Subscriber subscriber) {
Set<Subscriber> subscribers = listenersByName.get(eventName);
subscribers.add(subscriber);
}
@Override
public void unsubscribe(String eventName, Subscriber subscriber) {
Set<Subscriber> subscribers = listenersByName.get(eventName);
subscribers.remove(subscriber);
}
@Override
public void notify(Event event) {
listenersByName.get(event.getName())
.forEach(subscriber -> subscriber.update(event));
}
}
Then I created multiple subscribers (EventAListener, EventBListener) that would subscribe to different events.
class EventAListener implements Subscriber {
@Override
public void update(Event event) {
System.out.println("Received event by EventAListener: " + event);
}
}
class EventBListener implements Subscriber {
@Override
public void update(Event event) {
System.out.println("Received event by EventBListener: " + event);
}
}
Then the last bit to complete the entire setup, I needed an event producer. That would mimic producing different events. Here, I used String for event name that both subscribers and producers must respect. In production env, this will be pre-defined topics or event Id that would be unique across the board.
class EventProducer {
private final Publisher eventPublisher;
public EventProducer(Publisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
void produceEventA() {
eventPublisher.notify(new Event("PID_01_AVAILABLE", "link to item page: http://bma.com/product/pid01"));
}
void produceEventB() {
eventPublisher.notify(new Event("PID_01_SOLD_OUT", "Item sold out. Check back later."));
}
}
Last, have to put everything together to see it in action. That’s where the main method comes in 🙂
public static void main(String[] args) {
var eventManager = new EventManager("PID_01_AVAILABLE", "PID_01_SOLD_OUT");
eventManager.subscribe("PID_01_AVAILABLE", new EventAListener());
eventManager.subscribe("PID_01_SOLD_OUT", new EventBListener());
var eventProducer = new EventProducer(eventManager);
eventProducer.produceEventA();
eventProducer.produceEventA();
eventProducer.produceEventA();
eventProducer.produceEventB();
eventProducer.produceEventB();
eventProducer.produceEventB();
}
Received event by EventAListener: Event{name='PID_01_AVAILABLE', data='link to item page: http://bma.com/product/pid01'}
Received event by EventAListener: Event{name='PID_01_AVAILABLE', data='link to item page: http://bma.com/product/pid01'}
Received event by EventAListener: Event{name='PID_01_AVAILABLE', data='link to item page: http://bma.com/product/pid01'}
Received event by EventBListener: Event{name='PID_01_SOLD_OUT', data='Item sold out. Check back later.'}
Received event by EventBListener: Event{name='PID_01_SOLD_OUT', data='Item sold out. Check back later.'}
Received event by EventBListener: Event{name='PID_01_SOLD_OUT', data='Item sold out. Check back later.'}
By this, I hope you gained basic knowledge of the observer pattern. Next time if you come across a similar use case where there are multiple producers that needs subscribers then you can go for this approach.