La mejor manera de cargar la configuración de la aplicación

24

Una manera simple de mantener la configuración de una aplicación Java está representada por un archivo de texto con la extensión ".properties" que contiene el identificador de cada configuración asociada con un valor específico (este valor puede ser un número, cadena, fecha, etc.) . C # utiliza un enfoque similar, pero el archivo de texto debe llamarse "App.config". En ambos casos, en el código fuente debe inicializar una clase específica para leer la configuración: esta clase tiene un método que devuelve el valor (como cadena) asociado con el identificador de configuración especificado.

// 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"];
// ...

En ambos casos, deberíamos analizar las cadenas cargadas desde el archivo de configuración y asignar los valores convertidos a los objetos mecanografiados relacionados (podrían ocurrir errores de análisis durante esta fase). Después del paso de análisis, debemos verificar que los valores de configuración pertenecen a un dominio específico de validez: por ejemplo, el tamaño máximo de una cola debe ser un valor positivo, algunos valores pueden estar relacionados (ejemplo: min <max ), y así.

Suponga que la aplicación debe cargar la configuración tan pronto como se inicia: en otras palabras, la primera operación realizada por la aplicación es cargar la configuración. Cualquier valor no válido para la configuración debe reemplazarse automáticamente con los valores predeterminados: si esto le sucede a un grupo de configuraciones relacionadas, esas configuraciones se establecerán con valores predeterminados.

La forma más fácil de realizar estas operaciones es crear un método que primero analice todas las configuraciones, luego verifique los valores cargados y finalmente establezca los valores predeterminados. Sin embargo, el mantenimiento es difícil si utiliza este enfoque: a medida que aumenta el número de configuraciones mientras se desarrolla la aplicación, se vuelve cada vez más difícil actualizar el código.

Para resolver este problema, pensé en usar el patrón Método de plantilla , como sigue.

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;
    }
}

El problema es que de esta manera necesitamos crear una nueva clase para cada configuración, incluso para un solo valor. ¿Hay otras soluciones para este tipo de problema?

En resumen:

  1. Fácil mantenimiento: por ejemplo, la adición de uno o más parámetros.
  2. Extensibilidad: una primera versión de la aplicación podría leer un solo archivo de configuración, pero las versiones posteriores pueden dar la posibilidad de una configuración multiusuario (el administrador configura una configuración básica, los usuarios pueden establecer solo ciertas configuraciones, etc.).
  3. Diseño orientado a objetos.
enzom83
fuente
Para aquellos que proponen el uso de un archivo .properties, ¿dónde almacenan el archivo durante el desarrollo, las pruebas y luego la producción porque, con suerte, no estará en la misma ubicación? Luego, la aplicación deberá volver a compilarse con cualquier ubicación (desarrollo, prueba o producción) a menos que pueda detectar el entorno en tiempo de ejecución y luego tener ubicaciones codificadas dentro de su aplicación.

Respuestas:

8

Esencialmente, el archivo de configuración externo está codificado como un documento YAML. Esto se analiza durante el inicio de la aplicación y se asigna a un objeto de configuración.

El resultado final es robusto y, sobre todo, simple de gestionar.

Gary Rowe
fuente
7

Consideremos esto desde dos puntos de vista: la API para obtener los valores de configuración y el formato de almacenamiento. A menudo están relacionados, pero es útil considerarlos por separado.

API de configuración

El patrón del Método de plantilla es muy general, pero me pregunto si realmente necesita esa generalidad. Necesitaría una clase para cada tipo de valor de configuración. ¿Realmente tienes tantos tipos? Supongo que podrías sobrevivir con solo un puñado: cadenas, ints, flotantes, booleanos y enumeraciones. Dado esto, podría tener una Configclase que tenga un puñado de métodos:

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)

(Creo que obtuve los genéricos de ese último correcto).

Básicamente, cada método sabe cómo manejar el análisis del valor de cadena desde el archivo de configuración y manejar los errores y devolver el valor predeterminado, si corresponde. La comprobación de rango para los valores numéricos es probablemente suficiente. Es posible que desee tener sobrecargas que omitan los valores de rango, lo que sería equivalente a proporcionar un rango de Integer.MIN_VALUE, Integer.MAX_VALUE. Una enumeración es una forma segura de escribir de una cadena contra un conjunto fijo de cadenas.

Hay algunas cosas que esto no maneja, como valores múltiples, valores que están interrelacionados, búsquedas dinámicas de tablas, etc. Podría escribir rutinas especializadas de análisis y validación para estas, pero si esto se complica demasiado, comenzaría a cuestionar si estás intentando hacer demasiado con un archivo de configuración.

Formato de almacenamiento

Los archivos de propiedades Java parecen estar bien para almacenar pares clave-valor individuales, y admiten bastante bien los tipos de valores que describí anteriormente. También podría considerar otros formatos como XML o JSON, pero estos probablemente sean excesivos a menos que tenga datos anidados o repetidos. En ese punto, parece mucho más allá de un archivo de configuración ...

