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.