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:

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:

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:

//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…

Por Carballude

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

7 comentarios

  1. Yo no haría ID++; sólo jugaría con los valores 0 ó 1. Aunque es verdad que eso no impediría crear una segunda instancia, con lo que los datos no static se podrían repetir y el singleton no valdría para mucho. (Y además no se podría hacer el experimento que has hecho y saber qué pasó :-) )

    La única solución que veo es en tiempo de ejecución, pero supongo que no es lo que quieres. Si se invoca al constructor después de la primera vez… excepción que te crió :-)

    Yo no he usado reflectividad apenas, pero ya que estamos poniéndonos en plan bestia, y pensando en usar las mismas armas, ¿es posible, usando reflectividad en el propio constructor del singleton, descuajaringar el constructor después de la primera creación, de modo que ya nunca se pueda invocar? Ya me supongo que no…

  2. Puedes mandarle este post a Redondo, que seguro que te ilustra. O a Ortín, y pillar papel y lápiz para la clase magistral xD

    Lo primero, ¿para qué querrías saltarte un singleton? Si está así, será por algo, no? :-)
    Sobre la implementación, obviamente, no hay ninguna «resistente», salvo lo que comenta Guti: lanzar una excepción de seguridad en el constructor si la instancia no es null.

    Respecto a ponernos bestias, desde el propio constructor no lo sé, pero sí que podrías «borrar» el método constructor de la clase mediante reflexión en el getInstance (al menos creo que puedes hacerlo en la última versión de C#), de manera que no se puede llamar al método porque… no existe. Pero al margen de toda la diversión por enredar con el lenguaje, no le veo utilidad al tema :-)

  3. El problema que veo yo es que aunque elimines el constructor igual se invoca uno por defecto. Esa es la duda que tengo, si se puede realmente conseguir que typeof(Singleton).GetConstructor() diga que nones.

  4. El problema es que tu codigo reflectivo viola la encapsulación. Si un constructor es privado no deberías intentar acceder a el e invocarlo para crear una nueva instancia, más aún cuando tienes un método GetInstance que sirve precisamente para eso y al que deberías intentar llamar para crear nuevas instancias (puesto que es el contrato original que tiene tu clase).

    Por otra parte, lo que dice Guti es correcto, y ya
    que el hecho de que en el constructor el que no exista una instancia previa es una precondición a cumplir por ser un Singleton, deberías lanzar una excepción en el caso de que dicha precondición no se cumpla.

    El hecho de que la reflectividad pueda permitirte explorar partes privadas de una clase no significa que debas abusar de ello :)

    Saludos,

    Yo

  5. Sí sí, que el sistema es una salvajada lo sé. No está pensado para usarse, es más una curiosidad que otra cosa.

    En cuanto pueda le echo un ojo a ReflectionPermission y os cuento si logro evitar ese comportamiento ;)

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *