Hi. … 🤔, to be honest I don’t know how write an introduction to this new section 🫥.
I’ll get to the point at once.
This post be similar to:
“One Ring to rule them all, One Ring to find them, One Ring to bring them all and in the darkness bind them."
— The Ring’s inscription 📜💍
And also to
En el fin del mundo solo, solito, solo
— Calle Barcelona - La Vida Bohème
Singleton pattern 🏹 #
All the code of the post could be found in this link
The Singleton pattern is a creational design pattern that restricts the creation of a struct to a single instance throughout the entire program’s execution. This ensures that all parts of your code are working with the same object.
Ok, as you can read the singleton pattern bases all the responsibility in only one instance of class.
- There must be exactly one instance of a class and it must be accessible to clients from a well-known access point. When the sole instance should be extensible by subclassing and clients should be able to use an extended instance without modifying
- To be more in depth when using a singleton pattern is when we are working with classes that are used for logging, controller objects, caching, thread pool, database connections. These types of classes use a lot of computational resources and it is not a good idea to have a lot of them throughout our code 👀.
Note: For this section I’ll use go to explain the design pattern. And why not python like you used to do in the previous section? Well I really like Go as a language program, so like Python I want to master it.
A little break-off, I KNOW THAT GO IS NOT A COMPLETELY OOP, but works for the explanation, so shhh 🤫 To the end of the section I also write the Pythoner way to implement the pattern. But i didn’t explain in much detail like i use to do in Go That said, let’s get to code 🦾
Singleton pattern implementation #
For this example we are gonna create a class to connect to a Redis database, this to create a more realistic scenario. And why Redis, … It’s a cool database, that’s all … and all the posts that I read use MySQL drivers, and MySQL to explain this pattern is to normie 🙈.
Before starting to writing code 👁️ #
Create a cloud Redis database in this link It’s free $$$.
And to connect with the database we use this driver
Redis Package 📦 #
To create a client in go, we must create a redis package
.
It’s a good practice write package for every “general” functionality of your program.
settingsRedis.go #
In the Redis package we create settingsRedis files that contains all the const to connect to the database package redisclient
package redisclient
const (
publicEndPoint = "YourURLToConnectTOTheDataBase"
password = "YourPassword"
username = "default"
)
redis.go #
Okay, now we create a file redis.go that contains all the attributes and methods of the Redis client
In this file, we are gonna write the following code:
- RedisConnectionStruct, this struct save the
*redis.Client
as attribute - Global-package variable
redisSingletonInstance
- Constructor of the struct
GetRedisClient
package redisclient
import (
"time"
"github.com/redis/go-redis/v9"
)
// The only instance of the class
var redisSinletonInstance redisConnection
// The struct of the redis client
type redisConnection struct {
RedisClient *redis.Client
}
// The constructor of the class
func GetRedisClient() (*redisConnection, error) {
//URL of our connection redis database
redisURL := "redis://" + username + ":" + password + "@" + publicEndPoint
//SET the options of the client connection
opt, err := redis.ParseURL(redisURL)
if err != nil {
return nil, err
}
opt.DialTimeout = 5 * time.Second
//Create the connections
connection := redis.NewClient(opt)
//save the connection in our "global-package" variable
redisSinletonInstance := redisConnection{
RedisClient: connection,
}
return &redisSinletonInstance, nil
}
This is our first code, we did not implement any pattern yet, but as you can see, we only create a function that return a instance of the redisConnection
and we stored in redisSingletonInstance
.
If way stored the all the new instance of the redisConnection
structure in the redisSingletonInstance
, we only have one instance? 🤔
redisClient_test.go #
This file contains all the testing code for our redisClient.
Our first test will be to check our Redis connection to the cloud database
package redisclient_test
import (
"context"
"fmt"
redisclient "test/redisClient"
"testing"
)
func TestNewClient(t *testing.T) {
//Create our redis connection instance
redisConn, err := redisclient.GetRedisClient()
//Check if we have an error
if err != nil {
t.Errorf("Got an error creating the client: %d", err)
}
//Check the connection
conn := redisConn.RedisClient.Ping(context.Background())
//Check if the attribute Error is != than nil
if conn.Err() != nil {
t.Errorf("Could not establish the connection, Error: %d", err)
}
}
If we run our test, we get a PASS result, similar to:
=== RUN TestNewClient
--- PASS: TestNewClient (4.74s)
Check if it is single instance ☑️ #
Right now, we may think that we are only creating a single instance (or maybe overwriting the existing instance), thus we are using the redisSinletonInstance
in our constructor GetRedisClient()
.
Okay, so we’ll put this assumption to the test.
func TestIfIsSingleton(t *testing.T) {
//Create one instance
conn1, _ := redisclient.GetRedisClient()
//Create one instance
conn2, _ := redisclient.GetRedisClient()
//Check if the instances are the same
if conn1 != conn2 {
t.Error("conn1 and conn2 are not the same instance")
}
}
If we run the test, will get and output similar to this:
=== RUN TestIfIsSingleton
redisClient_test.go:37: conn1 and conn2 are not the same instance
--- FAIL: TestIfIsSingleton (0.00s)
FAIL
FAIL command-line-arguments 0.694s
FAIL
OMG, so our 2 instance of the redisClient are not the same instance 🫠 ???.
Well, yes, this happens because every time we call GetRedisClient()
we are rewriting a new value of the redisSinletonInstance
, then the GetRedisClient()
return a new value of the variable saved in the cache of the program.
Only one instance ❗ #
To solve the previous problem we may use a Golang approach.
The recommended approach to achieve a Singleton is by using a mutex from the sync package and change the redisSinletonInstance
to a pointer. This guarantees thread-safe access to the instance creation process, especially when dealing with concurrent routines (goroutines).
Rewriting our code:
package redisclient
import (
"sync"
"time"
"github.com/redis/go-redis/v9"
)
// The only instance of the class
var redisSinletonInstance *redisConnection
// For thread-safe
var lock = &sync.Mutex{}
// The struct of the redis client
type redisConnection struct {
RedisClient *redis.Client
}
// The constructor of the class
func GetRedisClient() (*redisConnection, error) {
//First check
if redisSinletonInstance == nil {
//Lock for safe thread
lock.Lock()
defer lock.Unlock()
//Second check
if redisSinletonInstance == nil {
//URL of our connection redis database
redisURL := "redis://" + username + ":" + password + "@" + publicEndPoint
//SET the options of the client connection
opt, err := redis.ParseURL(redisURL)
if err != nil {
return nil, err
}
opt.DialTimeout = 5 * time.Second
//Create the connections
connection := redis.NewClient(opt)
//save the connection in our "global-package" variable
redisSinletonInstance = &redisConnection{
RedisClient: connection,
}
}
}
return redisSinletonInstance, nil
}
As you can see, the so-called double-checked approach.
However, this code may appear a bit strange. Since the outer check redisSinletonInstance == nil
has already been performed, it seems redundant to perform a second redisSinletonInstance == nil
check after acquiring the lock. In fact, the outer redisSinletonInstance == nil
check is to improve the execution efficiency of the program. If instance already exists, there is no need to enter the if logic, and the program can return instance directly. This avoids the overhead of acquiring the lock for every GetSingleton() call and makes the locking more fine-grained. The inner redisSinletonInstance == nil
check is for considering concurrency safety. In extreme cases, when multiple goroutines reach the point of acquiring the lock simultaneously, the inner check comes into play.
If we run the TestIfIsSingleton()
again, the result of the test would be similar to this
=== RUN TestIfIsSingleton
--- PASS: TestIfIsSingleton (0.00s)
A more attractive way ✨ #
Right now we already know how to write a singleton pattern in Go, but the way we have done it doesn’t seem to be very understandable to read and pretty 🤢.
But it’s okay, Go have another way to do this
I will rewrite the code, and do the explanation later 😉 .
package redisclient
import (
"sync"
"time"
"github.com/redis/go-redis/v9"
)
// The only instance of the class
var redisSinletonInstance *redisConnection
// For thread-safe
var once sync.Once
// The struct of the redis client
type redisConnection struct {
RedisClient *redis.Client
}
// The constructor of the class
func GetRedisClient() (*redisConnection, error) {
var (
err error
opt *redis.Options
)
once.Do(func() {
//URL of our connection redis database
redisURL := "redis://" + username + ":" + password + "@" + publicEndPoint
//SET the options of the client connection
opt, err = redis.ParseURL(redisURL)
if err != nil {
return
}
opt.DialTimeout = 5 * time.Second
//Create the connections
connection := redis.NewClient(opt)
//save the connection in our "global-package" variable
redisSinletonInstance = &redisConnection{
RedisClient: connection,
}
})
if err != nil {
return nil, err
}
return redisSinletonInstance, nil
}
Go provides sync.Once
method in the sync
package, the function passed in once.Do only gets called once, even if GetCache gets called 1000s of times, the function inside once.Do will only get executed once and is concurrency safe. I encourage you to read more about.
This way is more attractive, isn’t it? 🤭
Python #
Python offers flexibility in implementing the singleton pattern. This post explores one approach using the __new__
dunder method.
The __new__
method is a static method that’s integral to the class itself. Its primary role is to create and return a new instance of the class. It receives the class as its first argument, followed by any additional arguments intended for the instance construction.
In simpler terms, the __new__
method deals with class-level object creation, not object manipulation.
In the code example below, we’ll establish a Redis client and store it within a dictionary. This strategy ensures that only one instance is created, for the different Redis databases you might interact with in your software project. (Remember, a project could potentially have numerous connections to various databases).
Code #
settings.py #
publicEndPoint = "YourURLToConnectTOTheDataBase"
port = 000
password = "YourPassword"
username = "default"
redis_client.py #
import redis
class RedisClient:
# Dictionary of instance to connect a differents redis database
__instances = {}
def __new__(cls, redis_host, redis_port, redis_username, redis_password):
#Create a tuple with the config of the connection to the database
config_key = (redis_host,redis_port,redis_username,redis_password)
if config_key not in cls.__instances:
#Create a new object
instance = super(RedisClient, cls).__new__(cls)
#Save our objectec in __instances dic
cls.__instances[config_key] = instance
#Call the constructor
instance.__init__(redis_host, redis_port, redis_username, redis_password)
return cls.__instances[config_key]
#Constructor of the class
def __init__(self, redis_host, redis_port, redis_username, redis_password) -> None:
#Atributes of our class
self.redis_host = redis_host
self.redis_port = redis_port
self.redis_username = redis_username
self.redis_pasword = redis_password
self.client = None
def connect(self):
"""
Create a connection to the redis DB
Return: A redis client
"""
#Check if the instance of the object has been created before
if self.client:
return self.client
try:
self.client = redis.Redis(
host=self.redis_host,
port=self.redis_port,
username= self.redis_username,
password=self.redis_pasword,
decode_responses=True
)
except:
self.client = None
def ping(self):
if self.client.ping():
print("Ping: Pong")
def close(self)-> None:
self.client.close()
test.py #
import unittest
import redis_client
import settings
class TestRedisClinet(unittest.TestCase):
def test_constructor(self):
client = redis_client.RedisClient(
redis_host= settings.publicEndPoint,
redis_port= settings.port,
redis_password= settings.password,
redis_username=settings.username
)
client.connect()
self.assertIsNotNone(client.client,"Could not create the Redis client")
def test_single_instance(self):
r1 = redis_client.RedisClient(
redis_host= settings.publicEndPoint,
redis_port= settings.port,
redis_password= settings.password,
redis_username=settings.username
)
r2 = redis_client.RedisClient(
redis_host= settings.publicEndPoint,
redis_port= settings.port,
redis_password= settings.password,
redis_username=settings.username
)
r1.connect()
r2.connect()
self.assertEqual(r1,r2,"Not singleton instance")
if __name__ == "__main__":
unittest.main()
Try this code by yourself!
Brief note #
I’ve been using redis for a short time, but I really like it, it’s quite different from MongoDB or a SQL database. Maybe later I will write some posts about Redis. 🤔
The songs of the post #
Mi niña (mi niña)
Mi niña bonita (mi niña bonita)
Mi niña no baila pa toa la gente
Mi niña no baila pa toa la gente
— Calle Barcelona - La Vida Bohème
I’ve never been a big fan of La Vida Bohème, but they released a new album Diáspora Live Vol.1
. This album is a compilation of a tour that they did last year, and it’s really good.
I have to admit that La Vida Bohéme has a really good live performance.
I encourage you to listen the entire album, you can’t miss it
My favorites from the album:
Sábado
Llegó la policía
Mi primo
Fue el que disparó
Bang
— Manos Arriba - La Vida Bohème