C ++: La classe doit-elle posséder ou observer ses dépendances?

16

Disons que j'ai une classe Foobarqui utilise (dépend) de la classe Widget. Au bon Widgetvieux temps, wolud serait déclaré comme un champ dans Foobar, ou peut-être comme un pointeur intelligent si un comportement polymorphe était nécessaire, et il serait initialisé dans le constructeur:

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)) {}
    (…)
};

Et nous serions fin prêts. Malheureusement, aujourd'hui, les enfants Java se moqueront de nous une fois qu'ils le verront, et à juste titre, en couple Foobaret Widgetensemble. La solution est apparemment simple: appliquer l'injection de dépendances pour sortir la construction des dépendances de la Foobarclasse. Mais alors, C ++ nous oblige à penser à la propriété des dépendances. Trois solutions me viennent à l'esprit:

Pointeur unique

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

Foobarrevendique la propriété exclusive de ce Widgetqui lui est transmis. Cela présente les avantages suivants:

  1. L'impact sur les performances est négligeable.
  2. Il est sûr, car il Foobarcontrôle la durée de vie de son appareil Widget, il s'assure donc qu'il Widgetne disparaît pas soudainement.
  3. C'est sûr qui Widgetne fuira pas et sera correctement détruit lorsqu'il ne sera plus nécessaire.

Cependant, cela a un coût:

  1. Il impose des restrictions sur la façon dont les Widgetinstances peuvent être utilisées, par exemple, aucune allocation de pile ne Widgetspeut être utilisée, aucune ne Widgetpeut être partagée.

Pointeur partagé

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

C'est probablement l'équivalent le plus proche de Java et d'autres langages récupérés. Avantages:

  1. Plus universel, car il permet de partager des dépendances.
  2. Maintient la sécurité (points 2 et 3) de la unique_ptrsolution.

Désavantages:

  1. Gaspille des ressources quand aucun partage n'est impliqué.
  2. Nécessite toujours l'allocation de segments et interdit les objets alloués à la pile.

Pointeur d'observation ordinaire

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

Placez le pointeur brut dans la classe et transférez le fardeau de la propriété à quelqu'un d'autre. Avantages:

  1. Aussi simple que possible.
  2. Universel, accepte n'importe lequel Widget.

Les inconvénients:

  1. Plus sûr.
  2. Présente une autre entité qui est responsable de la propriété des deux Foobaret Widget.

Une métaprogrammation de modèle fou

Le seul avantage auquel je peux penser est que je pourrais lire tous ces livres pour lesquels je n'ai pas trouvé le temps pendant que mon logiciel est en train de se développer;)

Je penche vers la troisième solution, car elle est la plus universelle, quelque chose doit être géré de Foobarstoute façon, donc la gestion Widgetsest un simple changement. Cependant, l'utilisation de pointeurs bruts me dérange, d'un autre côté, la solution de pointeur intelligent me semble mal, car ils font que le consommateur de dépendance restreint la façon dont cette dépendance est créée.

Suis-je en train de manquer quelque chose? Ou est-ce que l'injection de dépendances en C ++ n'est pas anodine? La classe doit-elle posséder ses dépendances ou simplement les observer?

