web-dev-qa-db-fra.com

L'utilisation de "nouveau" dans le constructeur est-elle toujours mauvaise?

J'ai lu que l'utilisation de "nouveau" dans un constructeur (pour tout autre objet que ceux à valeur simple) est une mauvaise pratique car elle rend les tests unitaires impossibles (car ces collaborateurs doivent également être créés et ne peuvent pas être moqués). Comme je n'ai pas vraiment d'expérience dans les tests unitaires, j'essaie de rassembler quelques règles que j'apprendrai d'abord. Est-ce également une règle qui est généralement valable, quelle que soit la langue utilisée?

37
Ezoela Vacca

Il y a toujours des exceptions, et je conteste le "toujours" dans le titre, mais oui, ce directive est généralement valide, et s'applique également en dehors du constructeur.

L'utilisation de new dans un constructeur viole le D dans SOLID (principe d'inversion de dépendance). Cela rend votre code difficile à tester car les tests unitaires sont tout au sujet de l'isolement; il est difficile d'isoler la classe s'il a du béton références.

Il ne s'agit pas seulement de tests unitaires. Que faire si je veux pointer un référentiel vers deux bases de données différentes à la fois? 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. Cela s'applique également aux langages qui peuvent utiliser des constructions autres que new pour l'initialisation d'objet.

Cependant, il est clair que vous devez faire preuve de bon jugement. Il y a de nombreuses fois où c'est bien d'utiliser new, ou où il vaut mieux ne pas le faire, mais vous n'aurez pas de conséquences négatives. Quelque part, quelque part, new doit être appelé. Soyez très prudent lorsque vous appelez new à l'intérieur d'une classe dont dépendent de nombreuses autres classes.

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

Plus une classe contient de références, plus vous devez faire attention de ne pas appeler new à l'intérieur.

36
TheCatWhisperer

Bien que je sois en faveur d'utiliser le constructeur pour simplement initialiser la nouvelle instance plutôt que pour créer plusieurs autres objets, les objets d'assistance sont corrects, et vous devez utiliser votre jugement pour savoir si quelque chose est une telle aide interne ou non.

Si la classe représente une collection, elle peut avoir un tableau ou une liste d'assistance interne ou un hashset. Il utiliserait new pour créer ces assistants et il serait considéré comme tout à fait normal. La classe n'offre pas d'injection pour utiliser différents assistants 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 à l'accumulation, la suppression et le remplacement d'éléments dans la collection.


Dans un certain sens, la construction de classe d'un langage de programmation est un mécanisme pour créer des abstractions de niveau supérieur, et nous créons de telles abstractions pour combler le fossé entre le domaine problématique et les primitives 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 juger si l'abstraction nécessite simplement un ou plusieurs objets internes/auxiliaires, tout en étant toujours vue 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 en utilisant la classe.

50
Erik Eidt

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 ultérieurs. Lorsque vous utilisez TDD, vous pouvez refactoriser ce "collaborateur" en extrayant une classe où elle est déjà entièrement testée à partir de votre premier processus de test.

27
Joppe

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

Soyez prudent en apprenant les "règles" pour les problèmes que vous n'avez jamais rencontrés. Si vous rencontrez une "règle" ou une "meilleure pratique", je suggérerais de trouver un simple exemple 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 dit la "règle".

Dans ce cas, vous pouvez essayer de trouver 2 ou 3 classes simples et certains comportements à mettre en œuvre. Implémentez les classes de la manière la plus 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 avez commencé avec des choses fonctionnant dans un sens, vous avez dû revenir en arrière et les changer plus tard; si vous vous trompez sur la façon dont les choses sont censées s'emboîter; si vous vous ennuyez à écrire du 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 lorsque vous respectez la règle, et lesquelles pourraient ne pas l'être.


Quant à votre question réelle, j'ai tendance à privilégier une approche ports et adaptateurs , où nous faisons une distinction entre la "logique de base" et les "services" (cela revient à distinguer entre les fonctions pures et les procédures efficaces) .

La logique principale consiste à calculer les choses "à l'intérieur" de l'application, en fonction du domaine problématique. Il peut contenir des classes comme User, Document, Order, Invoice, etc. C'est bien d'avoir des classes de base appeler new pour d'autres les classes de base, car ce sont des détails d'implémentation "internes". Par exemple, la création d'un Order peut également créer un Invoice et un Document détaillant ce qui a été commandé. Il n'est pas nécessaire de se moquer de ceux-ci pendant les tests, car ce sont les choses réelles que nous voulons tester!

Les ports et les adaptateurs sont la façon dont la logique principale interagit avec le monde extérieur. C'est là que vivent des choses comme Database, ConfigFile, EmailSender, etc. Ce sont ces choses qui rendent les tests difficiles, il est donc conseillé de les créer en dehors de la logique principale, et de les transmettre si nécessaire (soit avec l'injection de dépendances, ou comme arguments de méthode, etc.).

De cette façon, la logique de base (qui est la partie spécifique à l'application, où vit la logique métier importante, et qui est soumise à la plupart des désabonnements) peut être testée seule, sans avoir à se soucier des bases de données, des fichiers, des e-mails, 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, en utilisant des maquettes 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 quelques valeurs d'exemple et nous assurer qu'elles sont stockées/lues/envoyées/etc. de manière appropriée.

13
Warbo

Permettez-moi de répondre à la question en rassemblant ce que je considère comme les points clés ici. Je vais citer un utilisateur pour plus de brièveté.

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

L'utilisation de new dans un constructeur viole le D dans SOLID (principal d'inversion de dépendance). Cela rend votre code difficile à tester car les tests unitaires concernent l'isolement; il est difficile d'isoler une classe si elle a des références concrètes.

TheCatWhisperer -

Oui, l'utilisation de new à l'intérieur des constructeurs conduit souvent à des défauts de conception (par exemple un couplage serré) qui rendent notre conception rigide. Difficile à 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, il pourrait y avoir classes qui sont censées être étroitement couplées. David Arno a commenté un couple.

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

David Arno -

Exactement. Certains classes (par exemple les classes internes) pourraient être de simples détails d'implémentation de la classe principale. Ceux-ci sont destinés à être testés avec la classe principale, et ils ne sont pas nécessairement remplaçables ou extensibles.

De plus, si notre SOLIDE culte nous fait extraire ces classes, nous pourrions violer un autre bon principe. Le soi-disant loi de Déméter . Ce qui, d'autre part, je trouve que c'est vraiment important du point de vue du design.

Ainsi, la réponse probable, comme d'habitude, est dépend . L'utilisation de new à l'intérieur des constructeurs peut être une mauvaise pratique. Mais pas systématiquement.

Donc, il nous faut évaluer si les classes sont détails d'implémentation (la plupart des cas ne le seront pas) de la classe principale. S'ils le sont, laissez-les tranquilles. Si ce n'est pas le cas, envisagez des techniques telles que Racine de composition ou Injection de dépendances par conteneurs IoC .


1: l'objectif principal de SOLID n'est pas de rendre notre code plus testable. C'est de rendre notre code plus tolérant aux changements. Plus flexible et en conséquence, plus facile à tester

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

6
Laiv

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; }
}

Puisque le new est un détail d'implémentation pur de Foo, et les deux Foo::Bar et Foo::Baz font partie de Foo, lorsque les tests unitaires de Foo ne servent à rien de se moquer des parties de Foo. Vous ne vous moquez que des parties extérieurFoo lors des tests unitaires Foo.

3
MSalters