Inyección de dependencia y patrón de diseño singleton

89

¿Cómo identificamos cuándo usar la inyección de dependencia o el patrón singleton? He leído en muchos sitios web donde dicen "Use la inyección de dependencia sobre el patrón singleton". Pero no estoy seguro de estar totalmente de acuerdo con ellos. Para mis proyectos de pequeña o mediana escala, definitivamente veo el uso del patrón singleton sencillo.

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

SysAdmin
fuente

Respuestas:

66

Si desea verificar lo que se registra en una prueba, necesita una inyección de dependencia. Además, un registrador rara vez es un singleton; por lo general, tiene un registrador por cada uno de su clase.

Mire esta presentación sobre diseño orientado a objetos para comprobar la capacidad de prueba 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 de facto singleton, pero aún así puede obtenerse mediante inyección de dependencia, en lugar de mediante Singleton.getInstance().

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

Bozho
fuente
89

Los singleton son como el comunismo: ambos suenan muy bien en el 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 por completo el contexto al requerir que cada consumidor use un objeto de ámbito de dominio de aplicación, sin dejar opciones para diferentes implementaciones. Incorpora el conocimiento de la 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 Foodepende de Logger.GetInstance(), Fooestá ocultando efectivamente sus dependencias a los consumidores. Esto significa que no puede comprenderlo completamente Fooo usarlo con confianza a menos que lea su fuente y descubra el hecho de que depende Logger. Si no tiene la fuente, eso limita qué tan bien puede comprender y usar de manera efectiva 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. Le limita de innumerables formas sin ofrecer ningún beneficio discernible sobre las alternativas. Puede usarlo como desee, pero como existen alternativas viables que promueven un mejor diseño, nunca debería ser una práctica recomendada.

Bryan Watts
fuente
9
@BryanWatts Dicho todo esto, los Singleton siguen siendo mucho más rápidos y menos propensos a errores en una aplicación normal de escala media. Por lo general, no tengo más de una implementación posible para un registrador (personalizado), entonces, ¿por qué ** necesitaría: 1. Crear una interfaz para eso y cambiarla cada vez que necesito agregar / cambiar miembros públicos 2. Mantener una configuración DI 3. Ocultar el hecho de que hay un solo objeto de este tipo en todo el sistema 4. Atarme a una funcionalidad estricta causada por una Separación prematura de preocupaciones.
Uri Abramson
@UriAbramson: Parece que ya tomó la decisión sobre las compensaciones que prefiere, así que no intentaré convencerlo.
Bryan Watts
@BryanWatts No es el caso, tal vez mi comentario fue un poco demasiado duro, pero en realidad me interesa mucho escuchar lo que tienes que decir sobre el punto que mencioné ...
Uri Abramson
2
@UriAbramson: Bastante justo. ¿Al menos estaría de acuerdo en que el intercambio de implementaciones por aislamiento durante las pruebas es importante?
Bryan Watts
6
El uso de singletons y la inyección de dependencia no se excluyen mutuamente. Un singleton puede implementar una interfaz, por lo tanto, puede usarse para satisfacer una dependencia de otra clase. El hecho de que sea un singleton no obliga a todos los consumidores a obtener una referencia a través de su método / propiedad "GetInstance".
Oliver
18

Otros han explicado muy bien el problema de los singleton en general. Solo 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 raíz, para ser precisos) como singleton, a través de un método estático getInstance()o getRootLogger(). (a menos que desee ver lo que registra la clase que está probando, pero en mi experiencia, apenas puedo recordar casos en los que esto fue necesario. Por otra parte, para otros, esto podría ser una preocupación más urgente).

En mi opinión, un registrador 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 sobre el estado de la clase probada. Por lo tanto, no dificulta las pruebas unitarias.

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

Entonces, 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 manera segura como Singleton .

Péter Török
fuente
1
Incluso entonces, un singleton puede ser un fastidio. Si se da cuenta de que su registrador necesita algún parámetro adicional para algunos casos, puede crear un nuevo método (y dejar que el registrador se vuelva más feo) o romper a todos los consumidores del registrador, incluso si no les importa.
Kyoryu
4
@kyoryu, estoy hablando del caso "habitual", que en mi humilde opinión implica usar un marco de registro estándar (de facto). (Que normalmente se pueden configurar mediante archivos de propiedad / XML, por cierto). Por supuesto, hay excepciones, como siempre. Si sé que mi aplicación es excepcional a este respecto, no usaré un Singleton de hecho. Pero la sobreingeniería que dice "esto podría ser útil en algún momento" es casi siempre un esfuerzo en vano.
Péter Török
Si ya está utilizando DI, no es mucha ingeniería adicional. (Por cierto, no estoy en desacuerdo contigo, soy el voto a favor). Muchos registradores requieren alguna información de "categoría" o algo por el estilo, y agregar parámetros adicionales puede causar problemas. Esconderlo detrás de una interfaz puede ayudar a mantener limpio el código de consumo y puede facilitar el cambio a un marco de registro diferente.
kyoryu
1
@kyoryu, perdón por la suposición incorrecta. Vi un voto negativo y un comentario, así que conecté los puntos, de la manera incorrecta, ya que resulta :-( Nunca he experimentado la necesidad de cambiar a un marco de registro diferente, pero, de nuevo, entiendo que puede ser legítimo preocupación en algunos proyectos.
Péter Török
5
Corríjame si me equivoco, pero el singleton sería para LogFactory, no para el registrador. Además, es probable que LogFactory sea la fachada de registro de apache commons o Slf4j. Por lo tanto, cambiar las implementaciones de registro es sencillo de todos modos. ¿No es el verdadero dolor de usar DI para inyectar un LogFactory el hecho de que tiene que ir a applicationContext ahora para crear cada instancia en su aplicación?
HDave
9

Se trata principalmente, pero no completamente, de pruebas. Los singlton eran populares porque era fácil consumirlos, pero los singleton tienen una serie de desventajas.

  • Difícil de probar. Es decir, ¿cómo me aseguro de que el registrador haga lo correcto?
  • Difícil de probar. Lo que significa que si estoy probando código que usa el registrador, pero no es el foco de mi prueba, todavía necesito asegurarme de que mi entorno de prueba sea compatible con el registrador.
  • A veces no quieres un single, 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 se lo proporcionará, al mismo tiempo que le brinda la flexibilidad de prueba y construcción.

Scott Weinstein
fuente
Realmente no. Se trata de un estado mutable compartido y dependencias estáticas que hacen que las cosas sean dolorosas a largo plazo. Las pruebas son solo el ejemplo obvio y, con frecuencia, el más doloroso.
Kyoryu
2

Casi la única vez que debería 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 instintiva para un Singleton debería ser "¿estaría bien si esto fuera una variable global en lugar de un Singleton?" De lo contrario, está utilizando el patrón Singleton para empapelar una variable global y debería considerar un enfoque diferente.

Kyoryu
fuente
1

Acabo de consultar el artículo de Monostate: es una excelente alternativa 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 esto un poco aterrador, porque el asignador realmente depende de la conexión de la base de datos para realizar el guardado (), pero si se ha creado otro asignador antes, puede omitir este paso para adquirir sus dependencias? Aunque ordenado, también es un poco desordenado, ¿no?

sunwukung
fuente