el.pescado
la source
1
Dès le début, si vous pouvez compiler sous C ++ 11 et / ou jamais, avoir un pointeur simple comme moyen de gérer les ressources dans une classe n'est plus la méthode recommandée. Si la classe Foobar est le seul propriétaire de la ressource et que la ressource doit être libérée, lorsque Foobar sort du domaine, std::unique_ptrc'est la voie à suivre. Vous pouvez utiliser std::move()pour transférer la propriété d'une ressource de l'étendue supérieure à la classe.
Andy
1
Comment je sais que Foobarc'est le seul propriétaire? Dans l'ancien cas, c'est simple. Mais le problème avec DI, comme je le vois, est qu'il dissocie la classe de la construction de ses dépendances, il la dissocie également de la propriété de ces dépendances (car la propriété est liée à la construction). Dans les environnements récupérés comme Java, ce n'est pas un problème. En C ++, c'est.
el.pescado
1
@ el.pescado Il y a aussi le même problème en Java, la mémoire n'est pas la seule ressource. Il y a aussi des fichiers et des connexions par exemple. La manière habituelle de résoudre ce problème en Java est d'avoir un conteneur qui gère le cycle de vie de tous ses objets contenus. Vous n'avez donc pas à vous en préoccuper dans les objets contenus dans un conteneur DI. Cela pourrait également fonctionner pour les applications c ++ s'il existe un moyen pour le conteneur DI de savoir quand basculer vers quelle phase du cycle de vie.
SpaceTrucker
3
Bien sûr, vous pouvez le faire à l'ancienne sans toute la complexité et avoir un code qui fonctionne et est beaucoup plus facile à maintenir. (Notez que les garçons Java peuvent rire, mais ils sont toujours en train de refactoriser, donc le découplage ne les aide pas vraiment beaucoup)
gbjbaanb
3
De plus, faire rire les autres n'est pas une raison pour être comme eux. Écoutez leurs arguments (autres que «parce qu'on nous l'a dit») et implémentez DI si cela apporte un avantage tangible à votre projet. Dans ce cas, considérez le modèle comme une règle très générale, que vous devez appliquer d'une manière spécifique au projet - les trois approches (et toutes les autres approches potentielles auxquelles vous n'avez pas encore pensé) sont valides étant donné ils apportent un avantage global (c'est-à-dire qu'ils ont plus d'avantages que d'inconvénients).

Réponses:

2

Je voulais écrire ceci comme un commentaire, mais cela s'est avéré trop long.

Comment je sais que Foobarc'est le seul propriétaire? Dans l'ancien cas, c'est simple. Mais le problème avec DI, comme je le vois, est qu'il dissocie la classe de la construction de ses dépendances, il la dissocie également de la propriété de ces dépendances (car la propriété est liée à la construction). Dans les environnements récupérés comme Java, ce n'est pas un problème. En C ++, c'est.

Si vous devez utiliser std::unique_ptr<Widget>ou std::shared_ptr<Widget>, c'est à vous de décider et vient de votre fonctionnalité.

Supposons que vous en ayez un Utilities::Factory, qui est responsable de la création de vos blocs, comme Foobar. En suivant le principe DI, vous aurez besoin de l' Widgetinstance, pour l'injecter en utilisant Foobarle constructeur de, ce qui signifie à l'intérieur de l'une Utilities::Factorydes méthodes de, par exemple createWidget(const std::vector<std::string>& params), vous créez le Widget et l'injectez dans l' Foobarobjet.

Vous avez maintenant une Utilities::Factoryméthode qui a créé l' Widgetobjet. Cela signifie-t-il que la méthode doit être responsable de sa suppression? Bien sûr que non. Il n'est là que pour vous faire l'instance.


Imaginons, vous développez une application qui aura plusieurs fenêtres. Chaque fenêtre est représentée à l'aide de la Foobarclasse, donc en fait elle Foobaragit comme un contrôleur.

Le contrôleur utilisera probablement certains de vos Widgets et vous devez vous demander:

Si je vais sur cette fenêtre spécifique dans mon application, j'aurai besoin de ces Widgets. Ces widgets sont-ils partagés entre d'autres fenêtres d'application? Si c'est le cas, je ne devrais probablement pas les recréer, car ils seront toujours les mêmes, car ils sont partagés.

std::shared_ptr<Widget> est la voie à suivre.

Vous avez également une fenêtre d'application, où il y a Widgetune fenêtre spécifiquement liée à cette seule fenêtre, ce qui signifie qu'elle ne sera affichée nulle part ailleurs. Donc, si vous fermez la fenêtre, vous n'avez plus besoin de Widgetn'importe où dans votre application, ou du moins de son instance.

C'est là que std::unique_ptr<Widget>vient réclamer son trône.


Mise à jour:

Je ne suis pas vraiment d'accord avec @DominicMcDonnell , sur le problème de la durée de vie. L'appel std::moveà std::unique_ptrtransfère complètement la propriété, donc même si vous créez un object Adans une méthode et le passez à un autre en object Btant que dépendance, le object Bsera désormais responsable de la ressource de object Aet le supprimera correctement, quand object Bsortira du cadre.

Andy
la source
L'idée générale de propriété et de pointeurs intelligents est claire pour moi. Cela ne me semble pas jouer bien avec l'idée de DI. L'injection de dépendances consiste, selon moi, à découpler la classe de ses dépendances, mais dans ce cas, la classe influence toujours la façon dont ses dépendances sont créées.
el.pescado
Par exemple, le problème que j'ai avec unique_ptrest le test unitaire (DI est annoncé comme convivial pour les tests): je veux tester Foobar, donc je le crée Widget, le passe à Foobar, Foobarje fais de l' exercice et ensuite je veux inspecter Widget, mais à moins Foobarqu'il ne l' expose d'une manière ou d'une autre, je peux pas depuis qu'il a été revendiqué par Foobar.
el.pescado
@ el.pescado Vous mélangez deux choses ensemble. Vous mélangez accessibilité et propriété. Ce Foobarn'est pas parce que possède la ressource que personne d'autre ne devrait l'utiliser. Vous pouvez implémenter une Foobarméthode telle que Widget* getWidget() const { return this->_widget.get(); }, qui vous renverra le pointeur brut avec lequel vous pouvez travailler. Vous pouvez ensuite utiliser cette méthode comme entrée pour vos tests unitaires, lorsque vous souhaitez tester la Widgetclasse.
Andy
Bon point, cela a du sens.
el.pescado
1
Si vous avez converti un shared_ptr en un unique_ptr, comment voudriez-vous garantir l'unicité_ ???
Aconcagua
2

J'utiliserais un pointeur d'observation sous la forme d'une référence. Il donne une bien meilleure syntaxe lorsque vous l'utilisez et a l'avantage sémantique qu'il n'implique pas la propriété, ce qu'un simple pointeur peut.

Le plus gros problème avec cette approche est la durée de vie. Vous devez vous assurer que la dépendance est construite avant et détruite après votre classe dépendante. Ce n'est pas un problème simple. L'utilisation de pointeurs partagés (comme stockage de dépendances et dans toutes les classes qui en dépendent, option 2 ci-dessus) peut supprimer ce problème, mais introduit également le problème des dépendances circulaires qui est également non trivial, et à mon avis moins évident et donc plus difficile à résoudre. détecter avant qu'il ne cause des problèmes. C'est pourquoi je préfère ne pas le faire automatiquement et gérer manuellement les durées de vie et l'ordre de construction. J'ai également vu des systèmes qui utilisent une approche de modèle léger qui a construit une liste d'objets dans l'ordre de leur création et les a détruits dans l'ordre inverse, ce n'était pas infaillible, mais cela a rendu les choses beaucoup plus simples.

Mise à jour

La réponse de David Packer m'a fait réfléchir un peu plus sur la question. La réponse d'origine est vraie pour les dépendances partagées dans mon expérience, ce qui est l'un des avantages de l'injection de dépendance, vous pouvez avoir plusieurs instances en utilisant la seule instance d'une dépendance. Si toutefois votre classe doit avoir sa propre instance d'une dépendance particulière, alors std::unique_ptrc'est la bonne réponse.

Dominique McDonnell
la source
1

Tout d'abord - c'est C ++, pas Java - et ici beaucoup de choses vont différemment. Les gens de Java n'ont pas ces problèmes de propriété car il y a un ramasse-miettes automatique qui les résout pour eux.

Deuxièmement: il n'y a pas de réponse générale à cette question - cela dépend des exigences!

Quel est le problème du couplage de FooBar et Widget? FooBar veut utiliser un Widget, et si chaque instance de FooBar aura toujours son propre et le même Widget de toute façon, laissez-le couplé ...

En C ++, vous pouvez même faire des choses "bizarres" qui n'existent tout simplement pas en Java, par exemple avoir un constructeur de modèle variadique (enfin, il existe une notation ... en Java, qui peut également être utilisée dans les constructeurs, mais c'est juste du sucre syntaxique pour cacher un tableau d'objets, qui n'a en fait rien à voir avec de vrais modèles variadic!) - catégorie 'Quelques métaprogrammations de modèles fous':

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

