Approches graduelles de l'injection de dépendance


12

Je travaille à rendre mes classes testables à l'unité, en utilisant l'injection de dépendance. Mais certaines de ces classes ont beaucoup de clients, et je ne suis pas encore prêt à les refactoriser pour commencer à passer dans les dépendances. J'essaie donc de le faire progressivement; en conservant les dépendances par défaut pour l'instant, mais en les autorisant à être remplacées pour les tests.

Une approche que je conise consiste simplement à déplacer tous les «nouveaux» appels dans leurs propres méthodes, par exemple:

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Ensuite, dans mes tests unitaires, je peux simplement sous-classer cette classe et remplacer les fonctions de création, afin qu'elles créent de faux objets à la place.

Est-ce une bonne approche? Y a-t-il des inconvénients?

Plus généralement, est-il acceptable d'avoir des dépendances codées en dur, tant que vous pouvez les remplacer pour les tests? Je sais que l'approche préférée est de les exiger explicitement dans le constructeur, et j'aimerais y arriver éventuellement. Mais je me demande si c'est une bonne première étape.

Un inconvénient qui m'est venu à l'esprit: si vous avez de vraies sous-classes que vous devez tester, vous ne pouvez pas réutiliser la sous-classe de test que vous avez écrite pour la classe parente. Vous devrez créer une sous-classe de test pour chaque sous-classe réelle, et elle devra remplacer les mêmes fonctions de création.


Pouvez-vous expliquer pourquoi ils ne sont pas testables à l'unité maintenant?
Austin Salonen

Il existe de nombreux "nouveaux X" disséminés dans le code, ainsi que de nombreuses utilisations de classes statiques et de singletons. Donc, quand j'essaie de tester une classe, j'en teste vraiment un tas. Si je peux isoler les dépendances et les remplacer par de faux objets, j'aurai plus de contrôle sur les tests.
JW01

Réponses:


13

C'est une bonne approche pour commencer. Notez que le plus important est de couvrir votre code existant avec des tests unitaires; une fois que vous avez les tests, vous pouvez refaçonner plus librement pour améliorer encore votre conception.

Donc, le point initial n'est pas de rendre le design élégant et brillant - juste de rendre l'unité de code testable avec les changements les moins risqués . Sans tests unitaires, vous devez être extrêmement conservateur et prudent pour éviter de casser votre code. Ces modifications initiales peuvent même rendre le code plus maladroit ou laid dans certains cas - mais si elles vous permettent d'écrire les premiers tests unitaires, vous pourrez éventuellement refactoriser vers la conception idéale que vous avez en tête.

Le travail fondamental à lire sur ce sujet est Travailler efficacement avec le code hérité . Il décrit l'astuce que vous montrez ci-dessus et bien d'autres, en utilisant plusieurs langues.


Juste une remarque à ce sujet "une fois que vous avez les tests, vous pouvez refactoriser plus librement" - Si vous n'avez pas de tests, vous ne refactorisez pas, vous changez simplement le code.
ocodo

@Slomojo Je vois votre point, mais vous pouvez toujours faire beaucoup de refactorisation en toute sécurité sans tests, en particulier les ractorisations IDE automatisées comme, par exemple (dans eclipse pour Java) extraire du code dupliqué vers des méthodes, renommer tout, déplacer des classes et extraire des champs, des constantes etc.
Alb

Je ne fais que citer les origines du terme Refactoring, qui modifie le code en modèles améliorés protégés contre les erreurs de régression par un cadre de test. Bien sûr, le refactoring basé sur IDE a rendu le logiciel de refactorisation beaucoup plus trivial, mais j'ai tendance à préférer éviter l'auto-illusion, si mon logiciel n'a pas de tests et que je "refactorise" je ne fais que changer le code. Bien sûr, ce n'est peut-être pas ce que je dis à quelqu'un d'autre;)
ocodo

1
@Alb, refactoring sans tests est toujours plus risqué. Les refactorisations automatisées diminuent certes ce risque, mais pas à zéro. Il y a toujours des cas délicats que les outils peuvent ne pas gérer correctement. Dans le meilleur des cas, le résultat est un message d'erreur de l'outil, mais dans le pire des cas, il peut introduire silencieusement des bogues subtils. Il est donc conseillé de conserver les modifications les plus simples et les plus sûres possibles, même avec des outils automatisés.
Péter Török

3

Les gens de Guice recommandent d'utiliser

@Inject
public void setX(.... X x) {
   this.x = x;
}

pour toutes les propriétés au lieu d'ajouter simplement @Inject à leur définition. Cela vous permettra de les traiter comme des beans normaux, ce que vous pourrez newlors des tests (et définir les propriétés manuellement) et @Inject en production.


3

Lorsque je suis confronté à ce type de problème, je vais identifier chaque dépendance de ma classe à tester et créer un constructeur protégé qui me permettra de les transmettre. C'est en fait la même chose que de créer un setter pour chacun, mais je préfère déclarer des dépendances dans mes constructeurs pour ne pas oublier de les instancier à l'avenir.

Donc ma nouvelle classe testable unitaire aura 2 constructeurs:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}

2

Vous voudrez peut-être considérer l'idiome «getter crée» jusqu'à ce que vous puissiez utiliser la dépendance injectée.

Par exemple,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Cela vous permettra de refactoriser en interne afin de pouvoir référencer votre myObject via le getter. Vous n'avez jamais à appeler new. À l'avenir, lorsque tous les clients seront configurés pour autoriser l'injection de dépendances, tout ce que vous aurez à faire sera de supprimer le code de création en un seul endroit - le getter. Le setter vous permettra d'injecter des objets fictifs au besoin.

Le code ci-dessus n'est qu'un exemple et il expose directement l'état interne. Vous devez examiner attentivement si cette approche est appropriée pour votre base de code.

Je vous recommande également de lire « Travailler efficacement avec Legacy Code » qui contient une multitude de conseils utiles pour réussir une refactorisation majeure.


Merci. J'envisage cette approche pour certains cas. Mais que faites-vous lorsque le constructeur de MyObject requiert des paramètres qui ne sont disponibles que depuis l'intérieur de votre classe? Ou lorsque vous devez créer plus d'un MyObject pendant la durée de vie de votre classe? Devez-vous injecter une MyObjectFactory?
JW01

@ JW01 Vous pouvez configurer le code du créateur de getter pour lire l'état interne de votre classe et l'adapter. La création de plus d'une instance au cours de la vie sera difficile à orchestrer. Si vous rencontrez cette situation, je suggère une refonte, plutôt qu'une solution de contournement. Ne compliquez pas vos getters ou ils deviendront une meule autour de votre cou. Votre objectif est de faciliter le processus de refactoring. Si cela semble trop difficile, déplacez-vous dans une autre zone pour y remédier.
Gary Rowe

c'est un singleton. N'utilisez simplement pas de singletons O_O
CoffeDeveloper

@DarioOO En fait, c'est un singleton très pauvre. Une meilleure implémentation utiliserait une énumération selon Josh Bloch. Les singletons sont un modèle de conception valide et ont leurs utilisations (création unique coûteuse, etc.).
Gary Rowe
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.