Telastyn mencionó objetos serializados. Esta es una posibilidad, aunque la serialización tiene sus dificultades. Es binario, no texto, por lo que es difícil ver y editar los valores. Tienes que lidiar con la compatibilidad de serialización. Si faltan valores en la entrada serializada (por ejemplo, agregó un campo a la clase Config y está leyendo una forma serializada anterior), los nuevos campos se inicializan a nulo / cero. Debe escribir lógica para determinar si debe completar algún otro valor predeterminado. ¿Pero un cero indica ausencia de un valor de configuración, o se especificó que era cero? Ahora tienes que depurar esta lógica. Finalmente (no estoy seguro de si esto es una preocupación) aún puede necesitar validar valores en la secuencia de objetos serializados. Es posible (aunque inconveniente) que un usuario malintencionado modifique una secuencia de objetos serializados de forma indetectable.

Yo diría que seguir con las propiedades si es posible.

Stuart Marks
fuente
2
Hola Stuart, me alegro de verte aquí :-). Agregaré a la respuesta de Stuarts que creo que su idea general funcionará en Java si usa Generics para escribir fuertemente, por lo que también podría tener la Configuración <T> como una opción.
Martijn Verburg
@StuartMarks: Bueno, mi primera idea era escribir una Configclase y utilizar el enfoque propuesto por usted: getInt(), getByte(), getBoolean(), etc .. Continuando con esta idea, leí por primera vez todos los valores y pude asociar cada valor de una bandera (este indicador es falso si se produjo un problema durante la deserialización, por ejemplo, errores de análisis). Después de eso, podría comenzar una fase de validación para todos los valores cargados y establecer los valores predeterminados.
enzom83
2
Estoy a favor de algún tipo de enfoque JAXB o YAML para simplificar todos los detalles.
Gary Rowe
4

Como lo he hecho:

Inicialice todo a los valores predeterminados.

Analiza el archivo y almacena los valores a medida que avanzas. Los lugares que se establecen son responsables de garantizar que los valores sean aceptables, se ignoren los valores incorrectos (y, por lo tanto, conserven el valor predeterminado).

Loren Pechtel
fuente
Esto también podría ser una buena idea: una clase que carga los valores de la configuración puede tener que ocuparse solo para cargar los valores del archivo de configuración, es decir, su responsabilidad solo podría ser la de cargar los valores Desde el archivo de configuración; en cambio, cada módulo (que usa algunas configuraciones) tendrá la responsabilidad de validar los valores.
enzom83
2

¿Hay otras soluciones para este tipo de problema?

Si todo lo que necesita es una configuración simple, me gusta hacer una clase simple para ella. Inicializa los valores predeterminados y la aplicación puede cargarlos desde el archivo a través de las clases de serialización integradas. La aplicación luego lo pasa a cosas que lo necesitan. Sin reflexionar con el análisis o las conversiones, sin perder el tiempo con las cadenas de configuración, sin arrojar basura. Y hace que la configuración del modo más fácil de usar para los escenarios de código en las que debe guardar / cargar desde un servidor o como preajustes, y forma más fácil de usar en las pruebas unitarias.

Telastyn
fuente
1
Sin reflexionar con el análisis o las conversiones, sin perder el tiempo con las cadenas de configuración, sin arrojar basura. ¿Qué quieres decir?
enzom83
1
Quiero decir que: 1. No necesita tomar el resultado de AppConfig (una cadena) y analizarlo en lo que desea. 2. No necesita especificar ningún tipo de cadena para elegir qué parámetro de configuración desea; esa es una de esas cosas que es propensa a errores humanos y difícil de refactorizar, y 3. no necesita hacer otras conversiones de tipo cuando va a establecer el valor mediante programación.
Telastyn
2

Al menos en .NET, puede crear fácilmente sus propios objetos de configuración fuertemente tipados; consulte este artículo de MSDN para ver un ejemplo rápido.

Protip: envuelva su clase de configuración en una interfaz y deje que su aplicación hable con eso. Facilita la inyección de configuraciones falsas para pruebas o con fines de lucro.

Wyatt Barnett
fuente
Leí el artículo de MSDN: es interesante, esencialmente cada subclase de ConfigurationElementclase podría representar un grupo de valores, y para cualquier valor puede especificar un validador. Pero si, por ejemplo, quisiera representar un elemento de configuración que consta de cuatro probabilidades, los cuatro valores de probabilidad están correlacionados, ya que su suma debe ser igual a 1. ¿Cómo valido este elemento de configuración?
enzom83
1
En general, argumentaría que no es algo para la validación de configuración de bajo nivel: agregaría un método AssertConfigrationIsValid a mi clase de configuración para cubrir esto en el código. Si eso no funciona para usted, creo que puede crear sus propios validadores de configuración extendiendo la clase base del atributo. Tienen un validador de comparación, por lo que obviamente pueden hablar entre propiedades.
Wyatt Barnett