Entonces, los Singleton son malos, ¿entonces qué?

554

Ha habido mucha discusión últimamente sobre los problemas con el uso (y el uso excesivo) de Singletons. También he sido una de esas personas anteriormente en mi carrera. Puedo ver cuál es el problema ahora y, sin embargo, todavía hay muchos casos en los que no puedo ver una buena alternativa, y no muchas de las discusiones anti-Singleton realmente ofrecen una.

Aquí hay un ejemplo real de un importante proyecto reciente en el que estuve involucrado:

La aplicación era un cliente pesado con muchas pantallas y componentes separados que usa grandes cantidades de datos de un estado del servidor que no se actualiza con demasiada frecuencia. Estos datos se almacenaron básicamente en un objeto Singleton "manager": el temido "estado global". La idea era tener este lugar en la aplicación que mantenga los datos almacenados y sincronizados, y luego cualquier pantalla nueva que se abra puede consultar la mayor parte de lo que necesita desde allí, sin hacer solicitudes repetitivas de varios datos de respaldo del servidor. Solicitar constantemente al servidor tomaría demasiado ancho de banda, y estoy hablando de miles de dólares adicionales en facturas de Internet por semana, por lo que eso era inaceptable.

¿Hay algún otro enfoque que podría ser apropiado aquí que básicamente tener este tipo de objeto de caché de administrador de datos global? Este objeto no tiene que ser oficialmente un "Singleton", por supuesto, pero conceptualmente tiene sentido ser uno. ¿Cuál es una buena alternativa limpia aquí?

Mesas Bobby
fuente
10
¿Qué problema se supone que resuelve el uso de un Singleton? ¿Cómo es mejor para resolver ese problema que las alternativas (como una clase estática)?
Anon
14
@Anon: ¿Cómo el uso de una clase estática mejora la situación? Todavía hay un acoplamiento apretado?
Martin York
55
@ Martin: No estoy sugiriendo que lo haga "mejor". Estoy sugiriendo que en la mayoría de los casos, un singleton es una solución en busca de un problema.
Anon
99
@Anon: No es cierto. Las clases estáticas no le dan (casi) control sobre la creación de instancias y hacen que los subprocesos múltiples sean aún más difíciles que los Singleton (ya que debe serializar el acceso a cada método individual en lugar de solo la instancia). Singletons también puede al menos implementar una interfaz, que las clases estáticas no pueden. Las clases estáticas ciertamente tienen sus ventajas, pero en este caso el Singleton es definitivamente el menor de dos males considerables. Una clase estática que implementa cualquier estado mutable es como un gran neón intermitente "ADVERTENCIA: ¡MAL DISEÑO ADELANTE!" firmar.
Aaronaught
77
@Aaronaught: si solo está sincronizando el acceso al singleton, entonces su concurrencia se rompe . Su subproceso podría interrumpirse justo después de recuperar el objeto singleton, se enciende otro subproceso y blam, condición de carrera. Usar un Singleton en lugar de una clase estática, en la mayoría de los casos, es simplemente quitar las señales de advertencia y pensar que resuelve el problema .
Anon

Respuestas:

809

Es importante distinguir aquí entre instancias individuales y el patrón de diseño Singleton .

Las instancias individuales son simplemente una realidad. La mayoría de las aplicaciones solo están diseñadas para funcionar con una configuración a la vez, una IU a la vez, un sistema de archivos a la vez, etc. Si hay mucho estado o datos que mantener, entonces seguramente querrás tener una sola instancia y mantenerla viva el mayor tiempo posible.

El patrón de diseño Singleton es un tipo muy específico de instancia única, específicamente una que es:

  • Accesible a través de un campo de instancia global y estático;
  • Creado ya sea en la inicialización del programa o en el primer acceso;
  • Ningún constructor público (no puede instanciar directamente);
  • Nunca liberado explícitamente (liberado implícitamente al finalizar el programa).

Es debido a esta elección de diseño específica que el patrón introduce varios problemas potenciales a largo plazo:

  • Incapacidad para usar clases abstractas o de interfaz;
  • Incapacidad de subclase;
  • Alto acoplamiento a través de la aplicación (difícil de modificar);
  • Difícil de probar (no puede fingir / burlarse de las pruebas unitarias);
  • Difícil de paralelizar en el caso de estado mutable (requiere un bloqueo extenso);
  • y así.

Ninguno de estos síntomas es endémico en casos únicos, solo el patrón Singleton.

¿Qué puedes hacer en su lugar? Simplemente no use el patrón Singleton.

Citando de la pregunta:

La idea era tener este lugar en la aplicación que mantenga los datos almacenados y sincronizados, y luego cualquier pantalla nueva que se abra puede consultar la mayor parte de lo que necesita desde allí, sin hacer solicitudes repetitivas de varios datos de respaldo del servidor. Solicitar constantemente al servidor tomaría demasiado ancho de banda, y estoy hablando de miles de dólares adicionales en facturas de Internet por semana, por lo que eso era inaceptable.

Este concepto tiene un nombre, como insinúas pero suenas incierto. Se llama caché . Si quieres ponerte elegante, puedes llamarlo "caché sin conexión" o simplemente una copia sin conexión de datos remotos.

Un caché no necesita ser un singleton. Es posible que deba ser una única instancia si desea evitar obtener los mismos datos para varias instancias de caché; pero eso no significa que realmente deba exponer todo a todos .

Lo primero que haría es separar las diferentes áreas funcionales de la memoria caché en interfaces separadas. Por ejemplo, supongamos que estaba haciendo el peor clon de YouTube del mundo basado en Microsoft Access:

                          MSAccessCache
                                ▲
                                El |
              + ----------------- + ----------------- +
              El | El | El |
         IMediaCache IProfileCache IPageCache
              El | El | El |
              El | El | El |
          VideoPage MyAccountPage MostPopularPage

Aquí tiene varias interfaces que describen los tipos específicos de datos a los que una clase en particular podría necesitar acceso: medios, perfiles de usuario y páginas estáticas (como la página principal). Todo eso es implementado por un mega-caché, pero usted diseña sus clases individuales para aceptar las interfaces en su lugar, por lo que no les importa qué tipo de instancia tengan. Inicializa la instancia física una vez, cuando se inicia su programa, y ​​luego comienza a pasar las instancias (emitidas a un tipo de interfaz particular) a través de constructores y propiedades públicas.

Esto se llama inyección de dependencia , por cierto; no necesita usar Spring ni ningún contenedor de IoC especial, siempre que su diseño de clase general acepte sus dependencias de la persona que llama en lugar de instanciarlas por su cuenta o hacer referencia al estado global .

¿Por qué debería usar el diseño basado en interfaz? Tres razones:

  1. Hace que el código sea más fácil de leer; Desde las interfaces puede comprender claramente de qué datos dependen las clases dependientes.

  2. Si se da cuenta de que Microsoft Access no era la mejor opción para un back-end de datos, puede reemplazarlo por algo mejor, digamos SQL Server.

  3. Si se da cuenta de que SQL Server no es la mejor opción para medios específicamente , puede interrumpir su implementación sin afectar ninguna otra parte del sistema . Ahí es donde entra el verdadero poder de la abstracción.

Si desea ir un paso más allá, puede usar un contenedor IoC (marco DI) como Spring (Java) o Unity (.NET). Casi todos los marcos DI harán su propia gestión de por vida y específicamente le permitirán definir un servicio particular como una instancia única (a menudo llamándolo "singleton", pero eso es solo por familiaridad). Básicamente, estos marcos le ahorran la mayor parte del trabajo del mono de pasar manualmente las instancias, pero no son estrictamente necesarios. No necesita ninguna herramienta especial para implementar este diseño.

En aras de la exhaustividad, debo señalar que el diseño anterior tampoco es realmente ideal. Cuando se trata de un caché (tal como está), en realidad debería tener una capa completamente separada . En otras palabras, un diseño como este:

                                                        + - IMediaRepository
                                                        El |
                          Caché (genérico) --------------- + - IProfileRepository
                                ▲ |
                                El | + - IPageRepository
              + ----------------- + ----------------- +
              El | El | El |
         IMediaCache IProfileCache IPageCache
              El | El | El |
              El | El | El |
          VideoPage MyAccountPage MostPopularPage

El beneficio de esto es que nunca necesita romper su Cacheinstancia si decide refactorizar; puede cambiar la forma en que se almacenan los medios simplemente al alimentarlo con una implementación alternativa de IMediaRepository. Si piensa cómo encaja esto, verá que todavía solo crea una instancia física de un caché, por lo que nunca necesitará recuperar los mismos datos dos veces.

Nada de esto es para decir que cada pieza de software en el mundo necesita ser diseñada para estos estándares exigentes de alta cohesión y acoplamiento flexible; depende del tamaño y el alcance del proyecto, su equipo, su presupuesto, plazos, etc. Pero si está preguntando cuál es el mejor diseño (para usar en lugar de un singleton), entonces este es.

PD Como otros han dicho, probablemente no sea la mejor idea que las clases dependientes sean conscientes de que están usando una memoria caché ; ese es un detalle de implementación que simplemente nunca debería importarles. Dicho esto, la arquitectura general aún se vería muy similar a lo que se muestra arriba, simplemente no se referiría a las interfaces individuales como Caches . En cambio, los llamaría Servicios o algo similar.

