Un seul objet de configuration est-il une mauvaise idée?

43

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?

JW01
la source
La configuration obtient ses paramètres en lisant un fichier sur le disque, non? Alors pourquoi ne pas simplement avoir un fichier "test.config" qu'il peut lire?
Anon.
Cela résout le premier problème, mais pas le second.
JW01
@ JW01: Tous vos tests ont-ils besoin d'une configuration nettement différente, alors? Vous allez devoir configurer cette configuration quelque part pendant votre test, n'est-ce pas?
Anon.
Vrai. Mais si je continue à utiliser un seul groupe de paramètres (avec un groupe différent pour les tests), tous mes tests finissent par utiliser les mêmes paramètres. Et comme je peux vouloir tester le comportement avec différents paramètres, ce n'est pas idéal.
JW01
"Donc, dans un test, vous devez configurer la configuration de la classe testée, mais également TOUTES ses dépendances. Cela peut rendre votre code de test moche." Avoir un exemple? J'ai l'impression que ce n'est pas la structure de toute votre application qui se connecte à la classe de configuration, mais plutôt la structure de la classe de configuration elle-même qui rend les choses "laides". La configuration de la configuration d'une classe ne devrait-elle pas automatiquement configurer ses dépendances?
AndrewKS

Réponses:

35

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 .

Kramii réintègre Monica
la source
7

Je sépare des groupes de paramètres liés avec des interfaces.

Quelque chose comme:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

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).

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Je trouve que ce mélange est le meilleur de tous les mondes.

Quentin Starin
la source
3

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.

Péter Török
la source
J'ai vraiment aimé votre approche et je ne suggérerais pas que l'idée d'un passeur soit gardée «secrète» ou considérée comme un «piratage rapide». Si vous acceptez le fait que les tests unitaires sont un utilisateur / consommateur / une partie importante de votre application, alors avoir des 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.
Filip Dupanović
1
@kRON, ce sont des astuces extrêmement utiles et pratiques pour rendre testables les unités de code héritées, et je les utilise également de manière intensive. Cependant, idéalement, le code de production ne devrait contenir aucune fonctionnalité introduite uniquement à des fins de test. À long terme, c’est vers cela que je réfléchis.
Péter Török le
2

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.

Justin Beal
la source
2

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:

  • Aurez-vous besoin de le sous-classer?
  • Aurez-vous besoin de programmer une interface?
  • Avez-vous besoin d'exécuter des tests unitaires contre cela?
  • Aurez-vous besoin de le modifier fréquemment?
  • Votre application est-elle destinée à prendre en charge plusieurs plates-formes / environnements de production?
  • Êtes-vous même un peu préoccupé par l'utilisation de la mémoire?

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.

Aaronaught
la source
0

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.

Tim Butterfield
la source
0

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.

indyK1ng
la source
0

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.

Grant Palin
la source