C ++: A classe deve possuir ou observar suas dependências?

16

Digamos que eu tenha uma classe Foobarque use (depende de) classe Widget. Nos bons dias Widgetantigos , você seria declarado como um campo Foobarou, 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 Foobare se Widgetunem. A solução é aparentemente simples: aplique Injeção de Dependências para tirar a construção de dependências da Foobarclasse. 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) {}
    (…)
}

Foobaralega que a propriedade exclusiva Widgeté passada a ele. Isso tem as seguintes vantagens:

  1. O impacto no desempenho é insignificante.
  2. É seguro, pois Foobarcontrola a vida útil Widgete garante que Widgetnão desapareça repentinamente.
  3. É seguro que Widgetnão vazará e será destruído adequadamente quando não for mais necessário.

No entanto, isso tem um custo:

  1. Ele coloca restrições sobre como as Widgetinstâncias podem ser usadas, por exemplo, nenhuma pilha alocada Widgetspode ser usada, nenhuma Widgetpode 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:

  1. Mais universal, pois permite compartilhar dependências.
  2. Mantém a segurança (pontos 2 e 3) da unique_ptrsolução.

Desvantagens:

  1. Desperdiça recursos quando nenhum compartilhamento está envolvido.
  2. 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:

  1. Tão simples quanto possível.
  2. Universal, aceita qualquer Widget .

Contras:

  1. Não é mais seguro.
  2. Introduz outra entidade responsável pela propriedade de ambos Foobare Widget.

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 Foobarsqualquer 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?

el.pescado
fonte
1
Desde o início, se você pode compilar no C ++ 11 e / ou nunca, ter um ponteiro simples como forma de manipular recursos em uma classe não é mais o caminho recomendado. Se a classe Foobar é o único proprietário do recurso e o recurso deve ser liberado, quando Foobar sai do escopo, std::unique_ptré o caminho a percorrer. Você pode usar std::move()para transferir a propriedade de um recurso do escopo superior para a classe.
Andy Andy
1
Como sei que esse 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.
el.pescado
1
@ el.pescado Também existe o mesmo problema em Java, a memória não é o único recurso. Também há arquivos e conexões, por exemplo. A maneira usual de resolver isso em Java é ter um contêiner que gerencia o ciclo de vida de todos os seus objetos contidos. Portanto, você não precisa se preocupar com isso nos objetos contidos em um contêiner de DI. Isso também pode funcionar para aplicativos c ++ se houver uma maneira de o contêiner de DI saber quando alternar para qual fase do ciclo de vida.
SpaceTrucker
3
Obviamente, você poderia fazê-lo da maneira antiga, sem toda a complexidade e ter um código que funcione e que seja muito mais fácil de manter. (Note-se que os meninos Java podem rir, mas eles estão sempre acontecendo sobre refatoração, assim que a dissociação não seja exatamente ajudando-os muito)
gbjbaanb
3
Além disso, ter outras pessoas rindo de você não é uma razão para ser como elas. Ouça os argumentos deles (exceto "porque nos disseram para") e implemente o DI, se isso traz benefícios tangíveis ao seu projeto. Nesse caso, pense no padrão como uma regra muito geral, que você deve aplicar de uma maneira específica ao projeto - todas as três abordagens (e todas as outras abordagens em potencial que você ainda não pensou) são válidas, dada a eles trazem um benefício geral (ou seja, têm mais vantagens do que desvantagens).

Respostas:

2

Eu pretendia escrever isso como um comentário, mas acabou sendo muito longo.

Como sei que esse 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.

Se você deve usar std::unique_ptr<Widget>ou std::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, como Foobar. Seguindo o princípio da DI, você precisará da Widgetinstância para injetá-la usando Foobaro construtor de s, ou seja, dentro de um dos Utilities::Factorymétodos, por exemplo createWidget(const std::vector<std::string>& params), você cria o Widget e o injeta no Foobarobjeto.

Agora você tem um Utilities::Factorymétodo, que criou o Widgetobjeto. 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 Foobarclasse, portanto, de fato, Foobarage como um controlador.

O controlador provavelmente fará uso de alguns de seus Widgete 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 Widgetespecificamente 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 de Widgetnenhum 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::moveon std::unique_ptrtransfere completamente a propriedade, por isso mesmo se você criar um object Aem um método e passá-lo para outro object Bcomo uma dependência, o object Bagora será responsável para o recurso de object Ae irá excluí-lo corretamente, quando object Bsai do escopo.

Andy
fonte
A ideia geral de propriedade e indicadores inteligentes é clara para mim. Apenas não me parece agradável com a idéia de DI. Injeção de Dependência é, para mim, dissociar a classe de suas dependências, mas, nesse caso, a classe ainda influencia como suas dependências são criadas.
el.pescado
Por exemplo, o problema que tenho unique_ptré o teste de unidade (o DI é anunciado como compatível com o teste): quero testar Foobar, criar Widget, passar Foobar, exercitar Foobare depois quero inspecionar Widget, mas, a menos que Foobarexponha de alguma forma, posso desde que foi reivindicado por Foobar.
el.pescado
@ el.pescado Você está misturando duas coisas. Você está misturando acessibilidade e propriedade. Só porque Foobarpossuir o recurso não significa, ninguém mais deve usá-lo. Você pode implementar um Foobarmétodo como Widget* 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 a Widgetclasse.
Andy Andy
Bom ponto, isso faz sentido.
el.pescado
1
Se você convertesse um shared_ptr em um unique_ptr, como você gostaria de garantir a unique_ness ???
Aconcagua
2

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.

Dominique McDonnell
fonte
1

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':

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

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

Aconcágua
fonte
1
No C ++, não use classes 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.
Andy As
@DavidPacker Hmm, com certeza, você está certo - eu silenciosamente assumiu a classe de ter algumas partes internas privadas, mas que não é nem visíveis nem mencionados em outros lugares ...
Aconcagua
1

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.

mattnewport
fonte
0

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:

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

O código do cliente pode então escrever:

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

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

utnapistim
fonte
1
Isso exclui o comportamento polimórfico, que limita severamente o valor agregado.
el.pescado
Verdade. Eu apenas pensei em mencioná-lo, pois é a forma que eu mais uso (eu só uso herança na minha codificação para comportamento polimórfico - não para reutilização de código, por isso não tenho muitos casos de necessidade de comportamento polimórfico) ..
utnapistim