Patrones de diseño – Singleton y Monostate

Hoy vamos a ver 2 patrones de diseño, estos son el patron Singleton y Monostate, ambos son patrones de creación, el problema que intentan resolver es el mismo: que solo exista una única instancia de esa clase.

Patrón Singleton

Hay ocasiones en las que necesitamos que una clase tenga una única instancia. Podemos tener múltiples ventanas emergentes, pero lo conveniente sería tener un único gestor de ventanas que se encargue de gestionarlas.

Esto lo podríamos conseguir con una variable global donde guardar la instancia, pero aunque sería accesible por todo  el sistema, no nos previene de que alguien sobreescriba esta instancia.

Otra solución es hacer que la misma clase gestione su instancia de forma que se asegure de que solo existe una, esto es exactamente lo que hace el patrón Singleton.

singleton unity

El diagrama del patrón de diseño Singleton es muy simple, básicamente necesitamos una variable estática donde guardar la única instancia, y un método o propiedad con la que acceder a esta instancia.

Vamos a ver como implementarlo en Unity, supongamos una clase para guardar/cargar los datos del usuario como la siguiente:

public class PlayerPrefsAdapter : IDataSaver
    {
        public void SetString(string key, string value)
        {
            PlayerPrefs.SetString(key, value);
            PlayerPrefs.Save();
        }

        public string GetString(string key, string defaultValue = default)
        {
            return PlayerPrefs.GetString(key, defaultValue);
        }

        public void SetInt(string key, int value)
        {
            PlayerPrefs.SetInt(key, value);
            PlayerPrefs.Save();
        }

        public int GetInt(string key, int defaultValue = default)
        {
            return PlayerPrefs.GetInt(key, defaultValue);
        }
    }

Lo que estamos viendo aquí es un Adapter de la clase PlayerPrefs de Unity, que como ya vimos en el post sobre el patrón Adapter una de sus utilidades es convertir una clase estática en una clase que podamos instanciar.

Supongamos ahora que por el motivo que sea solo quiero que exista una instancia de esta clase, o bien porque quiero optimizarlo, o bien porque si existen dos instancias y las dos intentan guardar al mismo tiempo podrían explotar, o lo que sea.

Para esto podría aplicar el patrón Singleton, lo único que necesitamos es hacer el constructor privado para que nadie pueda construir más instancias, y proporcionar una forma de acceder a la instancia:

public class PlayerPrefsAdapter : IDataSaver
    {
        public static IDataSaver Instance => _instance ?? (_instance = new PlayerPrefsAdapter());
        private static IDataSaver _instance;
        
        private PlayerPrefsAdapter()
        {
            
        }
        
...
    }

Así de simple es aplicar este patrón. ¿Pero que ocurre si queremos utilizarlo con MonoBehaviors?

Patron Singleton en Unity

Si lo que tenemos es un MonoBehaviour y queremos asegurarnos de que solo existe una única instancia podemos hacerlo de la siguiente forma:

public class PlayerPrefsMonoBehaviourAdapter : MonoBehaviour, IDataSaver
    {
        public static IDataSaver Instance
        {
            get
            {
                if (_instance == null)
                {
                    var auxGameObject = new GameObject();
                    _instance = auxGameObject.AddComponent<PlayerPrefsMonoBehaviourAdapter>();
                }

                return _instance;
            }
        }
        private static IDataSaver _instance;
        
        ...
}

Como en este caso no tenemos un constructor, nos tenemos que asegurar que se construye el GameObject a través de la propiedad Instance que publiquemos.

Existen otras formas de implementar un Singleton con Unity, por ejemplo podemos utilizar una clase templatizada para no tener que repetir la propiedad Instance. En esta wiki de Unity podrás encontrar esta otra forma que te comento y un montón de utilidades para tu proyecto en Unity.

Consecuencias del patrón Singleton

La principal consecuencia de utilizar este patrón es que necesitamos acceder a un método o propiedad estáticos de la clase, lo cual puede hacer que el consumidor se convierta en una clase intesteable.

Esto es así porque nos acopla con la clase concreta y no con la interfaz, lo cual está incumpliendo el principio de inversión de dependencias y a la larga traerá consecuencias. Al no estar utilizando la interfaz no podremos poner un sustituto a esta clase.

Otra consecuencia, y la que andamos buscando, es que nos asegura de que solo exista una instancia, al mismo tiempo nos está proporcionando un acceso global, pero como comentaba arriba esto puede ser algo positivo o negativo.

Patrón Monostate

El patrón Monostate también es bastante simple y básicamente se basa en tener todas las propiedades y variables estáticas y los métodos <<normales>>, sin static.

Como ya sabéis, una variable estática se convierte en una variable de la clase y no de la instancia, con lo que solo hay una aunque tengamos 10 instancias de esa clase.

Para ilustrar esto voy a modificar la clase de PlayerPrefsAdapter que venimos utilizando para que al mismo tiempo que guardamos datos queremos iremos almacenando esta información en un log, pero solo queremos que exista un log y todo se guarde aquí.

public class PlayerPrefsAdapter : IDataSaver
    {
        private List<string> _log = new List<string>();
        
        public void SetString(string key, string value)
        {
            PlayerPrefs.SetString(key, value);
            PlayerPrefs.Save();
            _log.Add($"Guardado valor: ${value} en la key: ${key}");
        }

...

        public override string ToString()
        {
            return $"{nameof(_log)}: {string.Join(" ", _log)}";
        }
    }

Si lo dejamos así, por cada instancia que creemos tendremos un log totalmente independiente pero eso no es lo que queremos. Simplemente haciendo la variable log estática ya estaremos consiguiendo que el log sea único, que tenga un único estado.

public class PlayerPrefsAdapter : IDataSaver
    {
        private static List<string> _log = new List<string>();
...
    }

Ahora por muchas instancias que tenga solo tendré un estado, así de sencillo es el patrón Monostate.

Si añadiésemos más variables solo las tendríamos que hacer static para mantener este estado único, y los consumidores no tienen porque conocer cómo está estructurada la clase, ni preguntar por una instancia, solo necesitan hacer un new y consumirlo como cualquier otra clase.

Singleton vs Monostate

No hay uno mejor, personalmente me gusta el Monostate porque se consume como una clase normal, no tienes que ir preguntando por instancias.

Por otro lado el Monostate trabaja con variables estáticas y estas dan un poco de problemas a la hora de hacer testing en Unity ya que las variables estáticas no se borran entre test y test, aunque esto se soluciona limpiándolas en el constructor.

Lo que tienes que tener clarísimo es que empezar hacer el get instance en el Singleton o creando los objetos en el Monostate en todos los consumidores no es una buena idea porque el día de mañana querrás cambiar la clase y te tocará ir uno a uno a todos los sitios donde la estáis utilizando y cambiar la clase.

Pero si en lugar de esto lo inyectas en el constructor o en un método de configuración te evitaras este problema y podrás hacer tests.

Si quieres descargar el código de ejemplo que hemos utilizado y probarlo tu mismo aquí te dejo el enlace.

Libros sobre patrones de diseño

Otras entradas

Resumen
➤ Patrones de diseño - Singleton y Monostate
Nombre del artículo
➤ Patrones de diseño - Singleton y Monostate
Descripción
¿Estas buscando 🤔 como implementar un patrón SINGLETON o MONOSTATE utilizando UNITY? En este post explicamos estos dos patrones 😎.
Autor
Publisher Name
The Power Ups - Learning
Publisher Logo