Aaronaught
fuente
131
Primera publicación que he leído que explica la DI como una alternativa al estado global. Gracias por el tiempo y el esfuerzo dedicado a esto. Todos estamos mejor como resultado de esta publicación.
MrLane
44
¿Por qué el caché no puede ser un singleton? ¿No es un singleton si lo pasa y usa la inyección de dependencia? Singleton se trata solo de limitarnos a una instancia, no se trata de cómo se accede ¿verdad? Vea mi opinión sobre esto: assoc.tumblr.com/post/51302471844/the-misunderstood-singleton
Erik Engheim
29
@ AdamSmith: ¿Leíste realmente alguna de estas respuestas? Su pregunta se responde en los dos primeros párrafos. Patrón Singleton! == Instancia única.
Aaronaught
55
@Cawas y Adam Smith: al leer sus enlaces, tengo la sensación de que realmente no leyó esta respuesta: todo ya está allí.
Wilbert
19
@Cawas Siento que el meollo de esta respuesta es la distinción entre instancia única y singleton. Singleton es malo, la instancia única no lo es. La inyección de dependencias es una forma agradable y general de usar instancias individuales sin tener que usar singletons.
Wilbert
48

En el caso de dar, parece que el uso de un Singleton no es el problema, sino el síntoma de un problema : un problema arquitectónico más grande.

¿Por qué las pantallas consultan el objeto de caché en busca de datos? El almacenamiento en caché debe ser transparente para el cliente. Debe haber una abstracción apropiada para proporcionar los datos, y la implementación de esa abstracción podría utilizar el almacenamiento en caché.

Es probable que el problema sea que las dependencias entre partes del sistema no estén configuradas correctamente, y esto es probablemente sistémico.

¿Por qué las pantallas necesitan tener conocimiento de dónde obtienen sus datos? ¿Por qué las pantallas no cuentan con un objeto que pueda cumplir con sus solicitudes de datos (detrás del cual se oculta un caché)? A menudo, la responsabilidad de crear pantallas no está centralizada, por lo que no hay un punto claro de inyectar las dependencias.

Nuevamente, estamos analizando cuestiones arquitectónicas y de diseño a gran escala.

Además, es muy importante comprender que la vida útil de un objeto puede estar completamente divorciada de cómo se encuentra el objeto para su uso.

Una caché tendrá que vivir durante toda la vida útil de la aplicación (para ser útil), de modo que la vida útil de ese objeto sea la de un Singleton.

Pero el problema con Singleton (al menos la implementación común de Singleton como una clase / propiedad estática), es cómo otras clases que lo usan se encargan de encontrarlo.

Con una implementación estática de Singleton, la convención es simplemente usarla donde sea necesario. Pero eso oculta completamente la dependencia y une estrechamente las dos clases.

Si proporcionamos la dependencia a la clase, esa dependencia es explícita y todo lo que necesita saber la clase consumidora es el contrato disponible para su uso.

quentin-starin
fuente
2
Hay una cantidad gigantesca de datos que ciertas pantallas pueden necesitar, pero no necesariamente. Y no se sabe hasta que se han tomado acciones del usuario que definen esto, y hay muchas, muchas combinaciones. Entonces, la forma en que se hizo fue tener algunos datos globales comunes que se mantienen almacenados en caché y sincronizados en el cliente (principalmente obtenidos al iniciar sesión), y luego las solicitudes posteriores acumulan más caché, porque los datos explícitamente solicitados tienden a reutilizarse nuevamente en La misma sesión. El objetivo es reducir las solicitudes al servidor, de ahí la necesidad de un caché del lado del cliente. <cont>
Bobby Tables
1
<cont> Es esencialmente transparente. En el sentido de que hay una devolución de llamada del servidor si ciertos datos requeridos aún no se almacenan en caché. Pero la implementación (lógica y física) de ese administrador de caché es Singleton.
Bobby Tables
66
Estoy con qstarin aquí: los objetos que acceden a los datos no deberían saber (o necesitan saber) que los datos están almacenados en caché (eso es un detalle de implementación). Los usuarios de los datos simplemente solicitan los datos (o solicitan una interfaz para recuperar los datos).
Martin York
1
El almacenamiento en caché es esencialmente un detalle de implementación. Hay una interfaz a través de la cual se consultan los datos, y los objetos que los obtienen no saben si provienen del caché o no. Pero debajo de este administrador de caché hay un Singleton.
Bobby Tables
2
@Bobby Tables: entonces tu situación no es tan grave como parecía. Ese Singleton (suponiendo que se refiere a una clase estática, no solo un objeto con una instancia que dura tanto como la aplicación) sigue siendo problemático. Oculta el hecho de que su objeto que proporciona datos depende de un proveedor de caché. Es mejor si eso es explícito y externo. Desacoplarlos. Es esencial para la capacidad de prueba que pueda sustituir fácilmente los componentes, y un proveedor de caché es un excelente ejemplo de dicho componente (con qué frecuencia un proveedor de caché está respaldado por ASP.Net).
quentin-starin
45

Escribí un capítulo entero sobre esta pregunta. Principalmente en el contexto de los juegos, pero la mayoría debe aplicarse fuera de los juegos.

tl; dr:

