Injetando dependências (DI) em aplicativos c ++


8

Estou brincando com injeção de dependência, mas não tenho certeza se estou fazendo certo. Especialmente, não tenho certeza de qual deve ser a maneira correta de criar classes com dependências injetadas.

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?

  1. Pode ser a classe A. No entanto, em um sistema grande, a classe A pode acabar criando e montando um número muito grande de objetos.

  2. Uma classe de construtor separada que criará D, C e B. A usará essa classe de construtor.

  3. Alguma outra opção.

Além disso, eu li muito sobre contêineres DI. No entanto, parece que não há grandes estruturas para C ++. Além disso, se eu entendi direito, o DI pode ser executado bem, mesmo sem contêineres. Estou correcto?





... supondo, é claro, que você realmente precise de um contêiner DI. A maioria dos aplicativos não.
22414 Robert Harvey

Como sei se preciso de um contêiner?
Erik Sapir 26/03

Respostas:


6

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


1
A questão de deveria criar D ainda permanece. Eu ainda estou tentando entender qual parte do sistema é responsável por construto D
Erik Sapir


0

Quem deve ser responsável pela criação da classe D?

Toda vez que uma pergunta como essa aparece, indica uma dependência para injetar . A idéia toda é "delegar" a responsabilidade fora do objeto que parece estar tendo problemas para descobrir como lidar com isso.

Usando seu exemplo, como a classe A parece não possuir "conhecimento" suficiente para descobrir como criar D, você inverte o controle e o expõe como uma dependência necessária para a classe A, exigindo que uma fábrica saiba como criar instâncias da classe D.


Faz sentido, mas no final chegarei à primeira classe do sistema (Principal) que não pode obter nenhuma dependência. Parece que esta classe teria que inicializar um monte de fábricas / objets
Erik Sapir

Parece que a pergunta sobre como lidar com a responsabilidade pela criação da classe D está resolvida, não é? Quanto à forma como as coisas "ligam" na primeira classe do sistema (Principal), isso seria uma pergunta diferente , considere publicá-la separadamente.
precisa

Na verdade, essa não é exatamente uma pergunta diferente - a questão era quem é responsável por executar todas as inicializações (novas operações). Não tenho certeza se merece outras perguntas. Em qualquer caso, eu realmente gostaria de aqui uma resposta a esta pergunta
Erik Sapir


1
"Estruturas de software, retornos de chamada, agendadores, loops de eventos e injeção de dependência são exemplos de padrões de design que seguem a inversão do princípio de controle ..." ( Wikipedia )
gnat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.