Meilleure façon de charger les paramètres d'application

24

Un moyen simple de conserver les paramètres d'une application Java est représenté 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, une chaîne, une 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 une 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 de 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 façon, 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é:

  1. Maintenance facile: par exemple, l'ajout d'un ou plusieurs paramètres.
  2. Extensibilité: une première version de l'application pourrait lire un seul fichier de configuration, mais les versions ultérieures peuvent donner la possibilité d'une configuration multi-utilisateurs (l'administrateur configure une configuration de base, les utilisateurs ne peuvent définir que certains paramètres, etc.).
  3. Conception orientée objet.
enzom83
la source
Pour ceux qui proposent l'utilisation d'un fichier .properties, où stockez-vous le fichier lui-même pendant le développement, les tests et la production, car il ne sera pas au même endroit, espérons-le. Ensuite, l'application devra être recompilée avec n'importe quel emplacement (dev, test ou prod), sauf si vous pouvez détecter l'environnement au moment de l'exécution, puis avoir des emplacements codés en dur dans votre application.

Réponses:

8

Le fichier de configuration externe est essentiellement codé en tant que document YAML. Ceci 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.

Gary Rowe
la source
7

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. Compte tenu de ceux-ci, vous pourriez avoir une Configclasse qui a 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 et 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.

Stuart Marks
la source
2
Hé Stuart, ravi de te voir ici :-). J'ajouterai à la réponse de Stuarts que je pense que votre idée de tempalte fonctionnera en Java si vous utilisez des génériques pour taper fortement, de sorte que vous pouvez également avoir l'option Setting <T>.
Martijn Verburg
@StuartMarks: Eh bien, ma première idée était d'écrire une Configclasse et utiliser l'approche proposée par vous: getInt(), getByte(), getBoolean(), etc .. continue avec cette idée, je lis d' abord toutes les valeurs et je pouvais associer chaque valeur à un drapeau (cet indicateur est faux si un problème s'est produit pendant la désérialisation, par exemple des erreurs d'analyse). Après cela, j'ai pu démarrer une phase de validation pour toutes les valeurs chargées et définir toutes les valeurs par défaut.
enzom83
2
Je préférerais une sorte d'approche JAXB ou YAML pour simplifier tous les détails.
Gary Rowe
4

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

Loren Pechtel
la source
Cela pourrait également être une bonne idée: une classe qui charge les valeurs des paramètres peut avoir à traiter uniquement pour charger les valeurs à partir du fichier de configuration, c'est-à-dire que sa responsabilité ne peut être que celle de charger les valeurs À partir du fichier de configuration; à la place, chaque module (qui utilise certains paramètres) aura la responsabilité de valider les valeurs.
enzom83
2

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. 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 il rend la configuration ainsi plus facile à utiliser pour des scénarios en code où il doit être enregistré / chargé à partir du serveur ou sous forme de presets, et ainsi plus facile à utiliser dans vos tests unitaires.

Telastyn
la source
1
Pas de bavardage avec l'analyse ou les conversions, pas de vissage avec des chaînes de configuration, pas de lancer des ordures. Que voulez-vous dire?
enzom83
1
Je veux dire que: 1. Vous n'avez pas besoin de prendre le résultat AppConfig (une chaîne) et de l'analyser dans ce que vous voulez. 2. Vous n'avez pas besoin de spécifier une sorte de chaîne pour choisir le paramètre de configuration que vous souhaitez; c'est une de ces choses qui est sujette à l'erreur humaine et difficile à refactoriser et 3. vous n'avez pas besoin de faire ensuite d'autres conversions de type lorsque vous allez définir la valeur par programme.
Telastyn
2

Au moins dans .NET, vous pouvez assez facilement créer vos propres objets de configuration fortement typés - consultez cet 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.

Wyatt Barnett
la source
J'ai lu l'article MSDN: c'est intéressant, essentiellement chaque sous- ConfigurationElementclasse de classe pourrait représenter un groupe de valeurs, et pour toute valeur, vous pouvez spécifier un validateur. Mais si, par exemple, je voulais représenter un élément de configuration composé de quatre probabilités, les quatre valeurs de probabilité sont corrélées, car leur somme doit être égale à 1. Comment puis-je valider cet élément de configuration?
enzom83
1
Je dirais généralement que ce n'est pas quelque chose pour la validation de configuration de bas niveau - j'ajouterais une méthode AssertConfigrationIsValid à ma classe de configuration pour couvrir cela dans le code. Si cela ne fonctionne pas pour vous, je pense que vous pouvez créer vos propres validateurs de configuration en étendant la classe de base de l'attribut. Ils ont un validateur de comparaison afin qu'ils puissent évidemment parler de propriété croisée.
Wyatt Barnett