Patrón de Inyección de Dependencia y Diseño Único

¿Cómo identificamos cuándo usar la dependency injection o el patrón de singleton? He leído en muchos sitios web donde dicen “Use Dependency injection over singleton pattern”. Pero no estoy seguro si estoy totalmente de acuerdo con ellos. Para mis proyectos de pequeña o mediana escala, definitivamente veo el uso del patrón singleton directo.

Por ejemplo, Logger. Podría usar Logger.GetInstance().Log(...) Pero, en lugar de esto, ¿por qué tengo que inyectar todas las clases que creo, con la instancia del registrador?

Si desea verificar qué se registra en una prueba, necesita una dependency injection. Además, un registrador rara vez es un singleton; generalmente tiene un registrador por cada clase.

Mire esta presentación sobre el diseño orientado a objetos para probar y verá por qué los singleton son malos.

El problema con los singleton es que representan un estado global que es difícil de predecir, especialmente en las pruebas.

Tenga en cuenta que un objeto puede ser singleton de facto, pero aún se puede obtener a través de la dependency injection, en lugar de a través de Singleton.getInstance() .

Solo estoy enumerando algunos puntos importantes hechos por Misko Hevery en su presentación. Después de verlo, obtendrá una perspectiva completa de por qué es mejor tener un objeto que defina cuáles son sus dependencias, pero no definir una manera de cómo crearlas .

Los singletons son como el comunismo: ambos suenan bien en papel, pero explotan con problemas en la práctica.

El patrón singleton pone un énfasis desproporcionado en la facilidad de acceso a los objetos. Evita completamente el contexto al exigir que cada consumidor use un objeto con ámbito AppDomain, sin dejar opciones para implementaciones variables. Incorpora conocimiento de infraestructura en sus clases (la llamada a GetInstance() ) mientras agrega exactamente cero poder expresivo. En realidad, disminuye su poder expresivo, porque no puede cambiar la implementación utilizada por una clase sin cambiarla para todas . Simplemente no puede agregar piezas únicas de funcionalidad.

Además, cuando la clase Foo depende de Logger.GetInstance() , Foo efectivamente oculta sus dependencias de los consumidores. Esto significa que no puede comprender completamente a Foo ni usarlo con confianza a menos que lea su fuente y descubra el hecho de que depende de Logger . Si no tiene la fuente, eso limita lo bien que puede entender y usar efectivamente el código del que depende.

El patrón singleton, implementado con propiedades / métodos estáticos, es poco más que un truco para implementar una infraestructura. Te limita de muchas maneras sin ofrecer un beneficio discernible sobre las alternativas. Puede usarlo como lo desee, pero como existen alternativas viables que promueven un mejor diseño, nunca debe ser una práctica recomendada.

Otros han explicado muy bien el problema con los singletons en general. Me gustaría agregar una nota sobre el caso específico de Logger. Estoy de acuerdo con usted en que generalmente no es un problema acceder a un registrador (o al registrador de raíz, para ser precisos) como un singleton, a través de un método estático getInstance() o getRootLogger() . (a menos que quiera ver lo que registra la clase que está probando, pero en mi experiencia difícilmente puedo recordar casos en los que fue necesario. De nuevo, para otros esto podría ser una preocupación más apremiante).

La OMI, por lo general, un registrador de singleton no es una preocupación, ya que no contiene ningún estado relevante para la clase que está probando. Es decir, el estado del registrador (y sus posibles cambios) no tienen ningún efecto en el estado de la clase probada. Por lo tanto, no hace que las pruebas de su unidad sean más difíciles.

La alternativa sería inyectar el registrador a través del constructor, a (casi) cada clase en su aplicación. Para la coherencia de las interfaces, se debe inyectar incluso si la clase en cuestión no registra nada en la actualidad; la alternativa sería que cuando descubras en algún momento que ahora necesitas registrar algo de esta clase, necesitas un registrador, por lo tanto necesita agregar un parámetro de constructor para DI, rompiendo todo el código de cliente. No me gustan estas dos opciones, y creo que usar DI para registrar me complicaría la vida solo para cumplir con una regla teórica, sin ningún beneficio concreto.

Así que mi conclusión es: una clase que se usa (casi) universalmente, pero que no contiene un estado relevante para su aplicación, se puede implementar de forma segura como Singleton .

Es sobre todo, pero no completamente sobre pruebas. Singltons era popular porque era fácil consumirlos, pero hay una serie de inconvenientes para los singletons.

  • Difícil de probar. Es decir, cómo me aseguro de que el registrador haga lo correcto.
  • Difícil de probar con. Es decir, si estoy probando el código que usa el registrador, pero no es el foco de mi prueba, aún necesito asegurarme de que mi env de prueba admite el registrador
  • A veces no quieres una canción, pero más flexibilidad

DI le brinda el fácil consumo de sus clases dependientes, simplemente colóquelo en los argumentos del constructor, y el sistema lo proporciona por usted, mientras le brinda la flexibilidad de pruebas y construcción.

La única vez que debe usar un Singleton en lugar de Dependency Injection es si el Singleton representa un valor inmutable, como List.Empty o similar (asumiendo listas inmutables).

La prueba para Singleton debería ser “¿estaría bien si fuera una variable global en lugar de Singleton?” Si no, estás usando el patrón de Singleton para escribir sobre una variable global, y debes considerar un enfoque diferente.

Acabo de ver el artículo de Monostate: es una alternativa ingeniosa a Singleton, pero tiene algunas propiedades extrañas:

 class Mono{ public static $db; public function setDb($db){ self::$db = $db; } } class Mapper extends Mono{ //mapping procedure return $Entity; public function save($Entity);//requires database connection to be set } class Entity{ public function save(){ $Mapper = new Mapper(); $Mapper->save($this);//has same static reference to database class } $Mapper = new Mapper(); $Mapper->setDb($db); $User = $Mapper->find(1); $User->save(); 

No es tan aterrador, porque Mapper realmente depende de la conexión de la base de datos para ejecutar save (), pero si se ha creado otro mapper previamente, puede saltear este paso para adquirir sus dependencias. Si bien limpio, también es un poco complicado, ¿no?

Hay otras alternativas a Singleton: patrones Proxy y MonoState.

http://www.objectmentor.com/resources/articles/SingletonAndMonostate.pdf

¿Cómo se puede usar el patrón proxy para reemplazar un singleton?