Cosas mías

March 3, 2008

Singleton con y sin Singleton

Filed under: Programación

Ahora que están de moda los antipatrones, voy a explicar un patrón que es muy famoso pero que no me gusta absolutamente nada, no por el diseño del mismo, sino por los efectos laterales no deseados que genera. Por ello también voy a explicar otra forma de construirlo que me gusta más, aunque adolece de otras limitaciones.

Singleton es un patrón que nos obliga a tener una sola instancia de una clase, es decir, globalmente sólo podremos disponer de un objeto de ese tipo. El truco está en conseguir que, hagamos lo que hagamos, no podamos crear más de una instancia.

En lenguajes como C++ este patrón tiene menos utilidad que en C++/CLI, C# o VB.NET, ya que la forma clásica de hacer algo similar es tener una variable global accesible por todos los módulos, cosa de la que suele encargarse el enlazador de hacer.

Pero los lenguajes .NET adolecen de falta de variables globales; es decir, no puedes tener un objeto global y visible en toda la aplicación. Personalmente no entiendo este purismo tan quisquilloso, ya que los lenguajes .NET no son precisamente orientados a objetos puros, y menos aún con la especificación 3.0 de C#. Y si encima este hecho complica bastante ciertos desarrollos, el absurdo sube de nivel.

Pero es lo que hay.

La solución para tener un objeto global es disponer de una clase estática. Si bien a priori resulta una buena idea, en la práctica no lo es tanto. Independientemente del hecho de que los constructores estáticos en .NET no funcionan muy bien o, en otras palabras, tienen bastante bugs (al menos en la versión 2.0, aunque dudo mucho que lo hayan solucionado en los SP1), tenemos ciertas limitaciones, como la imposibilidad de que se llame a otro constructor estático de forma encadenada…

¿Y por qué comento esto? Pues porque el patrón Singleton utiliza una variable y un constructor estático. Veámoslo en C#:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4:  
   5: class Singleton
   6: {
   7:     public static Singleton Instancia=new Singleton();
   8:  
   9:     private Singleton() {}
  10:  
  11:     public int Numero;
  12:     public void DiNumero()
  13:     {
  14:         Console.WriteLine(Numero++.ToString());
  15:     }
  16: }
  17:  
  18: class Program
  19: {
  20:     static void Main(string[] args)
  21:     {
  22:         Singleton.Instancia.Numero = 33;
  23:         for (int i = 0; i < 10;i++)
  24:             Singleton.Instancia.DiNumero();
  25:     }
  26: }

Observamos que el truco está en definir un constructor privado. Al hacerlo, ya no podremos equivocarnos y crear varias instancias de esta clase, y la única forma de acceder al único objeto que pueda haber en toda la aplicación es mediante

   1: Singleton.Instancia

Si intentamos crearnos un objeto del tipo Singleton, el compilador protestará y nos dirá que el constructor no está accesible.

Si alguien quiere una explicación más en detalle, se puede pasar por la MSDN o por aquí.

¿Nadie ve el problema?

Pues hay dos. El primero ya lo hemos dicho: tenemos un objeto global al sistema, que es estático y por tanto sigue unas reglas algo diferentes, como la imposibilidad de hacer una llamada a otra variable estática que implemente su propio constructor estático dentro del constructor de nuestro Singleton… O en otras palabras: no podemos tener Singletones anidados, no al menos bajo .NET.

Pero el mayor problema es otro. Cuando se habla de Singleton, se pone un ejemplo con dos líneas de código y listo. Pero una clase de la Vida Real™ no es sencilla. Tendremos varios métodos miembro, así como variables. ¿Cómo las inicializamos? ¿En el constructor? ¡Pero si no tiene constructor al que podamos pasarle valores! ¿Las ponemos públicas? ¿Propiedades?

Una solución es la mostrada aquí:

   1: Singleton.Instancia.Numero = 33;
   2: for (int i = 0; i < 10;i++)
   3:     Singleton.Instancia.DiNumero();

Pero a mí al menos eso me parece una guarrería mayor, y más cuando haya varios datos miembro, como suele ser habitual en clases no triviales… La única cosa que podemos hacer es especificar valores por defecto en el constructor estático:

   1: private Singleton() { Numero = 50; }

Pero eso sólo alivia el problema, no lo soluciona.

Qué bonito sería que pudiéramos indicar algo como

   1: [Pattern(Singleton)]
   2: class Singleton
   3: {…

Y disponer de la clase como si tal cosa… Pero esa es otra guerra.

Otra aproximación

Veamos ahora una variante, que es la que yo implemento en mis propios programas. Primero el código:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4:  
   5: class MiSingleton
   6: {
   7:     private static int m_cuenta=0;
   8:  
   9:     private int m_numero;
  10:     public MiSingleton(int num)
  11:     {
  12:         m_cuenta++;
  13:         if (m_cuenta > 1)
  14:             throw new Exception("RFOG Singleton Exception");
  15:  
  16:         m_numero = num; 
  17:     }
  18:     public void DiNumero()
  19:     {
  20:         Console.WriteLine(m_numero++.ToString());
  21:     }
  22: }
  23: class Program
  24: {
  25:     static void Main(string[] args)
  26:     {
  27:         MiSingleton m = new MiSingleton(33);
  28:         for (int i = 0; i < 10;i++)
  29:             m.DiNumero();
  30:  
  31:         MiSingleton n=new MiSingleton(88);
  32:     }
  33: }

Aquí encontramos otros problemas y otras limitaciones, pero personalmente me gusta más así. Definimos una variable estática, un entero que nos servirá para contar cuántas instancias de esta clase tenemos.

Definimos un constructor público al que podremos pasarle cualquier número de parámetros que queramos, de hecho se trata de una clase normal y corriente que tiene un bloque de código concreto en el constructor:

   1: m_cuenta++;
   2: if (m_cuenta > 1)
   3:     throw new Exception("RFOG Singleton Exception");

La segunda vez que intentemos instanciar esta clase obtendremos una excepción. Aquí la limitación está en que nos daremos cuenta de nuestro error en tiempo de ejecución, no de compilación, lo que ciertamente es una desventaja seria pero que pierde fuerza cuando nos enfrentamos a las limitaciones y bugs de los constructores estáticos.

La segunda instanciación dentro del bloque de main() lanzará la excepción. La otra limitación a esto viene de la imposibilidad de tener variables globales y de que tengamos que pasar m siempre que queramos usarlo.

Lo ideal sería tener ambas opciones juntas, pero ciertamente dentro de .NET es imposible.

Get free blog up and running in minutes with Blogsome
Theme designed by Gary Rogers