J'aime ça?

Bien sûr, il y a des raisons pour lesquelles vous voulez ou devez découpler les deux classes - par exemple, si vous devez créer les widgets à un moment bien avant qu'une instance FooBar existe, si vous voulez ou devez réutiliser les widgets, ..., ou simplement car pour le problème actuel, il est plus approprié (par exemple, si Widget est un élément GUI et FooBar ne doit / ne peut / ne doit pas l'être).

Ensuite, nous revenons au deuxième point: pas de réponse générale. Vous devez décider quelle est, pour le problème réel , la solution la plus appropriée. J'aime l'approche de référence de DominicMcDonnell, mais elle ne peut être appliquée que si la propriété ne doit pas être prise par FooBar (enfin, en fait, vous pourriez, mais cela implique un code très, très sale ...). En dehors de cela, je rejoins la réponse de David Packer (celle qui devait être écrite comme commentaire - mais une bonne réponse, de toute façon).

Aconcagua
la source
1
En C ++, n'utilisez pas classes avec uniquement des méthodes statiques, même si c'est juste une usine comme dans votre exemple. Cela viole les principes OO. Utilisez plutôt des espaces de noms et regroupez vos fonctions en les utilisant.
Andy
@DavidPacker Hmm, bien sûr, vous avez raison - j'ai supposé en silence que la classe avait des internes privés, mais ce n'est ni visible ni mentionné ailleurs ...
Aconcagua
1

