L'utilisation de «new» dans le constructeur est-elle toujours mauvaise?

37

J'ai lu que l'utilisation de "new" dans un constructeur (pour tout autre objet que celui de simple valeur) est une mauvaise pratique, car elle rend les tests unitaires impossibles (dans la mesure où ces collaborateurs doivent également être créés et ne peuvent être simulés). Comme je n'ai pas vraiment d'expérience dans les tests unitaires, j'essaie de rassembler certaines règles que je vais apprendre en premier. En outre, s’agit-il d’une règle généralement valable, quelle que soit la langue utilisée?

Ezoela Vacca
la source
9
Cela ne rend pas les tests impossibles. Cela rend cependant plus difficile la maintenance et le test de votre code. Ayez une lecture de new is glue par exemple.
David Arno
38
"toujours" est toujours incorrect. :) Il existe des meilleures pratiques, mais les exceptions abondent.
Paul le
63
Quelle langue est-ce? newsignifie différentes choses dans différentes langues.
user2357112 prend en charge Monica le
13
Cette question dépend un peu de la langue, n'est-ce pas? Toutes les langues n'ont même pas de newmot clé. De quelles langues parlez-vous?
Bryan Oakley
7
Règle stupide. Ne pas utiliser l'injection de dépendance quand vous devriez avoir très peu à voir avec le "nouveau" mot clé. Au lieu de dire que "l'utilisation de new est un problème si vous l'utilisez pour rompre l'inversion de dépendance", dites simplement "ne rompez pas l'inversion de dépendance".
Matt Timmermans

Réponses:

36

Il y a toujours des exceptions, et je suis en désaccord avec le mot "toujours" dans le titre, mais oui, cette directive est généralement valable et s'applique également à l'extérieur du constructeur.

L'utilisation de new dans un constructeur enfreint le D de SOLID (principe d'inversion de dépendance). Cela rend votre code difficile à tester car les tests unitaires sont entièrement basés sur l'isolation; il est difficile d'isoler la classe si elle a des références concrètes.

Il ne s’agit pas seulement de tests unitaires. Que faire si je veux diriger un référentiel vers deux bases de données différentes en même temps? La possibilité de passer dans mon propre contexte me permet d’instancier deux référentiels différents pointant vers des emplacements différents.

Ne pas utiliser new dans le constructeur rend votre code plus flexible. Ceci s'applique également aux langages pouvant utiliser des constructions autres que newpour l'initialisation d'objet.

Cependant, il est clair que vous devez faire preuve de bon jugement. Il y a beaucoup de fois où il est bon d'utiliser newou qu'il serait préférable de ne pas utiliser, mais vous n'aurez pas de conséquences négatives. À un moment donné, newil faut appeler quelque part . Faites très attention lorsque vous appelez newdans une classe dont dépendent beaucoup d'autres.

Faire quelque chose comme initialiser une collection privée vide dans votre constructeur est acceptable, et l'injecter serait absurde.

Plus une classe a de références, plus vous devriez faire attention à ne pas appeler newde l'intérieur.

TheCatWhisperer
la source
12
Je ne vois vraiment aucune valeur dans cette règle. Il existe des règles et des directives sur la manière d'éviter un couplage étroit dans le code. Cette règle semble traiter exactement du même problème, à l'exception du fait qu'elle est incroyablement trop restrictive sans nous apporter de valeur supplémentaire. Alors à quoi ça sert? Pourquoi créer un objet d’entête factice pour mon implémentation LinkedList au lieu de traiter tous ces cas particuliers dus head = nullà l’amélioration du code? Pourquoi laisser les collections incluses comme nulles et les créer à la demande serait mieux que de le faire dans le constructeur?
Voo le
22
Vous manquez le point. Oui, un module de persistance est absolument quelque chose qu'un module de niveau supérieur ne devrait pas dépendre. Mais "ne jamais utiliser de nouveau" ne découle pas de "certaines choses doivent être injectées et non directement couplées". Si vous prenez votre copie fidèle de Agile Software Development, vous verrez que Martin parle généralement de modules et non de classes uniques: "Les modules de niveau supérieur traitent des stratégies de haut niveau de l’application. Celles-ci se soucient généralement peu des détails qui implémentent leur."
Voo le
11
Vous devez donc éviter les dépendances entre les modules, en particulier entre les modules de niveaux d'abstraction différents. Mais un module peut être plus qu'une classe et il n'y a tout simplement aucun intérêt à construire des interfaces entre le code étroitement couplé de la même couche d'abstraction. Dans une application bien conçue qui suit les principes SOLID, toutes les classes ne doivent pas implémenter une interface et tous les constructeurs ne doivent pas toujours prendre uniquement des interfaces en tant que paramètres.
Voo le
18
J'ai voté contre parce qu'il y a beaucoup de mot à la mode et qu'il n'y a pas beaucoup de discussions sur des considérations pratiques. Il y a quelque chose à propos d'un repo à deux bases de données, mais honnêtement, ce n'est pas un exemple du monde réel.
jpmc26
5
@ jpmc26, BJoBnh Sans entrer si je tolère ou non cette réponse, le point est exprimé très clairement. Si vous pensez que "principe d'inversion de dépendance", "référence concrète" ou "initialisation d'objet" ne sont que des mots à la mode, vous ne savez vraiment pas ce qu'ils signifient. Ce n'est pas la faute de la réponse.
R. Schmitz
50