El patrón Single of Gang of Four hace dos cosas: brindarte acceso conveniente a un objeto desde cualquier lugar y asegurarte de que solo se pueda crear una instancia. El 99% del tiempo, lo único que le importa es la primera mitad de eso, y llevarlo a lo largo de la segunda mitad para obtenerlo agrega una limitación innecesaria.

No solo eso, sino que hay mejores soluciones para brindar un acceso conveniente. Hacer que un objeto sea global es la opción nuclear para resolverlo, y facilita la destrucción de su encapsulación. Todo lo que es malo sobre los globales se aplica completamente a los singletons.

Si se está usando sólo porque usted tiene un montón de lugares en el código que necesitan para tocar el mismo objeto, tratar de encontrar una mejor manera de darle a sólo aquellos objetos sin exponerlo a todo el código base. Otras soluciones:

  • Deshazte por completo. He visto muchas clases de singleton que no tienen ningún estado y son solo bolsas de funciones auxiliares. Esos no necesitan una instancia en absoluto. Simplemente conviértalas en funciones estáticas o muévalas a una de las clases que la función toma como argumento. No necesitarías una Mathclase especial si pudieras hacerlo 123.Abs().

  • Pásalo alrededor. La solución simple si un método necesita algún otro objeto es simplemente pasarlo. No hay nada de malo en pasar algunos objetos.

  • Ponlo en la clase base. Si tiene muchas clases que necesitan acceso a algún objeto especial y comparten una clase base, puede hacer que ese objeto sea miembro de la base. Cuando lo construyas, pasa el objeto. Ahora todos los objetos derivados pueden obtenerlo cuando lo necesitan. Si lo protege, se asegura de que el objeto aún permanezca encapsulado.

munificente
fuente
1
¿Puedes resumir aquí?
Nicole
1
Hecho, pero todavía te animo a leer todo el capítulo.
munificente
44
Otra alternativa: ¡Inyección de dependencia!
Brad Cupit
1
@BradCupit también habla de eso en el enlace ... Debo decir que todavía estoy tratando de digerirlo todo. Pero ha sido la lectura más esclarecedora en singletons que he leído. Hasta ahora, yo era eran necesarios únicos positivos, al igual que VARs globales, y yo estaba promoviendo la caja de herramientas . Ahora ya no lo sé. Señor municipal, ¿ puede decirme si el localizador de servicios es solo una caja de herramientas estática ? ¿No sería mejor hacerlo un singleton (por lo tanto, una caja de herramientas)?
cregox
1
"Toolbox" se parece bastante a "Service Locator" para mí. Creo que si usa una estática para él o si lo convierte en un singleton no es tan importante para la mayoría de los programas. Tiendo a inclinarme hacia la estática porque ¿por qué lidiar con la inicialización diferida y la asignación del montón si no es necesario?
munificent
21

El problema no es el estado global per se.

Realmente solo debes preocuparte global mutable state. El estado constante no se ve afectado por los efectos secundarios y, por lo tanto, es un problema menor.

La principal preocupación con singleton es que agrega acoplamiento y, por lo tanto, hace que las cosas como las pruebas sean más difíciles. Puede reducir el acoplamiento obteniendo el singleton de otra fuente (por ejemplo, una fábrica). Esto le permitirá desacoplar el código de una instancia en particular (aunque esté más acoplado a la fábrica (pero al menos la fábrica puede tener implementaciones alternativas para diferentes fases)).

En su situación, creo que puede salirse con la suya siempre que su singleton implemente una interfaz (de modo que se pueda usar una alternativa en otras situaciones).

Pero otro inconveniente importante con los singletons es que una vez que están en su lugar, eliminarlos del código y reemplazarlos por algo más se convierte en una tarea realmente difícil (existe ese acoplamiento nuevamente).

// Example from 5 minutes (con't be too critical)
class ServerFactory
{
    public:
        // By default return a RealServer
        ServerInterface& getServer();

        // Set a non default server:
        void setServer(ServerInterface& server);
};

class ServerInterface { /* define Interface */ };

class RealServer: public ServerInterface {}; // This is a singleton (potentially)

class TestServer: public ServerInterface {}; // This need not be.
Martin York
fuente
Eso tiene sentido. Esto también me hace pensar que realmente nunca abusé de Singletons, sino que comencé a dudar de CUALQUIER uso de ellos. Pero no puedo pensar en ningún abuso directo que haya cometido, de acuerdo con estos puntos. :)
Bobby Tables
2
(probablemente te refieres per se no "per say")
nohat
44
@nohat: Soy hablante nativo del "Inglés de Queens" y, por lo tanto, rechazo todo lo que se vea en francés a menos que lo hagamos mejor (como le weekendUy, ese es uno de los nuestros). Gracias :-)
Martin York
21
per se es latino.
Anon
2
@Anon: OK. Eso no es tan malo entonces ;-)
Martin York
19

¿Y que? Como nadie lo dijo: Toolbox . Eso es si quieres variables globales .

