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í?
fuente
Respuestas:
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:
Es debido a esta elección de diseño específica que el patrón introduce varios problemas potenciales a largo plazo:
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:
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:
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:
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.
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.
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:
El beneficio de esto es que nunca necesita romper su
Cache
instancia si decide refactorizar; puede cambiar la forma en que se almacenan los medios simplemente al alimentarlo con una implementación alternativa deIMediaRepository
. 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.
fuente
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.
fuente
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
Math
clase especial si pudieras hacerlo123.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.
fuente
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).
fuente
le weekend
Uy, ese es uno de los nuestros). Gracias :-)¿Y que? Como nadie lo dijo: Toolbox . Eso es si quieres variables globales .
¿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,static
es 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
static
de 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 ...
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:
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.
fuente
java.awt.Toolkit
. Sin embargo, mi punto es el mismo:Toolbox
suena 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)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)
fuente
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.
fuente
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.
fuente
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
XxxService
clase que envuelve un singleton alrededor de la claseXxx
. El Singleton no está en la claseXxx
en absoluto, se separa en otra clase,XxxService
. Esto se debe a queXxx
puede tener varias instancias, aunque no es probable, pero aún queremos tener unaXxx
instancia accesible globalmente en cada sistema.XxxService
proporciona una buena separación de preocupaciones.Xxx
no tiene que aplicar una política de singleton, sin embargo, podemos usarloXxx
como un singleton cuando sea necesario.Algo como:
fuente
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
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:
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.
fuente
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:
vs
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.
fuente
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.
fuente