Injeção de dependência ; boas práticas para reduzir o código padrão

25

Eu tenho uma pergunta simples e nem tenho certeza se ela tem uma resposta, mas vamos tentar. Estou codificando em C ++ e usando injeção de dependência para evitar o estado global. Isso funciona muito bem e eu não corro em comportamentos inesperados / indefinidos com muita frequência.

No entanto, percebo que, conforme meu projeto cresce, estou escrevendo muito código que considero padrão. Pior: o fato de haver mais código padrão, do que o código real, às vezes é difícil de entender.

Nada supera um bom exemplo, então vamos:

Eu tenho uma classe chamada TimeFactory que cria objetos Time.

Para mais detalhes (não tenho certeza se é relevante): os objetos Time são bastante complexos porque o Time pode ter formatos diferentes, e a conversão entre eles não é linear nem direta. Cada "Horário" contém um Sincronizador para manipular conversões e, para garantir que eles tenham o mesmo sincronizador inicializado corretamente, eu uso um TimeFactory. O TimeFactory tem apenas uma instância e é de aplicação ampla, portanto se qualificaria para singleton, mas, como é mutável, não quero transformá-lo em singleton

No meu aplicativo, muitas classes precisam criar objetos Time. Às vezes, essas classes são profundamente aninhadas.

Digamos que eu tenho uma classe A que contém instâncias da classe B e assim por diante até a classe D. A classe D precisa criar objetos Time.

Na minha implementação ingênua, passo o TimeFactory para o construtor da classe A, que o passa para o construtor da classe B e assim por diante até a classe D.

Agora, imagine que eu tenho algumas classes como TimeFactory e duas hierarquias de classe como a acima: eu perco toda a flexibilidade e legibilidade que devo usar usando injeção de dependência.

Estou começando a me perguntar se não há uma falha de design importante no meu aplicativo ... Ou isso é um mal necessário do uso de injeção de dependência?

O que você acha ?

Dinaiz
fonte
5
Postei essa pergunta para programadores em vez de stackoverflow porque, como eu entendi, programadores é mais para missões relacionadas ao design e stackoverflow é para perguntas "quando você está na frente do seu compilador". Desculpe antecipadamente se estou errado.
Dinaiz
2
aspecto de programação orientada, também é bom para essas tarefas - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov
A classe A é uma fábrica para as classes B, C e D? Se não, por que ele cria instâncias deles? Se apenas a classe D precisa de objetos Time, por que você injetaria qualquer outra classe no TimeFactory? A classe D realmente precisa do TimeFactory ou apenas uma instância do Time pode ser injetada?
simoraman
D precisa criar objetos Time, com informações que apenas D deveria ter. Eu tenho que pensar no resto do seu comentário. Pode haver um problema no "quem está criando que" no meu código
Dinaiz
"quem está criando quem" é resolvido pelos contêineres IoC, veja minha resposta para detalhes .. "quem está criando quem" é uma das melhores perguntas que você pode fazer ao usar a Injeção de Dependência.
GameDeveloper 21/07

Respostas:

23

No meu aplicativo, muitas classes precisam criar objetos Time

Parece que sua Timeclasse é um tipo de dados muito básico que deve pertencer à "infraestrutura geral" do seu aplicativo. O DI não funciona bem para essas classes. Pense no que significa se uma classe como stringtivesse que ser injetada em todas as partes do código que usa strings, e você precisaria usar uma stringFactorycomo a única possibilidade de criar novas strings - a legibilidade do seu programa diminuiria em uma ordem de magnitude.

Então, minha sugestão: não use DI para tipos de dados gerais como Time. Escreva testes de unidade para a Timeprópria classe e, quando terminar, use-a em qualquer lugar do seu programa, assim como a stringclasse, ou uma vectorclasse ou qualquer outra classe da biblioteca padrão. Use DI para componentes que realmente devem ser dissociados um do outro.