El abuso de Singleton puede evitarse mirando el problema desde un ángulo diferente. Supongamos que una aplicación necesita solo una instancia de una clase y la aplicación configura esa clase en el inicio: ¿Por qué la clase misma debería ser responsable de ser un singleton? Parece bastante lógico que la aplicación asuma esta responsabilidad, ya que la aplicación requiere este tipo de comportamiento. La aplicación, no el componente, debe ser el singleton. Luego, la aplicación pone a disposición una instancia del componente para que la use cualquier código específico de la aplicación. Cuando una aplicación usa varios de estos componentes, puede agregarlos a lo que hemos llamado una caja de herramientas.

En pocas palabras, la caja de herramientas de la aplicación es un singleton que se encarga de configurarse a sí misma o de permitir que el mecanismo de inicio de la aplicación la configure ...

public class Toolbox {
     private static Toolbox _instance; 

     public static Toolbox Instance {
         get {
             if (_instance == null) {
                 _instance = new Toolbox(); 
             }
             return _instance; 
         }
     }

     protected Toolbox() {
         Initialize(); 
     }

     protected void Initialize() {
         // Your code here
     }

     private MyComponent _myComponent; 

     public MyComponent MyComponent() {
         get {
             return _myComponent(); 
         }
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object GetComponent (String componentName) {
         return components.Get(componentName); 
     }

     public void RegisterComponent(String componentName, Object component) 
     {
         components.Put(componentName, component); 
     }

     public void DeregisterComponent(String componentName) {
         components.Remove(componentName); 
     }

}

¿Pero adivina que? Es un singleton!

¿Y qué es un singleton?

Tal vez ahí es donde comienza la confusión.

Para mí, el singleton es un objeto obligado para tener una sola instancia solo y siempre. Puede acceder a él desde cualquier lugar, en cualquier momento, sin necesidad de crear una instancia. Por eso está tan estrechamente relacionado con static. En comparación, statices básicamente lo mismo, excepto que no es una instancia. No necesitamos instanciarlo, ni siquiera podemos hacerlo, porque se asigna automáticamente. Y eso puede y trae problemas.

Desde mi experiencia, el simple reemplazo staticde Singleton resolvió muchos problemas en un proyecto de bolsa de retazos de tamaño mediano en el que estoy. Eso solo significa que tiene algún uso para proyectos mal diseñados. Creo que hay demasiada discusión sobre si el patrón singleton es útil o no y realmente no puedo discutir si es realmente malo . Pero todavía hay buenos argumentos a favor de singleton sobre métodos estáticos, en general .

Lo único que estoy seguro es malo acerca de los singletons, es cuando los usamos mientras ignoramos las buenas prácticas. De hecho, eso no es tan fácil de manejar. Pero las malas prácticas se pueden aplicar a cualquier patrón. Y, lo sé, es demasiado genérico decir que ... quiero decir que es demasiado.

¡No me malinterpretes!

En pocas palabras, al igual que VARs globales , únicos deben todavía ser evitado en todo momento . Especialmente porque son abusados ​​excesivamente. Pero los vars globales no se pueden evitar siempre y debemos usarlos en ese último caso.

De todos modos, hay muchas otras sugerencias además de la Caja de herramientas, y al igual que la caja de herramientas, cada una tiene su aplicación ...

Otras alternativas

  • El mejor artículo que acabo de leer sobre singletons sugiere el Localizador de servicios como una alternativa. Para mí eso es básicamente una " Caja de herramientas estática ", por así decirlo. En otras palabras, convierta el Localizador de servicios en un Singleton y tendrá una Caja de herramientas. Eso va en contra de su sugerencia inicial de evitar el singleton, por supuesto, pero eso solo para hacer cumplir el problema de singleton es cómo se usa, no el patrón en sí mismo.

  • Otros sugieren el patrón de fábrica como alternativa. Fue la primera alternativa que escuché de un colega y la eliminamos rápidamente para nuestro uso como var . Global . Seguro tiene su uso, pero también tiene singletons.

Ambas alternativas anteriores son buenas alternativas. Pero todo depende de su uso.

Ahora, implicar que los singletons deben evitarse a toda costa es simplemente incorrecto ...

  • La respuesta de Aaronaught sugiere que nunca use singletons , por una serie de razones. Pero todas son razones contra su mal uso y abuso, no directamente contra el patrón en sí. Estoy de acuerdo con todas las preocupaciones sobre esos puntos, ¿cómo no puedo? Solo creo que es engañoso.

Las incapacidades (para abstraer o subclase) están ahí, pero ¿y qué? No es para eso. No hay incapacidad para interactuar , por lo que puedo decir . El acoplamiento alto también puede estar allí, pero eso es solo por cómo se usa comúnmente. No tiene por qué . De hecho, el acoplamiento en sí mismo no tiene nada que ver con el patrón singleton. Una vez aclarado, también elimina la dificultad de probar. En cuanto a la dificultad de paralelizar, eso depende del lenguaje y la plataforma, por lo que, nuevamente, no es un problema en el patrón.

Ejemplos prácticos

A menudo veo que se usan 2, tanto a favor como en contra de los singletons. Caché web (mi caso) y servicio de registro .

El registro, algunos argumentarán , es un ejemplo único perfecto, porque, y cito:

