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

15

Digamos que eu tenha uma classe Foobar que usa (depende da) classe Widget . Em bons dias, Widget wolud ser declarado como um campo em Foobar , ou talvez como um ponteiro inteligente se o comportamento polimórfico fosse necessário, 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 todos prontos e prontos. Infelizmente, hoje em dia, os garotos Java vão rir de nós quando o virem e por direito, já que acopla Foobar e Widget juntos. A solução é aparentemente simples: aplique a Injeção de Dependência para remover a construção de dependências da classe Foobar . Mas, em seguida, o C ++ nos obriga a pensar sobre a propriedade de 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 reivindica a propriedade exclusiva de Widget que é passado para ela. Isso tem as seguintes vantagens:

  1. O impacto no desempenho é insignificante.
  2. É seguro, pois Foobar controla o tempo de vida de seu Widget , portanto, garante que Widget não desapareça de repente.
  3. É seguro que Widget não vazará e será devidamente destruído quando não for mais necessário.

No entanto, isso tem um custo:

  1. Ele impõe restrições sobre como Widget instâncias podem ser usadas, por exemplo não é possível usar Widgets alocado em pilha, não é possível compartilhar Widget .

Ponteiro compartilhado

class Foobar {
    std::shared_ptr<Widget> widget;
    public:
    Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
    (…)
}

Este é provavelmente o equivalente mais próximo de Java e outros idiomas coletados pelo garbage. Vantagens:

  1. Mais universal, pois permite o compartilhamento de dependências.
  2. Mantém a segurança (pontos 2 e 3) de unique_ptr solution.

Desvantagens:

  1. desperdiça recursos quando não há compartilhamento envolvido.
  2. Ainda requer alocação de heap e não permite objetos alocados na pilha.

Simples ponteiro de observação

class Foobar {
    Widget *widget;
    public:
    Foobar(Widget *w) : widget(w) {}
    (…)
}

Colocar ponteiro bruto dentro da classe e transferir o ônus da propriedade para outra pessoa. Prós:

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

Contras:

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

Algum metaprogramação de modelos malucos

A única vantagem que posso pensar é que eu seria capaz de ler todos os livros que eu não encontrei tempo enquanto meu software está funcionando;)

Eu me inclino para a terceira solução, como é mais universal, algo tem que gerenciar Foobars de qualquer maneira, então gerenciar Widgets é 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 eles fazem com que o consumidor da dependência restrinja como essa dependência é criada.

Estou faltando alguma coisa? Ou é apenas Injeção de Dependência em C ++ não é trivial? Deve a classe possuir suas dependências ou apenas observá-las?

    
por el.pescado 07.09.2015 / 22:41
fonte

5 respostas

2

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

How I know that Foobar is the only owner? In the old case it's simple. But the problem with DI, as I see it, is as it decouples class from construction of its dependencies, it also decouples it from ownership of those dependencies (as ownership is tied to construction). In garbage collected environments such as Java, that's not a problem. In C++, this is.

Se você deve usar std::unique_ptr<Widget> ou std::shared_ptr<Widget> , isso depende de você decidir e vir 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 DI, você precisará da instância Widget , para injetá-lo usando o construtor Foobar , ou seja, dentro de um método Utilities::Factory , por exemplo createWidget(const std::vector<std::string>& params) , você cria o Widget e o injetará no objeto Foobar .

Agora você tem um método Utilities::Factory , que criou o objeto Widget . Isso significa que o método deveria ser responsável por sua exclusão? Claro que não. Está lá apenas para fazer você a instância.

Vamos imaginar, você está desenvolvendo um aplicativo, que terá várias janelas. Cada janela é representada usando a classe Foobar , então, de fato, o Foobar age como um controlador.

O controlador provavelmente usará alguns dos seus Widget s e você terá que se perguntar:

Se eu for nessa janela específica do meu aplicativo, precisarei desses Widgets. Esses widgets são compartilhados entre outras janelas do aplicativo? Se assim for, eu provavelmente não deveria estar criando tudo de novo, porque eles sempre serão iguais, porque eles são compartilhados.

std::shared_ptr<Widget> é o caminho a percorrer.

Você também tem uma janela de aplicativo, onde há um Widget especificamente vinculado a essa única janela, o que significa que ela não será exibida em nenhum outro lugar. Então, se você fechar a janela, não precisará mais do Widget em qualquer lugar de sua aplicação, ou pelo menos de sua instância.

