web-dev-qa-db-fra.com

Injection de dépendance: injection de terrain vs injection de constructeur?

Je sais que c'est un débat brûlant et les opinions ont tendance à changer avec le temps quant à la meilleure pratique d'approche.

J'avais l'habitude d'utiliser exclusivement l'injection de terrain pour mes cours, jusqu'à ce que je commence à lire sur différents blogs (exs: petrikainulainen et schauderhaft et fowler ) à propos de les avantages de l'injection de constructeur. J'ai depuis changé mes méthodologies pour utiliser l'injection de constructeur pour les dépendances requises et l'injection de setter pour les dépendances facultatives.

Cependant, j'ai récemment entamé un débat avec l'auteur de JMockit - un cadre moqueur - dans lequel il voit l'injection de constructeur et de setter comme une mauvaise pratique et indique que la communauté JEE est d'accord avec lui.

Dans le monde d'aujourd'hui, existe-t-il une façon préférée de faire l'injection? L'injection sur le terrain est-elle préférée?

Après être passé à l'injection de constructeur par injection de terrain au cours des deux dernières années, je le trouve beaucoup plus clair à utiliser, mais je me demande si je devrais réexaminer mon point de vue. L'auteur de JMockit (Rogério Liesenfeld) , est clairement bien versé en DI, donc je me sens obligé de revoir mon approche étant donné qu'il se sent si fortement contre l'injection constructeur/setter.

68
Eric B.

L'injection sur le terrain est un peu trop "action fantasmagorique à distance" à mon goût.

Prenons l'exemple que vous avez fourni dans votre message Google Groupes:

public class VeracodeServiceImplTest {


    @Tested(fullyInitialized=true)
    VeracodeServiceImpl veracodeService;
    @Tested(fullyInitialized=true, availableDuringSetup=true)
    VeracodeRepositoryImpl veracodeRepository;
    @Injectable private ResultsAPIWrapper resultsApiWrapper;
    @Injectable private AdminAPIWrapper adminApiWrapper;
    @Injectable private UploadAPIWrapper uploadApiWrapper;
    @Injectable private MitigationAPIWrapper mitigationApiWrapper;

    static { VeracodeRepositoryImpl.class.getName(); }
...
}

Donc, fondamentalement, ce que vous dites est que "j'ai cette classe avec un état privé, à laquelle j'ai attaché @injectable annotations, ce qui signifie que l'état peut être rempli automatiquement par un agent de l'extérieur, même si mon état a tous été déclaré privé. "

Je comprends les motivations de cela. C'est une tentative d'éviter une grande partie de la cérémonie qui est inhérente à la mise en place d'une classe correctement. Fondamentalement, ce que l'on dit est que "je suis fatigué d'écrire tout ce passe-partout, donc je vais juste annoter tout mon état, et laisser le conteneur DI se charger de le régler pour moi."

C'est un point de vue parfaitement valable. Mais c'est aussi une solution de contournement pour les fonctionnalités de langage qui ne devraient sans doute pas être contournées. Et pourquoi s'arrêter là? Traditionnellement, DI a compté sur chaque classe ayant une interface compagnon. Pourquoi ne pas également éliminer toutes ces interfaces avec des annotations?

Considérez l'alternative (cela va être C #, parce que je le connais mieux, mais il y a probablement un équivalent exact en Java):

public class VeracodeService
{        
    private readonly IResultsAPIWrapper _resultsApiWrapper;
    private readonly IAdminAPIWrapper _adminApiWrapper;
    private readonly IUploadAPIWrapper _uploadApiWrapper;
    private readonly IMitigationAPIWrapper _mitigationApiWrapper;

    // Constructor
    public VeracodeService(IResultsAPIWrapper resultsApiWrapper, IAdminAPIWrapper adminApiWrapper, IUploadAPIWrapper uploadApiWrapper,         IMitigationAPIWrapper mitigationApiWrapper)
    {
         _resultsAPIWrapper = resultsAPIWrapper;
         _adminAPIWrapper = adminAPIWrapper;
         _uploadAPIWrapper = uploadAPIWrapper;
         _mitigationAPIWrapper = mitigationAPIWrapper;
    }
}

Je connais déjà des choses sur ce cours. C'est une classe immuable; l'état ne peut être défini que dans le constructeur (références, dans ce cas particulier). Et parce que tout dérive d'une interface, je peux échanger des implémentations dans le constructeur, c'est là que vos maquettes entrent en jeu.