  • Los solicitantes necesitan un objeto conocido al que enviar solicitudes para iniciar sesión. Esto significa un punto de acceso global.
  • Dado que el servicio de registro es un único origen de eventos en el que pueden registrarse múltiples oyentes, solo debe haber una instancia.
  • Aunque diferentes aplicaciones pueden iniciar sesión en diferentes dispositivos de salida, la forma en que registran a sus oyentes es siempre la misma. Toda la personalización se realiza a través de los oyentes. Los clientes pueden solicitar el registro sin saber cómo o dónde se registrará el texto. Por lo tanto, cada aplicación usaría el servicio de registro exactamente de la misma manera.
  • Cualquier aplicación debería poder escapar con solo una instancia del servicio de registro.
  • Cualquier objeto puede ser un solicitante de registro, incluidos los componentes reutilizables, por lo que no deben acoplarse a ninguna aplicación en particular.

Mientras que otros argumentan que es difícil expandir el servicio de registro una vez que finalmente se da cuenta de que en realidad no debería ser solo una instancia.

Bueno, digo que ambos argumentos son válidos. El problema aquí, nuevamente, no está en el patrón singleton. Se trata de decisiones arquitectónicas y de ponderación si la refactorización es un riesgo viable. Es un problema adicional cuando, por lo general, la refactorización es la última medida correctiva necesaria.

cregox
fuente
@gnat Gracias! Estaba pensando en editar la respuesta para agregar alguna advertencia sobre el uso de Singletons ... ¡Su cita encaja perfectamente!
cregox
2
Me alegro de que te haya gustado. No estoy seguro si esto ayudará a evitar downvotes sin embargo - probablemente los lectores están teniendo dificultades para observación hecha en este post a un problema concreto expuesto en la cuestión de conexión, especialmente a la luz del análisis excelente proporcionada en respuesta antes
mosquito
@gnat sí, sabía que esta era una larga batalla. Espero que el tiempo lo diga. ;-)
cregox
1
Estoy de acuerdo con su dirección general aquí, aunque tal vez sea un poco demasiado entusiasta acerca de una biblioteca que parece no ser mucho más que un contenedor de IoC extremadamente básico (incluso más básico que, por ejemplo, Funq ). Service Locator es en realidad un antipatrón; es una herramienta útil principalmente en proyectos heredados / brownfield, junto con "DI de pobres", donde sería demasiado costoso refactorizar todo para usar un contenedor IoC correctamente.
Aaronaught
1
@Cawas: Ah, lo siento, me confundí con java.awt.Toolkit. Sin embargo, mi punto es el mismo: Toolboxsuena como una bolsa de mano de partes y piezas no relacionadas, en lugar de una clase coherente con un solo propósito. No me parece un buen diseño. (Tenga en cuenta que el artículo al que hace referencia es de 2001, antes de que la inyección de dependencia y los contenedores DI se volvieran comunes)
Jon Skeet
5

Mi principal problema con el patrón de diseño singleton es que es muy difícil escribir buenas pruebas unitarias para su aplicación.

Cada componente que tiene una dependencia de este "administrador" lo hace consultando su instancia de singleton. Y si desea escribir una prueba unitaria para dicho componente, debe inyectar datos en esta instancia única, lo que puede no ser fácil.

Si, por otro lado, su "administrador" se inyecta en los componentes dependientes a través de un parámetro constructor, y el componente no conoce el tipo concreto del administrador, solo una interfaz o clase base abstracta que implementa el administrador, entonces una unidad test podría proporcionar implementaciones alternativas del administrador al probar dependencias.

Si usa contenedores IOC para configurar e instanciar los componentes que componen su aplicación, entonces puede configurar fácilmente su contenedor IOC para crear solo una instancia del "administrador", lo que le permite lograr la misma, solo una instancia que controla el caché global de la aplicación .

Pero si no le importan las pruebas unitarias, un patrón de diseño único puede estar perfectamente bien. (pero no lo haría de todos modos)

Pete
fuente
Bien explicado, esta es la respuesta que mejor explica el problema sobre las pruebas de Singletons
José Tomás Tocino
4

Un singleton no es fundamentalmente malo , en el sentido de que cualquier diseño informático puede ser bueno o malo. Solo puede ser correcto (da los resultados esperados) o no. También puede ser útil o no, si hace que el código sea más claro o más eficiente.

Un caso en el que los singletons son útiles es cuando representan una entidad que realmente es única. En la mayoría de los entornos, las bases de datos son únicas, realmente solo hay una base de datos. Conectarse a esa base de datos puede ser complicado porque requiere permisos especiales o atravesar varios tipos de conexión. Organizar esa conexión en un singleton probablemente tenga mucho sentido solo por esta razón.

