Disons que j'ai une classe Foobar
qui utilise (dépend) de la classe Widget
. Au bon Widget
vieux 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 Foobar
et Widget
ensemble. La solution est apparemment simple: appliquer l'injection de dépendances pour sortir la construction des dépendances de la Foobar
classe. 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) {}
(…)
}
Foobar
revendique la propriété exclusive de ce Widget
qui lui est transmis. Cela présente les avantages suivants:
- L'impact sur les performances est négligeable.
- Il est sûr, car il
Foobar
contrôle la durée de vie de son appareilWidget
, il s'assure donc qu'ilWidget
ne disparaît pas soudainement. - C'est sûr qui
Widget
ne fuira pas et sera correctement détruit lorsqu'il ne sera plus nécessaire.
Cependant, cela a un coût:
- Il impose des restrictions sur la façon dont les
Widget
instances peuvent être utilisées, par exemple, aucune allocation de pile neWidgets
peut être utilisée, aucune neWidget
peut ê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:
- Plus universel, car il permet de partager des dépendances.
- Maintient la sécurité (points 2 et 3) de la
unique_ptr
solution.
Désavantages:
- Gaspille des ressources quand aucun partage n'est impliqué.
- 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:
- Aussi simple que possible.
- Universel, accepte n'importe lequel
Widget
.
Les inconvénients:
- Plus sûr.
- Présente une autre entité qui est responsable de la propriété des deux
Foobar
etWidget
.
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 Foobars
toute façon, donc la gestion Widgets
est 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?
la source
std::unique_ptr
c'est la voie à suivre. Vous pouvez utiliserstd::move()
pour transférer la propriété d'une ressource de l'étendue supérieure à la classe.Foobar
c'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.Réponses:
Je voulais écrire ceci comme un commentaire, mais cela s'est avéré trop long.
Si vous devez utiliser
std::unique_ptr<Widget>
oustd::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, commeFoobar
. En suivant le principe DI, vous aurez besoin de l'Widget
instance, pour l'injecter en utilisantFoobar
le constructeur de, ce qui signifie à l'intérieur de l'uneUtilities::Factory
des méthodes de, par exemplecreateWidget(const std::vector<std::string>& params)
, vous créez le Widget et l'injectez dans l'Foobar
objet.Vous avez maintenant une
Utilities::Factory
méthode qui a créé l'Widget
objet. 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
Foobar
classe, donc en fait elleFoobar
agit comme un contrôleur.Le contrôleur utilisera probablement certains de vos
Widget
s 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
Widget
une 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 deWidget
n'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_ptr
transfère complètement la propriété, donc même si vous créez unobject A
dans une méthode et le passez à un autre enobject B
tant que dépendance, leobject B
sera désormais responsable de la ressource deobject A
et le supprimera correctement, quandobject B
sortira du cadre.la source
unique_ptr
est le test unitaire (DI est annoncé comme convivial pour les tests): je veux testerFoobar
, donc je le créeWidget
, le passe àFoobar
,Foobar
je fais de l' exercice et ensuite je veux inspecterWidget
, mais à moinsFoobar
qu'il ne l' expose d'une manière ou d'une autre, je peux pas depuis qu'il a été revendiqué parFoobar
.Foobar
n'est pas parce que possède la ressource que personne d'autre ne devrait l'utiliser. Vous pouvez implémenter uneFoobar
méthode telle queWidget* 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 laWidget
classe.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_ptr
c'est la bonne réponse.la source
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':
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).
la source
class
es 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.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 ++.
la source
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:
Le code client peut alors écrire:
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).
la source