Critiques et inconvénients de l'injection de dépendance

118

L'injection de dépendance (ID) est un modèle bien connu et à la mode. La plupart des ingénieurs connaissent ses avantages, tels que:

  • Rendre l'isolement dans les tests unitaires possible / facile
  • Définir explicitement les dépendances d'une classe
  • Faciliter une bonne conception ( principe de responsabilité unique (SRP), par exemple)
  • Activer rapidement les implémentations de commutation ( DbLoggerau lieu de, ConsoleLoggerpar exemple)

Je pense qu’il existe un consensus général dans l’industrie selon lequel l’ID constitue un bon modèle utile. Il n'y a pas trop de critiques pour le moment. Les inconvénients mentionnés dans la communauté sont généralement mineurs. Certains d'entre eux:

  • Augmentation du nombre de cours
  • Création d'interfaces inutiles

Actuellement, nous discutons de la conception d'architecture avec mon collègue. Il est plutôt conservateur, mais ouvert d'esprit. Il aime poser des questions, ce que j'estime satisfaisant, car de nombreux informaticiens se contentent de copier la dernière tendance, de répéter les avantages et, en général, de ne pas trop réfléchir - n'analysez pas trop en profondeur.

Les choses que je voudrais demander sont:

  • Devrions-nous utiliser l'injection de dépendance quand nous n'avons qu'une seule implémentation?
  • Devrions-nous interdire la création de nouveaux objets, à l’exception des objets langage / framework?
  • L'injection d'une seule implémentation est-elle une mauvaise idée (disons que nous n'avons qu'une seule implémentation, nous ne voulons donc pas créer d'interface "vide") si nous n'envisageons pas de tester à l'unité une classe particulière?
Landeeyo
la source
33
Voulez-vous vraiment parler de l'injection de dépendance en tant que modèle, ou de l'utilisation de structures DI? Ce sont vraiment des choses distinctes, vous devriez clarifier quelle partie du problème vous intéresse ou demander explicitement les deux.
Frax
10
@Frax sur les motifs, pas les cadres
Landeeyo
10
Vous confondez l'inversion de dépendance avec l'injection de dépendance. Le premier est un principe de conception. Ce dernier est une technique (généralement mise en œuvre avec un outil existant) pour construire des hiérarchies d'objets.
jpmc26
3
J'écris souvent des tests en utilisant la base de données réelle et aucun objet fictif. Fonctionne très bien dans de nombreux cas. Et puis vous n’avez pas besoin d’interfaces la plupart du temps. Si vous avez UserServicecette classe, c'est juste un détenteur de logique. Une connexion à la base de données est injectée et des tests sont exécutés dans une transaction annulée. Beaucoup qualifieraient cette mauvaise pratique, mais j’ai trouvé que cela fonctionnait extrêmement bien. Il n'est pas nécessaire de contourner votre code uniquement pour effectuer des tests et vous obtenez le bogue qui permet de trouver le pouvoir des tests d'intégration.
usr
3
La DI est presque toujours bonne. La mauvaise chose avec cela, c'est que beaucoup de gens pensent qu'ils connaissent DI, mais tout ce qu'ils savent, c'est comment utiliser un cadre étrange sans même être sûr de ce qu'ils font. De nos jours, DI souffre énormément de la Cargo Cult Programming.
T. Sar

Réponses:

160

Premièrement, je voudrais séparer l’approche de conception du concept de cadre. L'injection de dépendance à son niveau le plus simple et le plus fondamental est simplement:

Un objet parent fournit toutes les dépendances requises pour l'objet enfant.

C'est ça. Notez que rien dans cela ne nécessite des interfaces, des frameworks, n'importe quel style d'injection, etc. Pour être juste, j'ai d'abord entendu parler de ce modèle il y a 20 ans. Ce n'est pas nouveau.

En raison de la confusion qui règne entre plus de 2 personnes dans le contexte injection de dépendance:

  • Le parent est l'objet qui instancie et configure l'objet enfant qu'il utilise
  • L' enfant est le composant conçu pour être instancié passivement. En d'autres termes, il est conçu pour utiliser toutes les dépendances fournies par le parent et ne pas instancier ses propres dépendances.

L'injection de dépendance est un modèle de composition d' objet .

Pourquoi des interfaces?

Les interfaces sont un contrat. Ils existent pour limiter le couplage étroit de deux objets. Toutes les dépendances n'ont pas besoin d'une interface, mais elles aident à l'écriture de code modulaire.

Lorsque vous ajoutez le concept de test unitaire, vous pouvez avoir deux implémentations conceptuelles pour une interface donnée: l'objet réel que vous souhaitez utiliser dans votre application et l'objet modulé que vous utilisez pour le code de test qui dépend de l'objet. Cela seul peut suffire à justifier l’interface.

Pourquoi des frameworks?

Initialiser et fournir des dépendances aux objets enfants peut être décourageant quand il y en a un grand nombre. Les cadres offrent les avantages suivants:

  • Dépendances automatiques des composants
  • Configurer les composants avec des paramètres
  • Automatiser le code de la plaque de la chaudière afin que vous n'ayez pas à le voir écrit à plusieurs endroits.

Ils présentent également les inconvénients suivants:

  • L'objet parent est un "conteneur", et rien dans votre code
  • Cela complique les tests si vous ne pouvez pas fournir les dépendances directement dans votre code de test
  • Il peut ralentir l'initialisation car il résout toutes les dépendances à l'aide de la réflexion et de nombreuses autres astuces.
  • Le débogage à l'exécution peut être plus difficile, en particulier si le conteneur injecte un proxy entre l'interface et le composant qui l'implémente (la programmation orientée aspect intégrée à Spring vient à l'esprit). Le conteneur est une boîte noire et ils ne sont pas toujours conçus de manière à faciliter le processus de débogage.

