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

62

Je sais que c'est un débat brûlant et que les opinions ont tendance à changer avec le temps en ce qui concerne la meilleure approche à adopter.

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

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

Dans le monde actuel, y a-t-il une méthode privilégiée d'injection? L'injection sur le terrain est-elle préférable?

Ayant changé pour l'injection de constructeur à partir d'injection de champ ces dernières années, je trouve qu'il est beaucoup plus clair d'utiliser, mais je me demande si je devrais réexaminer mon point de vue. L'auteur de JMockit (Rogério Liesenfeld) est clairement au courant des techniques de DI. Je me sens donc obligé de revoir mon approche étant donné qu'il se sent si fermement opposé à l'injection de constructeur / poseur.

Eric B.
la source
2
Si vous n'êtes pas autorisé à utiliser l'injection de constructeur ou l' injection de setter, comment êtes-vous censé remettre vos dépendances à la classe? Vous feriez peut-être mieux de faire le lien avec le débat, à moins que votre conversation avec le gars de Mockit soit privée.
Robert Harvey
2
@DavidArno: Dans une classe immuable, l'état est défini une fois ... dans le constructeur.
Robert Harvey
1
@Davkd ce que vous décrivez est le modèle du domaine anémique. Il a certes des partisans, mais il n’est plus vraiment orienté objet; où les données et les fonctions qui agissent sur ces données cohabitent.
Richard Tingle
3
@ David Il n'y a rien de mal avec la programmation fonctionnelle. Mais Java est fondamentalement conçu pour être orienté objet et vous devrez constamment le combattre et finir avec le modèle du domaine anémique si vous ne faites pas très attention. Il n'y a rien de mal avec du code orienté fonction, mais un outil approprié doit être utilisé, ce que java n'est pas (même si lambda a ajouté des éléments de programmation basée sur les fonctions)
Richard Tingle

Réponses:

48

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

Prenons l'exemple que vous avez fourni dans votre publication 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(); }
...
}

En gros, ce que vous dites, c’est que "j’ai cette classe avec un état privé, à laquelle j’ai joint des @injectableannotations, ce qui signifie que l’état peut être automatiquement peuplé par un agent extérieur, même si mon état a été déclaré privé. "

Je comprends les motivations pour cela. C'est une tentative pour éviter une grande partie de la cérémonie qui est inhérente à la mise en place d'une classe correctement. En gros, ce que l’on dit, c’est: "Je suis fatigué d’écrire tout ce passe-partout, alors je vais simplement annoter tout mon état et laisser le conteneur de 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. Aussi, pourquoi s'arrêter là? Traditionnellement, DI s’appuyait sur chaque classe ayant une interface compagnon. Pourquoi ne pas éliminer toutes ces interfaces avec des annotations?

Considérez l’alternative (cela va être C #, parce que je le connais mieux, mais il existe 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 sais déjà certaines choses à propos de ce cours. C'est une classe immuable. state ne peut être défini que dans le constructeur (références, dans ce cas particulier). Et comme tout provient d’une interface, je peux échanger des implémentations dans le constructeur, c’est là que vos simulacres entrent en jeu.

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

Certes, cela fait beaucoup de choses, mais c’est ainsi que le langage a été conçu. Les annotations semblent être un sale bidouillage pour quelque chose qui aurait dû être intégré au langage lui-même.

Robert Harvey
la source
À moins que je ne lise mal votre message, vous ne regardez pas vraiment l'exemple correctement. Mon implémentation de VeracodeServiceest presque identique à celle que vous avez écrite (bien que Java vs C #). Le VeracodeServiceImplTestest en fait une classe de test unitaire. Les @Injectablechamps sont essentiellement des objets fictifs insérés dans le contexte. Le @Testedchamp est l'objet / la classe avec le constructeur DI défini. Je suis d'accord avec votre point de vue qui préfère l'injection de constructeur à l'injection de champ. Cependant, comme je l'ai mentionné, l'auteur de JMockit pense le contraire et j'essaie de comprendre pourquoi
Eric B.
Si vous regardez le premier message de cette discussion, vous verrez que j'ai presque exactement la même définition dans mon cours Repo.
Eric B.
Si vous ne parlez que de cours de test, cela n’a peut-être pas d'importance. Parce que, testez des classes.
Robert Harvey
Non - je ne parle pas de classes de test. Je parle de classes de production. Il se trouve que l'exemple du groupe de discussion est un test unitaire, car le cadre est un cadre moqueur / de test. (Toute la discussion tourne autour de savoir si le cadre moqueur devrait supporter l'injection de constructeur)
Eric B.
3
+1: Oui, pas de magie dans la version C #. C'est une classe standard, elle a des dépendances, elles sont toutes injectées à un moment donné, avant que la classe ne soit utilisée. La classe peut être utilisée avec un conteneur DI ou non. Rien dans la classe ne le dit CIO ou le cadre "Automatic Dependency Injection".
Binary Worrier
27