Maintenant, tout ce que mon conteneur DI a à faire est de réfléchir sur le constructeur pour déterminer les objets dont il a besoin pour se renouveler. Mais cette réflexion se fait sur un membre public, d'une manière de première classe; c'est-à-dire que les métadonnées font déjà partie de la classe, ayant été déclarées dans le constructeur, ne méthode dont le but exprès est de fournir à la classe les dépendances dont elle a besoin.

Certes, c'est beaucoup de passe-partout, mais c'est ainsi que le langage a été conçu. Les annotations semblent être un sale hack pour quelque chose qui aurait dû être intégré dans la langue elle-même.

51
Robert Harvey

L'argument de la plaque de test d'initialisation avec moins de tests est valide, mais il y a d'autres problèmes à prendre en compte. Tout d'abord, vous devez répondre à une question:

Est-ce que je veux que ma classe ne soit instanciable qu'avec réflexion?

Utiliser des injections de champ signifie restreindre compatibilité d'une classe aux environnements d'injection de dépendance qui instancient des objets en utilisant la réflexion et prennent en charge ces annotations d'injection particulières. Certaines plates-formes basées sur Java ne prend même pas en charge la réflexion ( GWT ), donc la classe à injection de champ ne sera pas compatible avec eux.

Le deuxième problème est performance . Un appel constructeur (direct ou par réflexion) est toujours plus rapide qu'un tas d'affectations de champs de réflexion. Les frameworks d'injection de dépendances doivent utiliser l'analyse de réflexion pour construire un arbre de dépendance et créer des constructeurs de réflexion. Ce processus entraîne un impact supplémentaire sur les performances.

Les performances affectent maintenabilité . Si chaque suite de tests doit être exécutée dans une sorte de conteneur d'injection de dépendance, une exécution de quelques milliers de tests unitaires peut durer des dizaines de minutes. Selon la taille d'une base de code, cela peut être un problème.

Tout cela soulève de nombreuses nouvelles questions:

  1. Quelle est la probabilité d'utiliser des parties du code sur une autre plateforme?
  2. Combien de tests unitaires seront écrits?
  3. À quelle vitesse dois-je préparer chaque nouvelle version?
  4. ...

En règle générale, plus un projet est grand et important, plus ces facteurs sont importants. En outre, en termes de qualité, nous aimerions généralement garder le code élevé en termes de compatibilité, de testabilité et de maintenabilité. Du point de vue philosophique, l'injection de champs casse encapsulation , qui est l'un des quatre principes fondamentaux de programmation orientée objet , qui est le principal paradigme de Java.

Beaucoup, beaucoup d'arguments contre l'injection sur le terrain.

30
Maciej Chałapuk

L'injection sur le terrain obtient un "non" définitif de ma part.

Comme Robert Harvey, c'est un peu trop automagique à mon goût. Je préfère le code explicite à l'implicite, et je ne tolère l'indirection que si/quand elle offre des avantages clairs car elle rend le code plus difficile à comprendre et à raisonner.

Comme Maciej Chałapuk, je n'aime pas avoir besoin de réflexion ou d'un framework DI/IoC pour instancier une classe. C'est comme conduire à Rome pour se rendre à Paris depuis Amsterdam.

Attention, j'aime DI et tous les avantages qu'elle apporte. Je ne suis pas sûr d'avoir besoin des frameworks pour instancier une classe. Surtout pas l'effet que les conteneurs IoC ou autres frameworks DI peuvent avoir sur le code de test.

J'aime que mes tests soient très simples. Je n'aime pas avoir à passer par l'indirection de la mise en place d'un conteneur IoC ou d'un autre framework DI pour ensuite les configurer ma classe sous test. C'est juste gênant.

Et cela rend mes tests dépendants de l'état global du framework. C'est-à-dire que je ne peux plus exécuter deux tests en parallèle qui nécessitent la mise en place d'une instance de classe X avec différentes dépendances.

Au moins avec l'injection de constructeur et de setter, vous avez la possibilité de ne pas utiliser les frameworks et/ou la réflexion dans vos tests.

22
Marjan Venema

Eh bien, cela semble une discussion polémique. La première chose qui doit être adressée est que L'injection de champ est différente de l'injection Setter . Je travaille déjà avec certaines personnes qui pensent que l'injection Field et Setter est la même chose.