Cela dit, il y a des compromis à faire. Pour les petits projets où il n'y a pas beaucoup de pièces mobiles, et il n'y a pas de raison d'utiliser un cadre DI. Toutefois, pour les projets plus complexes où certains composants sont déjà conçus, le cadre peut être justifié.

Qu'en est-il de [article aléatoire sur Internet]?

Qu'en est-il? Souvent, les gens peuvent devenir trop zélés et ajouter un tas de restrictions et vous gronder si vous ne faites pas les choses de la "seule façon". Il n'y a pas une seule vraie façon. Voyez si vous pouvez extraire quelque chose d'utile de l'article et ignorer les choses avec lesquelles vous n'êtes pas d'accord.

En bref, réfléchissez par vous-même et essayez des solutions.

Travailler avec de "vieilles têtes"

Apprenez autant que vous le pouvez. Ce que vous constaterez avec beaucoup de développeurs qui travaillent dans leurs 70 ans, c'est qu'ils ont appris à ne pas être dogmatique sur beaucoup de choses. Ils ont des méthodes avec lesquelles ils travaillent depuis des décennies qui produisent des résultats corrects.

J'ai eu le privilège de travailler avec quelques-uns d'entre eux et ils peuvent fournir des commentaires brutalement honnêtes qui ont beaucoup de sens. Et là où ils voient de la valeur, ils ajoutent ces outils à leur répertoire.

