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:
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:
+ - 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 Cache
instancia 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.