Bien que je sois favorable à l'utilisation du constructeur pour initialiser simplement la nouvelle instance plutôt que pour créer plusieurs autres objets, les objets d'assistance fonctionnent bien et vous devez utiliser votre jugement pour déterminer si quelque chose est une telle assistance interne ou non.

Si la classe représente une collection, elle peut alors avoir un tableau d'assistance interne, une liste ou un hachage. Il serait utile newde créer ces aides et cela serait considéré comme tout à fait normal. La classe n'offre pas d'injection pour utiliser différentes aides internes et n'a aucune raison de le faire. Dans ce cas, vous souhaitez tester les méthodes publiques de l'objet, qui peuvent aller jusqu'à accumuler, supprimer et remplacer des éléments dans la collection.


D'une certaine manière, la construction de classe d'un langage de programmation est un mécanisme permettant de créer des abstractions de niveau supérieur, et nous créons de telles abstractions afin de combler le fossé entre les primitives du domaine du problème et du langage de programmation. Cependant, le mécanisme de classe n'est qu'un outil; il varie selon le langage de programmation et, certaines abstractions de domaine, dans certains langages, nécessitent simplement plusieurs objets au niveau du langage de programmation.

En résumé, vous devez déterminer si l'abstraction nécessite simplement un ou plusieurs objets internes / auxiliaires, tout en restant perçu par l'appelant comme une seule abstraction, ou si les autres objets seraient mieux exposés à l'appelant pour créer pour contrôle des dépendances, ce qui serait suggéré, par exemple, lorsque l'appelant voit ces autres objets lors de l'utilisation de la classe.

Erik Eidt
la source
4
+1 C'est exactement ça. Vous devez identifier le comportement souhaité de la classe et déterminer les détails d'implémentation à exposer et ceux qui ne le sont pas. Il y a une sorte de jeu d'intuition subtile auquel vous devez jouer pour déterminer quelle est la "responsabilité" de la classe.
jpmc26
27

Tous les collaborateurs ne sont pas suffisamment intéressants pour effectuer des tests unitaires séparément, vous pouvez (indirectement) les tester via la classe d'hébergement / d'instanciation. Cela peut ne pas correspondre à l'idée de certaines personnes de devoir tester chaque classe, chaque méthode publique, etc., en particulier lors des tests suivants. Lorsque vous utilisez TDD, vous pouvez refactoriser ce "collaborateur" en extrayant une classe où elle est déjà complètement testée à partir de votre premier processus de test.

Joppe
la source
14
Not all collaborators are interesting enough to unit-test separatelyfin de l'histoire :-), Cette affaire est possible et personne n'a osé le mentionner. @Joppe, je vous encourage à élaborer un peu la réponse. Par exemple, vous pouvez ajouter quelques exemples de classes qui ne sont que des détails d'implémentation (ne pouvant pas être remplacés) et comment elles peuvent être extraites ultérieurement si nous le jugeons nécessaire.
Laiv
Les modèles de domaine @Laiv sont généralement concrets et non abstraits. Vous ne devez donc pas y injecter d'objets imbriqués. Un objet valeur simple / objet complexe n'ayant pas de logique est également candidat.
Joppe
4
+1, absolument. Si une langue est mis en place afin que vous devez appeler new File()au fichier lié à quoi que ce soit, il n'y a pas de sens en interdisant cet appel. Qu'est-ce que tu vas faire, écrire des tests de régression par rapport au Filemodule stdlib ? Pas probable. D'autre part, faire appel newà une classe auto-inventée est plus douteux.
Kilian Foth
7
@KilianFoth: L'unité de bonne chance teste tout ce qui appelle directement new File().
Phoshi
1
C'est une conjecture . Nous pouvons trouver des cas où cela n’a pas de sens, des cas où cela n’est pas utile et des cas où cela a du sens et qui sont utiles. C'est une question de besoins et de préférences.
Laiv
13

