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 ?
fonte
Respostas:
Parece que sua
Time
classe é 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 comostring
tivesse que ser injetada em todas as partes do código que usa strings, e você precisaria usar umastringFactory
como 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 aTime
própria classe e, quando terminar, use-a em qualquer lugar do seu programa, assim como astring
classe, ou umavector
classe ou qualquer outra classe da biblioteca padrão. Use DI para componentes que realmente devem ser dissociados um do outro.fonte
Time
e as outras partes do seu programa. Então você pode aceitar também aceitar acoplamentos apertadosTimeFactory
. O que eu evitaria, no entanto, é ter um únicoTimeFactory
objeto 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.Time
eTimeFactory
, do grau de "capacidade de evolução" necessária para futuras extensões relacionadasTimeFactory
e assim por diante.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.
fonte
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
fonte
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):
Agora, o ponto positivo de um contêiner de IoC é que você não precisa passar o time factory até D.
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.
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.
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).
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"
fonte
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.
fonte
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.
fonte