Où charger et stocker les paramètres d'un fichier?

9

Je pense que cette question devrait s'appliquer à la plupart des programmes qui chargent les paramètres d'un fichier. Ma question est du point de vue de la programmation, et c'est vraiment comment gérer le chargement des paramètres d'un fichier en termes de classes différentes et d'accessibilité. Par exemple:

  • Si un programme avait un settings.inifichier simple , son contenu devrait-il être chargé dans une load()méthode d'une classe, ou peut-être le constructeur?
  • Les valeurs doivent-elles être stockées dans des public staticvariables, ou doit-il y avoir des staticméthodes pour obtenir et définir des propriétés?
  • Que doit-il se passer si le fichier n’existe pas ou n’est pas lisible? Comment pourriez-vous faire savoir au reste du programme qu'il ne peut pas obtenir ces propriétés?
  • etc.

J'espère que je pose cette question au bon endroit ici. Je voulais rendre la question aussi indépendante du langage que possible, mais je me concentre principalement sur les langages qui ont des choses comme l'héritage - en particulier Java et C # .NET.

Andy
la source
1
Pour .NET, il est préférable d'utiliser App.config et la classe System.Configuration.ConfigurationManager pour obtenir les paramètres
Gibson
@ Gibson Je commence tout juste dans .NET, alors pourriez-vous me lier à de bons tutoriels pour cette classe?
Andy
Stackoverflow contient de nombreuses réponses. La recherche rapide révèle: stackoverflow.com/questions/114527/… et stackoverflow.com/questions/13043530/… Il y aura également beaucoup d'informations sur MSDN, à partir d'ici msdn.microsoft.com/en-us/library/ms228063(v = vs.100) .aspx et msdn.microsoft.com/en-us/library/ms184658(v=vs.110).aspx
Gibson
@Gibson Merci pour ces liens. Ils vous seront très utiles.
Andy

Réponses:

8

C'est en fait une question vraiment importante et elle est souvent mal faite car on ne lui donne pas suffisamment d'importance même si elle est au cœur de presque toutes les applications. Voici mes directives:

Votre classe de configuration, qui contient tous les paramètres, doit être simplement un ancien type de données simple, struct / class:

class Config {
    int prop1;
    float prop2;
    SubConfig subConfig;
}

Il ne devrait pas avoir besoin de méthodes et ne devrait pas impliquer d'héritage (sauf si c'est le seul choix que vous avez dans votre langue pour implémenter un champ variant - voir le paragraphe suivant). Il peut et doit utiliser la composition pour regrouper les paramètres dans des classes de configuration spécifiques plus petites (par exemple, subConfig ci-dessus). Si vous le faites de cette façon, il sera idéal de passer dans les tests unitaires et l'application en général car elle aura des dépendances minimales.

Vous devrez probablement utiliser des types de variantes, dans le cas où les configurations pour différentes configurations sont hétérogènes dans la structure. Il est admis que vous aurez besoin de placer un cast dynamique à un moment donné lorsque vous lisez la valeur pour le cast dans la bonne (sous-) classe de configuration, et cela dépendra sans aucun doute d'un autre paramètre de configuration.

Vous ne devriez pas être paresseux à taper tous les paramètres en tant que champs en faisant simplement ceci:

class Config {
    Dictionary<string, string> values;
};

C'est tentant car cela signifie que vous pouvez écrire une classe de sérialisation généralisée qui n'a pas besoin de savoir avec quels champs il s'agit, mais c'est faux et j'expliquerai pourquoi dans un instant.

La sérialisation de la configuration se fait dans une classe complètement distincte. Quelle que soit l'API ou la bibliothèque que vous utilisez pour ce faire, le corps de votre fonction de sérialisation doit contenir des entrées qui reviennent essentiellement à être une carte du chemin / clé du fichier vers le champ de l'objet. Certaines langues offrent une bonne introspection et peuvent le faire à votre place, d'autres vous devrez écrire explicitement le mappage, mais l'essentiel est que vous ne deviez avoir à écrire le mappage qu'une seule fois. Par exemple, considérez cet extrait que j'ai adapté de la documentation de l'analyseur des options du programme c ++ boost:

