Burlando el patrón Singleton con reflectividad

Seguramente todos sabéis lo que es el patrón Singleton, pero por si alguien faltó a clase ese día, es un método de asegurarse de que una clase es instanciada una única vez independientemente del número de veces que se la llame.

El sistema de lograrlo es sencillo:

Se crea una variable estática del mismo tipo que la clase en la que se guardará una referencia a si misma (hasta la primera instancia valdrá null)

  • Se crea un método estático público que creará una instancia y asignará su valor a la variable estática.
  • Se hará privado el constructor, con lo que la única clase capaz de llamarlo (y por tanto crear el objeto) será ella misma.
  • Esto hace que nadie pueda usar new Clase(); sino que debe accederse llamando al método estático (típicamente llamado Clase.GetInstance();)

Bueno, supongamos que tenemos la siguiente implementación:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Collections.Generic;
using System.Text;
 
namespace ReflectionTest
{
    public class Singleton
    {
        private static Singleton INSTANCE = null;
        private static int ID = 0;
        private int _instanceId;
 
        private Singleton()
        {
            _instanceId = ++ID;
            Console.WriteLine("Singleton created with ID={0}", Singleton.ID);
        }
 
        public static Singleton GetInstance()
        {
            if (INSTANCE == null)
                INSTANCE = new Singleton();
            return INSTANCE;
        }
 
        public void WhoAmI()
        {
            Console.WriteLine("I am instance number: {0}", _instanceId);
        }
    }
}

Si intentamos hacer un new Singleton() el compilador nos dirá que nanai, que el constructor es privado y no podemos. Nada nos impide hacer dos llamadas al método GetInstance(), pero se comportará devolviéndonos siempre la primera instancia:

1
2
3
4
Singleton singleton = Singleton.GetInstance();
Singleton singleton1 = Singleton.GetInstance();
singleton.WhoAmI();
singleton1.WhoAmI();

El resultado sería:

Singleton created with ID=1
I am instance number: 1
I am instance number: 1

Es decir, aunque hacemos dos llamadas sólo se crea un objeto (vamos, que el Singleton funciona) y efectivamente la variable de control que he introducido para hacer las cosas más claras nos indica que ambas llamadas retornan la instancia número uno.

Vale, hasta aquí todo correcto. El problema viene cuando se juega con reflectividad. En algunos casos la gente carga las clases haciendo uso de esta técnica… pero no se da cuenta de los posibles efectos laterales que puede tener. ¿Qué pasaría si intentáramos cargar la clase Singleton haciendo uso de reflectividad? Vamos a probarlo con el siguiente código, que llamará una vez a la clase Singleton, luego la cargará por reflectividad y veremos a ver si se rompe alguna tripa:

1
2
3
4
5
6
7
8
9
10
//Llamamos a GetInstance de forma "normal"
Singleton singleton = Singleton.GetInstance();
//Por reflectividad obtenemos el constructor de la clase Singleton
ConstructorInfo constructor = typeof(Singleton).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, System.Type.EmptyTypes, null);
//Usando la información obtenida por reflectividad, invocamos al constructor
object compiled = constructor.Invoke(null);
//Como sabemos que es una clase Singleton le hacemos un ahormado para facilitarnos las cosas
Singleton reflected = (Singleton)compiled;
singleton.WhoAmI();
reflected.WhoAmI();

¿Qué salida esperaríais obtener? Siguiendo la teoría Singleton, esto debería ser exactamente igual que lo anterior, sin embargo la salida nos descoloca un poco:

Singleton created with ID=1
Singleton created with ID=2
I am instance number: 1
I am instance number: 2

¡Meeeeeec! Tripa rota. ¿Por qué ocurre esto? Bueno, la implementación que he usado del patrón de diseño Singleton se basa en la creencia (fundada) de que las variables estáticas de una clase sólo pueden tener un valor que será compartido por todas las instancias de la clase y que un método privado no puede ser accesible desde otras clases. Ambas cosas son correctas… casi siempre. La excepción está en la reflectividad.

A día de hoy no conozco ninguna implementación del Singleton que permita evitar este comportamiento. Claro que hasta hace 30 minutos que me dio por pensar esto no sabía que el Singleton podía ser burlado mediante reflectividad, así que a saber…

About the Author

Me llamo Pablo Carballude González, soy graduado en computación con master en HCI y Seguridad Informática. Actualmente trabajo para Amazon en Seattle como Software Developer Engineer. Soy de esas personas que no saben si los textos autobiográficos deben ser en primera o tercera persona. Lo intenté en segunda, pero no le entendí nada :P