Hola. … 🤔, sinceramente no sé cómo escribir una introducción para esta nueva sección 🫥.
Voy directo al grano.
Este post se parece a:
“One Ring to rule them all, One Ring to find them, One Ring to bring them all and in the darkness bind them.“
· The Fellowship of the Ring, J.R.R. Tolkien
Y también a
En el fin del mundo solo, solito, solo
· Calle Barcelona, La Vida Bohème
Patrón Singleton 🏹
Todo el código del post lo puedes encontrar en este link
El patrón Singleton es un patrón de diseño creacional que restringe la creación de un struct a una sola instancia durante toda la ejecución del programa. Esto asegura que todas las partes de tu código trabajen con el mismo objeto.
Ok, como puedes leer, el patrón Singleton pone toda la responsabilidad en una única instancia de la clase.

hugo new content posts/my-first-post.md Las razones principales son:
-
Tiene que existir exactamente una instancia de una clase y debe ser accesible para los clientes desde un punto de acceso conocido. Cuando la única instancia debería poder extenderse mediante subclases, y los clientes deberían poder usar una instancia extendida sin modificar nada.
-
Para profundizar un poco más, se usa un patrón Singleton sobre todo cuando trabajamos con clases usadas para logging, objetos controladores, caché, pools de hilos, conexiones a bases de datos. Este tipo de clases usa muchos recursos computacionales, y no es buena idea tener muchas instancias de ellas repartidas en el código 👀.
Nota: Para esta sección voy a usar Go para explicar el patrón de diseño. ¿Y por qué no Python, como acostumbraba en la sección anterior? Bueno, me gusta mucho Go como lenguaje, y así como con Python, quiero dominarlo.
Un pequeño paréntesis, SÉ QUE GO NO ES COMPLETAMENTE ORIENTADO A OBJETOS, pero funciona para la explicación, así que shhh 🤫. Al final de la sección también escribo la forma “pythonera” de implementar el patrón. Pero no la explico con tanto detalle como suelo hacer con Go. Dicho esto, vamos al código 🦾
Implementación del patrón Singleton
Para este ejemplo vamos a crear una clase para conectarnos a una base de datos Redis, para tener un escenario más realista. ¿Y por qué Redis? … Es una base de datos genial, eso es todo … y todos los posts que leo usan drivers de MySQL, y usar MySQL para explicar este patrón ya es muy normie 🙈.
Antes de empezar a escribir código 👁️
Crea una base de datos Redis en la nube en este link. Es gratis $$$.
Y para conectarnos a la base de datos usamos este driver
Paquete Redis 📦
Para crear un cliente en Go, tenemos que crear un redis package.
Es buena práctica escribir un paquete para cada funcionalidad “general” de tu programa.
settingsRedis.go
En el paquete Redis creamos los archivos de settingsRedis que contienen todas las constantes para conectarse a la base de datos. package redisclient
package redisclient
const (
publicEndPoint = "YourURLToConnectTOTheDataBase"
password = "YourPassword"
username = "default"
)
redis.go
Ok, ahora creamos un archivo redis.go que contiene todos los atributos y métodos del cliente Redis.
En este archivo vamos a escribir lo siguiente:
-
RedisConnectionStruct, este struct guarda el
*redis.Clientcomo atributo. -
Una variable global del paquete
redisSingletonInstance. -
El constructor del 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
}
Este es nuestro primer código, todavía no implementamos ningún patrón, pero como puedes ver, solo creamos una función que devuelve una instancia de redisConnection y la guardamos en redisSingletonInstance.
Si guardamos todas las nuevas instancias de la estructura redisConnection en redisSingletonInstance, ¿tenemos una sola instancia? 🤔
redisClient_test.go
Este archivo contiene todo el código de pruebas para nuestro redisClient.
Nuestra primera prueba será revisar la conexión de Redis a la base de datos en la nube.
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)
}
}
Si corremos la prueba, obtenemos un resultado PASS, parecido a:
=== RUN TestNewClient
--- PASS: TestNewClient (4.74s)
¿Es una sola instancia? ☑️
Ahora mismo podríamos pensar que solo estamos creando una única instancia (o quizás sobrescribiendo la existente), ya que usamos redisSinletonInstance dentro de nuestro constructor GetRedisClient().
Ok, así que vamos a poner esa suposición a prueba.
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")
}
}
Si corremos la prueba, obtendremos algo parecido a esto:
=== 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, entonces nuestras 2 instancias de redisClient no son la misma instancia 🫠 ???.
Bueno, sí, esto pasa porque cada vez que llamamos a GetRedisClient() estamos sobrescribiendo un nuevo valor de redisSinletonInstance, así que GetRedisClient() devuelve un nuevo valor de la variable guardada en la memoria del programa.
Solo una instancia ❗
Para resolver el problema anterior podemos usar un enfoque propio de Golang.