Berin Loritsch
la source
6
@CarlLeth, j'ai travaillé avec un certain nombre de cadres allant de Spring aux variantes .net. Spring vous permettra d’injecter des implémentations directement dans des champs privés en utilisant une magie noire de réflexion / classloader. La seule façon de tester des composants construits ainsi est d’utiliser le conteneur. Spring a des coureurs JUnit pour configurer l’environnement de test, mais c’est plus compliqué que de configurer vous-même. Alors oui, je viens de donner un exemple concret .
Berin Loritsch
17
Lorsque je porte mon chapeau de dépanneur / responsable de maintenance, je trouve un inconvénient supplémentaire que je trouve être un obstacle avec DI dans les frameworks: l'action fantasmagorique à distance qu'ils fournissent rend le débogage hors connexion plus difficile. Dans le pire des cas, je dois exécuter le code pour voir comment les dépendances sont initialisées et transmises. Vous en parlez dans le contexte de "testing", mais c'est bien pire si vous commencez juste à regarder la source, jamais l'esprit d'essayer de le faire fonctionner (ce qui peut impliquer une tonne de configuration). Empêcher ma capacité à dire ce que le code fait en jetant simplement un coup d'œil sur lui est une mauvaise chose.
Jeroen Mostert
1
Les interfaces ne sont pas des contrats, elles sont simplement des API. Les contrats impliquent une sémantique. Cette réponse utilise une terminologie spécifique au langage et des conventions spécifiques à Java / C #.
Frank Hileman
2
@BerinLoritsch Le point essentiel de votre propre réponse est que le principe de l'ID! = Tout cadre d'ID donné. Le fait que le printemps puisse faire des choses affreuses et impardonnables est un inconvénient du printemps, et non des cadres de DI en général. Un bon framework DI vous aide à suivre le principe DI sans tricherie.
Carl Leth
1
@CarlLeth: tous les frameworks DI sont conçus pour supprimer ou automatiser certaines choses que le programmeur ne souhaite pas épeler, ils varient simplement dans la manière. Autant que je sache, ils suppriment tous la capacité de savoir comment (ou si ) les classes A et B interagissent simplement en regardant A et B - à tout le moins, vous devez également consulter le menu DI setup / configuration / conventions. Pas un problème pour le programmeur (c'est exactement ce qu'ils veulent, en fait), mais un problème potentiel pour un mainteneur / débogueur (éventuellement le même programmeur, plus tard). C’est un compromis que vous faites même si votre cadre d’ID est «parfait».
Jeroen Mostert
88

L'injection de dépendance est, comme la plupart des modèles, une solution aux problèmes . Commencez donc par vous demander si vous avez le problème en premier lieu. Sinon, en utilisant le modèle le plus probable fera du code pire .

Considérez d’abord si vous pouvez réduire ou éliminer les dépendances. Toutes choses étant égales par ailleurs, nous souhaitons que chaque composant d’un système ait le moins de dépendances possible. Et si les dépendances ont disparu, la question de l'injection ou non devient sans objet!

Prenons un module qui télécharge des données d’un service externe, les analyse, effectue des analyses complexes et écrit dans un fichier.

Désormais, si la dépendance au service externe est codée en dur, il sera vraiment difficile de tester le traitement interne de ce module. Vous pouvez donc décider d’injecter le service externe et le système de fichiers en tant que dépendances d’interface, ce qui vous permettra d’injecter des mocks, ce qui rendra possible le test unitaire de la logique interne.

Mais une solution bien meilleure consiste simplement à séparer l'analyse de l'input / output. Si l'analyse est extraite dans un module sans effets secondaires, il sera beaucoup plus facile à tester. Notez que se moquer est une odeur de code - ce n'est pas toujours évitable, mais en général, il est préférable de pouvoir tester sans se moquer. Donc, en éliminant les dépendances, vous évitez les problèmes que l'ID est censé résoudre. Notez qu'une telle conception adhère également beaucoup mieux à SRP.

Je tiens à souligner que l’ID ne facilite pas nécessairement la SRP ou d’autres bons principes de conception, tels que la séparation des préoccupations, une forte cohésion / faible couplage, etc. Cela pourrait aussi bien avoir l’effet inverse. Considérons une classe A qui utilise une autre classe B en interne. B n’est utilisé que par A et est donc entièrement encapsulé et peut être considéré comme un détail de mise en œuvre. Si vous modifiez ceci pour injecter B dans le constructeur de A, vous avez exposé les détails de cette implémentation et, maintenant, des connaissances sur cette dépendance et sur la façon d'initialiser B, la durée de vie de B, etc., doit exister séparément à un autre endroit du système. de A. Vous avez donc une architecture généralement pire avec des problèmes de fuite.

D'autre part, dans certains cas, l'ID est vraiment utile. Par exemple, pour des services globaux ayant des effets secondaires tels qu'un enregistreur.

Le problème vient du moment où les modèles et les architectures deviennent des objectifs en eux-mêmes plutôt que des outils. Il suffit de demander "Devrions-nous utiliser DI?" est un peu comme mettre la charrue avant le cheval. Vous devriez demander: "Avons-nous un problème?" et "Quelle est la meilleure solution à ce problème?"

