Essa é realmente uma pergunta realmente importante e geralmente é feita de maneira errada, pois não recebe importância suficiente, mesmo sendo parte essencial de praticamente todos os aplicativos. Aqui estão minhas diretrizes:
Sua classe de configuração, que contém todas as configurações, deve ser apenas um tipo de dados antigo simples, struct / class:
class Config {
int prop1;
float prop2;
SubConfig subConfig;
}
Ele não precisa ter métodos e não deve envolver herança (a menos que seja a única opção que você tem no seu idioma para implementar um campo variante - veja o próximo parágrafo). Ele pode e deve usar a composição para agrupar as configurações em classes de configuração específicas menores (por exemplo, subConfig acima). Se você fizer isso dessa maneira, será ideal repassar os testes de unidade e o aplicativo em geral, pois terá dependências mínimas.
Você provavelmente precisará usar tipos de variantes, caso as configurações para diferentes configurações sejam heterogêneas na estrutura. É aceito que você precisará inserir uma conversão dinâmica em algum momento quando ler o valor para convertê-lo na (sub) classe de configuração correta, e sem dúvida isso dependerá de outra configuração.
Você não deve ter preguiça de digitar todas as configurações como campos, apenas fazendo o seguinte:
class Config {
Dictionary<string, string> values;
};
Isso é tentador, pois significa que você pode escrever uma classe de serialização generalizada que não precisa saber com quais campos está lidando, mas está errada e vou explicar o porquê em um momento.
A serialização da configuração é feita em uma classe completamente separada. Qualquer que seja a API ou biblioteca usada para fazer isso, o corpo da sua função de serialização deve conter entradas que basicamente equivalem a ser um mapa do caminho / chave no arquivo para o campo no objeto. Alguns idiomas fornecem boa introspecção e podem fazer isso imediatamente, outros você precisará escrever explicitamente o mapeamento, mas o principal é que você deve escrever o mapeamento apenas uma vez. Por exemplo, considere este extrato que eu adaptei da documentação do analisador de opções do programa boost do c ++:
struct Config {
int opt;
} conf;
po::options_description desc("Allowed options");
desc.add_options()
("optimization", po::value<int>(&conf.opt)->default_value(10);
Observe que a última linha basicamente diz "otimização" mapeia para Config :: opt e também que há uma declaração do tipo que você espera. Você deseja que a leitura da configuração falhe se o tipo não for o que você espera, se o parâmetro no arquivo não for realmente um float ou um int, ou se não existir. Ou seja, a falha deve ocorrer quando você lê o arquivo porque o problema está no formato / validação do arquivo e você deve lançar um código de exceção / retorno e relatar o problema exato. Você não deve atrasar isso para mais tarde no programa. É por isso que você não deve ser tentado a pegar todos os Confs estilo dicionário, como mencionado acima, que não falharão quando o arquivo for lido - pois a conversão é atrasada até que o valor seja necessário.
Você deve tornar a classe Config somente leitura de alguma maneira - definindo o conteúdo da classe uma vez ao criá-la e inicializá-la do arquivo. Se você precisar ter configurações dinâmicas em seu aplicativo que mudem, assim como as constantes que não, você deve ter uma classe separada para lidar com as dinâmicas, em vez de tentar permitir que os bits da sua classe de configuração não sejam somente leitura .
Idealmente, você lê o arquivo em um local do seu programa, ou seja, você só tem uma instância de um " ConfigReader
". No entanto, se você estiver enfrentando dificuldades para passar a instância do Config para onde você precisa, é melhor ter um segundo ConfigReader do que introduzir uma configuração global (o que eu acho que é o que o OP quer dizer com "static "), O que me leva ao meu próximo ponto:
Evite a música de sirene sedutora do singleton: "Vou evitar que você passe nessa aula, todos os seus construtores serão adoráveis e limpos. Continue, será tão fácil". A verdade é que com uma arquitetura testável bem projetada, você dificilmente precisará passar a classe Config, ou partes dela, através de muitas classes de seu aplicativo. O que você encontrará, em sua classe de nível superior, sua função main () ou o que quer que seja, desvendará o conf em valores individuais, que você fornecerá às classes de componentes como argumentos que serão reunidos novamente (dependência manual injeção). Um conf único / global / estático tornará o teste de unidade muito mais difícil de implementar e entender seu aplicativo - por exemplo, confundirá novos desenvolvedores para sua equipe que não saberão que precisam definir o estado global para testar coisas.
Se seu idioma suportar propriedades, você deverá usá-las para esse fim. O motivo é que significa que será muito fácil adicionar definições de configuração "derivadas" que dependem de uma ou mais configurações. por exemplo
int Prop1 { get; }
int Prop2 { get; }
int Prop3 { get { return Prop1*Prop2; }
Se o seu idioma não suportar nativamente o idioma da propriedade, pode haver uma solução alternativa para obter o mesmo efeito, ou você simplesmente cria uma classe de wrapper que fornece as configurações de bônus. Se você não pode conferir o benefício das propriedades, é uma perda de tempo escrever manualmente e usar getters / setters simplesmente com o objetivo de agradar a algum deus OO. Você ficará melhor com um campo antigo simples.
Pode ser necessário um sistema para mesclar e obter várias configurações de locais diferentes em ordem de precedência. Essa ordem de precedência deve ser bem definida e compreendida por todos os desenvolvedores / usuários, por exemplo, considere o registro do Windows HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE. Você deve fazer esse estilo funcional para manter suas configurações somente leitura, ou seja:
final_conf = merge(user_conf, machine_conf)
ao invés de:
conf.update(user_conf)
Finalmente, devo acrescentar que, é claro, se a estrutura / linguagem escolhida fornecer seus próprios mecanismos de configuração conhecidos e incorporados, você deverá considerar os benefícios de usá-lo em vez de usar o seu próprio.
Então. Muitos aspectos a serem considerados - acerte e isso afetará profundamente a arquitetura do aplicativo, reduzindo bugs, facilitando o teste de coisas e forçando você a usar um bom design em outro lugar.