Comme je n'ai pas vraiment d'expérience dans les tests unitaires, j'essaie de rassembler certaines règles que je vais d'abord apprendre.

Soyez prudent en apprenant des "règles" pour des problèmes que vous n'avez jamais rencontrés. Si vous rencontrez une "règle" ou une "meilleure pratique", je vous suggérerais de trouver un exemple simple de jouet où cette règle est "supposée" être utilisée, et d'essayer de résoudre ce problème vous - même , en ignorant ce que la "règle" dit.

Dans ce cas, vous pouvez essayer de créer 2 ou 3 classes simples et certains comportements à mettre en œuvre. Implémentez les classes de la manière qui vous semble naturelle et écrivez un test unitaire pour chaque comportement. Faites une liste de tous les problèmes que vous avez rencontrés, par exemple si vous aviez commencé avec des choses qui fonctionnaient dans un sens, vous deviez ensuite revenir en arrière et les changer plus tard; si vous avez du mal à comprendre comment les choses sont censées s'emboîter; si vous vous ennuyez en écrivant un message passe-partout; etc.

Ensuite, essayez de résoudre le même problème en suivant la "règle". Encore une fois, faites une liste des problèmes que vous avez rencontrés. Comparez les listes et réfléchissez aux situations qui pourraient être meilleures en suivant la règle et celles qui ne le pourraient pas.


En ce qui concerne votre question, j’ai tendance à privilégier une approche basée sur les ports et les adaptateurs , dans laquelle nous faisons une distinction entre "logique de base" et "services" (ceci est similaire à la distinction entre fonctions pures et procédures efficaces).

La logique de base consiste à calculer des éléments "à l'intérieur" de l'application, en fonction du domaine du problème. Il peut contenir des classes comme User, Document, Order, Invoice, etc. Il va bien d'avoir des classes de base appellent newà d' autres classes de base, car ils sont les détails de mise en œuvre « internes ». Par exemple, créer un Orderpeut également créer un Invoiceet un Documentdétail de ce qui a été commandé. Il n'est pas nécessaire de vous moquer de cela pendant les tests, car ce sont les choses que nous voulons tester!

Les ports et les adaptateurs permettent d’interagir entre la logique principale et le monde extérieur. C'est là des choses comme Database, ConfigFile, EmailSender, etc. vivent. Ce sont les choses qui rendent les tests difficiles, il est donc conseillé de les créer en dehors de la logique de base et de les transmettre au besoin (avec une injection de dépendance, ou comme arguments de méthode, etc.).

De cette façon, la logique principale (qui est la partie spécifique à l'application, où réside la logique métier importante et est soumise au plus grand nombre de désabonnements) peut être testée seule, sans avoir à se soucier des bases de données, des fichiers, des courriels, etc. Nous pouvons simplement passer quelques exemples de valeurs et vérifier que nous obtenons les bonnes valeurs de sortie.

Les ports et les adaptateurs peuvent être testés séparément, à l'aide de modèles pour la base de données, le système de fichiers, etc., sans avoir à se soucier de la logique métier. Nous pouvons simplement passer des exemples de valeurs et nous assurer qu'elles sont stockées / lues / envoyées / etc. de manière appropriée.

Warbo
la source
6

Permettez-moi de répondre à la question en regroupant ce que je considère être les points clés ici. Je citerai un utilisateur pour la brièveté.

Il y a toujours des exceptions, mais oui, cette règle est généralement valide et s'applique également à l'extérieur du constructeur.

L'utilisation de new dans un constructeur enfreint le D de SOLID (principe d'inversion de dépendance). Cela rend votre code difficile à tester car les tests unitaires sont entièrement basés sur l'isolation; il est difficile d'isoler la classe si elle a des références concrètes.

-TheCatWhisperer-

