DEV Community

dbigpot
dbigpot

Posted on

JS/TS: Using the Singleton Design Pattern

Classes were added to JavaScript in ES6 as a template for creating and managing objects in your application. They encapsulate data and the code inside each class helps us work with that data.

class Vehicle {
  constructor(speed, weight) {
    this.speed = speed;
    this.weight = weight;
  }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript is a superset of JavaScript which primarily provides optional static typing, classes and interfaces. Using TypeScript, you can convert the above example of Vehicle into

class Vehicle {
  constructor(speed: number, weight: number) {
    this.speed = speed;
    this.weight = weight;
  }
}
Enter fullscreen mode Exit fullscreen mode

With that simplistic example, we know what classes are and what they help us with. But, you already knew this.

So, what are Singleton classes?

Singleton classes are a design pattern, popularly used in Java - and perhaps other languages as well. In object-oriented programming, a singleton class is a class that can have only one object (an instance of the class) at a time and provide your application a global point of access to that instance. Let me give you an example/use-case right after the basic syntax of writing singleton classes

class MySingletonClass {
  private static INSTANCE: MySingletonClass;

  private name: string;
  private constructor() {
    this.name = "noobmaster69";
  }

  public static getInstance(): MySingletonClass {
    if (!this.INSTANCE) {
      this.INSTANCE = new MySingletonClass();
    }
    return this.INSTANCE;
  }

  public getGreetingMessage(): string {
    return `Hello, ${this.name}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, MySingletonClass is a simple class that follows the singleton pattern with the help of the static members INSTANCE and getInstance. The INSTANCE object is of type MySingletonClass because it will can contain an instance of the class. The initiation of the object is done inside the getInstance method. This is a personal preference but I like to make my constructors private so new objects of MySingletonClass cannot be made outside the scope of the class.

Now that you have created your singleton class, how would you access the methods? You'd do it by

console.log(MySingletonClass.getInstance().getGreetingMessage());
Enter fullscreen mode Exit fullscreen mode

That's all great, why would I use it?

That's a good question. Classes are essentially used to create new objects, meaning each object will encapsulate different - or even unique - data. Like mentioned at the beginning, singleton classes help us maintain a single instance of an object that can be accessed globally by different parts of your code. The best use-cases for singleton classes would be using them to maintain database services - which is the example I want to use. Here's a look at the code -

import mysql, { Connection, Query } from 'mysql';
import unnamed from 'named-placeholders';

const toUnnamed = unnamed();

export default class Mysql {
  private connection!: Connection;

  private static INSTANCE: Mysql;

  public static getInstance(): Mysql {
    if (!this.INSTANCE) {
      this.INSTANCE = new Mysql();
    }
    return Mysql.INSTANCE;
  }

  private constructor() {
    this.connection = mysql.createConnection({
      host: process.env.DB_HOST,
      user: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_DB,
    });

    this.connect();
  }

  connect() {
    this.connection.connect();
  }

  query(sqlString: string, values = []): Promise<Query> {
    return new Promise(async (resolve, reject) => {
      try {
        return this.connection.query(sqlString, values, (err, results) => {
          if (err) {
            return reject(err);
          }
          return resolve(results);
        });
      } catch (error) {
        reject(error);
      }
    });
  }

  namedQuery(sqlString: string, values: {}): Promise<Query> {
    const [query, queryValues] = toUnnamed(sqlString, values);
    return this.query(query, queryValues);
  }

  beginTransaction() {
    return new Promise((resolve, reject) => {
      try {
        this.connection.beginTransaction((err) => {
          if (err) {
            reject(err);
          } else {
            resolve(true);
          }
        });
      } catch (error) {
        reject(error);
      }
    });
  }

  commit() {
    return new Promise((resolve, reject) => {
      try {
        this.connection.commit();
        resolve(true);
      } catch (error) {
        reject(error);
      }
    });
  }

  rollback() {
    return new Promise((resolve, reject) => {
      try {
        this.connection.rollback();
        resolve(true);
      } catch (error) {
        reject(error);
      }
    });
  }

  release() {
    return new Promise((resolve, reject) => {
      try {
        if (this.connection) {
          this.connection.end();
        }
        resolve(true);
      } catch (error) {
        reject(error);
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

If you were to use the above snippet for maintaining a connection to your MySQL database, your entire application would have access to the singular connection/instance that is required throughout the lifetime of your application, i.e. you won't have to create new objects each time you want to run MySQL queries in your application. To give you an example, if you wanted to run a MySQL query, you'd simple use

Mysql.getInstance().query(myQuery);
Enter fullscreen mode Exit fullscreen mode

And, that's it! That's singleton classes for you - cleaner, meaner, and makes code maintenance simpler.

If you liked what you read, leave a <3 and a comment! :)

Discussion (0)