Singleton: El Patrón del mal

Variables globales permitidas y supuesto ahorro de memoria

Maximiliano Contieri
Diseño de Software
9 min readJul 11, 2020

--

Durante 20 años he dado clase en la universidad de Buenos Aires. En el curso de ingeniería de software enseñamos patrones de diseño y siempre se repite el mismo “esquema”, la misma secuencia que tuve oportunidad de ver en varios de mis trabajos y en el software libre que utilizo: la aparición ‘mágica’ del Singleton.

Los programadores aman al Singleton

Esperemos que, después de leer este artículo, empecemos a utilizarlo menos.

El origen del mal

El patrón se utiliza en la industria hace décadas. Su popularidad se atribuye al excelente libro Patrones de Diseño. Existen numerosas librerías y frameworks que lo utilizan y pocas veces encontramos literatura que desaconseje su uso.

A pesar de esto, en la entrada correspondiente de la Wikipedia podemos leer una advertencia dantesca:

Los críticos consideran al Singleton como un anti-patrón. Utilizado en escenarios donde no es beneficioso, agrega restricciones innecesarias, donde una única instancia de una clase no es realmente requerida e introduce un estado global en la aplicación.

Foto por Alessio Zaccaria en Unsplash

En la universidad, cuando lo definimos de manera teórica Hernan Wilkinson utiliza la frase:

El Singleton es el patrón del mal

La exitosa serie homónima sobre la vida de Pablo Escobar lleva este título. Este articulo no pretende estigmatizar al pueblo Colombiano y sólo utiliza el nombre de la popular serie.

Seamos pragmáticos como siempre, y veamos los argumentos a favor y en contra de su utilización:

Razones para no utilizarlo

1. Viola el principio de la biyección

Como vimos en artículos anteriores todo objeto en nuestro modelo computable tiene que estar mapeado en una relación 1:1 con un ente del mundo real.

Los singletons suelen vincularse con objetos que necesitan ser únicos. Deberemos separar, como de costumbre, los objetos que son esencialmente únicos (por razones del dominio del problema) y diferenciarlos de los accidentalmente únicos por motivos de implementación, eficiencia, consumo de recursos, acceso global, etc.

La mayoría de los objetos accidentalmente únicos no lo son en el mundo real, y veremos más adelante, que los esencialmente únicos tal vez no lo sean al tener que modelar distintos contextos, ambientes o situaciones.

2. Produce acoplamiento

Es una referencia global. Nuevamente según Wikipedia:

La implementación del patrón de diseño Singleton debe proveer un acceso global a dicha instancia única.

Esto, que a priori parece un beneficio porque nos ‘evita’ tener que pasar información de contexto, genera acoplamiento. La referencia al singleton no puede ser cambiada según el ambiente (desarrollo, producción), tampoco se pueden realizar cambios de estrategia dinámicos relacionados a la carga actual, no puede ser reemplazado por un test double y nos impide realizar cambios por el efecto de onda que causarían.

3. Habla mucho de la implementación (accidental) y poco de sus responsabilidades (esenciales)

Al enfocarnos de manera temprana en temas de implementación (el Singleton es un patrón de implementación) nos orientamos según la accidentalidad (el cómo) y menospreciamos lo más importante de un objeto: las responsabilidades que tiene (el qué).

Al realizar optimización prematura en nuestros diseños solemos nombrar como singleton a un concepto que acabamos de descubrir.

La separación entre problemas accidentales y esenciales es fundamental en el desarrollo de software.

4. Nos impide escribir buenos tests unitarios

El acoplamiento mencionado más arriba tiene como corolario la imposibilidad de tener control total sobre los efectos colaterales de un test para garantizar su determinismo. Deberemos depender del estado global referenciado por el Singleton.

5. No ahorra espacio de memoria

El argumento utilizado para proponer su utilización es evitar la construcción de múltiples objetos volátiles. Esta supuesta ventaja no lo es en las máquinas virtuales con mecanismos eficientes de garbage collection .

En dichas máquinas virtuales, utilizadas por la mayoría de los lenguajes modernos, mantener objetos en una zona de memoria cuyo algoritmo de Garbage Collector es de doble pasada (mark & sweep) es mucho más costoso que crear objetos volátiles y desalocarlos rápidamente.

