Inyección de dependencia ; buenas prácticas para reducir el código repetitivo

25

Tengo una pregunta simple, y ni siquiera estoy seguro de que tenga una respuesta, pero intentemos. Estoy codificando en C ++ y usando la inyección de dependencia para evitar el estado global. Esto funciona bastante bien, y no corro comportamientos inesperados / indefinidos muy a menudo.

Sin embargo, me doy cuenta de que, a medida que mi proyecto crece, escribo mucho código que considero repetitivo. Peor aún: el hecho de que haya más código repetitivo que el código real hace que a veces sea difícil de entender.

Nada es mejor que un buen ejemplo, así que vamos:

Tengo una clase llamada TimeFactory que crea objetos Time.

Para más detalles (no estoy seguro de que sea relevante): los objetos de tiempo son bastante complejos porque el tiempo puede tener diferentes formatos, y la conversión entre ellos no es lineal ni directa. Cada "Hora" contiene un Sincronizador para manejar las conversiones, y para asegurarse de que tengan el mismo sincronizador correctamente inicializado, utilizo un TimeFactory. TimeFactory tiene solo una instancia y es de aplicación amplia, por lo que calificaría para singleton pero, debido a que es mutable, no quiero convertirlo en singleton

En mi aplicación, muchas clases necesitan crear objetos de tiempo. Algunas veces esas clases están profundamente anidadas.

Digamos que tengo una clase A que contiene instancias de la clase B, y así sucesivamente hasta la clase D. La clase D necesita crear objetos de tiempo.

En mi ingenua implementación, paso el TimeFactory al constructor de la clase A, que lo pasa al constructor de la clase B y así sucesivamente hasta la clase D.

Ahora, imagine que tengo un par de clases como TimeFactory y un par de jerarquías de clases como la anterior: pierdo toda la flexibilidad y legibilidad que se supone que debo usar con la inyección de dependencia.

Estoy empezando a preguntarme si no hay una falla de diseño importante en mi aplicación ... ¿O es un mal necesario usar la inyección de dependencia?

Qué piensas ?

Dinaiz
fuente
55
Publiqué esta pregunta a los programadores en lugar de stackoverflow porque, como entendí, los programadores son más para las preguntas relacionadas con el diseño y stackoverflow es para las preguntas "cuando estás frente a tu compilador". Lo siento de antemano si me equivoco.
Dinaiz
2
programación orientada a aspectos, también es buena para estas tareas - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov
¿La clase A es una fábrica para las clases B, C y D? Si no, ¿por qué crea instancias de ellos? Si solo la clase D necesita objetos de tiempo, ¿por qué inyectarías cualquier otra clase con TimeFactory? ¿La clase D realmente necesita TimeFactory, o podría simplemente inyectarse una instancia de Time?
simoraman
D necesita crear objetos de tiempo, con información que solo D debe tener. Tengo que pensar en el resto de tu comentario. Puede haber un problema en "quién está creando quién" en mi código
Dinaiz
"quién está creando quién" se resuelve con contenedores de IoC, vea mi respuesta para más detalles ... "quién está creando quién" es una de las mejores preguntas que puede hacer al usar la inyección de dependencia.
GameDeveloper

Respuestas:

23

En mi aplicación, muchas clases necesitan crear objetos de tiempo

Parece que su Timeclase es un tipo de datos muy básico que debe pertenecer a la "infraestructura general" de su aplicación. DI no funciona bien para tales clases. Piense en lo que significa si una clase como se stringtuviera que inyectar en cada parte del código que usa cadenas, y necesitaría usar a stringFactorycomo la única posibilidad de crear nuevas cadenas: la legibilidad de su programa disminuiría en un orden de magnitud.

Entonces, mi sugerencia: no use DI para tipos de datos generales como Time. Escriba pruebas unitarias para la Timeclase en sí, y cuando haya terminado, úsela en todas partes de su programa, como la stringclase, o una vectorclase o cualquier otra clase de la biblioteca estándar. Use DI para componentes que realmente deberían estar desacoplados uno del otro.

