Digamos que eu tenha uma classe Foobar
que use (depende de) classe Widget
. Nos bons dias Widget
antigos , você seria declarado como um campo Foobar
ou, talvez, como um ponteiro inteligente se fosse necessário um comportamento polimórfico, e seria inicializado no construtor:
class Foobar {
Widget widget;
public:
Foobar() : widget(blah blah blah) {}
// or
std::unique_ptr<Widget> widget;
public:
Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
(…)
};
E estaríamos prontos e prontos. Infelizmente, hoje em dia, as crianças Java riem de nós quando a veem, e com razão, enquanto se unem Foobar
e se Widget
unem. A solução é aparentemente simples: aplique Injeção de Dependências para tirar a construção de dependências da Foobar
classe. Mas então, o C ++ nos obriga a pensar na propriedade das dependências. Três soluções vêm à mente:
Ponteiro exclusivo
class Foobar {
std::unique_ptr<Widget> widget;
public:
Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
(…)
}
Foobar
alega que a propriedade exclusiva Widget
é passada a ele. Isso tem as seguintes vantagens:
- O impacto no desempenho é insignificante.
- É seguro, pois
Foobar
controla a vida útilWidget
e garante queWidget
não desapareça repentinamente. - É seguro que
Widget
não vazará e será destruído adequadamente quando não for mais necessário.
No entanto, isso tem um custo:
- Ele coloca restrições sobre como as
Widget
instâncias podem ser usadas, por exemplo, nenhuma pilha alocadaWidgets
pode ser usada, nenhumaWidget
pode ser compartilhada.
Ponteiro compartilhado
class Foobar {
std::shared_ptr<Widget> widget;
public:
Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
(…)
}
Provavelmente é o equivalente mais próximo de Java e outras linguagens de coleta de lixo. Vantagens:
- Mais universal, pois permite compartilhar dependências.
- Mantém a segurança (pontos 2 e 3) da
unique_ptr
solução.
Desvantagens:
- Desperdiça recursos quando nenhum compartilhamento está envolvido.
- Ainda requer alocação de heap e não permite objetos alocados a pilha.
Ponteiro de observação simples
class Foobar {
Widget *widget;
public:
Foobar(Widget *w) : widget(w) {}
(…)
}
Coloque o ponteiro bruto dentro da classe e transfira o ônus da propriedade para outra pessoa. Prós:
- Tão simples quanto possível.
- Universal, aceita qualquer
Widget
.
Contras:
- Não é mais seguro.
- Introduz outra entidade responsável pela propriedade de ambos
Foobar
eWidget
.
Alguma metaprogramação de modelos loucos
A única vantagem que consigo pensar é que eu seria capaz de ler todos os livros pelos quais não encontrei tempo enquanto meu software está em execução;)
Eu me inclino para a terceira solução, como ela é mais universal, algo precisa ser gerenciado de Foobars
qualquer maneira, portanto, gerenciarWidgets
é uma mudança simples. No entanto, o uso de ponteiros brutos me incomoda, por outro lado, a solução de ponteiro inteligente parece errada para mim, pois faz o consumidor de dependência restringir como essa dependência é criada.
Estou esquecendo de algo? Ou apenas Injeção de Dependência em C ++ não é trivial? A classe deve possuir suas dependências ou apenas observá-las?
fonte
std::unique_ptr
é o caminho a percorrer. Você pode usarstd::move()
para transferir a propriedade de um recurso do escopo superior para a classe.Foobar
é o único proprietário? No caso antigo, é simples. Mas o problema com o DI, a meu ver, é que ele separa a classe da construção de suas dependências, mas também a propriedade dessas dependências (como a propriedade está ligada à construção). Em ambientes de coleta de lixo, como Java, isso não é um problema. Em C ++, é isso.Respostas:
Eu pretendia escrever isso como um comentário, mas acabou sendo muito longo.
Se você deve usar
std::unique_ptr<Widget>
oustd::shared_ptr<Widget>
, isso depende de você decidir e provém de sua funcionalidade.Vamos supor que você tenha um
Utilities::Factory
, responsável pela criação de seus blocos, comoFoobar
. Seguindo o princípio da DI, você precisará daWidget
instância para injetá-la usandoFoobar
o construtor de s, ou seja, dentro de um dosUtilities::Factory
métodos, por exemplocreateWidget(const std::vector<std::string>& params)
, você cria o Widget e o injeta noFoobar
objeto.Agora você tem um
Utilities::Factory
método, que criou oWidget
objeto. Isso significa que o método deveria ser responsável por sua exclusão? Claro que não. Está lá apenas para fazer de você a instância.Vamos imaginar, você está desenvolvendo um aplicativo, que terá várias janelas. Cada janela é representada usando a
Foobar
classe, portanto, de fato,Foobar
age como um controlador.O controlador provavelmente fará uso de alguns de seus
Widget
e você deve se perguntar:Se eu for a essa janela específica no meu aplicativo, precisarei desses Widgets. Esses widgets são compartilhados entre outras janelas de aplicativos? Nesse caso, eu provavelmente não deveria criá-los novamente, porque eles sempre terão a mesma aparência, porque são compartilhados.
std::shared_ptr<Widget>
é o caminho a percorrer.Você também tem uma janela de aplicativo, na qual há um
Widget
especificamente vinculado a essa janela, o que significa que ela não será exibida em nenhum outro lugar. Portanto, se você fechar a janela, não precisará mais deWidget
nenhum lugar do aplicativo ou, pelo menos, da instância dele.É aí que
std::unique_ptr<Widget>
vem reivindicar seu trono.Atualizar:
Eu realmente não concordo com @DominicMcDonnell , sobre a questão da vida. Chamando
std::move
onstd::unique_ptr
transfere completamente a propriedade, por isso mesmo se você criar umobject A
em um método e passá-lo para outroobject B
como uma dependência, oobject B
agora será responsável para o recurso deobject A
e irá excluí-lo corretamente, quandoobject B
sai do escopo.fonte
unique_ptr
é o teste de unidade (o DI é anunciado como compatível com o teste): quero testarFoobar
, criarWidget
, passarFoobar
, exercitarFoobar
e depois quero inspecionarWidget
, mas, a menos queFoobar
exponha de alguma forma, posso desde que foi reivindicado porFoobar
.Foobar
possuir o recurso não significa, ninguém mais deve usá-lo. Você pode implementar umFoobar
método comoWidget* getWidget() const { return this->_widget.get(); }
, que retornará o ponteiro bruto com o qual você pode trabalhar. Você pode usar esse método como uma entrada para seus testes de unidade, quando quiser testar aWidget
classe.Eu usaria um ponteiro de observação na forma de uma referência. Ele fornece uma sintaxe muito melhor quando você o usa e tem a vantagem semântica de que não implica propriedade, o que um ponteiro simples pode.
O maior problema com essa abordagem é a vida útil. Você precisa garantir que a dependência seja construída antes e destruída após a sua classe dependente. Não é um problema simples. O uso de ponteiros compartilhados (como armazenamento de dependência e em todas as classes que dependem dele, opção 2 acima) pode remover esse problema, mas também apresenta o problema de dependências circulares que também não são triviais e, na minha opinião, são menos óbvias e, portanto, mais difíceis de resolver. detectar antes que cause problemas. É por isso que prefiro não fazê-lo de forma automática e manual, gerando a vida útil e a ordem de construção. Também vi sistemas que usam uma abordagem de modelo leve, que construiu uma lista de objetos na ordem em que foram criados e os destruiu em ordem oposta, não era infalível, mas tornou as coisas muito mais simples.
Atualizar
A resposta de David Packer me fez pensar um pouco mais sobre a questão. A resposta original é verdadeira para dependências compartilhadas em minha experiência, que é uma das vantagens da injeção de dependência. Você pode ter várias instâncias usando a instância de uma dependência. Se, no entanto, sua classe precisa ter sua própria instância de uma dependência específica, então
std::unique_ptr
é a resposta correta.fonte
Primeiro de tudo - este é C ++, não Java - e aqui muitas coisas são diferentes. O pessoal do Java não tem esses problemas sobre propriedade, pois há uma coleta automática de lixo que resolve esses problemas.
Segundo: Não há resposta geral para essa pergunta - depende de quais são os requisitos!
Qual é o problema do acoplamento FooBar e Widget? O FooBar deseja usar um Widget, e se toda instância do FooBar sempre tiver seu próprio e o mesmo Widget, deixe-o acoplado ...
Em C ++, você pode até fazer coisas "estranhas" que simplesmente não existem em Java, por exemplo, ter um construtor de modelos variado (bem, existe uma ... notação em Java, que também pode ser usada em construtores, mas isso é apenas açúcar sintático para ocultar uma matriz de objetos, que na verdade não tem nada a ver com modelos variados reais!) - categoria 'Alguma metaprogramação maluca de modelos':
Gosto disso?
Claro, há são razões quando você quer ou necessidade de dissociar ambas as classes - por exemplo, se você precisa criar os widgets em um tempo muito antes de qualquer instância FooBar existe, se você quer ou necessidade de reutilizar os widgets, ..., ou simplesmente porque para o problema atual é mais apropriado (por exemplo, se o Widget é um elemento da GUI e o FooBar deve / pode / não deve ser um).
Então, voltamos ao segundo ponto: nenhuma resposta geral. Você precisa decidir qual é a solução mais apropriada para o problema real . Eu gosto da abordagem de referência de DominicMcDonnell, mas ela só pode ser aplicada se a propriedade não for tomada pelo FooBar (bem, na verdade, você poderia, mas isso implica um código muito, muito sujo ...). Além disso, entro na resposta de David Packer (a que deveria ser escrita como comentário - mas, de qualquer maneira, uma boa resposta).
fonte
class
es com nada além de métodos estáticos, mesmo que seja apenas uma fábrica, como no seu exemplo. Isso viola os princípios OO. Use namespaces e agrupe suas funções usando-os.Você está perdendo pelo menos mais duas opções disponíveis em C ++:
Uma é usar a injeção de dependência 'estática', onde suas dependências são parâmetros de modelo. Isso oferece a opção de manter suas dependências por valor e ainda permitir a injeção de dependência de tempo de compilação. Os contêineres STL usam essa abordagem para alocadores e funções de comparação e hash, por exemplo.
Outra é pegar objetos polimórficos por valor usando uma cópia profunda. A maneira tradicional de fazer isso é usar um método de clone virtual, outra opção que se tornou popular é usar o apagamento de tipo para criar tipos de valor que se comportam polimorficamente.
Qual opção é a mais apropriada realmente depende do seu caso de uso, é difícil dar uma resposta geral. Se você precisar apenas de polimorfismo estático, eu diria que os modelos são o caminho mais C ++ a seguir.
fonte
Você ignorou uma quarta resposta possível, que combina o primeiro código que você postou (os "bons e velhos dias" de armazenar um membro por valor) com a injeção de dependência:
O código do cliente pode então escrever:
A representação do acoplamento entre objetos não deve ser feita (puramente) nos critérios que você listou (ou seja, não se baseia puramente no "que é melhor para o gerenciamento seguro da vida útil").
Você também pode usar critérios conceituais ("um carro tem quatro rodas" vs. "um carro usa quatro rodas que o motorista deve levar").
Você pode ter critérios impostos por outras APIs (se o que você obtém de uma API é um wrapper personalizado ou um std :: unique_ptr, por exemplo, as opções no código do cliente também são limitadas).
fonte