Il vous manque au moins deux autres options disponibles en C ++:

La première consiste à utiliser l'injection de dépendance «statique» où vos dépendances sont des paramètres de modèle. Cela vous donne la possibilité de conserver vos dépendances par valeur tout en permettant l'injection de dépendance de temps de compilation. Les conteneurs STL utilisent cette approche pour les allocateurs et les fonctions de comparaison et de hachage par exemple.

Une autre consiste à prendre des objets polymorphes par valeur en utilisant une copie profonde. La méthode traditionnelle consiste à utiliser une méthode de clonage virtuel, une autre option devenue populaire consiste à utiliser l'effacement des types pour créer des types de valeurs qui se comportent de manière polymorphe.

L'option la plus appropriée dépend vraiment de votre cas d'utilisation, il est difficile de donner une réponse générale. Si vous n'avez besoin que d'un polymorphisme statique, je dirais que les modèles sont la méthode la plus C ++.

mattnewport
la source
0

Vous avez ignoré une quatrième réponse possible, qui combine le premier code que vous avez publié (les «bons vieux jours» de stockage d'un membre par valeur) avec l'injection de dépendance:

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

Le code client peut alors écrire:

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

La représentation du couplage entre les objets ne doit pas se faire (purement) sur les critères que vous avez énumérés (c'est-à-dire ne pas se baser uniquement sur "ce qui est mieux pour une gestion sûre de la vie").

Vous pouvez également utiliser des critères conceptuels ("une voiture a quatre roues" contre "une voiture utilise quatre roues que le conducteur doit apporter").

Vous pouvez avoir des critères imposés par d'autres API (si ce que vous obtenez d'une API est un wrapper personnalisé ou un std :: unique_ptr par exemple, les options de votre code client sont également limitées).

utnapistim
la source
1
Cela empêche le comportement polymorphe, ce qui limite considérablement la valeur ajoutée.
el.pescado
Vrai. J'ai juste pensé à le mentionner car c'est la forme que j'utilise le plus (j'utilise uniquement l'héritage dans mon codage pour le comportement polymorphe - pas la réutilisation du code, donc je n'ai pas beaucoup de cas nécessitant un comportement polymorphe) ..
utnapistim