Une partie de votre question se résume à: "Faut-il créer des interfaces superflues pour satisfaire les exigences du modèle?" Vous avez probablement déjà compris la réponse à cette question - absolument pas ! Quiconque vous dit le contraire essaie de vous vendre quelque chose - des heures de consultation souvent très coûteuses. Une interface n'a de valeur que si elle représente une abstraction. Une interface qui imite simplement la surface d'une classe unique est appelée "interface d'en-tête" et c'est un anti-modèle connu.

JacquesB
la source
15
Je ne pourrais pas être plus d'accord! Notez également que se moquer des choses pour le plaisir de parler signifie que nous ne testons pas réellement les implémentations réelles. Si des Autilisations Ben production ont été testées MockB, nos tests ne nous indiquent pas si cela fonctionnera en production. Lorsque des composants purs (sans effets secondaires) d'un modèle de domaine s'injectent et se moquent, le résultat est un énorme gaspillage de temps pour tout le monde, une base de code gonflée et fragile et une faible confiance en le système résultant. Mock aux frontières du système, pas entre des morceaux arbitraires du même système.
Warbo
17
@CarlLeth Pourquoi pensez-vous que DI rend le code "testable et maintenable", et le code sans lui moins? Jacques B a raison de dire que les effets secondaires nuisent à la testabilité / maintenabilité. Si le code n'a pas d'effets secondaires, nous ne nous soucions pas de quoi / où / quand / comment il appelle un autre code; nous pouvons garder les choses simples et directes. Si le code a des effets secondaires, nous devons nous en préoccuper. DI peut extraire les effets secondaires des fonctions et les définir dans des paramètres, ce qui les rend plus testables, mais le programme plus complexe. Parfois, c'est inévitable (par exemple, accès à une base de données). Si le code n'a pas d'effets secondaires, DI est simplement une complexité inutile.
Warbo
13
@CarlLeth: DI est une solution au problème de rendre le code testable de manière isolée si ses dépendances l'interdisent. Mais cela ne réduit pas la complexité globale et ne rend pas le code plus lisible, ce qui signifie qu'il n'augmente pas nécessairement la facilité de maintenance. Cependant, si toutes ces dépendances peuvent être éliminées par une meilleure séparation des préoccupations, cela "annule" parfaitement les avantages de DI, car elle annule le besoin de DI. C'est souvent une meilleure solution pour rendre le code plus testable et maintenable en même temps.
Doc Brown
5
@Warbo C'était l'original et probablement encore la seule utilisation valide de se moquer. Même aux limites du système, cela est rarement nécessaire. Les gens perdent vraiment beaucoup de temps à créer et à mettre à jour des tests presque sans valeur.
Frank Hileman
6
@CarlLeth: ok, je vois maintenant d'où vient le malentendu. Vous parlez d' inversion de dépendance . Mais, la question, cette réponse et mes commentaires concernent DI = injection de dépendance .
Doc Brown
36

D'après mon expérience, l'injection de dépendance présente plusieurs inconvénients.

Tout d'abord, l'utilisation de DI ne simplifie pas les tests automatisés autant que ceux annoncés. L'unité de test d'une classe avec une implémentation fictive d'une interface vous permet de valider comment cette classe va interagir avec l'interface. Autrement dit, cela vous permet de tester comment la classe sous test utilise le contrat fourni par l'interface. Cependant, cela donne une bien plus grande assurance que l'entrée de la classe sous test dans l'interface est comme prévu. Cela donne une assurance plutôt médiocre que la classe sous test répond comme prévu à la sortie de l'interface car il s'agit d'une sortie simulée presque universellement, elle-même sujette à des bugs, à des simplifications excessives, etc. En bref, cela ne vous permet PAS de valider que la classe se comportera comme prévu avec une implémentation réelle de l'interface.

Deuxièmement, DI rend la navigation dans le code beaucoup plus difficile. Lorsque vous essayez de naviguer vers la définition des classes utilisées en tant qu'entrée de fonctions, une interface peut être un désagrément mineur (par exemple, lorsqu'il n'y a qu'une seule implémentation) ou une perte de temps majeure (par exemple, lorsqu'une interface trop générique, telle qu'IDisposable, est utilisée) en essayant de trouver l'implémentation réelle utilisée. Cela peut transformer un exercice simple du type "J'ai besoin de corriger une exception de référence null dans le code qui se produit juste après que cette instruction de journalisation est imprimée" en un effort d'une journée.