6. Nos impide utilizar inyección de dependencias

Como defensores del buen diseño sólido, favorecemos la inversión de control a través de la inyección de dependencias para evitar el acoplamiento. De esta manera el proveedor de un servicio (antes un Singleton hardcodeado) se desacopla del servicio mismo, reemplazándolo por una dependencia inyectable que cumpla los requisitos definidos acoplándonos al qué y no al cómo.

7. Viola el contrato de creación de instancias

Cuando le pedimos a una clase la creación de una nueva instancia esperamos que se cumpla el contrato y nos entregue una instancia nueva y fresca. Sin embargo, muchas implementaciones de Singleton esconden la omisión de la creación de manera silenciosa, en vez de fallar rápidamente para indicar que, existe una regla de negocio por la que no deben crearse instancias de manera arbitraria.

Una mejor respuesta sería indicar con una excepción que no es válido crear nuevas instancias en este contexto de ejecución.

Esto nos obligará a tener un constructor privado para utilizarlo internamente. Violando el contrato de que todas las clases pueden crear instancias. Un nuevo code smell.

8. Nos obliga a acoplarnos a la implementación de manera explícita

Al invocar una clase para usarla (nuevamente, para usar su qué), tendremos que acoplarnos a que accidentalmente es un Singleton (su cómo), generando una relación que, de intentar romperla produciría el tan temido efecto de onda.

9. Dificulta la creación de tests automatizados

Si utilizamos la técnica de desarrollo TDD, los objetos se definen pura y exclusivamente en base a su comportamiento. Por lo tanto, en ningún caso, la construcción de software mediante TDD hará emerger el concepto de Singleton. Si las reglas del negocio indican que debe existir un único proveedor de un determinado servicio, esto será modelado a través de un punto de acceso controlado (que no debería ser una clase, y mucho menos un Singleton).

Intentar crear tests unitarios en sistema existente acoplado a algún Singleton puede ser una tarea casi imposible.

10. Los conceptos únicos son relativos al contexto

Cuando se enuncia el patrón se suele acompañar de alguna idea que en el mundo real parece única. Por ejemplo, si queremos modelar el comportamiento de Dios según la visión de la cristiandad no podría existir más de un Dios. Pero estas reglas son relativas al contexto y la visión subjetiva de cada religión. Pueden convivir en el mismo mundo varios sistemas de creencias con sus propios dioses (algunas creencias monoteístas y otras politeístas).

Estructura del patrón según el libro de patrones de diseño
La clase (y todo el metamodelo) no está presente en la biyección. Cualquier relación vinculada a la clase será inválida

11. Es difícil de mantener en ambientes multi-hilos

La instrumentación del patrón puede ser delicada en programas con múltiples hilos de ejecución. Si dos hilos de ejecución intentan crear la instancia al mismo tiempo y ésta todavía no existe, sólo uno de ellos debe lograr crear el objeto. La solución clásica para este problema es utilizar exclusión mutua en el método de creación de la clase que implementa el patrón, para garantizar que sea reentrante.

12. Acumula basura que ocupa espacio en memoria

Los singletons son referencias acopladas a las clases, como las clases son referencias globales estos no son alcanzados por el garbage collector. En caso de que el Singleton sea un objeto complejo, dicho objeto entero además de la clausura transitiva de todas sus referencias, permanecerán en memoria durante toda la ejecución.

13. El estado basura acumulado es enemigo de los tests unitarios

El estado persistente es enemigo de las pruebas unitarias. Una de las cosas que hace que las pruebas unitarias sean efectivas es que cada prueba debe ser independiente de todas las demás. Si esto no se cumple, entonces el orden en que se ejecutan las pruebas puede afectar el resultado de las mismas y los tests se tornan no-determinísticos . Esto puede conducir a casos en los que las pruebas fallan cuando no deberían y, lo que es peor, puede conducir a pruebas que pasan sólo por el orden en que se realizaron. Esto puede ocultar errores y es muy malo.

