DEV Community

Favour George
Favour George

Posted on • Originally published at psycode.hashnode.dev on

Singleton Design Pattern: Mastering the Art of Single Instance Classes

What are Design Patterns?

A design pattern is a reusable solution to a general obstacle faced by developers while designing and building software. They are models or blueprints for solving recurring problems in software development

Why are Design Patterns necessary?

Design patterns when used properly for the right problems can accelerate the speed of software development by helping developers to avoid reinventing the solution to an already solved problem. With that said, it is important to understand that design patterns are not an alternative to sound logic and problem-solving skills and as such, they shouldnt be used randomly and the developer should know under what circumstances a certain design pattern(s) should be used

In the next few weeks, Ill be uploading articles on various design patterns. For today, lets get started with the singleton design pattern.

Singleton Design Pattern

The singleton design pattern is a software design pattern that aims at making sure theres just one instance of a class in a program regardless of how many times an object is instantiated from said class.

To show the singleton design pattern in practice, well use it to build a logger for logging errors into a log file. In a previous article here, we built a logger but had an issue where we always had to specify the name of the log file as an argument in our logger function as shown below:

import datetime

def logger(level, msg, logger):
    date = datetime.datetime.now()
    with open(f'./{logger}.log', 'a') as logger:
        logger.write(f'\n [{level}] {msg} ---- {date}'.format())

Enter fullscreen mode Exit fullscreen mode

This could result in quite a messy file structure as we could end up logging into multiple log files which wont be clean. So using the singleton design pattern, well create one and only one instance of our logger, logging all the messages to one log file that wont need to be provided as an argument when creating an object from our class. Lets get started.

In the previous article where we built our logger here, we created a logger.py file where we stored the code for our logger. Now we are going to modify that file by creating a class that logs to just one file. To implement the singleton design pattern, well create a dunder method or special method ( these are Python methods that are identified by two trailing underscores before and after their names ). This method is responsible for creating a new instance of a class if none exists as shown below

import datetime

class Logger():
    instance = None

    def __new__ (cls):
        if cls.instance == None:
            cls.instance = super(). __new__ (cls)
        return cls.instance

Enter fullscreen mode Exit fullscreen mode

In the code above, we import the datetime module to keep track of the time a message is being logged to our log file. We then created a class- Logger, we created a variable, instance, which shows if an instance of the class exists, for now, we set it to None. We then set up the dunder method, new , which is responsible for creating a new instance of a class if none exists. This method takes a default argument, cls , which is a reference to the class itself (i.e. Logger ) and not an instance of the class as most would think. In the method, we then write an if statement to check if theres no instance of the class and if theres none, it creates one and then returns that instance. Likewise, if theres already an instance, it returns that instance.

With that out of the way, let's create a method in the class for writing messages to our log file as shown:

def __init__ (self) -> None:
        self.filename = "pyLogger.log"

    def log(self, level, msg):
        date = datetime.datetime.now()
        with open(self.filename, 'a') as logger:
            logger.write(f"[{level}] ---- {msg} ---- {date}\n")

Enter fullscreen mode Exit fullscreen mode

In the code above, we create a constructor method for our class which will automatically be called when an object is instantiated from the class and explicitly set a filename for our logger; this is where our logged messages will be written. Its important to note that we didnt pass the filename as an argument.

We then wrote a method for writing logged messages into our log file, taking the level of the message (e.g. ERROR, CRITICAL, DEBUG etc ) and the message itself as arguments. We then set up the date, and write to our log file the level and message.

To test if our singleton has been successfully implemented, well use the Logger class in a new file - main.py as shown below:

from logger import Logger

logger_1 = Logger()
logger_2 = Logger()

print(logger_1 == logger_2)
print(logger_1)
print(logger_2)

Enter fullscreen mode Exit fullscreen mode

In the code above, we import the Logger class from our logger.py file into our main.py file. We then created two objects from the class, and heres the tricky part, normally these would be two different instances of the class and these two instances would be stored in different memory addresses but since we had implemented the singleton design pattern, these two instances are just one and are stored in the same memory address as shown below from the output of the code above.

True
<logger.Logger object at 0x00000071F1397940>
<logger.Logger object at 0x00000071F1397940>

Enter fullscreen mode Exit fullscreen mode

Now to use it in our logger, in our main.py well keep the import of the Logger class and create an instance of it. Then well write a basic script, the same as we did in the previous article on creating a logger, in the script well try to divide a number by zero and catch the Zero Division error that occurs and then log it to our logger as shown:

from logger import Logger

logger_1 = Logger()

try:
    calc = 5 / 0
except ZeroDivisionError as err:
    logger_1.log("ERROR", err)
finally:
    print('done')

Enter fullscreen mode Exit fullscreen mode

If the code above runs successfully, our logger object would have created a logger file with the name we set it to in the class in our logger.py file and in the file you should see something like this:

[ERROR] ---- division by zero ---- 2023-08-07 08:49:07.513796

Enter fullscreen mode Exit fullscreen mode

Alright folks, thats it for this article, weve learnt about the singleton design pattern and showed it in practice by building a logger. So to recap, the singleton design pattern is useful when we want to create just one instance of a class regardless of how many objects are instantiated from the class.

So till next time nerds, dont break production.

Your employer wont be happy 😀

Top comments (0)