Oui, l'utilisation de newconstructeurs internes conduit souvent à des défauts de conception (par exemple, un couplage étroit) qui rend notre conception rigide. Difficile de tester oui, mais pas impossible. La propriété en jeu ici est la résilience (tolérance aux changements) 1 .

Néanmoins, la citation ci-dessus n'est pas toujours vraie. Dans certains cas, certaines classes peuvent être étroitement couplées . David Arno a commenté un couple.

Il y a bien sûr des exceptions où la classe est un objet de valeur immuable , un détail d'implémentation , etc. Où ils sont supposés être étroitement couplés .

-David Arno-

Exactement. Certaines classes (par exemple, les classes internes) pourraient n'être que des détails d'implémentation de la classe principale. Celles-ci sont destinées à être testées aux côtés de la classe principale et ne sont pas nécessairement remplaçables ni extensibles.

De plus, si notre culte SOLID nous oblige à extraire ces classes , nous pourrions violer un autre principe positif. La soi-disant loi de Demeter . Ce qui, au contraire, me semble très important du point de vue de la conception.

Donc, la réponse probable, comme d'habitude, dépend . Utiliser des newconstructeurs internes peut être une mauvaise pratique. Mais pas systématiquement toujours.

Il nous faut donc évaluer si les classes sont des détails d'implémentation (dans la plupart des cas, elles ne le seront pas) de la classe principale. S'ils le sont, laissez-les seuls. Si ce n'est pas le cas, envisagez des techniques telles que la composition en racine ou l' injection de dépendance par IoC Containers .


1: L’objectif principal de SOLID n’est pas de rendre notre code plus testable. C'est pour rendre notre code plus tolérant aux changements. Plus flexible et, par conséquent, plus facile à tester

Remarque: David Arno, TheWhisperCat, j'espère que cela ne vous dérange pas que je vous ai cité.

Laiv
la source
3

Comme exemple simple, considérons le pseudocode suivant

class Foo {
  private:
     class Bar {}
     class Baz inherits Bar {}
     Bar myBar
  public:
     Foo(bool useBaz) { if (useBaz) myBar = new Baz else myBar = new Bar; }
}

Comme il news’agit d’un détail purement lié à la mise en œuvre Foo, et aux deux Foo::Baret qui en Foo::Bazfont partie Foo, lorsque les tests unitaires ne servent à Foorien de se moquer de certains éléments Foo. Vous ne moquez que les pièces à l' extérieur Foo lors des tests unitaires Foo.

MSalters
la source
-3

Oui, utiliser 'new' dans votre classe racine d'application est une odeur de code. Cela signifie que vous obligez la classe à utiliser une implémentation spécifique et que vous ne pourrez pas en remplacer une autre. Toujours opter pour l'injection de la dépendance dans le constructeur. Ainsi, non seulement vous serez en mesure d’injecter facilement des dépendances simulées lors des tests, mais cela rendra votre application beaucoup plus flexible en vous permettant de remplacer rapidement différentes implémentations si nécessaire.

EDIT: Pour les utilisateurs qui descendent, voici un lien vers un livre de développement logiciel identifiant "nouvelle" comme une odeur de code possible: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = onepage & q = new% 20keyword% 20code% 20smell & f = false

Éternel21
la source
15
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.Pourquoi c'est un problème? Toutes les dépendances dans un arbre de dépendance ne doivent pas toutes être remplacées
Laiv
4
@Paul: Avoir une implémentation par défaut signifie que vous prenez une référence étroitement liée à la classe concrète spécifiée par défaut. Cela n'en fait cependant pas une "odeur de code".
Robert Harvey
8
@EzoelaVacca: Je serais prudent d'utiliser les mots "odeur de code" dans n'importe quel contexte. C'est un peu comme dire "républicain" ou "démocrate"; que veulent dire ces mots? Dès que vous donnez une étiquette de ce genre, vous cessez de penser aux vrais problèmes et vous arrêtez d'apprendre.
Robert Harvey
4
"Plus flexible" n'est pas automatiquement "meilleur".
quel nom
4
@EzoelaVacca: L'utilisation du newmot clé n'est pas une mauvaise pratique et ne l'a jamais été. C'est la façon dont vous utilisez l'outil qui compte. Vous n'utilisez pas de marteau, par exemple, où un marteau à pointe plate suffira.
Robert Harvey