Lets learn "Singleton design pattern"

 In this tutorial, we'll learn about the singleton design patterns as in what it is, in which use-cases it is being used and how it works. If you've are not familiar with what are software design patterns and what are its different types check out this tutorial.

Introduction

A factory design pattern is a creational design pattern that is related to object creation. This design pattern restricts to create only one object of the class. Whenever the client tries to instantiate a class, if this is the first time we are instantiating it will create an actual object, from next time onwards it will return the same object instead of creating a new one.

Usecases 

There are a lot of use-cases where this pattern is useful. One of the common use cases is creating a DB connection. If you have a monolithic application and you wanna perform DB operation from multiple places in the app. To do any DB operation you need to connect to the DB first. And we should not be creating a new connection every time. Instead, we can create one and use that connection every time. To achieve that we can use the singleton pattern and have a method NewDBConnection() which will always return the same connection object it exists, otherwise, create one.

Examples

#include<iostream>
#include<map>
#include<string>
#include <mutex>
using namespace std;

class DB {
    private:
    static DB* connection;
    map<string, string> dataStore;

    // private constructor it cannot be called outside the class
    DB() {}

    public:
    static DB* newDBConnection() {
        if(connection == NULL){
            connection = new DB;
        }
        return connection;
    }

    void put(string key, string value) {
        dataStore.insert(make_pair(key, value));
    }

    string get(string key) {
        return dataStore[key];
    }
};

//Initialize pointer to zero so that it can be initialized in first call to getDBConnection
DB *DB::connection = 0;

int main(){
    DB* conn1 = conn1->newDBConnection();
    conn1->put("lets", "learn");

    // re-instantiation should not create new instance
    DB* conn2 = conn2->newDBConnection();
    
    // hence should use the same datastore and return "learn"
    cout<<conn2->get("lets")<<endl;
    return 0;
}

The issue with the above example is that the getDBConnection is not thread-safe which means if multiple threads try to instantiate the DB class at the same time we might end up having multiple of instances of DB class created. Now let's see how we can avoid that.

#include<iostream>
#include<map>
#include<string>
#include <mutex>
using namespace std;

mutex mtx;
class DB {
    private:
    static DB* connection;
    map<string, string> dataStore;

    // private constructor it cannot be called outside the class
    DB() {}

    public:
    static DB* newDBConnection() {
        mtx.lock();
        if(connection == NULL){
            connection = new DB;
        }
        mtx.unlock();
        return connection;
    }

    void put(string key, string value) {
        dataStore.insert(make_pair(key, value));
    }

    string get(string key) {
        return dataStore[key];
    }
};

//Initialize pointer to zero so that it can be initialized in first call to getDBConnection
DB *DB::connection = 0;

int main(){
    DB* conn1 = conn1->newDBConnection();
    conn1->put("lets", "learn");

    // re-instantiation should not create new instance
    DB* conn2 = conn2->newDBConnection();
    
    // hence should use the same datastore and return "learn"
    cout<<conn2->get("lets")<<endl;
    return 0;
}

But having mutex lock while object creation will affect the performance. Because as you can see in the above program every time we call NewDBConnection we need to lock and unlock mutex irrespective of an object is created before or not. Let's see how we can improve it further.

#include<iostream>
#include<map>
#include<string>
#include <mutex>
using namespace std;

mutex mtx;
class DB {
    private:
    static DB* connection;
    map<string, string> dataStore;

    // private constructor it cannot be called outside the class
    DB() {}

    public:
    static DB* newDBConnection() {
        if(connection == NULL){
            mtx.lock();
            if(connection == NULL) {
                connection = new DB;
            }
            mtx.unlock();
        }
        return connection;
    }

    void put(string key, string value) {
        dataStore.insert(make_pair(key, value));
    }

    string get(string key) {
        return dataStore[key];
    }
};

//Initialize pointer to zero so that it can be initialized in first call to getDBConnection
DB *DB::connection = 0;

int main(){
    DB* conn1 = conn1->newDBConnection();
    conn1->put("lets", "learn");

    // re-instantiation should not create new instance
    DB* conn2 = conn2->newDBConnection();
    
    // hence should use the same datastore and return "learn"
    cout<<conn2->get("lets")<<endl;
    return 0;
}

Now with above program, we'll be locking/unlocking if the object is not created before for which no. of hits will be less. So this won't affect performance much and that's the best way to use a singleton pattern.

HOPE YOU LIKE THIS TUTORIAL. FEEL FREE TO COMMENT BELOW IF YOU HAVE ANY DOUBTS. AND STAY TUNED FOR MORE TUTORIALS :)
Happy Learning!

Comments

Popular posts from this blog

Lets learn "About kube proxy in iptables mode"

Lets learn "System design for paste bin (or any text sharing website)"

Lets learn "Factory design pattern"