Troisièmement, l'utilisation de DI et de cadres est une arme à double tranchant. Cela peut grandement réduire la quantité de code de plaque de chaudière nécessaire aux opérations courantes. Toutefois, cela nécessite au minimum une connaissance détaillée du cadre ID spécifique pour comprendre comment ces opérations courantes sont réellement connectées. Pour comprendre comment les dépendances sont chargées dans le framework et ajouter une nouvelle dépendance dans le framework à injecter, vous devrez peut-être lire une assez grande quantité de documentation et suivre quelques tutoriels de base sur le framework. Cela peut transformer certaines tâches simples en tâches prenant beaucoup de temps.

Eric
la source
J'ajouterais également que plus vous injectez, plus votre temps de démarrage sera long. La plupart des infrastructures DI créent toutes les instances singleton injectables au moment du démarrage, quel que soit l'endroit où elles sont utilisées.
Rodney P. Barbati
7
Si vous souhaitez tester une classe avec une implémentation réelle (pas une maquette), vous pouvez écrire des tests fonctionnels - des tests similaires aux tests unitaires, mais sans utilisation de simulacres.
Février
2
Je pense que votre deuxième paragraphe doit être plus nuancé: DI en soi ne rend pas plus difficile (plus) la navigation dans le code. Dans sa forme la plus simple, DI est simplement une conséquence de la suite de SOLID. Ce qui accroît la complexité, c’est l’utilisation de cadres indirectionnels et DI inutiles . Autre que cela, cette réponse frappe le clou sur la tête.
Konrad Rudolph
4
L'injection de dépendance, en dehors des cas où elle est réellement nécessaire, est également un signe avertissant qu'un autre code superflu peut être présent en grande quantité. Les dirigeants sont souvent surpris d'apprendre que les développeurs ajoutent de la complexité à la complexité.
Frank Hileman
1
+1 C'est la vraie réponse à la question qui a été posée, et devrait être la réponse acceptée.
Mason Wheeler
13

J'ai suivi le conseil de Mark Seemann de "Dependency injection in .NET" - à supposer.

DI devrait être utilisé lorsque vous avez une «dépendance volatile», par exemple, il y a une chance raisonnable que cela change.

Donc, si vous pensez avoir plus d'une implémentation dans le futur ou si la mise en œuvre peut changer, utilisez DI. Sinon newc'est bien.

Rob
la source
5
Notez qu'il donne également des conseils différents pour OO et les langages fonctionnels blog.ploeh.dk/2017/01/27/…
jk.
1
C'est bon point. Si nous créons des interfaces pour chaque dépendance par défaut, alors c'est en quelque sorte contre YAGNI.
Landeeyo
4
Pouvez-vous fournir une référence à "injection de dépendance dans .NET" ?
Peter Mortensen
1
Si vous testez des unités, il est fort probable que votre dépendance soit volatile.
Jaquez
11
La bonne chose est que les développeurs sont toujours capables de prédire l'avenir avec une précision parfaite.
Frank Hileman
5

Ma plus grande bête noire à propos de DI a déjà été mentionnée dans quelques réponses, mais je vais en dire un peu plus ici. DI (comme c'est le cas la plupart du temps aujourd'hui, avec les conteneurs, etc.) vraiment, nuit vraiment à la lisibilité du code. Et la lisibilité du code est sans doute la raison derrière la plupart des innovations de programmation actuelles. Comme quelqu'un l'a dit - écrire du code est facile. La lecture du code est difficile. Mais c’est aussi extrêmement important, à moins que vous n’écriviez une sorte de petit utilitaire jetable une seule fois.

Le problème avec DI à cet égard est qu’il est opaque. Le conteneur est une boîte noire. Les objets apparaissent simplement de quelque part et vous n'avez aucune idée - qui les a construits et quand? Qu'est-ce qui a été transmis au constructeur? Avec qui je partage cette instance? Qui sait...