Doc Brown
fuente
Lo entendiste completamente bien. "la legibilidad de su programa disminuiría en un orden de magnitud". : eso es exactamente lo que sucedió sí. Si todavía quiero usar una fábrica, ¿no es TAN malo hacerla única?
Dinaiz
3
@Dinaiz: la idea es aceptar un acoplamiento estrecho entre Timey las otras partes de su programa. Por lo tanto, podría aceptar también aceptar un acoplamiento estrecho TimeFactory. Sin embargo, lo que evitaría es tener un único TimeFactoryobjeto global con un estado (por ejemplo, una información local o algo así), que podría causar efectos secundarios desagradables en su programa y hacer que las pruebas generales y la reutilización sean muy difíciles. Hazlo apátrida o no lo uses como un singleton.
Doc Brown
En realidad tiene que tener un estado, así que cuando dices "No lo uses como un singleton", te refieres a "seguir usando la inyección de dependencia, es decir, pasar la instancia a cada objeto que lo necesite", ¿verdad?
Dinaiz
1
@Dinaiz: honestamente, eso depende del tipo de estado, del tipo y la cantidad de partes de su programa que usa Timey TimeFactorydel grado de "capacidad de evolución" que necesita para futuras extensiones relacionadas TimeFactory, etc.
Doc Brown
OK ¡Intentaré mantener la inyección de dependencia pero tengo un diseño más inteligente que no requiere tanto repetitivo entonces! muchas gracias doc '
Dinaiz
4

¿Qué quiere decir con "pierdo toda la flexibilidad y legibilidad que se supone que debo obtener con la inyección de dependencia"? DI no se trata de legibilidad. Se trata de desacoplar la dependencia entre objetos.

Parece que tienes Clase A creando Clase B, Clase B creando Clase C y Clase C creando Clase D.

Lo que debe tener es la clase B inyectada en la clase A. La clase C inyectada en la clase B. La clase D inyectada en la clase C.

C0deAttack
fuente
DI puede ser sobre la capacidad de lectura. Si tiene una variable global "mágicamente" que aparece en el medio de un método, no ayuda entender el código (desde el punto de vista del mantenimiento). ¿De dónde es esta variable? ¿Quién es el dueño? ¿Quién lo inicializa? Y así sucesivamente ... Para su segundo punto, probablemente tenga razón, pero no creo que se aplique en mi caso. Gracias por tomarse el tiempo para responder
Dinaiz
DI aumenta la lectura principalmente porque "nuevo / eliminar" será eliminado mágicamente de su código. Si no tiene que preocuparse por el código de asignación dinámica, es más fácil de leer.
GameDeveloper
También olvide decir que esto hace que el código sea un poco más fuerte porque ya no puede hacer suposiciones sobre "cómo" se crea una dependencia, "solo la necesita". Y eso hace que la clase sea más fácil de mantener ... mira mi respuesta
GameDeveloper
3

No estoy seguro de por qué no quieres convertir tu fábrica de tiempo en un singleton. Si solo hay una instancia de ella en toda su aplicación, es de hecho un singleton.

Dicho esto, es muy peligroso compartir un objeto mutable, excepto si está debidamente protegido por bloques de sincronización, en cuyo caso, no hay razón para que no sea un singleton.

Si desea realizar una inyección de dependencia, es posible que desee ver la primavera u otros marcos de inyección de dependencia, lo que le permitiría asignar parámetros automáticamente mediante una anotación

molyss
fuente
Spring es para Java y uso C ++. Pero, ¿por qué te votaron 2 personas? Hay puntos en tu publicación con los que no estoy de acuerdo pero ...
Dinaiz
Voy a suponer que el voto negativo se debió a que no respondía la pregunta per se, tal vez tan bien como la mención de Spring. Sin embargo, la sugerencia de usar un Singleton era válida en mi opinión, y puede que no responda la pregunta, pero sugiere una alternativa válida y plantea un problema de diseño interesante. Por esta razón tengo +1.
Fergus en Londres
1

Su objetivo es ser una respuesta complementaria a Doc Brown, y también responder a los comentarios no respondidos de Dinaiz que todavía están relacionados con la Pregunta.

Lo que probablemente necesite es un marco para hacer DI. Tener jerarquías complejas no significa necesariamente un mal diseño, pero si tiene que inyectar un TimeFactory de abajo hacia arriba (de A a D) en lugar de inyectar directamente a D, entonces probablemente haya algo mal con la forma en que está haciendo la Inyección de dependencia.