Pero también debe asegurarse de que el singleton realmente sea un singleton, y no una variable global. Esto es importante cuando la base de datos única y única es realmente 4 bases de datos, una para producción, preparación, desarrollo y accesorios de prueba. Una base de datos Singleton determinará a cuál de las personas debería conectarse, tomar la instancia única para esa base de datos, conectarla si es necesario y devolverla a la persona que llama.

Cuando un singleton no es realmente un singleton (esto es cuando la mayoría de los programadores se enojan), es un global perezosamente instanciado, no hay oportunidad de inyectar una instancia correcta.

Otra característica útil de un patrón singleton bien diseñado es que a menudo no es observable. La persona que llama solicita una conexión. El servicio que lo proporciona puede devolver un objeto agrupado, o si está realizando una prueba, puede crear uno nuevo para cada persona que llama, o en su lugar puede proporcionar un objeto simulado.

SingleNegationElimination
fuente
3

El uso del patrón singleton que representa objetos reales es perfectamente aceptable. Escribo para el iPhone, y hay muchos singletons en el marco Cocoa Touch. La aplicación en sí está representada por un singleton de la clase UIApplication. Solo tiene una aplicación, por lo que es apropiado representarla con un singleton.

Usar un singleton como clase de administrador de datos está bien siempre que esté diseñado correctamente. Si se trata de un conjunto de propiedades de datos, eso no es mejor que el alcance global. Si se trata de un conjunto de getters y setters, eso es mejor, pero aún así no es genial. Si se trata de una clase que realmente gestiona toda la interfaz con los datos, incluida la obtención de datos remotos, el almacenamiento en caché, la configuración y el desmontaje ... Eso podría ser muy útil.

Dan Ray
fuente
2

Los Singletons son solo la proyección de una arquitectura orientada a servicios en un programa.

Una API es un ejemplo de un singleton a nivel de protocolo. Accede a Twitter, Google, etc. a través de lo que son esencialmente singletons. Entonces, ¿por qué los singletons se vuelven malos dentro de un programa?

Depende de cómo pienses en un programa. Si piensa en un programa como una sociedad de servicios en lugar de instancias en caché unidas aleatoriamente, los singletons tienen mucho sentido.

Los Singletons son un punto de acceso al servicio. La interfaz pública a una biblioteca de funcionalidad estrechamente vinculada que oculta quizás una arquitectura interna muy sofisticada.

Así que no veo un singleton tan diferente de una fábrica. El singleton puede tener parámetros de constructor pasados. Puede ser creado por algún contexto que sepa cómo resolver la impresora predeterminada contra todos los mecanismos de selección posibles, por ejemplo. Para las pruebas, puede insertar su propio simulacro. Por lo tanto, puede ser bastante flexible.

La clave está internamente en un programa cuando ejecuto y necesito un poco de funcionalidad. Puedo acceder al singleton con plena confianza de que el servicio está listo y listo para usar. Esta es la clave cuando hay diferentes subprocesos que comienzan en un proceso que debe pasar por una máquina de estado para considerarse listo.

Por lo general, envolvería una XxxServiceclase que envuelve un singleton alrededor de la clase Xxx. El Singleton no está en la clase Xxxen absoluto, se separa en otra clase, XxxService. Esto se debe a que Xxxpuede tener varias instancias, aunque no es probable, pero aún queremos tener una Xxxinstancia accesible globalmente en cada sistema. XxxServiceproporciona una buena separación de preocupaciones. Xxxno tiene que aplicar una política de singleton, sin embargo, podemos usarlo Xxxcomo un singleton cuando sea necesario.

Algo como:

//XxxService.h:
/**
 * Provide singleton wrapper for Xxx object. This wrapper
 * can be autogenerated so is not made part of the object.
 */

#include "Xxx/Xxx.h"


class XxxService
{
    public:
    /**
     * Return a Xxx object as a singleton. The double check
     * singleton algorithm is used. A 0 return means there was
     * an error. Developers should use this as the access point to
     * get the Xxx object.
     *
     * <PRE>
     * @@ #include "Xxx/XxxService.h"
     * @@ Xxx* xxx= XxxService::Singleton();
     * <PRE>
     */

     static Xxx*     Singleton();

     private:
         static Mutex  mProtection;
};


//XxxService.cpp:

#include "Xxx/XxxService.h"                   // class implemented
#include "LockGuard.h"     

// CLASS SCOPE
//
Mutex XxxService::mProtection;

