Patrones de diseño: Singleton

Cuando hablamos de un patrón de diseño nos referimos a una solución a un problema concreto en el desarrollo de software. Pero no cualquier solución, sólo aquellas que se ha desmostrado que son eficientes en diferentes escenariosreutilizables en gran cantidad de contextos de aplicaciones. Por lo tanto, aunque los ejemplos que podamos dar estén en un lenguaje de programación concreto, la idea será extrapolable a diferentes lenguajes de programación orientada a objetos.

Singleton

El patrón singleton consiste en crear una instancia de un objeto y solo una, para toda nuestra aplicación. Sería como una especie de variable global que almacena nuestro objeto.

En un primer momento esta definición puede sonar muy extraña. Por lo general, siempre se recomienda no usar variables globales en una aplicación, y mucho menos en programación orientada a objetos. Pero cuando hablamos de singleton, estamos jugando a crear una especie de variable global de forma encubierta. ¿Cómo puede ser esto un patrón?

Para responder a esta pregunta vamos a proponeros dos escenarios diferentes:

  • Piensa en una aplicación Web, que almacena en un objeto una serie de valores tipo parámetros de configuración. Estos parámetros son comunes para toda la aplicación. Se guardan en una base de datos y si son modificados por un administrador, quedan modificados para todos los usuarios que acceden a la página.
  • Ahora vamos a imaginar que tenemos un recurso compartido como puede ser un fichero en el que escribimos un log de la aplicación. Este log puede ser accedido desde cualquier parte de la aplicación. Pero sabemos que un fichero no se puede abrir si otro proceso lo abrió anteriormente y aún no lo ha cerrado.

Para ambos problemas podemos encontrar una solución usando el patrón singleton. Crearemos una especie de variable global, pero con unas características concretas:

  • solo se puede instanciar una vez (single-instance)
  • no se debe instanciar si nunca fue utilizada 
  • es thread-safe, que quiere decir que sus métodos son accesibles desde diferentes hilos de ejecución, sin crear bloqueos ni excepciones debido a la concurrencia
  • no tiene un constructor público, luego el objeto que la usa no puede instanciarla directamente
  • posee un mecanismo para acceder a la instancia que se ha creado (mediante una propiedad estática, por ejemplo)

Teniendo en cuenta estas características vamos a desarrollar una clase singleton:

public sealed class Singleton
{
    private static Singleton instance;

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            if (instance == null) instance = new Singleton();
            return instance;
        }
    }
}

En esta pequeña porción de código hemos conseguido realizar una única instancia en el momento en el que se llama por primera vez. Además hemos creado un constructor con acceso privado para que nadie pueda instanciar la clase. Y para terminar hemos creado una propiedad de solo lectura con la que se puede acceder a la instancia creada. Pero ésta no será Thread-safe. Para conseguirlo podríamos modificar la clase de la siguiente forma:

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();

    private Singleton() { }

    public static Singleton Instance { get { return instance; } }
}

Al crear el atributo que almacena la instancia como readonly, y al ser estática, se instanciará al arrancar la aplicación. Así conseguiremos que sea una clase thread-safe. Es decir, que no habrá problemas si varios procesos acceden a esta clase al mismo tiempo. No obstante, si quisieramos respetar que solo se instanciara el objeto bajo demanda, deberíamos usar bloqueos:

public sealed class Singleton
{
    private static readonly object locker = new object();
    private static volatile Singleton instance;

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (locker)
                {
                    if (instance == null) instance = new Singleton();
                }
            }

            return instance;
        }
    }
}

Gracias al bloqueo ya podremos ejecutar nuestra clase singleton en un contexto multihilo, instanciándola sólo cuando se ha solicitado la primera vez. A este efecto de carga en diferido se le denomina en inglés “Lazy Loading”. Y desde la versión 4.0 de la framework .net se nos provee un objeto que nos ayuda a realizarla: Lazy. Por lo que podríamos simplificar nuestro ejemplo usándolo:

public sealed class Singleton
{
    private static readonly Lazy<Singleton> instance = new Lazy<Singleton>(() => new Singleton());

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            return instance.Value;
        }
    }
}

El objeto Lazy ya es de por si thread-safe y en su declaración simplemente debemos indicarle de qué forma se debe instanciar el objeto que contiene. Por esta razón es posiblemente la mejor implementación del patrón singleton.

Si por ejemplo estuvieramos desarrollando la herramientas de log de nuestra aplicación, bastaría con que añadieramos las funciones necesarias para escribir en el log a nuestra clase singleton:

public sealed class Logger
{
    private static readonly Lazy<Logger> instance = new Lazy<Logger>(() => new Logger());

    private Logger() { }

    public static Logger Current
    {
        get
        {
            return instance.Value;
        }
    }

    public void WriteInformation(string message)
    {
       // ...
    }

    public void WriteWarning(string message)
    {
       // ...
    }

    public void WriteError(string message)
    {
       // ...
    }
}

Viendo este código en nuestra aplicación, está claro que para poder escribir en el log desde cualquier punto de la misma sólo tendremos que hacer esta llamada:

