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).