Un moyen simple de conserver les paramètres d'une application Java est représentée par un fichier texte avec l'extension ".properties" contenant l'identifiant de chaque paramètre associé à une valeur spécifique (cette valeur peut être un nombre , chaîne, date, etc.). C # utilise une approche similaire, mais le fichier texte doit être nommé "App.config". Dans les deux cas, dans le code source, vous devez initialiser une classe spécifique pour la lecture des paramètres: cette classe a un méthode qui renvoie la valeur (sous forme de chaîne) associée à l'identificateur de paramètre spécifié.
// Java example
Properties config = new Properties();
config.load(...);
String valueStr = config.getProperty("listening-port");
// ...
// C# example
NameValueCollection setting = ConfigurationManager.AppSettings;
string valueStr = setting["listening-port"];
// ...
Dans les deux cas, nous devons analyser les chaînes chargées à partir du fichier de configuration et affecter les valeurs converties aux objets typés associés (des erreurs d'analyse peuvent se produire pendant cette phase). Après l'étape d'analyse, nous devons vérifier que les valeurs des paramètres appartiennent à un domaine de validité spécifique: par exemple, la taille maximale d'une file d'attente doit être une valeur positive, certaines valeurs peuvent être liées (exemple: min <max ), etc.
Supposons que l'application charge les paramètres dès son démarrage: en d'autres termes, la première opération effectuée par l'application consiste à charger les paramètres. Toutes les valeurs non valides pour les paramètres doivent être remplacées automatiquement par des valeurs par défaut: si cela se produit pour un groupe de paramètres associés, ces paramètres sont tous définis avec des valeurs par défaut.
La façon la plus simple d'effectuer ces opérations consiste à créer une méthode qui analyse d'abord tous les paramètres, puis vérifie les valeurs chargées et définit enfin les valeurs par défaut. Cependant, la maintenance est difficile si vous utilisez cette approche: à mesure que le nombre de paramètres augmente lors du développement de l'application, il devient de plus en plus difficile de mettre à jour le code.
Afin de résoudre ce problème, j'avais pensé à utiliser le modèle Méthode de modèle, comme suit.
public abstract class Setting
{
protected abstract bool TryParseValues();
protected abstract bool CheckValues();
public abstract void SetDefaultValues();
/// <summary>
/// Template Method
/// </summary>
public bool TrySetValuesOrDefault()
{
if (!TryParseValues() || !CheckValues())
{
// parsing error or domain error
SetDefaultValues();
return false;
}
return true;
}
}
public class RangeSetting : Setting
{
private string minStr, maxStr;
private byte min, max;
public RangeSetting(string minStr, maxStr)
{
this.minStr = minStr;
this.maxStr = maxStr;
}
protected override bool TryParseValues()
{
return (byte.TryParse(minStr, out min)
&& byte.TryParse(maxStr, out max));
}
protected override bool CheckValues()
{
return (0 < min && min < max);
}
public override void SetDefaultValues()
{
min = 5;
max = 10;
}
}
Le problème est que de cette manière, nous devons créer une nouvelle classe pour chaque paramètre, même pour une seule valeur. Existe-t-il d'autres solutions à ce type de problème?
En résumé:
Le fichier de configuration externe est essentiellement codé en tant que document YAML. Il est ensuite analysé lors du démarrage de l'application et mappé à un objet de configuration.
Le résultat final est robuste et surtout simple à gérer.
Examinons cela de deux points de vue: l'API pour obtenir les valeurs de configuration et le format de stockage. Ils sont souvent liés, mais il est utile de les considérer séparément.
API de configuration
Le modèle de méthode de modèle est très général, mais je me demande si vous avez vraiment besoin de cette généralité. Vous auriez besoin d'une classe pour chaque type de valeur de configuration. Avez-vous vraiment autant de types? Je suppose que vous pourriez vous en tirer avec seulement une poignée: chaînes, entiers, flotteurs, booléens et énumérations. Dans ces conditions, vous pourriez avoir une classe Config
contenant une poignée de méthodes:
int getInt(name, default, min, max)
float getFloat(name, default, min, max)
boolean getBoolean(name, default)
String getString(name, default)
<T extends Enum<T>> T getEnum(name, Class<T> enumClass, T default)
(Je pense que j'ai bien compris les génériques sur ce dernier.)
Fondamentalement, chaque méthode sait comment gérer l'analyse de la valeur de chaîne du fichier de configuration, gérer les erreurs et renvoyer la valeur par défaut, le cas échéant. La vérification des plages pour les valeurs numériques est probablement suffisante. Vous souhaiterez peut-être des surcharges qui omettent les valeurs de plage, ce qui équivaudrait à fournir une plage de Integer.MIN_VALUE, Integer.MAX_VALUE. Une énumération est un moyen sûr de type de valider une chaîne par rapport à un ensemble fixe de chaînes.
Il y a certaines choses que cela ne gère pas, telles que les valeurs multiples, les valeurs qui sont interdépendantes, les recherches de tables dynamiques, etc. si vous essayez d'en faire trop avec un fichier de configuration.
Format de stockage
Les fichiers de propriétés Java semblent bien pour stocker des paires clé-valeur individuelles, et ils prennent assez bien en charge les types de valeurs que j'ai décrits ci-dessus. Vous pouvez également envisager d'autres formats tels que XML ou JSON, mais ceux-ci sont probablement exagérés, sauf si vous avez des données imbriquées ou répétées. À ce stade, il semble bien au-delà d'un fichier de configuration ....
Telastyn a mentionné les objets sérialisés. C'est une possibilité, bien que la sérialisation ait ses difficultés. C'est binaire, pas de texte, il est donc difficile de voir et de modifier les valeurs. Vous devez gérer la compatibilité de sérialisation. Si des valeurs manquent dans l'entrée sérialisée (par exemple, vous avez ajouté un champ à la classe Config et que vous en lisez une ancienne forme sérialisée), les nouveaux champs sont initialisés à null/zéro. Vous devez écrire une logique pour déterminer s'il faut remplir une autre valeur par défaut. Mais un zéro indique-t-il l'absence d'une valeur de configuration, ou a-t-il été spécifié comme étant zéro? Vous devez maintenant déboguer cette logique. Enfin (je ne sais pas s'il s'agit d'un problème), vous devrez peut-être encore valider les valeurs dans le flux d'objets sérialisés. Il est possible (bien que peu pratique) pour un utilisateur malveillant de modifier un flux d'objets sérialisés de manière indétectable.
Je dirais de m'en tenir aux propriétés si possible.
Comment je l'ai fait:
Initialisez tout aux valeurs par défaut.
Analysez le fichier en stockant les valeurs au fur et à mesure. Les emplacements définis sont chargés de garantir que les valeurs sont acceptables, les mauvaises valeurs sont ignorées (et conservent ainsi la valeur par défaut.)
Existe-t-il d'autres solutions à ce type de problème?
Si tout ce dont vous avez besoin est une configuration simple, j'aime en faire une ancienne classe simple. Il initialise les valeurs par défaut et peut être chargé à partir d'un fichier par l'application via les classes de sérialisation intégrées. L'application la transmet ensuite aux éléments qui en ont besoin. Pas de bavardage avec l'analyse ou les conversions, pas de vissage avec des chaînes de configuration, pas de lancer des ordures. Et cela rend la configuration manière plus facile à utiliser pour les scénarios dans le code où elle doit être enregistrée/chargée à partir du serveur ou en tant que préréglages, et manière plus facile à utiliser dans votre unité tests.
Au moins dans .NET, vous pouvez assez facilement créer vos propres objets de configuration fortement typés - voir ceci article MSDN pour un exemple rapide.
Protip: enveloppez votre classe de configuration dans une interface et laissez votre application en parler. Il est facile d'injecter une fausse configuration pour des tests ou à but lucratif.