Un singleton? No, gracias. Si solo necesita una isancia, haga que se comparta en el contexto de su aplicación (el uso de un contenedor de IoC para DI como Infector ++ solo requiere vincular TimeFactory como una sola istance), este es el ejemplo (C ++ 11 por cierto, pero entonces C ++. Tal vez mover a C ++ 11 ya? Obtiene la aplicación sin fugas de forma gratuita):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

Ahora, lo bueno de un contenedor de IoC es que no necesita pasar la fábrica de tiempo a D. Si su clase "D" necesita fábrica de tiempo, simplemente coloque fábrica de tiempo como parámetro de construcción para la clase D.

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

como ves, inyectas TimeFactory solo una vez. ¿Cómo usar "A"? Muy simple, cada clase se inyecta, se construye en general o se certifica con una fábrica.

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

cada vez que cree la clase A, se inyectará automáticamente (dependencia perezosa) todas las dependencias hasta D y D se inyectará con TimeFactory, por lo que al llamar solo 1 método tendrá lista su jerarquía completa (e incluso las jerarquías complejas se resuelven de esta manera eliminar MUCHO código de placa de caldera): no tiene que llamar "nuevo / eliminar" y eso es muy importante porque puede separar la lógica de la aplicación del código de pegamento.

D puede crear objetos de tiempo con información que solo D puede tener

Eso es fácil, su TimeFactory tiene un método de "creación", luego use una firma diferente "crear (parámetros)" y listo. Los parámetros que no son dependencias a menudo se resuelven de esta manera. Esto también elimina el deber de inyectar cosas como "cadenas" o "enteros" porque eso solo agrega placa de caldera adicional.

¿Quién crea a quién? El contenedor de IoC crea istances y fábricas, las fábricas crean el resto (las fábricas pueden crear diferentes objetos con parámetros arbitrarios, por lo que realmente no necesita un estado para las fábricas). Todavía puede usar las fábricas como envoltorios para el Contenedor de IoC: en general, Inyectar en el Contenedor de IoC es muy malo y es lo mismo que usar un localizador de servicios. Algunas personas resolvieron el problema envolviendo el contenedor de IoC con una fábrica (esto no es estrictamente necesario, pero tiene la ventaja de que el contenedor resuelve la jerarquía y que todas sus fábricas se vuelven aún más fáciles de mantener).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Tampoco abuses de la inyección de dependencia, los tipos simples pueden ser miembros de la clase o variables locales. Esto parece obvio, pero vi personas inyectando "std :: vector" solo porque había un marco DI que lo permitía. Recuerde siempre la ley de Demeter: "Inyecte solo lo que realmente necesita inyectar"

Desarrollador de juegos
fuente
Wow, no vi tu respuesta antes, lo siento! Muy informativo señor! Upvoted
Dinaiz
0

¿Tus clases A, B y C también necesitan crear instancias de tiempo o solo clase D? Si es solo clase D, entonces A y B no deberían saber nada sobre TimeFactory. Cree una instancia de TimeFactory dentro de la clase C y páselo a la clase D. Tenga en cuenta que "crear una instancia" no necesariamente quiero decir que la clase C tenga que ser responsable de crear instancias de TimeFactory. Puede recibir DClassFactory de la clase B, y DClassFactory sabe cómo crear una instancia de Time.

Una técnica que también uso a menudo cuando no tengo ningún marco DI es proporcionar dos constructores, uno que acepte una fábrica y otro que cree una fábrica predeterminada. El segundo generalmente tiene un acceso protegido / paquete, y se utiliza principalmente para pruebas unitarias.

rmaruszewski
fuente
-1

Implementé otro marco de inyección de dependencia de C ++, que recientemente se propuso para impulsar - https://github.com/krzysztof-jusiak/di - la biblioteca es menos macro (gratis), solo encabezado, C ++ 03 / C ++ 11 / C ++ 14 biblioteca que proporciona tipo seguro, tiempo de compilación, inyección de dependencia del constructor libre de macros.

krzychoo
fuente
3
Responder viejas preguntas sobre P.SE es una mala manera de publicitar su proyecto. Considere que hay decenas de miles de proyectos por ahí que pueden tener cierta relevancia para alguna pregunta. Si todos sus autores publicaran esto aquí, la calidad de respuesta del sitio se desplomaría.