Evitar variables estáticas es una buena manera de evitar que el estado se preserve entre prueba y prueba. Los singletons, por su propia naturaleza, dependen de una instancia que se mantiene en una variable estática. Esta es una invitación para la prueba de dependencia.

Foto por Brian Yurasits en Unsplash

14. Limitar la creación de nuevos objetos viola el principio de única responsabilidad.

La única responsabilidad de una clase es crear instancias

Agregarle a cualquier clase otra responsabilidad implica violar el principio de responsabilidad único (la S de Solid). Una clase no debería preocuparse por ser o no ser Singleton. Únicamente debería ser responsable de sus compromisos con las reglas de negocio. En caso de necesitar la unicidad de dichas instancias esto sería responsabilidad de un tercer objeto en el medio como una Factory o un Builder.

15. El costo de tener una referencia global no es únicamente el acoplamiento

Los Singletons se utilizan con frecuencia para proporcionar un punto de acceso global a algún servicio. Lo que termina sucediendo es que las dependencias en el diseño quedan ocultas dentro del código y no son visibles al examinar las interfaces de sus clases y métodos.

La necesidad de crear algo global para evitar pasarlo explícitamente es un code smell. Siempre existen mejores soluciones y alternativas a utilizar una referencia global, y que no requieran pasar todos los colaboradores entre métodos.

16. Es el amigo fácil de la fiesta

Muchos singletons son, a su vez abusados como repositorio de referencias globales

La tentación de utilizar el singleton como punto de entrada de nuevas referencias es grande. Existen muchos ejemplos donde se utiliza al Singleton como contenedor de referencias de alcance rápido.

Como si no fuera suficiente con ser el patrón del mal también es el amigo fácil de la fiesta. En grandes proyectos, suele ir acumulando basura para salir del paso.

Al no tener un ente correspondiente en la biyección, agregarle responsabilidades que no le corresponden es como agregarle una mancha más al tigre. Aparentemente sin hacer daño pero generando un efecto de onda al querer hacer un sano desacoplamiento.

Foto por Omar Lopez en Unsplash

Razones para utilizarlo

Enunciados los argumentos en contra del Singleton intentemos ver los posibles beneficios:

1. Nos permite ahorrar memoria

Este argumento es falaz según el estado del arte actual de los lenguajes con una máquina virtual y garbage collector decentes. Basta realizar un benchmark y buscar evidencia para convencernos.

2. Modela conceptos únicos

El Singleton se puede utilizar para garantizar la unicidad de un concepto. Pero no es la única forma ni la mejor.

Reescribamos el ejemplo anterior:

El acceso y la creación de la única instancia no están acoplados. La creación se realiza a través de una factory y se desacoplan las referencias directas a las clases. Además dicha factory puede ser fácilmente mockeada en casos de test.

3. Nos evita repetir inicializaciones costosas

Existen objetos que requieren un determinado costo de recursos para crearse. Si este costo es grande no podremos generarlos constantemente. Un posible cómo consiste en utilizar un Singleton y tenerlo siempre disponible.

Como siempre nos enfocaremos en el qué y buscaremos otros cómos que nos generen un menor acoplamiento. Si necesitamos un punto de control o un caché tendremos que acceder a un objeto conocido según un determinado contexto (y fácilmente reemplazables según el ambiente, el setup de los tests, etc.).

Ciertamente el Singleton no deberá ser nuestra primera opción.

Solución

Existen múltiples técnicas para eliminar gradualmente el (ab)uso de Singletons.

En este artículo enumeramos algunas:

Conclusiones

Las desventajas enumeradas en este artículo son mucho más grandes que las ventajas y la evidencia de los ejemplos en la industria debería ser un indicador contundente para la no utilización del patrón del mal en ningún caso.

A medida que nuestra profesión madure, dejaremos atrás este tipo de malas soluciones.

Parte del objetivo de esta serie de artículos es generar espacios de debate y discusión sobre diseño de software.

Esperamos comentarios y sugerencias sobre este artículo.

Este artículo también está disponible en inglés aquí.

--

--

Maximiliano Contieri
Diseño de Software

I’m a senior software engineer specialized in declarative designs. S.O.L.I.D. and agile methodologies fan. Maximilianocontieri.com