Logger.Current.WriteInformation("Una información");
Logger.Current.WriteWarning("Un aviso");
Logger.Current.WriteError("Un error");

Al pararnos a pensar las consecuencias de escribir este código, caeremos en la cuenta de que singleton nos está creando una dependencia en todo el programa donde queramos tener información del proceso en forma de logs (eso es a lo largo de toda la aplicación). Algo que comunmente conocemos como acoplamiento entre clases.

El acoplamiento puede dar varios problemas a lo largo del ciclo de vida de un software. Como por ejemplo a la hora de realizar pruebas unitarias. Pero no es objeto de este artículo centrarse en este problema. Aunque si lo es proponer soluciones de implementación del patrón singleton que se adapten a un desarrollo sólido.

Si quisieramos evitar este acoplamiento, es recomendable usar un IoC Container (Inversion Of Control Container) para respetar la “D” de los pincipios SOLID: Dependency Inversion Principle. Esta, por así llamarla, norma nos dice que debemos depender de las abstraciones (las interfaces, los contratos) no de las concreciones (clases que implementan esas interfaces). 

En las frameworks de inversión de control más conocidas se han implementado mecanismos que nos permiten crear objetos singleton desde el propio contenedor. Esto quiere decir que simplemente tendríamos que crear una interfaz y una implementación de la misma, sin preocuparnos de como se intancia. Visto en forma de código sería esto:

public interface ILogger
{
    void WriteInformation(string message);
    void WriteWarning(string message);
    void WriteError(string message);
}

public class Logger : ILogger
{
    public Logger()
    { 
        // ...
    }
    public void WriteInformation(string message)
    {
        // ...
    }
    public void WriteWarning(string message)
    {
        // ...
    }
    public void WriteError(string message)
    {
        // ...
    }
}

De esta forma, delegaríamos la gestión del ciclo de vida de las instancias al IoC Container que hayamos decidido. A continuación mostraremos cómo podemos configurar una instancia singleton usando las frameworks de inyección de dependencias (DI) más conocidas:

  • Usando Structure maps:
// configurar
ObjectFactory.Initialize(x =>
{
    x.For<ILogger>().Singleton().Use<Logger>();
}
// recoger valor
var x = ObjectFactory.GetInstance<ILogger>();
  • Con Ninject:
// configurar
IKernel ninject = new StandardKernel(new InlineModule(
              x => x.Bind<ILogger>().To<Logger>(),
              x => x.Bind<Logger>().ToSelf().InSingletonScope()));
// recoger valor
var x = ninject.Get<ILogger>();
  • Con Unity:
// configurar
IUnityContainer container = new UnityContainer();
container.RegisterType<ILogger, Logger>(new ContainerControlledLifetimeManager());
// recoger valor
var x = container.Resolve<ILogger>();
  • O con autofact:
var builder = new ContainerBuilder();
builder
   .Register(c => new Logger())
   .As<ilogger>()
   .SingleInstance();
var container = builder.Build(); 
var x = container.Resolve<ilogger>();

Pero esto no quiere decir que no nos sirva la implementación de singleton que hicimos anteriormente, ya que es posible que no nos fiemos o que nuestro contenedor no tenga ningún artefacto que nos facilite la implementación singleton. Para estos casos, podríamos hacer que un contenedor como Unity nos devolviera la instancia singleton que gestiona nuestra clase usando la propiedad estática. Simplemente tendríamos que seguir usando una interface, implementarla en nuestra clase singleton y registrar una instancia en lugar de una clase en el contenedor:

public interface ILogger
{
    void WriteInformation(string message);
    void WriteWarning(string message);
    void WriteError(string message);
}

public sealed class Logger : ILogger
{
    private static readonly Lazy<Logger> instance = new Lazy<Logger>(() => new Logger());

    private Logger() { }

    public static Logger Current
    {
        get
        {
            return instance.Value;
        }
    }

    public Logger()
    { 
        // ...
    }
    public void WriteInformation(string message)
    {
        // ...
    }
    public void WriteWarning(string message)
    {
        // ...
    }
    public void WriteError(string message)
    {
        // ...
    }
}

De esta forma, por ejemplo, si usamos el contenedor de Unity, tendríamos que registrar su valor así:

var container = new UnityContainer();
container.RegisterInstance<ILogger>(Logger.Current);

Con este código sería nuestro singleton Logger quien gestione el ciclo de vida y conseguiríamos desacoplarnos de la implementación gracias al IoC.

Podríamos hacer lo mismo con Structure maps:

ObjectFactory.Initialize(x =>
{
    x.For<ILogger>().Use(Logger.Current);
}

var x = ObjectFactory.GetInstance<ILogger>();

Y para finalizar, con Ninject:

IKernel ninject = new StandardKernel(new InlineModule(
              x => x.Bind<ILogger>().ToConstant(Logger.Current)));

var x = ninject.Get<ILogger>();

 

A lo largo de este artículo hemos visto diferentes formas de implementar el patrón singleton. Un patrón de desarrollo sigue siendo vigente y válido. Lo único que tenemos que tener en cuenta, es evitar aplicarlo donde no corresponde o de una forma incorrecta. Algo que conocemos como el antipatrón singletonitis.

deja tu comentario