language agnostic - refactory - ¿Cómo debería refactorizar mi código para eliminar los singletons innecesarios?
refactorizar sinonimo (8)
Estaba confundido cuando empecé a ver comentarios anti-singleton. He usado el patrón singleton en algunos proyectos recientes y funcionaba muy bien. Tanto es así, de hecho, que lo he usado muchas, muchas veces.
Ahora, después de encontrarme con algunos problemas, al leer esta pregunta SO, y especialmente esta publicación en el blog, entiendo el mal que traigo al mundo.
Entonces, ¿cómo hago para eliminar singletons del código existente?
Por ejemplo:
En un programa de administración de tiendas minoristas, utilicé el patrón MVC. Los objetos de mi modelo describen la tienda, la interfaz de usuario es la vista y tengo un conjunto de controladores que actúan como enlace entre los dos. Estupendo. Excepto que hice la tienda en un singleton (ya que la aplicación solo administra una tienda a la vez), y también hice que la mayoría de mis clases de controlador fueran singletons (una ventana principal, una barra de menú, un editor de producto ...). Ahora, la mayoría de mis clases de Controladores tienen acceso a los otros singletons como este:
Store managedStore = Store::getInstance();
managedStore.doSomething();
managedStore.doSomethingElse();
//etc.
Debería yo en cambio:
- Crear una instancia de cada objeto y pasar referencias a cada objeto que necesita acceder a ellos?
- Usa los globos globales
- ¿Algo más?
Globales aún sería malo, pero al menos no fingirían .
Veo que el # 1 conduce rápidamente a llamadas de constructor horriblemente infladas:
someVar = SomeControllerClass(managedStore, menuBar, editor, sasquatch, ...)
¿Alguien más ha pasado por esto todavía? ¿Cuál es la forma OO de dar acceso a muchas clases individuales a una variable común sin que sea un global o un singleton?
De acuerdo, antes que nada, la noción de "los singletons son siempre malvados" es incorrecta. Utiliza un Singleton siempre que tenga un recurso que no se duplicará o no podrá duplicarse. No hay problema.
Dicho esto, en su ejemplo, hay un grado obvio de libertad en la aplicación: alguien podría venir y decir "pero quiero dos tiendas".
Hay varias soluciones. El que ocurre antes que nada es construir una clase de fábrica; cuando solicite una tienda, le dará una que se nombre con algún nombre universal (por ejemplo, una URI). Dentro de esa tienda, debe asegurarse de que las copias múltiples no se pisen entre sí, a través de regiones críticas o algún método de asegurando la atomicidad de las transacciones.
Me gusta fomentar el uso de singletons cuando sea necesario y desalentar el uso del patrón de Singleton. Tenga en cuenta la diferencia en el caso de la palabra. El singleton (minúsculas) se usa donde sea que solo necesite una instancia de algo. Se crea al comienzo de su programa y se pasa al constructor de las clases que lo necesitan.
class Log
{
void logmessage(...)
{ // do some stuff
}
};
int main()
{
Log log;
// do some more stuff
}
class Database
{
Log &_log;
Database(Log &log) : _log(log) {}
void Open(...)
{
_log.logmessage(whatever);
}
};
El uso de un singleton brinda todas las capacidades del anti-patrón de Singleton, pero hace que el código sea más fácilmente extensible, y lo hace comprobable (en el sentido de la palabra definida en el blog de pruebas de Google). Por ejemplo, podemos decidir que también necesitamos la posibilidad de iniciar sesión en un servicio web, utilizando el singleton podemos hacerlo fácilmente sin cambios significativos en el código.
En comparación, el patrón de Singleton es otro nombre para una variable global. Nunca se usa en el código de producción.
No es el Singleton-ness ese es el problema. Está bien tener un objeto que solo habrá una vez. El problema es el acceso global. Las clases que usan Store deben recibir una instancia de Store en el constructor (o tener un elemento de propiedad / datos de Tienda que se puede establecer) y todas pueden recibir la misma instancia. La tienda puede incluso mantener la lógica dentro de ella para garantizar que solo se cree una sola instancia.
Dependency Injection es tu amigo.
Eche un vistazo a estas publicaciones en el excelente Blog de pruebas de Google :
- Los solteros son mentirosos patológicos (pero probablemente ya entiendas esto si haces esta pregunta)
- Una charla sobre la Inyección de Dependencia
- Guía para escribir un código testable
¿Esperemos que alguien haya creado un marco / contenedor DI para el mundo de C ++? Parece que Google lanzó un Marco de prueba de C ++ y un Marco de burla de C ++ , que podría ayudarte.
Miško Hevery tiene una buena serie de artículos sobre la capacidad de prueba, entre otras cosas, el singleton , donde no solo habla de los problemas, sino también de cómo se puede resolver (ver ''Reparar el defecto'').
Mi manera de evitar los singletons se deriva de la idea de que "aplicación global" no significa "VM global" (es decir, static
). Por lo tanto, presento una clase ApplicationContext
que contiene mucha información antigua singleton static
que debería ser una aplicación global, como el almacén de configuración. Este contexto se pasa a todas las estructuras. Si usa cualquier contenedor de IOC o administrador de servicio, puede usar esto para obtener acceso al contexto.
No hay nada malo con el uso de un programa global o singleton en su programa. No dejes que nadie se vuelva dogmático contigo sobre ese tipo de mierda. Las reglas y patrones son buenas reglas prácticas. Pero al final es su proyecto y debe hacer sus propios juicios sobre cómo manejar situaciones que involucran datos globales.
El uso irrestricto de los globales es una mala noticia. Pero mientras seas diligente, no van a matar tu proyecto. Algunos objetos en un sistema merecen ser singleton. La entrada y salida estándar. Su sistema de registro. En un juego, sus subsistemas de gráficos, sonido y entrada, así como la base de datos de entidades de juego. En una GUI, su ventana y los principales componentes del panel. Sus datos de configuración, su administrador de complementos, sus datos de servidor web. Todas estas cosas son más o menos inherentemente globales para su aplicación. Creo que tu clase de Store pasaría por eso también.
Está claro cuál es el costo de usar globals. Cualquier parte de tu aplicación podría modificarla. Rastrear bugs es difícil cuando cada línea de código es sospechosa en la investigación.
Pero, ¿qué pasa con el costo de NO usar globales? Como todo lo demás en la programación, es una compensación. Si evita el uso de globales, termina teniendo que pasar esos objetos con estado como parámetros de función. Alternativamente, puede pasarlos a un constructor y guardarlos como una variable miembro. Cuando tienes varios de esos objetos, la situación empeora. Usted ahora está enhebrando su estado. En algunos casos, esto no es un problema. Si sabe que solo dos o tres funciones necesitan manejar ese objeto de tienda con estado, es la mejor solución.
Pero en la práctica, ese no es siempre el caso. Si cada parte de su aplicación toca su Tienda, la enhebrará a una docena de funciones. Además de eso, algunas de esas funciones pueden tener lógica empresarial complicada. Cuando rompes esa lógica de negocios con las funciones de ayuda, tienes que enhebrar tu estado un poco más. Digamos, por ejemplo, que se da cuenta de que una función profundamente anidada necesita algunos datos de configuración del objeto Store. De repente, debe editar 3 o 4 declaraciones de función para incluir ese parámetro de tienda. Luego debe regresar y agregar la tienda como un parámetro real a todas partes donde se llame a una de esas funciones. Puede ser que el único uso que una función tenga para una Tienda sea pasarlo a alguna subfunción que lo necesite.
Los patrones son solo reglas generales. ¿ Siempre usa sus intermitentes antes de hacer un cambio de carril en su automóvil? Si eres la persona promedio, por lo general seguirás la regla, pero si conduces a las 4 a.m. en una vía alta vacía, ¿a quién le importa? ¿Verdad? A veces te morderá en el trasero, pero ese es un riesgo administrado.
Con respecto a su problema de llamada de constructor inflado, puede introducir clases de parámetros o métodos de fábrica para aprovechar este problema.
Una clase de parámetro mueve algunos de los datos de parámetros a su propia clase, por ejemplo, de esta manera:
var parameterClass1 = new MenuParameter(menuBar, editor);
var parameterClass2 = new StuffParameters(sasquatch, ...);
var ctrl = new MyControllerClass(managedStore, parameterClass1, parameterClass2);
Sin embargo, de alguna manera mueve el problema a otro lado. Es posible que desee mantener a su constructor en su lugar. Solo guarde los parámetros que son importantes al construir / iniciar la clase en cuestión y haga el resto con métodos getter / setter (o propiedades si está haciendo .NET).
Un método de fábrica es un método que crea todas las instancias que necesita de una clase y tiene el beneficio de encapsular la creación de dichos objetos. También son bastante fáciles de refactorizar desde Singleton, porque son similares a los métodos getInstance que se ven en los patrones de Singleton. Supongamos que tenemos el siguiente ejemplo simple de singleton no seguro de hilos:
// The Rather Unfortunate Singleton Class
public class SingletonStore {
private static SingletonStore _singleton
= new MyUnfortunateSingleton();
private SingletonStore() {
// Do some privatised constructing in here...
}
public static SingletonStore getInstance() {
return _singleton;
}
// Some methods and stuff to be down here
}
// Usage:
// var singleInstanceOfStore = SingletonStore.getInstance();
Es fácil refactorizar esto hacia un método de fábrica. La solución es eliminar la referencia estática:
public class StoreWithFactory {
public StoreWithFactory() {
// If the constructor is private or public doesn''t matter
// unless you do TDD, in which you need to have a public
// constructor to create the object so you can test it.
}
// The method returning an instance of Singleton is now a
// factory method.
public static StoreWithFactory getInstance() {
return new StoreWithFactory();
}
}
// Usage:
// var myStore = StoreWithFactory.getInstance();
El uso sigue siendo el mismo, pero no estás empantanado con tener una sola instancia. Naturalmente, movería este método de fábrica a su propia clase ya que la clase Store
no debería preocuparse por la creación de sí mismo (y casualmente seguir el Principio de Responsabilidad Individual como un efecto de mover el método de fábrica).
Desde aquí tienes muchas opciones, pero lo dejaré como un ejercicio para ti. Es fácil sobre-diseñar (o sobrecalentar) en patrones aquí. Mi consejo es que solo apliques un patrón cuando lo necesites .