Inyección de dependencias (DI) en aplicaciones c ++


8

Estoy jugando con la inyección de dependencia, pero no estoy seguro de hacerlo bien. Especialmente, no estoy seguro de cuál debería ser la forma correcta de crear clases con dependencias inyectadas.

Digamos que tengo una clase A que crea la clase B. La clase B depende de la clase C y la clase C depende de la clase D. ¿Quién debería ser responsable de crear la clase D?

  1. Podría ser de clase A. Sin embargo, en un sistema grande, la clase A podría terminar creando y ensamblando una gran cantidad de objetos.

  2. Una clase de generador separada que creará D, C y B. A usará esta clase de generador.

  3. Alguna otra opción.

Además, leí mucho sobre contenedores DI. Sin embargo, parece que no hay marcos principales para C ++. Además, si entiendo correctamente, la DI puede realizarse bien incluso sin contenedores. ¿Estoy en lo correcto?





... suponiendo, por supuesto, que realmente necesita un contenedor DI. La mayoría de las aplicaciones no.
Robert Harvey

¿Como se si necesito un contenedor?
Erik Sapir

Respuestas:


6

Digamos que tengo una clase A que crea la clase B. La clase B depende de la clase C y la clase C depende de la clase D. ¿Quién debería ser responsable de crear la clase D?

Estás saltando pasos. Considere un conjunto de convenciones optimizadas para acoplamiento flojo y seguridad de excepción. Las reglas son así:

  • R1: si A contiene una B, entonces el constructor de A recibe una B completamente construida (es decir, no "dependencias de construcción de B"). Del mismo modo, si la construcción de B requiere una C, recibirá una C, no las dependencias de C.

  • R2: si se requiere una cadena completa de objetos para construir un objeto, la construcción encadenada se extrae / automatiza dentro de una fábrica (función o clase).

Código (std :: mover llamadas omitidas por simplicidad):

struct D { int dummy; };
struct C { D d; };
struct B { C c; }

struct A { B make_b(C c) {return B{c}; };

En dicho sistema, "quién crea D" es irrelevante, porque cuando llama a make_b, necesita una C, no una D.

Codigo del cliente:

A a; // factory instance

// construct a B instance:
D d;
C c {d};
B = a.make_b(c);

Aquí, D es creado por el código del cliente. Naturalmente, si este código se repite más de una vez, es libre de extraerlo en una función (ver R2 arriba):

B make_b_from_d(D& d) // you should probably inject A instance here as well
{
    C c {d};
    A a;
    return a.make_b(c);
}

Existe una tendencia natural a omitir la definición de make_b (ignorar R1 ) y escribir el código directamente así:

struct D { int dummy; };
struct C { D d; };
struct B { C c; }

struct A { B make_b(D d) { C c; return B{c}; }; // make B from D directly

En este caso, tiene los siguientes problemas:

  • tienes un código monolítico; Si llega a una situación en el código del cliente donde necesita hacer una B a partir de una C existente, no puede usar make_b. Necesitará escribir una nueva fábrica, o la definición de make_b, y todo el código del cliente usando el antiguo make_b.

  • Su visión de las dependencias se confunde cuando mira la fuente: ahora, al mirar la fuente, puede pensar que necesita una instancia D, cuando de hecho puede necesitar una C.

Ejemplo:

void sub_optimal_solution(C& existent_c) {
    // you cannot create a B here using existent_C, because your A::make_b
    // takes a D parameter; B's construction doesn't actually need a D
    // but you cannot see that at all if you just have:
    // struct A { B make_b(D d); };
}
  • La omisión de struct A { B make_b(C c); }aumentará en gran medida el acoplamiento: ahora A necesita conocer las definiciones de B y C (en lugar de solo C). También tiene restricciones sobre cualquier código de cliente que use A, B, C y D, impuesto en su proyecto porque omitió un paso en la definición de un método de fábrica ( R1 ).

TLDR: En resumen, no pase la dependencia más externa a una fábrica, sino las más cercanas. Esto hace que su código sea robusto, fácilmente modificable y convierte la pregunta que planteó ("quién crea D") en una pregunta irrelevante para la implementación de make_b (porque make_b ya no recibe una D sino una dependencia más inmediata - C - y esto es inyectado como un parámetro de make_b).


1
La cuestión de debería crear D aún permanece. Todavía estoy tratando de entender qué parte del sistema es responsable de construir D
Erik Sapir


0

¿Quién debería ser responsable de crear la clase D?

Cada vez que aparece una pregunta como esa, indica una dependencia para inyectar . La idea es "delegar" la responsabilidad fuera del objeto que parece tener problemas para determinar cómo manejarlo.

Usando su ejemplo, dado que la clase A no parece poseer suficiente "conocimiento" para imaginar cómo crear D, invierte el control y lo expone como una dependencia necesaria para la clase A, al hacer que requiera una fábrica que sepa cómo crear instancias de clase D.


Tiene sentido, pero al final llegaré a la primera clase en el sistema (Principal) que no puede obtener ninguna dependencia. Parece que esta clase tendría que inicializar un montón de fábricas / objetos
Erik Sapir

Parece que la pregunta sobre cómo manejar la responsabilidad de la creación de la clase D está resuelta, ¿no? En cuanto a cómo las cosas "se conectan" en la primera clase en el sistema (Principal), esa sería una pregunta diferente , considere publicarlo por separado.
mosquito

En realidad, esa no es exactamente una pregunta diferente: la pregunta era quién es responsable de realizar todas las inicializaciones (nuevas operaciones). No estoy seguro de que merezca otras preguntas. En cualquier caso, me encantaría tener una respuesta a esta pregunta
Erik Sapir,


1
"Plataformas de software, devoluciones de llamada, programadores, bucles de eventos y la inyección de dependencias son ejemplos de patrones de diseño que siguen a la inversión de control principio ..." ( Wikipedia )
mosquito
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.