Melhor maneira de carregar as configurações do aplicativo

24

Uma maneira simples de manter as configurações de um aplicativo Java é representada por um arquivo de texto com a extensão ".properties", contendo o identificador de cada configuração associada a um valor específico (esse valor pode ser um número, string, data, etc.) . O C # usa uma abordagem semelhante, mas o arquivo de texto deve ter o nome "App.config". Nos dois casos, no código fonte, você deve inicializar uma classe específica para ler as configurações: essa classe possui um método que retorna o valor (como string) associado ao identificador de configuração 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"];
// ...

Nos dois casos, devemos analisar as seqüências carregadas do arquivo de configuração e atribuir os valores convertidos aos objetos digitados relacionados (erros de análise podem ocorrer durante esta fase). Após a etapa de análise, devemos verificar se os valores de configuração pertencem a um domínio específico de validade: por exemplo, o tamanho máximo de uma fila deve ser um valor positivo, alguns valores podem estar relacionados (exemplo: min <max ), e assim por diante.

Suponha que o aplicativo carregue as configurações assim que for iniciado: em outras palavras, a primeira operação executada pelo aplicativo é carregar as configurações. Quaisquer valores inválidos para as configurações devem ser substituídos automaticamente pelos valores padrão: se isso acontecer com um grupo de configurações relacionadas, essas configurações serão todas definidas com valores padrão.

A maneira mais fácil de executar essas operações é criar um método que primeiro analise todas as configurações, verifique os valores carregados e, finalmente, defina quaisquer valores padrão. No entanto, a manutenção é difícil se você usar esta abordagem: à medida que o número de configurações aumenta durante o desenvolvimento do aplicativo, fica cada vez mais difícil atualizar o código.

Para resolver esse problema, pensei em usar o padrão Template Method , da seguinte maneira.

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

O problema é que, dessa maneira, precisamos criar uma nova classe para cada configuração, mesmo para um único valor. Existem outras soluções para esse tipo de problema?

Em suma:

  1. Manutenção fácil: por exemplo, a adição de um ou mais parâmetros.
  2. Extensibilidade: uma primeira versão do aplicativo pode ler um único arquivo de configuração, mas versões posteriores podem dar a possibilidade de uma configuração para vários usuários (o administrador define uma configuração básica, os usuários podem definir apenas determinadas configurações, etc.).
  3. Projeto orientado a objetos.
enzom83
fonte
Para aqueles que propõem o uso de um arquivo .properties, onde você armazena o próprio arquivo durante o desenvolvimento, o teste e a produção, porque não estará no mesmo local, espero. Em seguida, o aplicativo precisará ser recompilado com qualquer local (dev, test ou prod), a menos que você possa detectar o ambiente em tempo de execução e, em seguida, ter locais codificados no seu aplicativo.

Respostas:

8

Essencialmente, o arquivo de configuração externa é codificado como um documento YAML. Isso é analisado durante a inicialização do aplicativo e mapeado para um objeto de configuração.

O resultado final é robusto e acima de tudo simples de gerenciar.

Gary Rowe
fonte
7

Vamos considerar isso de dois pontos de vista: a API para obter os valores de configuração e o formato de armazenamento. Eles são frequentemente relacionados, mas é útil considerá-los separadamente.

API de configuração

O padrão do método de modelo é muito geral, mas questiono se você realmente precisa dessa generalidade. Você precisaria de uma classe para cada tipo de valor de configuração. Você realmente tem tantos tipos? Eu acho que você poderia conviver com apenas um punhado: strings, ints, floats, booleans e enums. Dado isso, você pode ter uma Configclasse que possui vários 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)

(Acho que comprei os genéricos nesse último.)

Basicamente, cada método sabe como lidar com a análise do valor da string no arquivo de configuração e como lidar com erros e retornar o valor padrão, se apropriado. A verificação de faixa para os valores numéricos provavelmente é suficiente. Convém ter sobrecargas que omitam os valores do intervalo, o que seria equivalente a fornecer um intervalo de Integer.MIN_VALUE, Integer.MAX_VALUE. Um Enum é uma maneira segura de validar uma string contra um conjunto fixo de strings.

Há algumas coisas que isso não lida, como vários valores, valores inter-relacionados, pesquisas de tabela dinâmica etc. Você pode escrever rotinas especializadas de análise e validação para elas, mas se isso ficar muito complicado, eu começaria a questionar se você está tentando fazer muito com um arquivo de configuração.

Formato de armazenamento

Os arquivos de propriedades Java parecem bons para armazenar pares de valores-chave individuais e suportam muito bem os tipos de valores que descrevi acima. Você também pode considerar outros formatos, como XML ou JSON, mas esses provavelmente são um exagero, a menos que você tenha dados aninhados ou repetidos. Nesse ponto, parece muito além de um arquivo de configuração ....

