Design patterns are reusable templates that help us solve software design problems using best practices. In this way, they help us build applications using code that is easier to maintain, understand, reuse and test.
Escape Velocity Labs
You can find all our articles, courses, and tutorials on our website:
What is this pattern for?
This pattern restricts the creation of objects of a class to a single instance.
Continuing with the example of the app for our social network, in this article we are going to implement the class that will be in charge of logging the operations of the app in an external file, so that when the users have an error, we can debug it.
This class, called Logger, will record absolutely everything that happens in our app, so it will have to be accessible from all the classes in it. And this creates a problem for us. To create an instance of the Logger class within each class of the app, we will have to pass the dependencies of Logger as arguments to all other classes.
class SQLiteDatabase: def __init__(self, dep_a, dep_b): self.logger = Logger(dep_a, dep_b) ...
As you can see in the class above, we will have to pass the Logger dependencies to the database, although they have nothing to do with the database itself. Another possibility is to inject the Logger instance directly into the database:
class SQLiteDatabase: def __init__(self, logger): self.logger = logger ...
But, as we said, Logger will be present in almost every class in the app, so our code is still unnecessarily complicated.
The solution: create a single instance of the Logger class and call it from inside the classes where it will be used.
How does it work?
The first time we call the Logger class, we will create the only instance of this class. Successive calls will simply return that instance:
class Logger: __instance = None def __new__(cls, *args): if cls.__instance is None: cls.__instance = object.__new__(cls) return cls.__instance
As you can see, the Logger class will store a hidden variable called __instance where we will store the instance once it is created. The __new__ method is in charge of checking if that instance exists and, if not, it will create it.
In all the classes in which we use Logger, we will simply obtain the unique instance and use it normally, without having to expose that class to Logger’s dependencies:
class Database: def __init__(self): self.logger = Logger() def insert(self, row): self.logger.log('Starting insertion of a row.')
- We ensure that there is only one instance of classes that may contain sensitive resources.
- Allow access from anywhere in the app.
- Delays the creation of the object until the time it needs to be used (see pattern #4: delayed initialization).
- It makes the unit testing of the different parts of the app difficult since it is necessary to create mock objects to replace the Singletons used.
- We bind all the classes of the app to the use of the Logger class. If, later on, we introduce another class called Logger2, we will have to replace all the points where Logger is used in the app.
In general, this pattern is quite controversial due to its drawbacks. Therefore, its use is discouraged except in situations where its state does not affect the functioning of the app (as in the case of a logging tool).