Doc Brown
fonte
Você acertou completamente. "a legibilidade do seu programa diminuiria em uma ordem de magnitude." : foi exatamente o que aconteceu sim. Desde que eu ainda queira usar uma fábrica, não é tão ruim torná-lo um singleton então?
Dinaiz 6/08/12
3
@Dinaiz: a idéia é aceitar um acoplamento rígido entre Timee as outras partes do seu programa. Então você pode aceitar também aceitar acoplamentos apertados TimeFactory. O que eu evitaria, no entanto, é ter um único TimeFactoryobjeto global com um estado (por exemplo, informações de localidade ou algo parecido) - que poderia causar efeitos colaterais desagradáveis ​​ao seu programa e dificultar o teste geral e a reutilização. Torne-o apátrida ou não o use como um singleton.
Doc Brown
Na verdade, ele precisa ter um estado; portanto, quando você diz "Não use como um singleton", você quer dizer "continue usando injeção de dependência, ou seja, passando a instância para todos os objetos que precisam", certo?
Dinaiz
1
@ Dinaiz: honestamente, isso depende - do tipo de estado, do tipo e do número de partes do seu programa usando Timee TimeFactory, do grau de "capacidade de evolução" necessária para futuras extensões relacionadas TimeFactorye assim por diante.
Doc Brown
OK, vou tentar manter a dependência injetar, mas ter um design mais inteligente que não exija tanto clichê! muito obrigado doc '
Dinaiz 07/08/2012
4

O que você quer dizer com "Eu perco toda a flexibilidade e legibilidade que devo usar com injeção de dependência" - DI não é sobre legibilidade. Trata-se de dissociar a dependência entre objetos.

Parece que você tem Classe A criando Classe B, Classe B criando Classe C e Classe C criando Classe D.

O que você deve ter é Classe B injetada na Classe A. Classe C injetada na Classe B. Classe D injetada na Classe C.

C0deAttack
fonte
DI pode ser sobre legibilidade. Se você tem uma variável global "magicamente" aparecendo no meio de um método, não ajuda a entender o código (do ponto de vista de manutenção). De onde é essa variável? Quem é o dono? Quem o inicializa? E assim por diante ... Para o seu segundo ponto, você provavelmente está certo, mas acho que não se aplica ao meu caso. Obrigado por tomar o tempo para responder
Dinaiz 6/08/2012
As DI aumentam a legibilidade principalmente porque "new / delete" será magicamente retirado do seu código. Se você não precisa se preocupar com o código de alocação dinâmica, é mais fácil ler.
GameDeveloper 21/07
Também esqueça de dizer que isso torna o código um pouco mais forte, porque não pode mais fazer suposições sobre "como" uma dependência é criada, mas "apenas precisa". E isso torna a classe mais fácil de manter .. ver a minha resposta
GameDeveloper
3

Não sei por que você não quer transformar sua fábrica de tempo em um singleton. Se houver apenas uma instância em todo o aplicativo, é de fato um singleton.

Dito isto, é muito perigoso compartilhar um objeto mutável, exceto se ele estiver devidamente protegido por blocos de sincronização; nesse caso, não há razão para não ser um singleton.

Se você deseja fazer a injeção de dependência, consulte estruturas de injeção de primavera ou outras, o que permitiria atribuir automaticamente parâmetros usando uma anotação

molyss
fonte
A primavera é para java e eu uso C ++. Mas por que duas pessoas votaram contra você? Há pontos em seu post eu discordo, mas ainda assim ...
Dinaiz
Suponho que o voto negativo não deveria responder à pergunta em si, talvez tão bem quanto à menção de Spring. No entanto, a sugestão de usar um Singleton era válida em minha mente e pode não responder à pergunta - mas sugere uma alternativa válida e traz uma questão interessante de design. Por esse motivo, marquei +1.
Fergus In London
1

Isso visa ser uma resposta complementar ao Doc Brown e também responder a comentários não respondidos de Dinaiz que ainda estão relacionados à Questão.

O que você provavelmente precisa é de uma estrutura para executar DI. Ter hierarquias complexas não significa necessariamente um design ruim, mas se você precisar injetar um TimeFactory de baixo para cima (de A a D) em vez de injetar diretamente em D, provavelmente há algo errado com o modo como você está injetando dependências.

