Design Pattern Singleton - FernandoCalmet/csharp-essential GitHub Wiki

Proyecto inicial

Vamos a comenzar con una aplicación de consola simple en la que vamos a leer todos los datos de un archivo (que consiste en ciudades con su población) y luego usar esos datos. Entonces, para comenzar, creemos una única interfaz:

public interface ISingletonContainer
{
    int GetPopulation(string name);
}

Después de eso, tenemos que crear una clase para implementar la interfaz ISingletonContainer. Lo vamos a llamar SingletonDataContainer:

public class SingletonDataContainer: ISingletonContainer
{
    private Dictionary<string, int> _capitals = new Dictionary<string, int>();

    public SingletonDataContainer()
    {
        Console.WriteLine("Initializing singleton object");

        var elements = File.ReadAllLines("capitals.txt");
        for (int i = 0; i < elements.Length; i+=2)
        {
            _capitals.Add(elements[i], int.Parse(elements[i + 1]));
        }
    }

    public int GetPopulation(string name)
    {
        return _capitals[name];
    }
}

Entonces, tenemos un diccionario en el que almacenamos los nombres de las mayúsculas y su población de nuestro archivo. Como podemos ver, estamos leyendo de un archivo en nuestro constructor. Y eso está muy bien. Ahora estamos listos para usar esta clase en cualquier consumidor simplemente creando una instancia. Pero, es esto realmente lo que tenemos que hacer, crear una instancia de la clase que lee de un archivo que nunca cambia (en este proyecto en particular. La población de las ciudades cambia a diario). Por supuesto que no, por lo que obviamente usar un patrón Singleton sería muy útil aquí.

Entonces, implementémoslo.

Implementación Singleton

Para implementar el patrón Singleton, cambiemos la clase SingletonDataContainer:

public class SingletonDataContainer: ISingletonContainer
{
    private Dictionary<string, int> _capitals = new Dictionary<string, int>();

    private SingletonDataContainer()
    {
        Console.WriteLine("Initializing singleton object");

        var elements = File.ReadAllLines("capitals.txt");
        for (int i = 0; i < elements.Length; i+=2)
        {
            _capitals.Add(elements[i], int.Parse(elements[i + 1]));
        }
    }

    public int GetPopulation(string name)
    {
        return _capitals[name];
    }

    private static SingletonDataContainer instance = new SingletonDataContainer();

    public static SingletonDataContainer Instance => instance;
}

Entonces, lo que hemos hecho aquí es ocultar nuestro constructor de las clases de consumidores hacióndolo privado. Luego, hemos creado una única instancia de nuestra clase y la hemos expuesto a través de la propiedad Instance.

En este punto, podemos llamar a la propiedad Instance tantas veces como queramos, pero nuestro objeto será instanciado solo una vez y compartido para cada otra llamada.

Comprobemos esa teoría:

class Program
{
    static void Main(string[] args)
    {
        var db = SingletonDataContainer.Instance;
        var db2 = SingletonDataContainer.Instance;
        var db3 = SingletonDataContainer.Instance;
        var db4 = SingletonDataContainer.Instance;
    }
}

Si iniciamos nuestra aplicación, veremos esto:

Initializing singleton object
Press any key to continue . . .

Podemos ver que estamos llamando a nuestra instancia cuatro veces, pero se inicializa solo una vez, que es exactamente lo que queremos.

Pero nuestra implementación no es la ideal. Construyamos nuestro objeto de forma perezosa.

Implementación de un singleton seguro para subprocesos

Modifiquemos nuestra clase para implementar un Singleton seguro para subprocesos usando el tipo Lazy:

public class SingletonDataContainer : ISingletonContainer
{
    private Dictionary<string, int> _capitals = new Dictionary<string, int>();

    private SingletonDataContainer()
    {
        Console.WriteLine("Initializing singleton object");

        var elements = File.ReadAllLines("capitals.txt");
        for (int i = 0; i < elements.Length; i+=2)
        {
            _capitals.Add(elements[i], int.Parse(elements[i + 1]));
        }
    }

    public int GetPopulation(string name)
    {
        return _capitals[name];
    }

    private static Lazy<SingletonDataContainer> instance = new Lazy<SingletonDataContainer>(() => new SingletonDataContainer());

    public static SingletonDataContainer Instance => instance.Value;
}

En este momento, nuestra clase es completamente segura para subprocesos. Se carga de forma diferida, lo que significa que nuestra instancia se creará solo cuando sea realmente necesaria. Incluso podemos verificar si nuestro objeto se crea con la propiedad IsValueCreated si es necesario.

Excelente, hemos terminado nuestra implementación de Singleton.

Ahora podemos consumirlo por completo en nuestra clase de consumidor:

class Program
{
    static void Main(string[] args)
    {
        var db = SingletonDataContainer.Instance;
        Console.WriteLine(db.GetPopulation("Washington, D.C."));
        var db2 = SingletonDataContainer.Instance;
        Console.WriteLine(db2.GetPopulation("London"));
    }
}