Essential Design Patterns -An Easy Explanation through Real-World Examples - Part 1:  Singleton and Factory

Photo by JJ Ying on Unsplash

Essential Design Patterns -An Easy Explanation through Real-World Examples - Part 1: Singleton and Factory

Welcome! to the first part of the series on Object-Oriented Design Patterns through Python. In this series, we will explore different design patterns that can be used to solve common software design problems. In this particular post, we will focus on two important design patterns: the Singleton pattern and the Factory pattern.

Introduction

The object-oriented programming paradigm focuses on the creation and uses of objects and their interaction in software development. OOP allows for a more modular and flexible design, making it easier to develop, test, and maintain complex software systems.

Object-oriented design patterns are a set of best practices and guidelines for solving common software design problems. These patterns provide developers with a set of standardized solutions that have been developed and refined over time

These design patterns are broadly classified into three categories

  1. Creational:

    Concerned with object creation mechanisms. Example: Factory, Singleton

  2. Structural:

    These design patterns are about organizing different classes and objects to form larger structures and provide new functionality. Example: Decorator

  3. behavioral

    They are about identifying common communication patterns between objects and realizing these patterns. Example: Chain of responsibility, Observer

No need to worry too much about these categories right now. As we progress through this series, we will explore each of these categories in greater detail, with real-world use cases and comparisons between them. For now, Let's focus on two of the most popular creational design patterns: Singleton and Factory, which falls under creational design pattern.

Singleton

As the name suggests it ensures that a class has only one instance always and provides a global point of access to everyone.

Before diving into it let's understand a few use cases for it

Use Cases and Common Problems

  1. Database Connection:

Consider that you have developed a class called DatabaseConnection, which enables you to create and close a database connection. Any object that requires a database connection can create a new connection through this class. However, this approach may lead to two significant issues for your application:

  1. Performance: Creating and closing database connections for each object interaction can be a resource-intensive process, which could impact the overall performance of your application.

  2. Lack of control over the number of concurrent connections: If every object can create a connection, your application may not have control over the number of database connections being established, leading to potential overflow and disallowing additional connections to your database server.

To address the above issues, a solution would be to have a single database connection object with a pool of database connections that can be shared globally among all objects or clients. This is where the Singleton pattern comes into play.

  1. Caching Objects

    Caching objects store frequently used data in memory to avoid repeatedly retrieving data from a database, network, or file system. Since caching objects are used across the application, it is important to ensure that there is only one instance of the caching object.

Some other use case includes Logging objects, A Common ThreadPool required for application, Configuration objects shared across the application

By discussing the above use cases, we can understand the significance of the Singleton pattern and why it is commonly utilized in software development.

Singleton UML Diagram

Two important Notions to Understand UML Diagram

is - a: Inheritance Example: Apple(Child class) is Fruit(Prent class)

has -a : Composition - In this scenario, the Client has a Singleton class object

Singleton - Design Patterns In Python

Let's dive into its implementation example using a database connection.

Implementation Example

The below implementation creates a database connection pool which is created only at the first instantiation of the class.

import psycopg2
from psycopg2 import pool

class DatabaseConnection:
    __instance = None

    @staticmethod 
    def getInstance():
        if DatabaseConnection.__instance == None:
            DatabaseConnection()
        return DatabaseConnection.__instance

    def __init__(self):
        if DatabaseConnection.__instance != None:
            raise Exception("Singleton object cannot be created more than once.")
        else:
            try:
                self.connection_pool = psycopg2.pool.SimpleConnectionPool(
                    1,  # Minimum number of connections
                    10,  # Maximum number of connections
                    host="localhost",
                    port = 5432,
                    database="mydatabase",
                    user="myuser",
                    password="mypassword"
                )
                self.cursor = None  # We will create a new cursor for every query
                DatabaseConnection.__instance = self
                print("Database connection pool created successfully.")
            except psycopg2.Error as e:
                print("Error creating database connection pool:", e)
                exit()

    def get_connection(self):
        return self.connection_pool.getconn()

    def return_connection(self, conn):
        self.connection_pool.putconn(conn)

    def close_all_connections(self):
        self.connection_pool.closeall()

# Testing the Singleton database connection object
connection1 = DatabaseConnection.getInstance()
connection2 = DatabaseConnection.getInstance()

print(connection1)
print(connection2)

# Both connection objects should be the same instance
print(connection1 is connection2)

# Close all connections
connection1.close_all_connections()

The DatabaseConnection class follows the Singleton pattern. It uses a private class variable called __instance to store the Singleton object. The class level variable is shared among all objects of the class and the __init method has a condition in place to create a connection pool only when the instance is null. This ensures that only one pool is created and the same instance of the object is returned every time the class is instantiated.

Lets now move to another frequently used design pattern for our today's discussion

Factory Pattern

It provides an interface for creating objects without specifying the exact class to create. It encapsulates the object creation logic and allows the client to use the same code to create objects of different classes.

Let's understand this with a use case

Use case

Suppose you have an application that can support multiple database platforms such as MySQL, and PostgreSQL. Each database platform has its own database driver and specific implementation details.

To connect to the database, you can use a database connection factory that creates a connection to the appropriate database based on the user's preference or the platform's configuration.

For example, if you want to select MySQL as the preferred database, the factory will create a MySQL database connection object to interact with the MySQL database. Similarly, if you select PostgreSQL, the factory will create a PostgreSQL database connection object.

By using a factory pattern, you can ensure that your app code is not tightly coupled to any particular database platform and can be switched seamlessly based on configurations or preferences.

UML Diagram

Implementation Example

First, we would define a common interface for creating database connections.

from abc import ABC, abstractmethod

class DBConnection(ABC):

    @abstractmethod
    def connect(self):
        pass

    @abstractmethod
    def close(self):
        pass

    @abstractmethod
    def execute_query(self, query):
        pass

Next, we create concrete implementations of the DBConnection interface for each type of SQL database we want to support, as MySQLConnection and PgSQLConnection.


class MySQLConnection(DBConnection):
    def connect(self):
        # create MySQL connection logic

    def close(self):
        # close MySQL connection logic

    def execute_query(self, query):
        # execute MySQL query logic


class PgSQLConnection(DBConnection):
    def connect(self):
        # create PgSQL connection logic

    def close(self):
        # close PgSQL connection logic

    def execute_query(self, query):
        # execute PgSQL query logic

Now, we create a DBConnectionFactory class that encapsulates the logic for creating the appropriate database connection based on a given configuration. This class takes a configuration object that specifies the database type, host, username, password, and any other necessary parameters.

class DBConnectionFactory:
    @staticmethod
    def create_connection(config):
        if config['db_type'] == 'mysql':
            return MySQLConnection(config)
        elif config['db_type'] == 'pgsql':
            return PgSQLConnection(config)
        else:
            raise ValueError("Invalid database type")

Finally, the client code can use the DBConnectionFactory class to create database connections in a flexible and reusable way, without requiring to know the exact class of each connection. For example:

config = {
    'db_type': 'mysql',
    'host': 'localhost',
    'username': 'root',
    'password': 'password',
    'database': 'mydb'
}

connection = DBConnectionFactory.create_connection(config)

This will create a MySQLConnection object using the configuration specified in the config dictionary. If the client later decides to switch the database type it can simply change db_type and the application will continue working without any other change.

That concludes part one of our discussion on object-oriented design patterns in Python, with a focus on the Singleton and Factory patterns. In the next part, we'll explore Observer and Decorator design patterns and their use cases, so stay tuned for more insights and examples.