Xxx* XxxService::Singleton()
{
    static Xxx* singleton;  // the variable holding the singleton

    // First check to see if the singleton has been created.
    //
    if (singleton == 0)
    {
        // Block all but the first creator.
        //
        LockGuard lock(mProtection);

        // Check again just in case someone had created it
        // while we were blocked.
        //
        if (singleton == 0)
        {
            // Create the singleton Xxx object. It's assigned
            // to a temporary so other accessors don't see
            // the singleton as created before it really is.
            //
            Xxx* inprocess_singleton= new Xxx;

            // Move the singleton to state online so we know that is has
            // been created and it ready for use.
            //
            if (inprocess_singleton->MoveOnline())
            {
                LOG(0, "XxxService:Service: FAIL MoveOnline");
                return 0;
            }

            // Wait until the module says it's in online state.
            //
            if (inprocess_singleton->WaitTil(Module::MODULE_STATE_ONLINE))
            {
                LOG(0, "XxxService:Service: FAIL move to online");
                return 0;
            }

            // The singleton is created successfully so assign it.
            //
            singleton= inprocess_singleton;


        }// still not created
    }// not created

    // Return the created singleton.
    //
    return singleton;

}// Singleton  
Todd Hoff
fuente
1

Primera pregunta, ¿encuentra muchos errores en la aplicación? ¿quizás olvidando actualizar la memoria caché, o la memoria caché incorrecta o es difícil cambiarla? (Recuerdo que una aplicación no cambiaría los tamaños a menos que también cambiaras el color ... sin embargo, puedes cambiar el color y mantener el tamaño).

Lo que haría es tener esa clase, pero QUITE TODOS LOS MIEMBROS ESTÁTICOS. Ok, esto no es nessacary pero lo recomiendo. Realmente solo inicializa la clase como una clase normal y PASA el puntero. No digas ClassIWant.APtr (). LetMeChange.ANYTHINGATALL (). Andhave_no_structure ()

Es más trabajo pero realmente, es menos confuso. Algunos lugares donde no deberías cambiar las cosas que ahora no puedes ya que ya no son globales. Todas mis clases de manager son clases regulares, solo trátelo como eso.


fuente
1

OMI, tu ejemplo suena bien. Sugeriría factorizar como sigue: objeto de caché para cada objeto de datos (y detrás de cada uno); Los objetos de caché y los objetos de acceso db tienen la misma interfaz. Esto brinda la capacidad de intercambiar cachés dentro y fuera del código; Además, ofrece una ruta de expansión fácil.

Gráfico:

DB
|
DB Accessor for OBJ A
| 
Cache for OBJ A
|
OBJ A Client requesting

El acceso al DB y el caché pueden heredar del mismo objeto o tipo de pato para parecerse al mismo objeto, lo que sea. Siempre que pueda enchufar / compilar / probar y todavía funciona.

Esto desacopla las cosas para que pueda agregar nuevas memorias caché sin tener que entrar y modificar algún objeto de Uber-Cache. YMMV. IANAL ETC.

Paul Nathan
fuente
1

Un poco tarde para la fiesta, pero de todos modos.

Singleton es una herramienta en una caja de herramientas, como cualquier otra cosa. Esperemos que tenga más en su caja de herramientas que un solo martillo.

Considera esto:

public void DoSomething()
{
    MySingleton.Instance.Work();
}

vs

public void DoSomething(MySingleton singleton)
{
    singleton.Work();
}
DoSomething(MySingleton.instance);

El primer caso conduce a un alto acoplamiento, etc. La segunda forma no tiene problemas que @Aaronaught está describiendo, por lo que puedo decir. Se trata de cómo lo usas.

Evgeni
fuente
Discrepar. Aunque la segunda forma es "mejor" (disminución del acoplamiento), todavía tiene problemas que resuelve DI. No debe confiar en el consumidor de su clase para proporcionar la implementación de sus servicios, eso se hace mejor en el constructor cuando se crea la clase. Su interfaz solo debe requerir lo mínimo en términos de argumentos. También hay una posibilidad decente de que su clase requiera una sola instancia para operar, una vez más, confiar en el consumidor para hacer cumplir esta regla es arriesgado e innecesario.
AlexFoxGill
A veces, Di es una exageración para una tarea determinada. El método en un código de muestra podría ser un constructor, pero no necesariamente, sin mirar un ejemplo concreto es un argumento discutible. Además, DoSomething podría tomar ISomething, y MySingleton podría estar implementando esa interfaz, bueno, eso no está en una muestra ... pero es solo una muestra.
Evgeni
1

Haga que cada pantalla tome el Administrador en su constructor.

Cuando inicia su aplicación, crea una instancia del administrador y la pasa.

Esto se llama Inversión de control y le permite cambiar el controlador cuando la configuración cambia y en las pruebas. Además, puede ejecutar varias instancias de su aplicación o partes de su aplicación en paralelo (¡bueno para probar!). Por último, su administrador morirá con su objeto propietario (la clase de inicio).

Así que estructura tu aplicación como un árbol, donde las cosas de arriba poseen todo lo que se usa debajo de ellas. No implemente una aplicación como una malla, donde todos se conocen y se encuentran a través de métodos globales.

Alexander Torstling
fuente