L'argument de moins de test initialisation boiletplate est valide, mais il y a d'autres préoccupations à prendre en compte. Tout d'abord, vous devez répondre à une question:

Est-ce que je veux que ma classe ne soit instanciable que par réflexion?

Utiliser des injections sur le terrain signifie réduire la 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 le langage Java ne prennent même pas en charge la réflexion ( GWT ). Par conséquent, les classes à injection de champ ne seront pas compatibles avec celles-ci.

Le deuxième problème est la performance . Un appel de constructeur (direct ou par réflexion) est toujours plus rapide qu'un groupe d'affectations de champs de réflexion. Les infrastructures d'injection de dépendance doivent utiliser l'analyse de réflexion pour créer un arbre de dépendance et créer des constructeurs de réflexion. Ce processus entraîne des pertes de performances supplémentaires.

La performance affecte la maintenabilité . Si chaque suite de tests doit être exécutée dans un conteneur d'injection de dépendance, des milliers de tests unitaires peuvent durer des dizaines de minutes. En fonction de 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 plate-forme?
  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, sur le plan de la qualité, nous aimerions généralement garder le code élevé en ce qui concerne la compatibilité, la testabilité et la maintenabilité. Du point de vue philosophique, l’injection de champ rompt l’ encapsulation , qui est l’un des quatre principes fondamentaux de la programmation orientée objet , qui est le paradigme principal de Java.

Beaucoup, beaucoup d'arguments contre l'injection de champ.

Maciej Chałapuk
la source
3
+1 Vous touchez un nerf. 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. Remarquez que j'aime DI. Juste pas sûr sur les cadres.
Marjan Venema
1
Grands arguments. Il exprime beaucoup de mes pensées mais je suis heureux de voir les autres ressentir le même sentiment. Aussi incroyable que j'aie eu l'habitude de trouver l'injection sur le terrain, je pense que je l'aimais tellement simplement parce que c'était "plus facile". Je trouve l'injection de constructeur tellement plus claire maintenant.
Eric B.
19

L'injection sur le terrain obtient un vote "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 au code implicite, et ne tolère l'indirection que comme un avantage évident, car il rend le code plus difficile à comprendre et à raisonner.

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

Remarquez que j'aime DI et tous les avantages que cela apporte. Juste pas sûr d'avoir besoin des frameworks pour instancier une classe. Surtout pas l'effet que les conteneurs IoC ou d'autres cadres DI peuvent avoir sur le code de test.

J'aime que mes tests soient très simples. Je n'aime pas avoir à passer par la configuration d'un conteneur IoC ou d'un autre framework DI pour le faire configurer ensuite ma classe à tester. Cela me semble bizarre.

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 nécessitant la configuration 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 les réflexions dans vos tests.

Marjan Venema
la source
Si vous utilisez, par exemple, Mockito mockito.org , vous n'avez pas besoin d'un cadre DI / IoC, même avec une injection sur le terrain, et vous pouvez exécuter des tests en parallèle, etc.
Hans-Peter Störr
1
@hstoerr Nice. Cependant, cela doit signifier que Mockito utilise la réflexion (ou son équivalent Java) ...
Marjan Venema
5

