El Patrón de Diseño Singleton
1. Descripción General
- Propósito: Garantizar que una clase tenga una única instancia y proporcionar un punto de acceso global a ella.
- Categoría: Creacional
- Motivación: Útil cuando se necesita un único punto de acceso, como en el manejo de configuraciones, donde es necesario tener una única instancia disponible en toda la aplicación para evitar conflictos de concurrencia sobre el mismo recurso
2. Diagrama UML del Patrón
classDiagram class Singleton { -Singleton instance -Singleton() +getInstance()$ Singleton }
3. Estructura del Patrón
- Clase Singleton: Es la única clase involucrada en el patrón. Se asegura de que haya solo una instancia de ella a través de un mecanismo de control de instancias.
- Atributo instance: Este atributo es una referencia estática que almacena la única instancia de la clase Singleton. Se inicializa como None.
- Método getInstance: Método de clase que proporciona acceso a la instancia única, asegurando que no se cree más de una instancia de la clase.
4. Implementación en Python
La forma de implementarlo sería:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
@classmethod
def getInstance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
s1 = Singleton.getInstance()
El método __new__ es el núcleo del patrón en Python. Este método es responsable de verificar si ya existe una instancia de la clase. Si no existe, crea una nueva instancia usando super(Singleton, cls).new(cls) y la asigna a instance. Si ya existe, simplemente devuelve la instancia existente. De esta forma, se garantiza que nunca habrá más de una instancia de la clase Singleton.
El diseño que hemos visto es el más genérico y válido para la inmensa mayoría de los lenguajes orientados a objetos. Aprovechando algunas de las características propias de Python podemos simplificarlo ligeramente para que quede una implementación más compacta y sencilla:
class Singleton(object):
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
def some_business_method(self):
# Método de ejemplo que podría realizar alguna operación
print("Método de negocio ejecutado.")
s1 = Singleton()
5. Ventajas y Desventajas
- Ventajas
- Facilita el Acceso a un Recurso Compartido: El patrón Singleton asegura que todos los componentes de una aplicación tengan acceso a una única instancia de un recurso compartido, lo que simplifica la gestión de dicho recurso.
- Elimina el Uso de Variables Globales: Al proporcionar un punto de acceso controlado a través de una clase, el patrón Singleton evita el uso de variables globales, reduciendo el riesgo de colisiones y errores asociados con ellas.
- Ahorro y Optimización de Recursos: Dado que solo se crea una instancia de la clase Singleton, se evita el coste de crear múltiples instancias innecesarias. Esto es especialmente beneficioso en situaciones donde la creación de objetos es costosa en términos de recursos o tiempo.
- Simplicidad de Implementación: En la mayoría de los lenguajes de programación, incluido Python, el patrón Singleton es relativamente fácil de implementar y no requiere estructuras de código complejas.
- Desventajas
- Cambio de Estado Complicado de Gestionar: Dado que la instancia es compartida, cualquier cambio en el estado del objeto afecta a todas las partes del programa que dependen de ese objeto. Aunque este comportamiento puede ser deseado, también puede causar problemas inesperados si no se gestiona cuidadosamente o si los desarrolladores no son conscientes de estos efectos colaterales.
- Problemas en Entornos Multihilo: Si no se implementa correctamente, el patrón Singleton puede tener problemas de concurrencia en entornos multihilo, donde múltiples hilos podrían intentar crear instancias simultáneamente.
- Reducción de Flexibilidad: Una vez implementado como Singleton, puede ser difícil cambiar la clase para permitir múltiples instancias o diferentes configuraciones, lo que reduce la flexibilidad del diseño.
6. Aplicaciones Prácticas
El patrón Singleton se utiliza comúnmente en situaciones donde se necesita un punto de acceso global a un recurso único y centralizado. Algunos ejemplos prácticos incluyen:
- Gestión del Spool de Impresión: Asegura que todas las solicitudes de impresión se gestionen a través de un único punto de control.
- Acceso a la Configuración de una Aplicación: Permite que todos los componentes de la aplicación accedan a los mismos parámetros de configuración sin la necesidad de leer el archivo de configuración repetidamente.
- Acceso a Bases de Datos: Proporciona un punto de acceso único para la conexión a bases de datos, evitando la creación de múltiples conexiones costosas y facilitando la reutilización de la misma conexión.
- Gestión de Registros de Logs: Centraliza el acceso a los registros de logs, asegurando que todos los componentes registren sus eventos en un único lugar de manera coherente.
7. Ejemplos del Mundo Real
El siguiente código muestra la implementación de la clase Config, que utilizo en todos mis proyectos. Esta clase se encarga de leer el archivo de configuración de la aplicación y proporciona a los demás componentes un acceso centralizado para leer y escribir diferentes parámetros de configuración. La implementación del patrón Singleton en esta clase ofrece dos ventajas clave:
- Acceso Centralizado a los Parámetros de Configuración: Gracias al Singleton, todos los componentes de la aplicación acceden a la misma instancia de la clase Config. Esto garantiza que cualquier cambio en los parámetros de configuración sea inmediatamente visible para todos los componentes, manteniendo la coherencia en toda la aplicación.
- Eficiencia en la Lectura del Archivo de Configuración: El archivo de configuración se carga una única vez cuando se crea la instancia única de la clase Config. Esto optimiza el uso de recursos y mejora el rendimiento al evitar lecturas repetidas del archivo cada vez que se necesita acceder a los parámetros de configuración.
from configparser import ConfigParser, NoSectionError, NoOptionError
from src.model.exception.ModelException import ModelException
DEFAULT_FILE = './src/config/config.ini'
class Config():
def __new__(cls, file = DEFAULT_FILE):
if not hasattr(cls, '_singleton'):
cls._singleton = super(Config, cls).__new__(cls)
return cls._singleton
def __init__(self, file = './src/config/config.ini'):
if not hasattr(self, '_config'):
self._config = ConfigParser()
try:
self._config.read(file)
except Exception as e:
raise ModelException(f'Error reading config file: {file}')
def _get(self, section : str, parameter : str) -> str | int | float | None:
try:
return self._config.get(section, parameter)
except NoSectionError as e:
raise ModelException(f'Error setting section {section}')
except NoOptionError as e:
raise ModelException(f'Error setting parameter {parameter} in section {section}')
except Exception as e:
raise ModelException('Error setting a parameter in config file')
def _set(self, section: str, parameter : str, value : float) -> None:
try:
self._config.set(section, parameter, value)
except NoSectionError as e:
raise ModelException(f'Error getting section {section}')
except NoOptionError as e:
raise ModelException(f'Error getting parameter {parameter} from section {section}')
except Exception as e:
raise ModelException('Error accesing a parameter from config file')
def get_str(self, section : str, parameter : str) -> str | None:
try:
return self._get(section, parameter)
except Exception as e:
raise e
def set_str(self, section: str, parameter : str, value : str) -> None:
try:
return self._set(section, parameter, value)
except Exception as e:
raise e
def get_int(self, section : str, parameter : str) -> int | None:
try:
return self._get(section, parameter)
except Exception as e:
raise e
def set_int(self, section: str, parameter : str, value : int) -> None:
try:
return self._set(section, parameter, value)
except Exception as e:
raise e
def get_float(self, section : str, parameter : str) -> float | None:
try:
return self._get(section, parameter)
except Exception as e:
raise e
def set_flotat(self, section: str, parameter : str, value : float) -> None:
try:
return self._set(section, parameter, value)
except Exception as e:
raise e
def write(self, file = DEFAULT_FILE) -> None:
try:
with open(file, 'w') as fp:
self._config.write(fp)
except Exception as e:
raise ModelException(f'Error writing config file: {file}')
Podéis acceder al proyecto completo en GitHub: Newswave. Este proyecto automatiza la recopilación de noticias desde fuentes RSS y envía un resumen diario por correo electrónico. Es un ejemplo práctico de cómo aplicar el patrón Singleton para gestionar la configuración y el acceso a recursos compartidos en una aplicación real.
8. Variaciones y Alternativas
El patrón Singleton tiene algunas variantes y adaptaciones que pueden ser útiles dependiendo del contexto y de los requisitos de la aplicación. Las dos variantes más comunes son Lazy Instantiation y el Monostate Singleton.
8.1. Lazy Instantiation
La instanciación tardía es una técnica utilizada para retrasar la creación de la instancia única de la clase Singleton hasta que sea realmente necesaria. Esta técnica es especialmente útil en los siguientes casos:
- Optimización de Recursos: En aplicaciones donde la creación de la instancia puede ser costosa en términos de recursos (por ejemplo, cuando implica cargar configuraciones pesadas, establecer conexiones de red, etc.), la instanciación tardía evita el gasto inicial hasta que sea necesario.
- Mejor Rendimiento Inicial: Reduce el tiempo de inicio de la aplicación, ya que no hay necesidad de crear la instancia hasta que sea requerida.
8.2. Monostate Singleton
El Monostate Singleton, también conocido como Borg Pattern, es una variación del patrón Singleton en la que todas las instancias de la clase comparten el mismo estado en lugar de tener una única instancia. En este enfoque, se permite la creación de múltiples instancias de la clase, pero todas comparten los mismos atributos de clase, logrando un comportamiento similar al Singleton tradicional.
- Ventaja Principal: A diferencia del Singleton tradicional, no se requiere un control estricto sobre la creación de instancias, lo que puede simplificar la implementación y mejorar la flexibilidad.
- Uso del Estado Compartido: Todas las instancias comparten el mismo diccionario de atributos, lo que permite que los cambios realizados en una instancia se reflejen en todas las demás.
9. Referencias y Recursos Adicionales
- Learning Python Design Patterns (Second Edition)
- Autor: Chetan Giridhar
- Editorial: Packt Publishing
- Año: 2017
- Enlace: Learning Python Design Patterns – Second Edition
- Descripción: Este libro ofrece una guía completa sobre los patrones de diseño en Python, proporcionando ejemplos prácticos y técnicas para implementar estos patrones de manera efectiva. Es una excelente referencia para desarrolladores que buscan comprender y aplicar patrones de diseño en proyectos Python.
- Design Patterns: Elements of Reusable Object-Oriented Software
- Autores: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
- Editorial: Addison-Wesley
- Año: 1994
- Enlace: Design Patterns: Elements of Reusable Object-Oriented Software
- Descripción: Conocido como el libro de los “Gang of Four” (GoF), esta obra es un clásico en el campo de los patrones de diseño de software. Proporciona una base sólida en los patrones de diseño orientados a objetos y es fundamental para entender los principios detrás de los patrones y su aplicación en el diseño de software.