Et lorsque vous travaillez principalement avec des interfaces, toutes les fonctionnalités "aller à la définition" de votre IDE partent en fumée. Il est extrêmement difficile de retracer le flux du programme sans l'exécuter et se contenter de voir exactement quelle implémentation de l'interface a été utilisée à CET emplacement particulier. Et parfois, des obstacles techniques vous empêchent de franchir le pas. Et même si vous le pouvez, si cela implique de traverser les entrailles tordues du conteneur d’ID, l’affaire devient rapidement un exercice frustrant.

Pour travailler efficacement avec un morceau de code qui utilise DI, vous devez le connaître et déjà savoir ce qui se passe où.

Vilx-
la source
3

Activer rapidement les implémentations de commutation (DbLogger au lieu de ConsoleLogger par exemple)

Bien que l’ID en général soit sûrement une bonne chose, je suggérerais de ne pas l’utiliser aveuglément pour tout. Par exemple, je n’injecte jamais de bûcheron. Un des avantages de DI est de rendre les dépendances explicites et claires. Il est inutile d’énumérer ILoggerla dépendance de presque toutes les classes, c’est un fouillis. Il est de la responsabilité de l'enregistreur de vous fournir la flexibilité dont vous avez besoin. Tous mes enregistreurs sont des membres finaux statiques, je peux envisager d’injecter un enregistreur lorsque j’ai besoin d’un enregistreur non statique.

Augmentation du nombre de cours

Ceci est un inconvénient du cadre DI donné ou du cadre moqueur, et non du DI lui-même. Dans la plupart des endroits, mes cours dépendent de cours concrets, ce qui signifie qu’il n’ya pas de passe-partout. Guice (un framework Java DI) lie par défaut une classe à elle-même et je n'ai qu'à remplacer la liaison dans les tests (ou les câbler manuellement à la place).

Création d'interfaces inutiles

Je crée uniquement les interfaces en cas de besoin (ce qui est plutôt rare). Cela signifie que parfois, je dois remplacer toutes les occurrences d'une classe par une interface, mais l'EDI peut le faire pour moi.

Devrions-nous utiliser l'injection de dépendance quand nous n'avons qu'une seule implémentation?

Oui, mais évitez d'ajouter un passe-partout .

Devrions-nous interdire la création de nouveaux objets, à l’exception des objets langage / framework?

Non. Il y aura de nombreuses classes value (immuable) et data (mutable), où les instances sont simplement créées et transmises et où il est inutile de les injecter, car elles ne sont jamais stockées dans un autre objet (ou uniquement dans un autre). ces objets).

Pour eux, vous devrez peut-être injecter une usine, mais la plupart du temps cela n’a aucun sens (imaginez, par exemple, @Value class NamedUrl {private final String name; private final URL url;}vous n’avez vraiment pas besoin d’une usine ici et il n’ya rien à injecter).

