Dans la plupart de mes applications, j'ai un objet "config" unique ou statique, chargé de lire divers paramètres à partir du disque. Presque toutes les classes l'utilisent à des fins diverses. Il s’agit essentiellement d’une table de hachage composée de paires nom / valeur. C'est en lecture seule, donc je ne m'inquiète pas trop du fait que j'ai un état tellement global. Mais maintenant que je commence à utiliser les tests unitaires, cela commence à devenir un problème.
Un problème est que vous ne voulez généralement pas tester avec la même configuration que celle que vous utilisez. Il y a deux solutions à cela:
- Attribuez à l'objet de configuration un paramètre à utiliser UNIQUEMENT à des fins de test afin que vous puissiez définir différents paramètres.
- Continuez à utiliser un seul objet de configuration, mais changez-le d'un singleton en une instance que vous transmettez partout où vous en avez besoin. Ensuite, vous pouvez le construire une fois dans votre application et une fois dans vos tests, avec des paramètres différents.
Mais dans les deux cas, il reste un deuxième problème: presque toutes les classes peuvent utiliser l’objet config. Ainsi, lors d'un test, vous devez configurer la configuration de la classe en cours de test, mais également TOUTES ses dépendances. Cela peut rendre votre code de test moche.
Je commence à en venir à la conclusion que ce type d'objet de configuration est une mauvaise idée. Qu'est-ce que tu penses? Quelles sont les alternatives? Et comment commencez-vous à refactoriser une application qui utilise la configuration partout?
la source
Réponses:
Je n'ai pas de problème avec un seul objet de configuration, et je peux voir l'avantage de garder tous les paramètres au même endroit.
Cependant, l'utilisation de cet objet unique partout entraînera un niveau élevé de couplage entre l'objet config et les classes qui l'utilisent. Si vous devez modifier la classe de configuration, vous devrez peut-être visiter chaque instance d'utilisation et vérifier que rien n'a été cassé.
Une façon de résoudre ce problème consiste à créer plusieurs interfaces qui exposent des parties de l'objet config nécessaires à différentes parties de l'application. Plutôt que d'autoriser d'autres classes à accéder à l'objet config, vous transmettez les instances d'une interface aux classes qui en ont besoin. De cette manière, les parties de l'application qui utilisent config dépendent de la plus petite collection de champs de l'interface plutôt que de la classe de configuration entière. Enfin, pour les tests unitaires, vous pouvez créer une classe personnalisée qui implémente l'interface.
Si vous souhaitez approfondir ces idées, je vous recommande de lire davantage sur les principes SOLID , en particulier le principe de séparation des interfaces et le principe d'inversion de dépendance .
la source
Je sépare des groupes de paramètres liés avec des interfaces.
Quelque chose comme:
etc.
Désormais, dans un environnement réel, une seule classe implémentera plusieurs de ces interfaces. Cette classe peut extraire une base de données, la configuration de l'application ou quoi que ce soit d'autre, mais souvent, elle sait comment obtenir la plupart des paramètres.
Mais la ségrégation par interface rend les dépendances réelles beaucoup plus explicites et plus détaillées, ce qui constitue une amélioration évidente pour la testabilité.
Mais .. Je ne veux pas toujours être obligé d'injecter ou de fournir les paramètres en tant que dépendance. On pourrait faire valoir que je devrais faire preuve de cohérence ou de clarté. Mais dans la vraie vie, cela semble une restriction inutile.
Donc, je vais utiliser une classe statique comme façade, offrant une entrée facile depuis n'importe où vers les paramètres, et au sein de cette classe statique, je vais localiser l'implémentation de l'interface et obtenir le paramètre.
Je sais que la plupart des sites d’emplacement de services l’approuvent, mais avouons-le, fournir une dépendance à travers un constructeur est un poids, et parfois ce poids est plus important que ce que je me soucie de supporter. Service Location est la solution permettant de maintenir la testabilité via la programmation d'interfaces et de permettre des implémentations multiples tout en offrant l'avantage d'un point d'entrée singleton statique (dans des situations soigneusement mesurées et appropriées).
Je trouve que ce mélange est le meilleur de tous les mondes.
la source
Oui, comme vous l'avez compris, un objet de configuration globale rend les tests unitaires difficiles. Avoir un setter "secret" pour les tests unitaires est un hack rapide, qui, bien que peu pratique, peut s'avérer très utile: cela vous permet de commencer à écrire des tests unitaires, de sorte que vous puissiez refactoriser votre code vers une meilleure conception au fil du temps.
(Référence obligatoire: Travailler efficacement avec le code hérité . Il contient cette astuce et bien d'autres astuces précieuses pour le test du code hérité des unités.)
D'après mon expérience, le mieux est d'avoir le moins possible de dépendances sur la configuration globale. Ce qui n’est pas nécessairement nul, tout dépend des circonstances. Avoir quelques classes "d'organisateur" de haut niveau qui accèdent à la configuration globale et transmettent les propriétés de configuration réelles aux objets qu'ils créent et utilisent peut bien fonctionner, à condition que les organisateurs ne fassent que cela, par exemple ne contiennent pas beaucoup de code testable. Cela vous permet de tester la plupart ou la totalité des fonctionnalités importantes de l'application pouvant être testées par l'unité, sans pour autant perturber complètement le schéma de configuration physique existant.
Ceci est déjà proche d'une véritable solution d'injection de dépendance, telle que Spring for Java. Si vous pouvez migrer vers un tel cadre, c'est bien. Mais dans la vie réelle, et en particulier lorsqu'il s'agit d'une application héritée, le refactoring lent et méticuleux vers DI est le meilleur compromis possible.
la source
test_
attributs et des méthodes n'est plus une si mauvaise chose, n'est-ce pas? Je conviens que la solution véritable au problème consiste à utiliser un cadre DI, mais dans des exemples comme le vôtre, lorsque vous travaillez avec du code hérité ou dans d’autres cas simples, le code de test non aliénant s’applique de manière propre.D'après mon expérience, dans le monde Java, c'est à quoi le printemps est destiné. Il crée et gère des objets (beans) en fonction de certaines propriétés définies au moment de l'exécution et est par ailleurs transparent pour votre application. Vous pouvez aller avec le fichier test.config que Anon. mentionne ou vous pouvez inclure une logique dans le fichier de configuration que Spring gérera pour définir les propriétés en fonction d’une autre clé (par exemple, nom d’hôte ou environnement).
En ce qui concerne votre deuxième problème, vous pouvez le contourner par une nouvelle architecture mais ce n’est pas aussi grave qu’il ne le semble. Dans votre cas, cela signifie que vous n'auriez pas, par exemple, d'objet de configuration global utilisé par diverses autres classes. Vous ne feriez que de configurer ces autres classes via Spring, puis aucun objet de configuration, car tout ce qui nécessitait une configuration le faisait via Spring. Vous pouvez donc utiliser ces classes directement dans votre code.
la source
Cette question concerne en réalité un problème plus général que la configuration. Dès que j'ai lu le mot "singleton", j'ai immédiatement pensé à tous les problèmes associés à ce modèle, dont le moindre est la faible testabilité.
Le motif Singleton est "considéré comme nuisible". Cela signifie que ce n'est pas toujours la mauvaise chose, mais c'est généralement le cas . Si vous envisagez d'utiliser un motif singleton pour quelque chose que ce soit , arrêtez de considérer:
Si votre réponse est "oui" à l'un de ces éléments (et probablement à plusieurs autres choses auxquelles je n'avais pas pensé), vous ne voudrez probablement pas utiliser un singleton. Les configurations ont souvent besoin de beaucoup plus de flexibilité qu'un singleton (ou même une classe sans instance) peut en fournir.
Si vous voulez presque tous les avantages d'un singleton sans la moindre douleur, utilisez un framework d'injection de dépendance comme Spring ou Castle ou tout ce qui est disponible pour votre environnement. De cette façon, vous ne devez jamais le déclarer qu'une seule fois et le conteneur fournira automatiquement une instance à ceux qui en ont besoin.
la source
L'une des façons dont j'ai géré cela en C # consiste à avoir un objet singleton qui se verrouille pendant la création de l'instance et initialise toutes les données en même temps pour une utilisation par tous les objets clients. Si ce singleton peut gérer des paires clé / valeur, vous pouvez stocker toutes sortes de données, y compris des clés complexes, destinées à être utilisées par différents clients et types de clients. Si vous souhaitez conserver le dynamisme et charger les nouvelles données du client selon vos besoins, vous pouvez vérifier l'existence d'une clé principale du client et, le cas échéant, charger les données de ce client, en l'ajoutant au dictionnaire principal. Il est également possible que le singleton de configuration principal puisse contenir des ensembles de données client dans lesquelles des multiples du même type de client utilisent les mêmes données, toutes accessibles via le singleton de configuration principal. Une grande partie de cette organisation d'objet de configuration peut dépendre de la manière dont vos clients de configuration ont besoin d'accéder à ces informations et de leur caractère statique ou dynamique. J'ai utilisé les deux configurations clé / valeur ainsi que des API d'objet spécifiques en fonction de ce qui était nécessaire.
L'un de mes singletons de configuration peut recevoir des messages à recharger à partir d'une base de données. Dans ce cas, je charge dans une deuxième collection d'objets et ne verrouille que la collection principale afin d'échanger des collections. Cela empêche le blocage des lectures sur d'autres threads.
Si vous chargez des fichiers de configuration, vous pouvez avoir une hiérarchie de fichiers. Les paramètres d'un fichier peuvent spécifier lequel des autres fichiers doit être chargé. J'ai utilisé ce mécanisme avec des services Windows C # comportant plusieurs composants facultatifs, chacun avec ses propres paramètres de configuration. Les modèles de structure de fichier étaient les mêmes pour tous les fichiers, mais ont été chargés ou non en fonction des paramètres de fichier principaux.
la source
La classe que vous décrivez sonne comme un anti-modèle d'objet divin, sauf avec des données plutôt que des fonctions. C'est un problème. Les données de configuration devraient probablement être lues et stockées dans l'objet approprié, tout en ne les relisant que si cela est nécessaire pour une raison quelconque.
De plus, vous utilisez un Singleton pour une raison non appropriée. Les singletons ne devraient être utilisés que lorsque l'existence de plusieurs objets créera un mauvais état. L'utilisation d'un Singleton dans ce cas est inappropriée car le fait d'avoir plusieurs lecteurs de configuration ne devrait pas provoquer immédiatement un état d'erreur. Si vous en avez plusieurs, vous le faites probablement mal, mais il n'est pas absolument nécessaire que vous n'ayez qu'un lecteur de configuration.
Enfin, la création d'un tel état global est une violation de l'encapsulation, car vous autorisez plus de classes que nécessaire pour accéder directement aux données.
la source
Je pense que s’il ya beaucoup de paramètres, avec des "groupes" apparentés, il est judicieux de les séparer en interfaces séparées avec des implémentations respectives - puis d’injecter ces interfaces si nécessaire avec DI. Vous gagnez en testabilité, en couplage inférieur et en SRP.
Joshua Flanagan de Los Techies m'a inspiré à ce sujet; il a écrit un article à ce sujet il y a quelque temps.
la source