Par exemple pratique, imaginez une classe Gripper
qui représente une pince robotique dans une simulation. Gripper
a une méthode TryGrip
, qui vérifie s'il y a un GrippableItem
dans la position correcte (dans le rectangle de la pince). S'il y a, la pince prend le contrôle de l'article jusqu'à ce qu'il soit chuté:
class GripperItemLocator //Struggling to find a good name
{
public:
virtual std::unique_ptr<GrippableItem>
FindItemInRange(Rect rect) = 0;
};
class Gripper
{
public:
bool
TryGrip(GripperItemLocator& itemLocator)
{
auto item = itemLocator.FindItemInRange(CalcCurrentRect());
if (item)
{
TakeControlOfItem(std::move(item));
}
}
private:
Rect
CalcCurrentRect() const;
void
TakeControlOfItem(std::unique_ptr<GrippableItem> item);
};
Conformément au principe d'inversion de dépendance, nous avons une abstraction GripperItemLocator
afin de découpler la pince du reste du monde. Si l'endroit où et comment GrippableItem
s est situé dans nos modifications de programme, cela ne concerne pas la pince, seule la classe (ES) qui implémente GripperItemLocator
doit changer.
Je me demande si nous avons vraiment besoin de classes d'interface pour cela? Disons que nous avons juste une classe régulière faire le même travail:
class GripperItemLocator
{
public:
std::unique_ptr<GrippableItem>
FindItemInRange(Rect rect)
{
//Immediate implementation
}
};
Je me rends compte que cela n'a plus la capacité de sélectionner différentes implémentations au moment de l'exécution, mais que nous disons que nous sommes sûrs que vous n'ayez jamais besoin de cela, nous ne sommes intéressés que par le découplage. Perdons-nous autre chose que je suis négligeable, ou est-ce tout aussi bien?
La trempette n'est pas une fin en soi, c'est un moyen d'atteindre une fin.
Par exemple, supposons que vous utilisez le trempage pour les tests unitaires. Si vous voulez être capable de tester Gripper
isolément, sans le "réel" GripperItemLocator
, vous avez besoin d'un moyen de remplacer ce dernier par un moqueur. La manière canonique de faire cela consiste à utiliser une interface ou en C++ une classe de base virtuelle abstraite.
Mais si vous dites que cela vous suffit de créer des tests pour Gripper
toujours à l'aide d'un objet réel GripperItemLocator
, vous pouvez rester avec cette approche et ne pas fournir une classe d'interface/de base virtuelle.
Comment faire de ce compromis dépend fortement de la complexité des classes, à quel point il est simple ou difficile de les déboguer lorsque vous ne pouvez pas les tester isolément, et combien de temps vous devez corriger une erreur au cas où l'on se produit, mais vous n'avez pas t savoir dans lequel des deux classes la cause première est cachée.
L'inverse "inversion" de l'inversion de dépendance nécessite des interfaces ou une interface comme des constructions.
L'idée est votre classe avec une dépendance ne spécifie plus ce que cette dépendance est. Au lieu de cela, le code d'appel lui dit quelle est la dépendance.
L'interface dit simplement, tout ce que vous passez, je suis gona essaie de l'appeler comme ça.
Si vous avez utilisé une classe ou même une classe abstraite pour définir que vous voudriez que la classe soit vide de mise en œuvre. Efficacement une interface.
Le principe d'inversion de dépendance vise à découpler haut niveau à partir de classes de niveau bas à l'aide d'une interface: classes de haut niveau Utilisez l'interface et les classes de niveau bas implémentent l'interface.
L'équivalent de C++ habituel à une interface est une classe abstraite qui n'a que des fonctions de membre virtuel pur. Cependant, il y a d'autres alternatives.
En fait, toute classe polymorphe pourrait être utilisée pour l'inversion de dépendance, à condition que la classe de haut niveau n'invoque que des fonctions de membre virtuel. Exemple:
class GripperItemLocator
{
public:
std::unique_ptr<GrippableItem>
virtual FindItemInRange(Rect rect) // <-- it should be virtual
{
//Immediate implementation
}
};
Vous pouvez plus tard définir une classe dérivée qui remplace la fonction virtuelle. Remplacer signifie que même le Gripper
, utiliserait la fonction remplacée si le constructeur est fourni avec la classe dérivée.
class GripperFragileItemLocator : public GripperItemLocator
{
public:
std::unique_ptr<GrippableItem>
FindItemInRange(Rect rect) override
{
//Immediate implementation that takes extra care of fragile items
}
};
C'est toujours une inversion de dépendance: vous ne dépendez pas d'une seule implémentation, mais que vous dépendez de quelque chose qui est abstrait, qui peut être spécialisé plus loin.
Cependant, soyez très prudent. Si FindItemInRange()
ne serait pas virtuel, le Gripper
_ _ invoquerait toujours cette fonction, même si vous fournissez une décision dérivée GripperItemLocator
avec sa propre mise en œuvre. Cela ressemblerait à une inversion de dépendance, mais ce n'est pas le cas: en réalité Gripper
dépendrait pleinement d'une classe de béton inférieure. itemLocater
serait alors juste un argument comme n'importe quel autre (sans rien inverser).
L'utilisation d'une interface est donc l'approche la plus sûre: aucun risque d'utilisation accidentelle d'un membre non virtuel, aucun risque de (MIS) en utilisant une abstraction qui n'est pas suffisamment abstraite. Les limites sont très claires.
Un mot sur les modèles
Lorsque R.c.Martin a écrit son article séminal sur l'inversion de dépendance en mai 1996, les modèles C++ n'existaient pas.
Le modèle propose une autre façon de concevoir des éléments de haut niveau qui ne dépendent pas du niveau bas: un modèle utilise simplement une abstraction de niveau inférieure et fait des hypothèses sur l'interface qu'elle devrait fournir. Un style qui utilise cette approche largement est conception pilotée par la police .
Cependant, alors qu'il réalise des objectifs similaires, il est significativement différent de DIP: les modèles de facto définissent l'interface selon laquelle la classe de niveau inférieure doit être appliquée. Donc, si le niveau supérieur dépend d'une abstraction, le niveau inférieur dépend toujours du niveau supérieur et non d'une abstraction elle-même.
Vous pouvez utiliser une baseClass au lieu d'une interface et passer des instances de la classe de base ou d'une sous-classe. Si vous utilisez le modèle de test, une classe de stub que vous essayez de remplacer n'est probablement pas dérivée de la base de la base, mais totalement indépendante, vous risquez de perdre.
Vous pouvez utiliser une instance fixe d'une classe fixe, mais l'injection de dépendance devient alors inutile. Ne pas dire que vous ne pouvez pas le faire, mais pourquoi voudriez-vous?
Bien sûr, si vous avez commencé avec une base de base et la détermination, vous auriez dû utiliser une interface, c'est un changement trivial. Sauf si vous avez utilisé des connaissances sur la classe que vous ne devriez pas avoir.