L'injection d'une seule implémentation est-elle une mauvaise idée (disons que nous n'avons qu'une seule implémentation, nous ne voulons donc pas créer d'interface "vide") si nous n'envisageons pas de tester à l'unité une classe particulière?

IMHO c'est bien tant que cela ne provoque pas de gonflement du code. N'injectez pas la dépendance mais ne créez pas l'interface (et pas de XML de configuration fou!), Car vous pourrez le faire plus tard sans problème.

En fait, dans mon projet actuel, il existe quatre classes (sur des centaines), que j'ai décidé d' exclure de DI car ce sont des classes simples utilisées dans trop d'endroits, y compris des objets de données.


La surcharge d'exécution est un autre inconvénient de la plupart des structures DI. Cela peut être déplacé pour la compilation (pour Java, il y a Dagger , aucune idée des autres langages).

Un autre inconvénient est la magie qui se produit partout, qui peut être atténuée (par exemple, j’ai désactivé la création de proxy lors de l’utilisation de Guice).

maaartinus
la source
-4

Je dois dire qu'à mon avis, la notion d'injection de dépendance est surestimée.

DI est l'équivalent moderne des valeurs globales. Les éléments que vous injectez sont des singletons globaux et des objets de code purs. Sinon, vous ne pourriez pas les injecter. La plupart des utilisations de DI vous sont imposées pour utiliser une bibliothèque donnée (JPA, Spring Data, etc.). Dans la plupart des cas, le DI fournit l’environnement idéal pour nourrir et cultiver les spaghettis.

En toute honnêteté, le moyen le plus simple de tester une classe est de s'assurer que toutes les dépendances sont créées dans une méthode qui peut être remplacée. Créez ensuite une classe Test dérivée de la classe réelle et substituez cette méthode.

Ensuite, vous instanciez la classe Test et testez toutes ses méthodes. Cela ne sera pas clair pour certains d'entre vous - les méthodes que vous testez appartiennent à la classe sous test. Et tous ces tests de méthodes ont lieu dans un fichier de classe unique, la classe de tests unitaires associée à la classe en cours de test. Il n'y a pas de frais généraux ici - c'est ainsi que fonctionnent les tests unitaires.

En code, ce concept ressemble à ceci ...

class ClassUnderTest {

   protected final ADependency;
   protected final AnotherDependency;

   // Call from a factory or use an initializer 
   public void initializeDependencies() {
      aDependency = new ADependency();
      anotherDependency = new AnotherDependency();
   }
}

class TestClassUnderTest extends ClassUnderTest {

    @Override
    public void initializeDependencies() {
      aDependency = new MockitoObject();
      anotherDependency = new MockitoObject();
    }

    // Unit tests go here...
    // Unit tests call base class methods
}

Le résultat est exactement équivalent à l'utilisation de DI: le ClassUnderTest est configuré pour les tests.

Les seules différences sont que ce code est parfaitement concis, complètement encapsulé, plus facile à coder, plus facile à comprendre, plus rapide, utilise moins de mémoire, ne nécessite pas de configuration alternative, ne nécessite pas de framework, ne sera jamais la cause de 4 pages (WTF!) Trace de pile qui inclut exactement ZERO (0) classes que vous avez écrites et qui est tout à fait évident pour quiconque possède la moindre connaissance en langage d’utilisation, du débutant au gourou (on pourrait penser, mais se tromper).

Cela étant dit, bien sûr, nous ne pouvons pas l'utiliser - c'est trop évident et pas assez tendance.

À la fin de la journée, cependant, ma plus grande préoccupation avec DI est celle des projets que j’ai vu échouer lamentablement. Ce sont tous des bases de code gigantesques où DI était le ciment qui a tout réuni. DI n’est pas une architecture - elle n’est utile que dans une poignée de situations, qui vous sont généralement imposées pour utiliser une autre bibliothèque (JPA, Spring Data, etc.). Généralement, dans une base de code bien conçue, la plupart des utilisations de DI se produiraient à un niveau inférieur à celui de vos activités de développement quotidiennes.

Rodney P. Barbati
la source
6
Vous avez décrit non pas l'équivalent, mais le contraire de l'injection de dépendance. Dans votre modèle, chaque objet doit connaître l'implémentation concrète de toutes ses dépendances, mais l'utilisation de DI relève de la responsabilité du composant "principal" - pour coller les implémentations appropriées ensemble. Gardez à l'esprit que l'ID va de pair avec l' autre inversion d'ID, à savoir que vous ne voulez pas que les composants de haut niveau aient des dépendances dures sur les composants de bas niveau.
Logan Pickup
1
sa belle quand vous avez un seul niveau d'héritage de classe et un niveau de dépendances. Il va sûrement se transformer en enfer sur terre à mesure qu'il se dilate?
Ewan
4
si vous utilisez des interfaces et déplacez initializeDependencies () dans le constructeur, le même mais plus net. La prochaine étape, en ajoutant des paramètres de construction, signifie que vous pouvez supprimer toutes vos TestClasses.
Ewan
5
Il y a tellement de mal à cela. Comme d'autres l'ont déjà dit, votre exemple «d'équivalent DI» n'est pas du tout une injection de dépendance, c'est une antithèse qui démontre une incompréhension totale du concept et introduit également d'autres pièges: les objets partiellement initialisés sont une odeur de code As Ewan suggère, Déplacez l’initialisation vers le constructeur et transmettez-la via les paramètres du constructeur. Ensuite , vous avez DI ...
Mr.Mindor
3
Ajout à @ Mr.Mindor: il existe un "couplage séquentiel" anti-motif plus général qui ne s'applique pas seulement à l'initialisation. Si les méthodes d'un objet (ou, plus généralement, les appels d'une API) doivent être exécutées dans un ordre particulier, par exemple, barne peuvent être appelées qu'après foo, c'est une mauvaise API. Il prétend fournir des fonctionnalités ( bar), mais nous ne pouvons pas les utiliser (car elles foon'auraient peut-être pas été appelées). Si vous souhaitez conserver votre initializeDependenciesmodèle (anti?), Vous devez au moins le rendre privé / protégé et l'appeler automatiquement à partir du constructeur, pour que l'API soit sincère.
Warbo
-6

En réalité, votre question se résume à la question suivante: "Le test unitaire est-il mauvais?"

99% de vos classes de substitution à injecter seront des simulacres permettant les tests unitaires.

Si vous effectuez des tests unitaires sans DI, vous avez des difficultés à obtenir que les classes utilisent des données fictives ou un service fictif. Disons «partie de la logique», car vous n’êtes peut-être pas séparé en services.

Il existe d'autres moyens de le faire, mais l'ID est bon et flexible. Une fois que vous avez le temps de le tester, vous êtes pratiquement obligé de l'utiliser partout, car vous avez besoin d'un autre élément de code, même s'il s'agit de la soi-disant «DI du pauvre» qui instancie les types concrets.

Il est difficile d'imaginer un inconvénient si grave que l'avantage des tests unitaires est dépassé.

Ewan
la source
13
Je ne suis pas d'accord avec votre affirmation selon laquelle l'ID concerne les tests unitaires. Faciliter les tests unitaires n'est qu'un des avantages de l'ID, et sans doute pas le plus important.
Robert Harvey
5
Je ne suis pas d'accord avec votre hypothèse selon laquelle les tests unitaires et les DI sont si proches. En utilisant une maquette / un bout, nous faisons en sorte que la suite de tests nous mente un peu plus: le système sous test s'éloigne du système réel. C'est objectivement mauvais. Parfois, un inconvénient l'emporte: les appels fictifs dans les services fixes ne nécessitent pas de nettoyage; Les requêtes HTTP simulées sont rapides, déterministes et fonctionnent hors ligne; etc. En revanche, chaque fois que nous utilisons une newméthode codée en dur dans une méthode, nous savons que le même code exécuté en production a été exécuté pendant les tests.
Warbo
8
Non, ce n'est pas «est-ce que les tests unitaires sont mauvais?», C'est «est-ce que se moquer (a) est vraiment nécessaire et (b) vaut la complexité croissante?» C'est une question très différente. Les tests unitaires ne sont pas mauvais (personne ne le dit littéralement), et cela en vaut vraiment la peine en général. Cependant, tous les tests unitaires ne nécessitent pas de moqueries, et les moqueries ont un coût substantiel et doivent donc, au moins, être utilisées judicieusement.
Konrad Rudolph
6
@Ewan Après votre dernier commentaire, je ne pense pas que nous sommes d'accord. Je dis que la plupart des tests unitaires n'ont pas besoin de DI [frameworks] , car la plupart des tests unitaires n'ont pas besoin de se moquer. En fait, je l'aurais même utilisé comme une heuristique pour la qualité du code: si la plupart de votre code ne peut pas être testé par unité sans objets DI / mock, vous avez écrit un code incorrect, trop correctement couplé. La plupart des codes devraient être fortement découplés, à responsabilité unique et à usage général, et pouvoir être testés de manière triviale de manière isolée.
Konrad Rudolph
5
@Ewan Votre lien donne une bonne définition des tests unitaires. Selon cette définition, mon Orderexemple est un test unitaire: il teste une méthode (la totalméthode de Order). Vous vous plaignez qu'il appelle le code de 2 classes, ma réponse est donc quoi ? Nous ne testons pas "2 classes à la fois", nous testons la totalméthode. Nous ne devrions pas nous soucier de la façon dont une méthode fait son travail: ce sont des détails de mise en œuvre; les tester provoque une fragilité, un couplage étroit, etc. Nous nous soucions seulement du comportement de la méthode (valeur de retour et effets secondaires), pas des classes / modules / registres de la CPU / etc. il a utilisé dans le processus.
Warbo