Donc, pour être clair:

Injection sur le terrain:

@Autowired
private SomeService someService;

Setter injection:

@Autowired
public void setSomeService(SomeService someService) {
    this.someService = someService;
}

Mais, pour moi, les deux partagent les mêmes préoccupations.

Et, mon préféré, l'injection constructeur:

@AutoWired
public MyService(SomeService someService) {
    this.someService = someService;
}

J'ai les raisons suivantes de croire que l'injection de constructeur est meilleure que l'injection setter/field (je citerai quelques liens Spring pour démontrer mon point):

  1. Empêcher les dépendances circulaires : Spring est capable de détecter les dépendances circulaires entre les Beans, comme l'a expliqué @Tomasz. Voir aussi Équipe de printemps disant ça.
  2. Les dépendances de la classe sont évidentes : Les dépendances de la classe sont évidentes dans le constructeur mais caché avec injection de champ/setter. Je ressens mes cours comme une "boîte noire" avec injection sur le terrain/setter. Et il y a (d'après mon expérience) une tendance à ce que les développeurs ne se soucient pas de la classe avec de nombreuses dépendances, ce qui est évident lorsque vous essayez de vous moquer de la classe en utilisant l'injection de constructeur (voir l'élément suivant). Un problème qui dérange les développeurs est le plus susceptible d'être résolu. En outre, une classe avec trop de dépendances viole probablement le principe de responsabilité unique (SRP).
  3. Facile et fiable pour se moquer : par rapport à l'injection sur le terrain , il est plus facile de se moquer dans un test unitaire, parce que vous ne le faites pas besoin de n'importe quelle simulation de contexte ou technique de réflexion pour atteindre le Bean privé déclaré dans la classe et vous ne serez pas dupe de lui . Vous instanciez juste la classe et passez les haricots moqués. Simple à comprendre et facile.
  4. Les outils de qualité du code peuvent vous aider : Des outils comme Sonar peuvent vous avertir que la classe devient trop complexe, car une classe avec beaucoup de paramètres est probablement trop classe complexe et besoin d'un remaniement. Ces outils ne peuvent pas identifier le même problème lorsque la classe utilise l'injection Field/Setter.
  5. Code meilleur et indépendant : Avec moins @AutoWired réparti entre votre code, votre code est moins dépendant du framework. Je sais que la possibilité de changer simplement le cadre DI dans un projet ne justifie pas cela (qui fait ça, non?). Mais cela montre, pour moi, un meilleur code (j'ai appris cela à la dure avec EJB et recherches). Et avec Spring, vous pouvez même supprimer le @AutoWired annotation utilisant l'injection de constructeur.
  6. Plus d'annotations ne sont pas toujours la bonne réponse : Pour certains raisons , j'essaie vraiment d'utiliser les annotations uniquement lorsqu'elles sont vraiment utiles.
  7. Vous pouvez rendre les injections immuables . L'immuabilité est une fonctionnalité très appréciée .

Si vous recherchez plus d'informations, je recommande cet ancien article (mais toujours pertinent) de Spring Blog nous expliquant pourquoi ils utilisent autant l'injection de setter et la recommandation d'utiliser l'injection de constructeur:

l'injection de setter est utilisée beaucoup plus souvent que vous ne le pensez, c'est le fait que les frameworks comme Spring en général, sont beaucoup plus adaptés pour être configurés par injection de setter que par injection de constructeur

Et cette note expliquant pourquoi ils croient que le constructeur convient mieux au code d'application:

Je pense que l'injection de constructeur est beaucoup plus utilisable pour le code d'application que pour le code framework. Dans le code d'application, vous avez intrinsèquement moins besoin de valeurs facultatives que vous devez configurer

Dernières pensées

Si vous n'êtes pas vraiment sûr de l'avantage du constructeur, peut-être l'idée de mélanger le setter (pas le champ) avec l'injection du constructeur est une option, comme l'explique le Spring team :

Étant donné que vous pouvez mélanger les DI basés sur le constructeur et sur le Setter, il est préférable d'utiliser des arguments de constructeur pour les dépendances obligatoires et des paramètres pour les dépendances facultatives.

Mais rappelez-vous que l'injection sur le terrain est probablement celle à éviter parmi les trois.

8
Dherik