This is the 3rd part of the Low level design series. Where I will introduce you to design pattern. In this post we will talk about design patterns that I found most useful because they address some very common problems. So following are a discussion on design pattern with 6 design patterns you can start using right away.
Design patterns are instantiations of SOLID principles in one way or another. Think of SOLIDs as specifications and design patterns are their implementation. Therefore, before reading this you should definitely read THIS article on solid principles.
You are more prone to misusing design patterns than most ideas in computer science. This is because, design patterns are ready-made solutions to some recurring problems in low level design. But unlike a 3rd party library, you can’t just download and use these patterns. You must understand what problem they solve and why they exist.
Following are few rules of thumb to remember to not misuse design patterns:
1) Don’t use design patterns just because you know them.
2) 3 place principle: Don’t create an abstraction if you have not done something 3 times. Let something messy develop before you clean it up, because premature abstraction is very hard to maintain.
3) If any design pattern don’t suit you problem, don’t use one. Create your own design. ( or read about Anti-patterns)
4) If your language of choice permits cool and efficient ways to handle situations, don’t force a design pattern on it. E.g. in Python, few features like monkey patching, decorators, passing functions as argument, kwargs etc. solves a lot problems that design patterns are trying to address, in an efficient and clean way. So use them instead.
5) Design patterns will always complicate your code. Always. So make sure the pros of using a design pattern outweigh the complications. Otherwise you will end up in an unmaintainable mess of a code base.
Coupling is a measure of how dependent different modules of a system are. If you need to touch a lot of modules of to bring a change to your system you have a tightly coupled system. The goal of low level design it to minimize the extent to which subsystems are coupled with one another.
And that is all you need to know to move forward in this article.
The intention of creational patterns is to decouple two classes which get tangled during the process of object creation. For example, if you instantiate class A somewhere inside class B, you couple them permanently. Creational patterns uses (dependency inversion principle) to invert the dependencies in such situations. Both client and implementations are made to depend on abstractions instead of concretion.
Following are three most frequently used creational design patterns.
Builder pattern lets you create complex object in a step by step manner. If creation of an object is very complicated, i.e. you have big complicated contractors, you can use builder pattern to separate initialization of an object from the class itself. You can isolate complex construction from the business logic of class. So Builder pattern conforms to single responsibility principle.
Builder can be implemented in two ways: In the first approach you expose the set components that goes into construction of the said complex object, and let the client choose various components and assemble them to create the final product. Or you can hide the component objects entirely and expose concrete builder classes that knows which component to use to construct a variant of said complex object. Both of these approaches are useful. Use the first one if it makes sense to give the client customizability of the object. Use the second one if the client is going to use a few variant of the complex object.
#include<iostream>
// --------------- Interfaces ---------------
class IEngine
{
public:
virtual void provide_power() = 0;
};
class ISteering
{
public:
virtual void steer() = 0;
};
class IStereo
{
public:
virtual void play_song() = 0;
};
// ------------- Car is the main product our builders will build ---------------------
class Car
{
protected:
IEngine *engine;
ISteering *steering;
IStereo *stereo;
public:
Car(IEngine *e, ISteering *s, IStereo *st)
{
engine = e;
steering = s;
stereo = st;
}
void run()
{
if(stereo != nullptr)
stereo->play_song();
engine->provide_power();
steering->steer();
}
};
// ------------- Different types engines -------------------
class V8Engine : public IEngine
{
void provide_power()
{
std::cout<<"Engine: Feel the power!"<<std::endl;
}
};
class Inline3Engine : public IEngine
{
void provide_power()
{
std::cout<<"Engine: I am fuel efficient."<<std::endl;
}
};
// ----------- different types steerings ---------------
class PowerSteering : public ISteering
{
void steer()
{
std::cout<<"Steering: I work like a butter.."<<std::endl;
}
};
class NormalSteering : public ISteering
{
void steer()
{
std::cout<<"Steering: I will work out you bisepts."<<std::endl;
}
};
// ----------- Stereos ---------------
class LoudStereo : public IStereo
{
void play_song()
{
std::cout<<"Stereo: LA LA LA LA LA...."<<std::endl;
}
};
class NoramlStereo : public IStereo
{
void play_song()
{
std::cout<<"Stereo: pss psss pss pssss...."<<std::endl;
}
};
// -------------------- This is the interface all builders has to impliment -------------------------
class ICarBuilder
{
public:
virtual ICarBuilder* build_engine() = 0;
virtual ICarBuilder* build_steering() = 0;
virtual ICarBuilder* build_stereo() = 0;
virtual Car* get_car() = 0;
};
// ----------- Concrete builders ------------------
class FamilyWagonBuilder : public ICarBuilder
{
private:
IEngine *engine;
ISteering *steering;
IStereo *stereo;
Car *car;
public:
FamilyWagonBuilder()
{
engine = nullptr;
steering = nullptr;
stereo = nullptr;
car = nullptr;
}
ICarBuilder* build_engine()
{
engine = new Inline3Engine;
return this;
}
ICarBuilder* build_steering()
{
steering = new NormalSteering;
return this;
}
ICarBuilder* build_stereo()
{
stereo = new NoramlStereo;
return this;
}
Car* get_car()
{
if (engine == nullptr)
{
std::cout<<"Cannot build a car without engine."<<std::endl;
return nullptr;
}
if ( steering == nullptr )
{
std::cout<<"Cannot build a car without steering."<<std::endl;
return nullptr;
}
car = new Car(engine,steering,stereo);
return car;
}
};
/*
- Argumented car builders allows client to create the components themselves and assamble the car.
- Problem:
- Client will be coupled with the various components that goes into the car, since client knows about their
existance and knows how to create them
- Usage:
- You can pass sush builders to manufectures of various components and they will add each component to the builder
and at the end client can call the build method to get final product.
- Think of such builder object as a chasis. It will flow through a production line, where someone who knows how to build a engine will build and add
and engine to the car and someone else will add an streering and so on. At the end you will get the final carj
*/
class ArgumentedCarBuilder
{
private:
IEngine *engine;
ISteering *steering;
IStereo *stereo;
Car *final_car;
public:
ArgumentedCarBuilder()
{
engine = nullptr;
steering = nullptr;
stereo = nullptr;
}
ArgumentedCarBuilder* add_engine(IEngine *e)
{
engine = e;
return this;
}
ArgumentedCarBuilder* add_steering(ISteering *s)
{
steering = s;
return this;
}
ArgumentedCarBuilder* add_stereo(IStereo *s)
{
stereo = s;
return this;
}
void reset()
{
engine = nullptr;
steering = nullptr;
stereo = nullptr;
final_car = nullptr;
}
Car* build()
{
if (engine == nullptr)
{
std::cout<<"Cannot build a car without engine."<<std::endl;
return nullptr;
}
if ( steering == nullptr )
{
std::cout<<"Cannot build a car without steering."<<std::endl;
return nullptr;
}
final_car = new Car(engine,steering,stereo);
return final_car;
}
};
// ----------
int main(int argc, char const *argv[])
{
ArgumentedCarBuilder *carBuilder = new ArgumentedCarBuilder;
Car *familyWagon = carBuilder
->add_engine(new Inline3Engine)
->add_steering(new PowerSteering)
->add_stereo(new NoramlStereo)
->build();
carBuilder->reset();
Car *fastCar = carBuilder
->add_engine(new V8Engine)
->add_steering(new PowerSteering)
->add_stereo(new LoudStereo)
->build();
carBuilder->reset();
Car *basicCar = carBuilder
->add_engine(new Inline3Engine)
->add_steering(new NormalSteering)
->build();
std::cout<<"Test driving Family wagon."<<std::endl;
familyWagon->run();
std::cout<<std::endl;
std::cout<<"Test driving Fastcar."<<std::endl;
fastCar->run();
std::cout<<std::endl;
std::cout<<"Test driving basic car."<<std::endl;
basicCar->run();
std::cout<<std::endl;
// Concrete builder
FamilyWagonBuilder *fwb = new FamilyWagonBuilder;
Car *fw = fwb->build_engine()->build_steering()->build_stereo()->get_car();
return 0;
}
Singleton is one of the simplest to implement. An entity is singleton if there can be only one instance of it in the entire program.Singleton pattern can be used in the following cases:
Most of the times. Singleton classes often are exposed to a large portion of the system. Why? Because you are going to use singleton only when there is a resource (encapsulated by a class) that multiple parts of your system wants access to and you don’t want the system to have multiple instances of the resource for above mentioned reasons. So singleton classes are often a dependency for significant number of components of the system, which makes your system tightly coupled with one such specific class. That’s bad design. You don’t want such tight dependencies in your system.
Use singleton when is absolutely necessary. And put a hard limit of number of classes that are using this singleton class, if you have to use singleton.
Singleton pattern is implemented by making the constructors private and then exposing a public static function to obtain a stored copy of the object.
The implementation is as follows:
#include<iostream>
class SingletonClass
{
private:
int m_value;
static SingletonClass *instance;
SingletonClass(){}
SingletonClass(int v)
{
m_value = v;
}
public:
void set_value(int v) { m_value = v; }
int get_value(){ return m_value; }
static SingletonClass* get_instance()
{
if(!instance)
instance = new SingletonClass(0);
return instance;
}
};
// In C++ we gotta define the static memeber variables
// Read this to know why: https://www.learncpp.com/cpp-tutorial/static-member-variables/ Read
SingletonClass* SingletonClass::instance = 0;
int main()
{
// Following are not possible, becuse constructors are inaccessible
// SingletonClass obj;
// SingletonClass *obj = new SingletonClass(5);
SingletonClass *obj_1 = SingletonClass::get_instance();
obj_1->set_value(5);
// The following will print 5
std::cout<<"Object 1: "<<obj_1->get_value()<<std::endl;
SingletonClass *obj_2 = SingletonClass::get_instance();
// this will also print 5
std::cout<<"Object 2: "<<obj_2->get_value()<<std::endl;
obj_2->set_value(2);
// This will print 2
std::cout<<"Object 1: "<<obj_1->get_value()<<std::endl;
// why? because obj_1 and and obj_2 points to the same object
// This will print 1
std::cout<<(obj_1 == obj_2)<<std::endl;
}
Factory method lets you obtain objects of classes which implements a given interface by specifying few parameters. These parameters specifies a job to be done. The factory method knows which class can handle the specified job appropriately and returns an instance of that class.
You can specify a simple set of parameters in the factory method and expose that to the client. This way the client does not have to know about any of the concrete implementations and hence are decoupled from them.
Moreover, you can introduce new implementations under the hood and alter the body of the factory method to include the new implementation. The makes the client compliant to Open closed principle, since you are effectively altering client’s behavior without touching clients code.
Let’s look at an example: Let’s say you have a courier service that delivers worldwide. Following might be a few scenarios:
Let’s implement this.
#include<iostream>
class Transport
{
protected:
int m_weight;
int m_distance;
Transport(int w, int d)
{
m_weight = w;
m_distance = d;
}
public:
static Transport* get_tranport(int weight, int distance);
virtual void deliver() = 0;
};
class Ship : public Transport
{
public:
Ship(int w, int d) : Transport(w,d){ }
void deliver()
{
float price = ( (m_distance/100) * (m_weight/1000) ) * 100;
std::cout<<"Delivering via ship. Price: "<<price<<std::endl;
}
};
class Truck : public Transport
{
public:
Truck(int w, int d) : Transport(w,d){ }
void deliver()
{
float price = ( (m_distance/100) * (m_weight/100) ) * 20;
std::cout<<"Delivering via truck. Price: "<<price<<std::endl;
}
};
class Flight : public Transport
{
public:
Flight(int w, int d) : Transport(w,d){ }
void deliver()
{
float price = ( (m_distance/1000) * (m_weight/20) ) * 200;
std::cout<<"Delivering via Flight. Price: "<<price<<std::endl;
}
};
Transport* Transport::get_tranport(int weight, int distance)
{
if( distance > 5000 )
{
if( weight < 100 )
return new Flight(weight, distance);
else
return new Ship(weight,distance);
}
else if (distance > 1000)
{
if( weight < 100 )
return new Flight(weight, distance);
else
return new Truck(weight,distance);
}
return new Truck(weight,distance);
}
int main(int argc, char const *argv[])
{
Transport *transport = Transport::get_tranport(100, 100);
transport->deliver();
return 0;
}
Here the client is unaware of the mode to transportation. The factory method decides on which class is best suited for the job and hands over an instance to the client. All the client cares about is the deliver() function. Even though the implementation of the functionality of deliver() can wildly vary depending on the modes of transportation, client don’t have to worry about any such details.
Classes model entities which in relationship with each other defines a system. Structural design patterns let you design these relationship while keeping the system as loosely coupled as possible.
Here we will discuss only one wildly useful structural pattern which is adapter pattern.
Adapter pattern introduces a layer of translation between two incompatible classes to make them compatible.
The classic analogy is as follows. If you want to charge your laptop that has an Indian style plug using an American socket, you need an Indian to American power adapter.
In software if you have a class A that has some functionality that you want to use, but your class B is not compatible with class A, then instead of re-writing class B, you can write an adapter class that conforms to class A’s interface and wrap class B inside that wrapper.
For example: Let’s say you have a legacy system that expects XML as input but your web servers are returning the same data but in a JSON format. You make your legacy system work with the webserver by introducing a JSON to XML adapter in between that accepts JSON from the webserver and converts it into the XML format that the legacy system accepts and pass it on.
Following is a code example to explain the idea using the wall socket analory:
#include<iostream>
// This is the interface for indian style power sockets
class IndianSocketPlugable
{
public:
virtual void three_pin_plug() = 0;
};
// This is the interface for american power sockets
class AmericanSocketPlugable
{
public:
virtual void two_slab_plug() = 0;
};
// Sockets
class IndianSocket
{
public:
void deliver_power(IndianSocketPlugable *p)
{
p->three_pin_plug();
}
};
class AmericanSocket
{
public:
void transefer_power(AmericanSocketPlugable *p)
{
p->two_slab_plug();
}
};
// This is a charer that is plugable only in Indian sockets
class LaptopCharger : public IndianSocketPlugable
{
public:
void three_pin_plug()
{
std::cout<<"Charging Laptop"<<std::endl;
}
};
class IndianToAmericanPlugableAdapter: public AmericanSocketPlugable
{
private:
IndianSocketPlugable *p;
public:
IndianToAmericanPlugableAdapter(IndianSocketPlugable *p) : p(p) { }
void two_slab_plug()
{
p->three_pin_plug();
}
};
int main(int argc, char const *argv[])
{
// A Indian plug will work with indian socket, it will not work with americal sockets
IndianSocket *indianSocket = new IndianSocket;
IndianSocketPlugable *charger = new LaptopCharger;
indianSocket->deliver_power(charger);
AmericanSocket *americanSocket = new AmericanSocket;
// This is not possible, because our LaptopCharger is not compatible with american socket
// americanSocket->transefer_power(charger);
// We can charge our laptop using a american socket by puting our charger in a Indian -> American adapter
AmericanSocketPlugable *adapter = new IndianToAmericanPlugableAdapter(charger);
americanSocket->transefer_power(adapter);
return 0;
}
In a system various modules or classes have different responsibilities. There is a way such classes talk to each other or use each other to create functionalities of the system. Behavioral patterns can be thought of as a set of readymade protocols which delegates work to classes and defines how a class will communicate with the rest of the system.
Following are few design pattern that are used the most.
Imagine, you are interested in a particular product which is currently out of stock. So you’d have to keep visiting the store in some interval to check whether it is restocked or not. The better approach to this problem is, you leave your phone number with the store and ask them to send you a message when this item is back in stock. That way you are saving a lot of useless visits to the store.
Let’s say there is an entity which when is changed some action is to be triggered. One way to facilitate this is all who want to do something based on change of such state can keep looking at the entity and then dispatch action when it is found to be changed, which is similar to you going to the store repeatedly. We want a store where a user can ask for notification.
In observer pattern, there is a class Publisher who has an entity that other Observers are interested in.
Let’s implement this to see it in action.
#include<iostream>
#include<vector>
class Notifiable
{
public:
virtual void nofity() = 0;
};
class Observable
{
protected:
std::vector<Notifiable*> m_subsribers;
public:
virtual int subscribe(Notifiable *) = 0;
virtual int unsubscribe(Notifiable *) = 0;
};
class Shop : public Observable
{
public:
int subscribe(Notifiable *subscriber)
{
m_subsribers.push_back(subscriber);
return 0;
}
int unsubscribe(Notifiable *subscriber)
{
int found = -1;
for(int i = 0; i < m_subsribers.size(); i++)
{
if( m_subsribers[i] == subscriber)
{
found = i;
break;
}
}
if(found < 0)
return -1;
m_subsribers.erase(m_subsribers.begin()+found);
return 0;
}
void restock_item()
{
for(auto i : m_subsribers)
i->nofity();
}
};
class Customer: public Notifiable
{
std::string m_name;
public:
Customer(std::string name) : m_name(name){}
void nofity()
{
std::cout<<"Notification to "<<m_name<<" from shop: Item back in stock.\n";
}
};
int main(int argc, char const *argv[])
{
Shop *shop = new Shop;
Customer c_1("MALAY");
Customer c_2("ABC");
Customer c_3("XYZ");
shop->subscribe(&c_1);
shop->subscribe(&c_2);
shop->subscribe(&c_3);
shop->restock_item();
return 0;
}
In this example, the Shop implements an Observable interface, using which Customers can register themselves to get notification from Shop. The Shop invokes notify method of all the subscribed customers when there is any notification to be pushed.
There can be multiple states which an Observer can observe, in such case the subscriber method can be extended to accept a parameter specifying which state to observe in the Observable.
This system can be extended further by making the notifications asynchronous. Or we can make the entire system network base, where subscriber and publishers are in a network. (This is called a pub-sub model in high level design)
This is one of the most useful patterns. It allows you to alter behavior of a class at runtime. Meaning you can make same interface behave differently at run time.
This is very similar to factory pattern. But there is an important distinction. In factory pattern the factory method hides the logic behind choosing an object which handles the intended job. Here the client don’t care about how the object returned handles the job as long as it is getting done. But in strategy pattern the client is aware of different ways of doing the intended job and the client cares about the way it is done.
For example: Let’s say you go to a restaurant and ordered a steak. You like your steaks to be medium rare. So you ordered it. Now the Chief needs to cook it. You have specified how he will cook it. Because even though the task is “cook steak”, the end result will vary depending on “how” it is cooked and hence you care about the “how”.
Another example: Let’s say you are building a navigation app. It has a method called “build route” between source and destination. If you travel from source to destination using different modes of transportation, your route will differ. Meaning, if you walk from A to B, there can be shorter path then using the highway or conversely you might not be able to drive a car in that short path. Even though you are just building a route, the algorithms need to take care of different aspects of building a route differently based on “how” you are doing to travel from A to B.
The naïve approach of handling this will be to create different classes that builds a route and use them based on a conditional. This violates Open closed principle, because every time you need to introduce a new strategy you need to alter these if-else/switch-case statements to include a new case.
The better way of handling this is to create the strategies using same interface and let the client choose the strategy directly. Let’s see the code to understand what I mean.
#include <iostream>
class IRouter
{
public:
virtual void find_best_rout(std::string&, std::string&) = 0;
};
class NavigationApp
{
private:
IRouter *rout_finder;
public:
NavigationApp() : rout_finder(nullptr) {}
void set_strategy(IRouter *s)
{
rout_finder = s;
}
void build_route(std::string a, std::string b)
{
rout_finder->find_best_rout(a,b);
}
};
// ---------- Strategies --------------------
class BicleRouteFinder: public IRouter
{
void find_best_rout(std::string &a, std::string &b)
{
std::cout<<"Finding best route for riding bicycle: From "<<a<<" To "<<b<<"\n";
}
};
class CarRouteFinder: public IRouter
{
void find_best_rout(std::string &a, std::string &b)
{
std::cout<<"Finding best route for driving: From "<<a<<" To "<<b<<"\n";
}
};
class WalkRouteFinder: public IRouter
{
void find_best_rout(std::string &a, std::string &b)
{
std::cout<<"Finding best route for walking: From "<<a<<" To "<<b<<"\n";
}
};
int main(int argc, char const *argv[])
{
NavigationApp nav_app;
nav_app.set_strategy(new CarRouteFinder);
nav_app.build_route("Banglore", "Mumbai");
nav_app.set_strategy(new WalkRouteFinder);
nav_app.build_route("Banglore", "Mumbai");
nav_app.set_strategy(new BicleRouteFinder);
nav_app.build_route("Banglore", "Mumbai");
return 0;
}
Here the navigation app is building the route differently depending on the strategy that the client chooses. You can extend this to add new strategies just by creating another class implementing the IRouter interface and exposing that class to the client. That way your navigation app stays complaint to open closed principle.
In the chain of responsibility pattern you chain a bunch of handlers together, each of such handler can handle the request, produce and error or pass it down to next handler.
Let’s look at an example to understand it.
#include<iostream>
// This class will contain all the information about the request
class Request
{
private:
std::string m_req;
public:
void set_req(std::string str)
{
m_req = str;
}
std::string get_req()
{
return m_req;
}
};
// this is the response of request
class Response
{
private:
std::string m_resp;
public:
std::string get_resp() { return m_resp; }
void set_resp(std::string val) { m_resp = val; }
};
class IHandler
{
protected:
IHandler *next_handler;
public:
void set_next_handler(IHandler *h) { next_handler = h; }
virtual void handle_request(Request*, Response*) = 0;
};
// Checks for validity of a request and sends it down to next handler
// Returns if request is invalid
// Here We cosider a request to be invalid if it has an empty
class RequestGateway: public IHandler
{
public:
void handle_request(Request *req, Response *resp) override
{
if(req->get_req().empty())
{
resp->set_resp("Invalid request");
return;
}
if(next_handler == nullptr)
{
std::cout<<"Error: next handler not set.\n";
return;
}
next_handler->handle_request(req,resp);
}
};
class SpamFilter: public IHandler
{
public:
void handle_request(Request *req, Response *resp) override
{
if(req->get_req() == "spam")
{
resp->set_resp("Spam request. Access denied.");
return;
}
if(next_handler == nullptr)
{
std::cout<<"Error: next handler not set.\n";
return;
}
next_handler->handle_request(req,resp);
}
};
class RequestProcessor: public IHandler
{
public:
void handle_request(Request *req, Response *resp) override
{
resp->set_resp("This is a response to request: "+req->get_req());
return;
}
};
int main(int argc, char const *argv[])
{
RequestGateway gateway;
SpamFilter spam_filter;
RequestProcessor processor;
gateway.set_next_handler(&spam_filter);
spam_filter.set_next_handler(&processor);
Request request;
Response resp;
request.set_req("Do this.");
gateway.handle_request(&request,&resp);
std::cout<<resp.get_resp()<<std::endl;
request.set_req("spam");
gateway.handle_request(&request,&resp);
std::cout<<resp.get_resp()<<std::endl;
request.set_req("");
gateway.handle_request(&request,&resp);
std::cout<<resp.get_resp()<<std::endl;
return 0;
}
In this example the Gateway accepted the request and validated it, then it passed down the message to the Spam filter which detects spam and then passes it down to the Request processor to process the request. Each of this class is doing just one thing. So Chain of responsibility is Single Responsibility compliant. And you can replace one element in the chain to make the chain behave differently, hence it is compliant with open-closed principle.
This is very useful when you are first designing your prototype application. You can chain together components in this fashion to create a monolith that does the job. And when you scale up you can break up the chain into the components and deploy each on separate machine(s) as microservices to scale horizontally.
This ends the introduction to design pattern. You can, hopefully, get something out of this that you can use in your projects.
Happy coding!