É aí que std::unique_ptr<Widget> vem reivindicar seu trono.

Atualização:

Eu realmente não concordo com o @DominicMcDonnell , sobre a questão da vida. Chamar std::move on std::unique_ptr transfere completamente a propriedade, por isso, mesmo se você criar um object A em um método e passá-lo para outro object B como dependência, o object B será agora responsável pelo recurso de object A e irá apagá-lo corretamente, quando object B sair do escopo.

    
por 08.09.2015 / 08:46
fonte
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 não implicar em propriedade, o que um ponteiro simples pode fazer.

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 sua classe dependente. Não é um problema simples. Usando ponteiros compartilhados (como armazenamento de dependência e em todas as classes que dependem dele, a opção 2 acima) pode remover esse problema, mas também introduz o problema de dependências circulares que também são não-triviais e, na minha opinião, menos óbvias e, portanto, mais difíceis detectar antes que cause problemas. É por isso que eu prefiro não fazer isso automaticamente e gerenciar manualmente as vidas úteis e a ordem de construção. Eu também vi sistemas que usam uma abordagem de modelo de luz que construiu uma lista de objetos na ordem em que foram criados e os destruiu em ordem oposta, não era infalível, mas tornava 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 turma precisar ter sua própria instância de uma dependência específica, então std::unique_ptr é a resposta correta.

    
por 08.09.2015 / 01:14
fonte
1

Primeiro de tudo - isso é C ++, não Java - e aqui muitas coisas são diferentes. O pessoal de Java não tem esses problemas sobre propriedade, pois há uma coleta automática de lixo que resolve os problemas para eles.

Segundo: Não há uma resposta geral para esta questão - depende de quais são os requisitos!

Qual é o problema do acoplamento FooBar e Widget? O FooBar quer usar um Widget, e se cada instância do FooBar sempre tiver seu próprio Widget e o mesmo, deixe-o acoplado ...

Em C ++, você pode até fazer coisas "estranhas" que simplesmente não existem em Java, e. g. ter um construtor de modelo variádico (bem, existe uma ... notação em Java, que pode ser usada em construtores, também, mas isso é apenas açúcar sintático para esconder uma matriz de objetos, que na verdade não tem nada a ver com modelos variádicos reais! ) - categoria "Alguma metaprogramação de modelos malucos":

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

Gosta?

É claro que existem razões quando você quer ou precisa separar ambas as classes - e. g. se você precisar criar os widgets de uma vez antes de existir qualquer ocorrência de FooBar, se você quiser ou precisar reutilizar os widgets, ..., ou simplesmente porque o problema atual é mais apropriado (por exemplo, se Widget for um elemento GUI e FooBar deve / pode / não deve ser um).

Então, voltamos ao segundo ponto: Nenhuma resposta geral. Você precisa decidir o que - para o problema real - é a solução mais apropriada. Eu gosto da abordagem de referência do DominicMcDonnell, mas ela só pode ser aplicada se a posse não for tomada pelo FooBar (bem, na verdade, você poderia, mas isso implica código muito, muito sujo ...). Além disso, eu me junto à resposta de David Packer (aquela que deveria ser escrita como comentário - mas uma boa resposta, de qualquer forma).

    
por 10.09.2015 / 03:09
fonte
1

Você está faltando pelo menos mais duas opções disponíveis em C ++:

Um é usar a injeção de dependência 'estática' onde suas dependências são parâmetros de modelo. Isso lhe dá a opção de manter suas dependências por valor enquanto ainda permite 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 detalhada. A maneira tradicional de fazer isso é usar um método clone virtual, outra opção que se tornou popular é usar o tipo de eliminação para criar tipos de valor que se comportam polimorficamente.

Qual opção é mais apropriada depende do seu caso de uso, é difícil dar uma resposta geral. Se você só precisa de polimorfismo estático, eu diria que os templates são o caminho mais C ++.

    
por 10.09.2015 / 23:32
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 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 gravar:

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

Representar o acoplamento entre objetos não deve ser feito (puramente) nos critérios que você listou (isto é, não baseado puramente em "o que é melhor para um gerenciamento de vida seguro").

Você também pode usar critérios conceituais ("um carro tem quatro rodas" versus "um carro usa quatro rodas que o motorista tem que levar junto").

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 em seu código de cliente também são limitadas).

    
por 08.09.2015 / 11:00
fonte