Eh bien, cela semble être une discussion polémique. La première chose à laquelle il faut remédier est que l’ injection sur site est différente de l’injection Setter . Je travaille déjà avec certaines personnes qui pensent que l'injection de Field and 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, ils partagent les mêmes préoccupations. Et, mon préféré, l'injection de 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 de setter / field (je citerai quelques liens Spring pour illustrer mon propos):

  1. Empêcher les dépendances circulaires : Spring est capable de détecter les dépendances circulaires entre les Beans, comme l'explique @Tomasz. Voir aussi l' équipe du printemps en disant cela.
  2. Les dépendances de la classe sont évidentes : Les dépendances de la classe sont évidentes dans le constructeur mais masquées par l' injection de champ / setter. Je ressens mes cours comme une "boîte noire" avec injection de champ / setter. Et il ya (selon mon expérience) une tendance aux développeurs de ne pas se soucier 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’article 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 enfreint probablement le principe de responsabilité unique (SRP).
  3. Facile et fiable à simuler : comparé à l'injection sur le terrain , il est plus facile de simuler un test unitaire, car vous n'avez besoin d'aucune simulation de contexte ni technique de réflexion pour atteindre le Bean privé déclaré dans la classe et vous ne serez pas dupe . Vous venez d’instancier la classe et de passer les haricots ridicules. Simple à comprendre et facile.
  4. Les outils de qualité de code peuvent vous aider : Des outils tels que Sonar peuvent vous avertir que la classe devient trop complexe, car une classe avec beaucoup de paramètres est probablement une classe trop complexe et nécessite un refactor. 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 : moins @AutoWiredvotre code étant répandu, votre code dépend moins du framework. Je sais que la possibilité de changer simplement le cadre de l'ID dans un projet ne justifie pas cela (qui fait cela, non?). Mais cela montre, pour moi, un meilleur code (je l’ai appris de manière difficile avec les EJB et les recherches). Et avec Spring, vous pouvez même supprimer l' @AutoWiredannotation à l'aide de l'injection de constructeur.
  6. Plus d'annotations n'est pas toujours la bonne réponse : Pour certaines raisons , j'essaie vraiment d'utiliser des 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 vous recommande cet article ancien (mais toujours pertinent) de Spring Blog nous expliquant pourquoi ils utilisent autant d'injection de setter et la recommandation d'utiliser l'injection de constructeur:

L’injection de setter est utilisée beaucoup plus souvent que prévu, c’est le fait que les frameworks comme Spring en général sont bien plus adaptés à la configuration par injection de setter que par injection de constructeur.

Et cette note sur la raison pour laquelle ils croient que le constructeur est plus approprié pour le code d'application:

Je pense que l'injection de constructeur est beaucoup plus utilisable pour le code d'application que pour le code-cadre. 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 que l'idée de mélanger un setter (pas de terrain) avec une injection de constructeur est une option, comme l'a expliqué l' équipe de Spring :

Etant donné que vous pouvez combiner les DI, constructeur et setter, il est judicieux 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.

Dherik
la source
-5

Votre dépendance est-elle susceptible de changer pendant la durée du cours? Si c'est le cas, utilisez l'injection de champ / propriété. Sinon, utilisez l'injection de constructeur.

Darren Young
la source
2
Cela n'explique vraiment rien. Pourquoi injecter des champs privés alors que vous pouvez le faire correctement?
Robert Harvey
Où dit-il quelque chose sur les champs privés? Je parle de l'interface publique d'une classe /
Darren Young
Il n'y a aucun argument sur l'injection de méthode vs l'injection de constructeur dans la question. L'auteur de JMockit voit les deux comme mauvais. Regardez le titre de la question.
Robert Harvey
La question ne dit-elle pas injection sur site vs injection constructeur?
Darren Young
1
L'injection sur le terrain est un animal complètement différent. Vous inculquez des valeurs à des membres privés .
Robert Harvey