struct Config {
   int opt;
} conf;
po::options_description desc("Allowed options");
desc.add_options()
    ("optimization", po::value<int>(&conf.opt)->default_value(10);

Notez que la dernière ligne dit essentiellement "optimisation" mappe sur Config :: opt et également qu'il existe une déclaration du type que vous attendez. Vous voulez que la lecture de la configuration échoue si le type n'est pas celui que vous attendez, si le paramètre dans le fichier n'est pas vraiment un flottant ou un int, ou n'existe pas. C'est-à-dire qu'un échec doit se produire lorsque vous lisez le fichier car le problème est lié au format / validation du fichier et vous devez lancer un code d'exception / retour et signaler le problème exact. Vous ne devez pas retarder cela plus tard dans le programme. C'est pourquoi vous ne devriez pas être tenté d'avoir une capture de tout le style de dictionnaire Conf comme mentionné ci-dessus qui n'échouera pas lors de la lecture du fichier - car la conversion est retardée jusqu'à ce que la valeur soit nécessaire.

Vous devez rendre la classe Config en lecture seule d'une certaine manière - en définissant le contenu de la classe une fois lorsque vous la créez et l'initialisez à partir du fichier. Si vous devez avoir des paramètres dynamiques dans votre application qui changent, ainsi que des paramètres const qui ne le font pas, vous devriez avoir une classe distincte pour gérer les dynamiques plutôt que d'essayer de permettre aux bits de votre classe de configuration de ne pas être en lecture seule .

Idéalement, vous lisez le fichier à un endroit de votre programme, c'est-à-dire que vous n'avez qu'une seule instance de " ConfigReader". Cependant, si vous avez du mal à faire passer l'instance Config là où vous en avez besoin, il est préférable d'avoir un deuxième ConfigReader que d'introduire une configuration globale (ce que je suppose, c'est ce que l'OP signifie par "statique"). "), Ce qui m'amène à mon prochain point:

Évitez la chanson sirène séduisante du singleton: "Je vous éviterai d'avoir à passer cette classe, tous vos constructeurs seront charmants et propres. Allez, ce sera si facile." La vérité est qu'avec une architecture testable bien conçue, vous n'aurez guère besoin de passer la classe Config, ou des parties de celle-ci à travers autant de classes de votre application. Ce que vous trouverez, dans votre classe de niveau supérieur, votre fonction main () ou quoi que ce soit, vous démêlerez la conf en valeurs individuelles, que vous fournirez à vos classes de composants sous forme d'arguments que vous remettrez ensuite ensemble (dépendance manuelle injection). Une conf singleton / global / statique rendra les tests unitaires de votre application beaucoup plus difficiles à implémenter et à comprendre - par exemple, cela confondra les nouveaux développeurs avec votre équipe qui ne saura pas qu'ils doivent définir l'état global pour tester les choses.

Si votre langue prend en charge les propriétés, vous devez les utiliser à cette fin. La raison en est que cela signifie qu'il sera très facile d'ajouter des paramètres de configuration «dérivés» qui dépendent d'un ou plusieurs autres paramètres. par exemple

int Prop1 { get; }
int Prop2 { get; }
int Prop3 { get { return Prop1*Prop2; }

Si votre langue ne prend pas en charge nativement l'idiome de la propriété, il peut y avoir une solution de contournement pour obtenir le même effet, ou vous créez simplement une classe wrapper qui fournit les paramètres bonus. Si vous ne pouvez pas autrement conférer l'avantage des propriétés, c'est sinon une perte de temps pour écrire manuellement et utiliser des getters / setters simplement dans le but de plaire à un dieu OO. Vous serez mieux avec un vieux champ ordinaire.

Vous pourriez avoir besoin d'un système pour fusionner et prendre plusieurs configurations de différents endroits par ordre de priorité. Cet ordre de priorité doit être bien défini et compris par tous les développeurs / utilisateurs, par exemple, considérer le registre Windows HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE. Vous devriez faire ce style fonctionnel pour pouvoir garder vos configurations en lecture seule, c'est-à-dire:

final_conf = merge(user_conf, machine_conf)

plutôt que:

conf.update(user_conf)

Je devrais enfin ajouter que, bien sûr, si votre framework / langage choisi fournit ses propres mécanismes de configuration intégrés et bien connus, vous devriez considérer les avantages de l'utiliser au lieu de rouler les vôtres.

Donc. Beaucoup d'aspects à prendre en compte - faites-le bien et cela affectera profondément l'architecture de votre application, réduisant les bogues, rendant les choses facilement testables et vous obligeant en quelque sorte à utiliser une bonne conception ailleurs.

Benoît
la source
+1 Merci pour la réponse. Auparavant, lorsque j'avais stocké les paramètres utilisateur, je venais de lire un fichier tel qu'un .iniafin qu'il soit lisible par l'homme, mais proposez-vous que je devrais sérialiser une classe avec les variables en?
Andy
.ini est facile à analyser, et il existe également de nombreuses API qui incluent .ini comme format possible. Je suggère que vous utilisiez de telles API pour vous aider à faire le travail de grognement de l'analyse textuelle, mais que le résultat final devrait être que vous avez initialisé une classe POD. J'utilise le terme sérialiser dans le sens général de copier ou de lire dans les champs d'une classe à partir d'un certain format, pas la définition plus spécifique de sérialiser une classe directement vers / depuis le binaire (comme on le comprend généralement pour disons java.io. ).
Benedict
Maintenant je comprends un peu mieux. Donc, essentiellement, vous dites avoir une classe avec tous les champs / paramètres et en avoir une autre pour définir les valeurs de cette classe à partir du fichier? Alors, comment dois-je obtenir les valeurs de la première classe dans d'autres classes?
Andy
Au cas par cas. Certaines de vos classes de niveau supérieur peuvent simplement avoir besoin d'une référence à l'ensemble de l'instance Config qui leur est transmise si elles s'appuient sur autant de paramètres. D'autres classes ont peut-être besoin d'un ou deux paramètres, mais il n'est pas nécessaire de les coupler à l'objet Config (ce qui les rend encore plus faciles à tester), il suffit de passer les quelques paramètres dans votre application. Comme mentionné dans ma réponse, si vous construisez avec une architecture orientable / orientable DI, obtenir les valeurs là où vous en avez besoin ne sera généralement pas difficile. Utilisez l'accès global à vos risques et périls.
Benedict
Alors, comment suggéreriez-vous que l'objet config est passé aux classes si l'héritage n'a pas besoin d'être impliqué? Par un constructeur? Ainsi, dans la méthode principale du programme, la classe de lecture est lancée qui stocke les valeurs dans la classe Config, puis la méthode principale transmet l'objet Config, ou des variables individuelles à d'autres classes?
Andy
4

En général (à mon avis), il est préférable de laisser l'application gérer la façon dont la configuration est stockée et de transmettre la configuration à vos modules. Cela permet une flexibilité dans la façon dont les paramètres sont enregistrés afin que vous puissiez cibler des fichiers ou des services Web ou des bases de données ou ...

De plus, cela met le fardeau de "ce qui se passe quand les choses échouent" sur l'application, qui sait le mieux ce que signifie cet échec.

Et cela rend les tests unitaires beaucoup plus faciles lorsque vous pouvez simplement passer un objet de configuration plutôt que d'avoir à toucher le système de fichiers ou à traiter les problèmes de concurrence introduits par l'accès statique.

Telastyn
la source
Merci pour votre réponse mais je ne la comprends pas très bien. Je parle lors de l'écriture de l'application, donc il n'y a rien à voir avec la configuration, et donc, en tant que programmeur, je sais ce que signifie un échec.
Andy
@andy - bien sûr, mais si les modules accèdent directement à la configuration, ils ne savent pas dans quel contexte ils travaillent, ils ne peuvent donc pas déterminer comment gérer les problèmes de configuration. Si l'application effectue le chargement, elle peut traiter tous les problèmes, car elle connaît le contexte. Certaines applications peuvent vouloir basculer sur une valeur par défaut, d'autres veulent abandonner, etc.
Telastyn
Juste pour clarifier ici, par modules, faites-vous référence à des classes ou à des extensions réelles d'un programme? Parce que je demande où stocker au mieux les paramètres dans le code du programme principal et comment autoriser d'autres classes à accéder aux paramètres. Je veux donc charger un fichier de configuration, puis stocker le paramètre quelque part / en quelque sorte, donc je n'ai pas à continuer de faire référence au fichier.
Andy
0

Si vous utilisez .NET pour programmer les classes, vous avez différentes options telles que Resources, web.config ou même un fichier personnalisé.

Si vous utilisez Resources ou web.config, les données sont réellement stockées dans un fichier XML, mais le chargement est plus rapide.

Récupérer les données de ces fichiers et les stocker dans un autre emplacement sera comme une double utilisation de la mémoire car celles-ci sont chargées en mémoire par défaut.

Pour tout autre fichier ou langage de programmation, la réponse ci-dessus de Benedict fonctionnera.

Kiran
la source
Merci pour votre réponse et désolé pour la réponse lente. Utiliser les ressources serait une bonne option, mais je viens de réaliser que ce n'est probablement pas la meilleure option pour moi car je prévois de développer le même programme dans d'autres langues, donc je préfère avoir un fichier XML ou JSON mais le lire dans ma propre classe afin que le même fichier puisse être lu dans d'autres langages de programmation.
Andy