Um singleton? Não, obrigado. Se você precisar de apenas uma instância para compartilhá-lo no contexto de seu aplicativo (o uso de um contêiner de IoC para DI, como o Infector ++, requer apenas a ligação do TimeFactory como uma única instância), eis o exemplo (C ++ 11, a propósito, mas C ++.) para C ++ 11? Você obtém o aplicativo Leak-Free gratuitamente):

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>();

Agora, o ponto positivo de um contêiner de IoC é que você não precisa passar o time factory até 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

quando você injeta o TimeFactory apenas uma vez. Como usar "A"? Muito simples, todas as classes são injetadas, construídas em geral ou com uma 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

toda vez que você criar a classe A, ela será automaticamente injetada (atenuação lenta) com todas as dependências até D e D serão injetadas com TimeFactory, portanto, chamando apenas 1 método, você terá sua hierarquia completa pronta (e até hierarquias complexas são resolvidas dessa maneira) removendo MUITO código da placa da caldeira): Você não precisa chamar "novo / excluir" e isso é muito importante porque é possível separar a lógica do aplicativo do código da cola.

D pode criar objetos Time com informações que apenas D pode ter

Isso é fácil, o TimeFactory possui um método "create", basta usar uma assinatura diferente "create (params)" e pronto. Parâmetros que não são dependências geralmente são resolvidos dessa maneira. Isso também remove o dever de injetar coisas como "strings" ou "inteiros", porque isso apenas adiciona uma placa de caldeira extra.

Quem cria quem? O contêiner de IoC cria instâncias e fábricas, as fábricas criam o restante (fábricas podem criar objetos diferentes com parâmetros arbitrários, para que você realmente não precise de um estado para fábricas). Você ainda pode usar as fábricas como invólucros para o contêiner de IoC: geralmente o Injectin no contêiner de IoC é muito ruim e é o mesmo que usar um localizador de serviço. Algumas pessoas resolveram o problema envolvendo o Container IoC com uma fábrica (isso não é estritamente necessário, mas tem a vantagem de que a hierarquia é resolvida pelo Container e todas as suas fábricas ficam ainda mais fáceis de manter).

//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); 
}

Além disso, não abuse da injeção de dependência, tipos simples podem ser apenas membros da classe ou variáveis ​​de escopo local. Isso parece óbvio, mas vi pessoas injetando "std :: vector" apenas porque havia uma estrutura de DI que permitia isso. Lembre-se sempre da lei de Deméter: "Injete apenas o que você realmente precisa injetar"

Desenvolvedor de jogos
fonte
Uau, eu não vi sua resposta antes, desculpe! Senhor muito informativo! Voto votado
Dinaiz 29/07
0

Suas classes A, B e C também precisam criar instâncias de horário ou apenas classe D? Se for apenas classe D, A e B não devem saber nada sobre o TimeFactory. Crie uma instância do TimeFactory dentro da classe C e passe-a para a classe D. Observe que "criar uma instância" não significa necessariamente que a classe C seja responsável por instanciar o TimeFactory. Ele pode receber DClassFactory da classe B e o DClassFactory sabe como criar a instância Time.

Uma técnica que também uso frequentemente quando não tenho nenhuma estrutura DI está fornecendo dois construtores, um que aceita uma fábrica e outro que cria uma fábrica padrão. O segundo geralmente possui um acesso protegido / pacote e é usado principalmente para testes de unidade.

rmaruszewski
fonte
-1

Eu implementei ainda outra estrutura de injeção de dependência de C ++, que recentemente foi proposta para impulsionar - https://github.com/krzysztof-jusiak/di - library is macro less (free), only header, C ++ 03 / C ++ 11 / C ++ 14, fornecendo injeção de dependência de construtor segura, sem tempo de compilação e sem tipo de tipo.

krzychoo
fonte
3
Responder a perguntas antigas sobre o P.SE é uma maneira ruim de anunciar seu projeto. Considere que existem dezenas de milhares de projetos por aí que podem ter alguma relevância para alguma pergunta. Se todos os autores os publicassem aqui, a qualidade da resposta do site diminuiria.