El enfoque recomendado para lograr un Singleton es usar un mutex del paquete sync y cambiar redisSinletonInstance a un puntero. Esto garantiza acceso seguro entre hilos al proceso de creación de la instancia, sobre todo cuando trabajamos con rutinas concurrentes (goroutines).
Reescribiendo nuestro código:
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
}
Como puedes ver, es el llamado enfoque de doble verificación.
Sin embargo, este código puede parecer un poco raro. Ya que la verificación externa redisSinletonInstance == nil ya se hizo, parece redundante hacer una segunda verificación redisSinletonInstance == nil después de tomar el lock. De hecho, la verificación externa redisSinletonInstance == nil existe para mejorar la eficiencia de ejecución del programa. Si la instancia ya existe, no hace falta entrar a la lógica del if, y el programa puede devolver la instancia directamente. Esto evita el costo de tomar el lock en cada llamada a GetSingleton() y hace el bloqueo más fino. La verificación interna redisSinletonInstance == nil existe por seguridad de concurrencia. En casos extremos, cuando varias goroutines llegan al punto de tomar el lock al mismo tiempo, ahí es donde entra en juego la verificación interna.
Si volvemos a correr TestIfIsSingleton(), el resultado será parecido a esto:
=== RUN TestIfIsSingleton
--- PASS: TestIfIsSingleton (0.00s)
Una forma más bonita ✨
Ahora ya sabemos cómo escribir un patrón Singleton en Go, pero la forma en que lo hicimos no parece muy fácil de leer ni bonita 🤢.
Pero está bien, Go tiene otra forma de hacerlo.
Voy a reescribir el código, y explicarlo después 😉.
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 ofrece el método sync.Once en el paquete sync, la función que se pasa dentro de once.Do solo se ejecuta una vez, incluso si GetCache se llama miles de veces, la función dentro de once.Do solo se va a ejecutar una vez y es segura para concurrencia. Te recomiendo leer más al respecto.
Esta forma es más bonita, ¿no? 🤭
Python
Python ofrece flexibilidad para implementar el patrón Singleton. Este post explora un enfoque usando el método dunder __new__.
El método __new__ es un método estático que forma parte integral de la propia clase. Su rol principal es crear y devolver una nueva instancia de la clase. Recibe la clase como primer argumento, seguido de cualquier otro argumento destinado a la construcción de la instancia.
En términos más simples, el método __new__ se encarga de la creación de objetos a nivel de clase, no de la manipulación de objetos.
En el siguiente ejemplo de código, vamos a crear un cliente de Redis y guardarlo dentro de un diccionario. Esta estrategia asegura que solo se cree una instancia para cada una de las distintas bases de datos Redis con las que podrías interactuar en tu proyecto. (Recuerda, un proyecto podría potencialmente tener numerosas conexiones a distintas bases de datos).
Código
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()
¡Prueba este código tú mismo!
Nota rápida
Llevo poco tiempo usando Redis, pero me gusta mucho, es bastante diferente a MongoDB o a una base de datos SQL. A lo mejor más adelante escribo algunos posts sobre Redis. 🤔
Las canciones del 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
Nunca fui muy fan de La Vida Bohème, pero sacaron un álbum nuevo, Diáspora Live Vol.1. Este álbum es una compilación de una gira que hicieron el año pasado, y está muy bueno.
Tengo que admitir que La Vida Bohème suena muy bien en vivo.
Te recomiendo escuchar el álbum completo, no te lo puedes perder.
Mis favoritas del álbum:
Sábado
Llegó la policía
Mi primo
Fue el que disparó
Bang
· Manos Arriba, La Vida Bohème