Telastyn mencionou objetos serializados. Essa é uma possibilidade, embora a serialização tenha suas dificuldades. É binário, não texto, por isso é difícil ver e editar os valores. Você precisa lidar com a compatibilidade de serialização. Se faltarem valores na entrada serializada (por exemplo, você adicionou um campo à classe Config e está lendo uma forma serializada antiga), os novos campos são inicializados como nulos / zero. Você precisa escrever a lógica para determinar se deve preencher outro valor padrão. Mas um zero indica ausência de um valor de configuração ou foi especificado como zero? Agora você precisa depurar essa lógica. Finalmente (não tenho certeza se isso é uma preocupação), você ainda pode precisar validar valores no fluxo de objetos serializados. É possível (embora inconveniente) para um usuário mal-intencionado modificar um fluxo de objetos serializados de forma indetectável.

Eu diria para ficar com propriedades, se possível.

Stuart Marks
fonte
2
Ei Stuart, prazer em vê-lo aqui :-). Acrescentarei à resposta da Stuarts que acho que sua ideia de tempalte funcionará em Java se você usar Generics para digitar fortemente, para que você também possa ter a opção Setting <T>.
Martijn Verburg
@StuartMarks: Bem, minha primeira idéia era apenas para escrever uma Configclasse e usar a abordagem proposta por você: getInt(), getByte(), getBoolean(), etc .. Continuando com essa idéia, li pela primeira vez todos os valores e eu poderia associar cada valor a uma bandeira (esse sinalizador será falso se ocorrer um problema durante a desserialização, por exemplo, erros de análise). Depois disso, eu poderia iniciar uma fase de validação para todos os valores carregados e definir quaisquer valores padrão.
Enzom83 16/08/12
2
Eu preferiria algum tipo de abordagem JAXB ou YAML para simplificar todos os detalhes.
Gary Rowe
4

Como eu fiz isso:

Inicialize tudo com os valores padrão.

Analise o arquivo, armazenando os valores à medida que avança. Os locais que estão sendo definidos são responsáveis ​​por garantir que os valores sejam aceitáveis, valores ruins são ignorados (e, portanto, retêm o valor padrão).

Loren Pechtel
fonte
Isso também pode ser uma boa idéia: uma classe que carrega os valores das configurações pode ter que lidar apenas para carregar os valores do arquivo de configuração, ou seja, sua responsabilidade pode ser a única a carregar os valores Do arquivo de configuração; em vez disso, cada módulo (que usa algumas configurações) terá a responsabilidade de validar os valores.
enzom83
2

Existem outras soluções para esse tipo de problema?

Se tudo o que você precisa é de uma configuração simples, eu gostaria de criar uma classe antiga simples para ela. Ele inicializa os padrões e pode ser carregado do arquivo pelo aplicativo por meio das classes de serialização incorporadas. O aplicativo passa para coisas que precisam. Não se preocupe com a análise ou conversões, não se mexa com as seqüências de configuração, não lança lixo. E torna a configuração muito mais fácil de usar em cenários em código, onde ela precisa ser salva / carregada do servidor ou como predefinições, e muito mais fácil de usar em seus testes de unidade.

Telastyn
fonte
11
Não se preocupe com a análise ou conversões, não se mexa com as seqüências de configuração, não lança lixo. O que você quer dizer?
enzom83
11
Quero dizer que: 1. Você não precisa pegar o resultado do AppConfig (uma string) e analisá-lo como quiser. 2. Você não precisa especificar nenhum tipo de string para escolher qual parâmetro de configuração você deseja; essa é uma daquelas coisas que são propensas a erros humanos e difíceis de refatorar e 3. você não precisa fazer outras conversões de tipo ao definir o valor programaticamente.
Telastyn
2

Pelo menos no .NET, você pode criar com facilidade seus próprios objetos de configuração fortemente tipados - consulte este artigo do MSDN para obter um exemplo rápido.

Protip: envolva sua classe de configuração em uma interface e deixe seu aplicativo falar com isso. Facilita a injeção de configurações falsas para testes ou para fins lucrativos.

Wyatt Barnett
fonte
Eu li o artigo do MSDN: é interessante, essencialmente cada subclasse de ConfigurationElementclasse pode representar um grupo de valores e, para qualquer valor, você pode especificar um validador. Mas se, por exemplo, eu queria representar um elemento de configuração que consiste em quatro probabilidades, os quatro valores de probabilidade são correlacionados, pois sua soma deve ser igual a 1. Como validar esse elemento de configuração?
enzom83
11
Eu geralmente argumentaria que isso não é algo para validação de configuração de baixo nível - eu adicionaria um método AssertConfigrationIsValid à minha classe de configuração para cobrir isso no código. Se isso não funcionar para você, acho que você pode criar seus próprios validadores de configuração estendendo a classe base do atributo. Eles têm um validador de comparação para que obviamente possam falar de propriedades cruzadas.
Wyatt Barnett