Digamos que eu tenha uma classe A que crie a classe B. A classe B depende da classe C e a classe C depende da classe D. Quem deve ser responsável pela criação da classe D?
Você está pulando passos. Considere um conjunto de convenções otimizadas para acoplamento flexível e segurança de exceção. As regras são assim:
R1: se A contém um B, então o construtor de A recebe um B totalmente construído (isto é, não "dependências de construção de B"). Da mesma forma, se a construção de B exigir um C, ele receberá um C, não as dependências de C.
R2: se uma cadeia completa de objetos é necessária para construir um objeto, a construção encadeada é extraída / automatizada dentro de uma fábrica (função ou classe).
Código (chamadas std :: move omitidas por simplicidade):
struct D { int dummy; };
struct C { D d; };
struct B { C c; }
struct A { B make_b(C c) {return B{c}; };
Nesse sistema, "quem cria D" é irrelevante, porque quando você chama make_b, precisa de um C, não de um D.
Código do cliente:
A a; // factory instance
// construct a B instance:
D d;
C c {d};
B = a.make_b(c);
Aqui, D é criado pelo código do cliente. Naturalmente, se esse código for repetido mais de uma vez, você poderá extraí-lo em uma função (consulte R2 acima):
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 uma tendência natural para pular a definição de make_b (ignorar R1 ) e escrever o código diretamente assim:
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
Nesse caso, você tem os seguintes problemas:
você tem código monolítico; Se você encontrar uma situação no código do cliente em que precisa criar um B a partir de um C existente, não poderá usar o make_b. Você precisará escrever uma nova fábrica ou a definição de make_b e todo o código do cliente usando o antigo make_b.
Sua visão das dependências é confusa quando você olha para a fonte: Agora, olhando para a fonte, você pensa que precisa de uma instância D, quando na verdade você pode apenas precisar de um C.
Exemplo:
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); };
}
- A omissão de
struct A { B make_b(C c); }
aumentará muito o acoplamento: agora A precisa conhecer as definições de B e C (em vez de apenas C). Você também tem restrições em qualquer código de cliente usando A, B, C e D, impostas ao seu projeto porque pulou uma etapa na definição de um método de fábrica ( R1 ).
TLDR: Em resumo, não passe a dependência mais externa para uma fábrica, mas as mais próximas. Isso torna seu código robusto, facilmente alterável e transforma a pergunta que você fez ("quem cria D") em uma pergunta irrelevante para a implementação do make_b (porque o make_b não recebe mais um D, mas uma dependência mais imediata - C - e isso é injetado como um parâmetro de make_b).