Onde carregar e armazenar configurações de um arquivo?

9

Eu acho que essa pergunta deve se aplicar à maioria dos programas que carregam configurações de um arquivo. Minha pergunta é do ponto de vista de programação, e é realmente como lidar com o carregamento de configurações de um arquivo em termos de diferentes classes e acessibilidade. Por exemplo:

  • Se um programa tivesse um settings.iniarquivo simples , seu conteúdo deveria ser carregado no load()método de uma classe, ou talvez no construtor?
  • Os valores devem ser armazenados em public staticvariáveis ​​ou deve haver staticmétodos para obter e definir propriedades?
  • O que deve acontecer no caso de o arquivo não existir ou não ser legível? Como você deixaria o resto do programa saber que não pode obter essas propriedades?
  • etc.

Espero estar perguntando isso no lugar certo aqui. Eu queria tornar a pergunta o mais independente possível da linguagem, mas estou focada principalmente em linguagens que possuem coisas como herança - especialmente Java e C # .NET.

Andy
fonte
11
Para .NET, é melhor usar App.config eo System.Configuration.ConfigurationManager classe para obter as configurações
Gibson
@ Gibson Estou apenas começando no .NET, então você poderia me vincular a algum bom tutorial para essa classe?
21413 Andy
O Stackoverflow tem muitas respostas. A pesquisa rápida revela: stackoverflow.com/questions/114527/… e stackoverflow.com/questions/13043530/… Também haverá muitas informações no MSDN, começando aqui msdn.microsoft.com/en-us/library/ms228063(v = vs.100) .aspx e msdn.microsoft.com/en-us/library/ms184658(v=vs.110).aspx
Gibson
@ Gibson Obrigado por esses links. Eles serão muito úteis.
Andy

Respostas:

8

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.

Bento
fonte
+1 Obrigado pela resposta. Anteriormente, quando eu armazenava as configurações do usuário, acabei de ler um arquivo como um .inipara que seja legível por humanos, mas você está sugerindo que eu serialize uma classe com as variáveis?
Andy
O .ini é fácil de analisar e também existem muitas APIs que incluem o .ini como um formato possível. Estou sugerindo que você use essas APIs para ajudá-lo a fazer o trabalho pesado da análise textual, mas que o resultado final deve ser o fato de você ter inicializado uma classe POD. Eu uso o termo serializar no sentido geral de copiar ou ler nos campos de uma classe de algum formato, não a definição mais específica de serializar uma classe diretamente de / para o binário (como normalmente é entendido por dizer java.io.Serializable )
Bento
Agora eu entendo um pouco melhor. Então, basicamente, você está dizendo ter uma classe com todos os campos / configurações e outra para definir os valores nessa classe a partir do arquivo? Então, como devo obter os valores da primeira classe em outras classes?
Andy
Caso a caso. Algumas de suas classes de nível superior podem precisar apenas de uma referência para toda a instância do Config transmitida para elas se elas dependerem de muitos parâmetros. Outras classes talvez precisem de um ou dois parâmetros, talvez, mas não há necessidade de associá-las ao objeto Config (tornando-as ainda mais fáceis de testar), basta passar os poucos parâmetros através do aplicativo. Como mencionado na minha resposta, se você construir com uma arquitetura orientável testável / DI, obter os valores onde precisa deles geralmente não será difícil. Use o acesso global por sua conta e risco.
Bento
Então, como você sugere que o objeto de configuração seja passado para as classes se a herança não precisar estar envolvida? Através de um construtor? Assim, no método principal do programa, é iniciada a classe reader que armazena valores na classe Config, e o método principal passa o objeto Config, ou variáveis ​​individuais para outras classes?
Andy
4

Em geral (na minha opinião), é melhor deixar o aplicativo lidar com como a configuração é armazenada e passar a configuração para seus módulos. Isso permite flexibilidade na maneira como as configurações são salvas, para que você possa direcionar arquivos ou serviços da web, bancos de dados ou ...

Além disso, coloca o ônus do "o que acontece quando as coisas falham" no aplicativo, quem sabe melhor o que essa falha significa.

E isso facilita muito o teste de unidade quando você pode apenas passar em um objeto de configuração em vez de precisar tocar no sistema de arquivos ou lidar com problemas de simultaneidade introduzidos pelo acesso estático.

Telastyn
fonte
Obrigado por sua resposta, mas não a entendo direito. Eu estou falando sobre ao escrever o aplicativo, então não há nada para lidar com a configuração e, portanto, como programador, eu sei o que significa uma falha.
Andy
@andy - claro, mas se os módulos acessarem a configuração diretamente, eles não saberão em que contexto estão trabalhando, portanto, não poderão determinar como lidar com os problemas de configuração. Se o aplicativo estiver carregando, ele poderá lidar com qualquer problema, pois conhece o contexto. Alguns aplicativos pode querer failover para um valor padrão, outros querem abortar, etc.
Telastyn
Apenas para esclarecer aqui, por módulos você está se referindo a classes ou extensões reais de um programa? Porque eu estou perguntando sobre onde melhor armazenar configurações realmente dentro do código para o programa principal e como permitir que outras classes acessem as configurações. Então, eu quero carregar um arquivo de configuração e, em seguida, armazenar a configuração em algum lugar / de alguma forma, para que eu não precise continuar me referindo ao arquivo.
Andy
0

Se você estiver usando o .NET para programar as classes, terá opções diferentes, como Resources, web.config ou mesmo um arquivo personalizado.

Se você usar Resources ou web.config, os dados serão realmente armazenados no arquivo XML, mas o carregamento será mais rápido.

Recuperar os dados desses arquivos e armazená-los em outro local será como um uso duplo da memória, pois estes são carregados na memória por padrão.

Para qualquer outro arquivo ou linguagem de programação, a resposta acima de Benedict funcionará.

Kiran
fonte
Obrigado pela sua resposta e desculpe pela resposta lenta. Usar os recursos seria uma boa opção, mas acabei de perceber que provavelmente não é a melhor opção para mim, pois planejo desenvolver o mesmo programa em outros idiomas; portanto, prefiro ter um arquivo XML ou JSON, mas leia-o na minha própria classe para que o mesmo arquivo possa ser lido em outras linguagens de programação.
Andy