Um único objeto de configuração é uma má ideia?

41

Na maioria dos meus aplicativos, eu tenho um objeto "config" singleton ou static, encarregado de ler várias configurações do disco. Quase todas as classes usam, para vários propósitos. Essencialmente, é apenas uma tabela de hash de pares nome / valor. É somente leitura, então eu não tenho me preocupado muito com o fato de eu ter tanto estado global. Mas agora que estou começando a testar a unidade, está começando a se tornar um problema.

Um problema é que você geralmente não quer testar com a mesma configuração que você usa. Existem algumas soluções para isso:

  • Dê ao objeto de configuração um setter usado APENAS para teste, para que você possa transmitir configurações diferentes.
  • Continue usando um único objeto de configuração, mas altere-o de um singleton para uma instância que você passa em todos os lugares necessários. Então você pode construí-lo uma vez em sua aplicação e uma vez em seus testes, com diferentes configurações.

Mas de qualquer forma, você ainda tem um segundo problema: quase qualquer classe pode usar o objeto de configuração. Então, em um teste, você precisa configurar a configuração para a classe que está sendo testada, mas também todas as suas dependências. Isso pode tornar seu código de teste feio.

Estou começando a concluir que esse tipo de objeto de configuração é uma má ideia. O que você acha? Quais são algumas alternativas? E como você começa a refatorar um aplicativo que usa configuração em todos os lugares?

    
por JW01 26.01.2011 / 21:09
fonte

8 respostas

32

Não tenho um problema com um único objeto de configuração e vejo a vantagem de manter todas as configurações em um só lugar.

No entanto, usar esse único objeto em todos os lugares resultará em um alto nível de acoplamento entre o objeto de configuração e as classes que o utilizam. Se você precisar alterar a classe de configuração, talvez seja necessário visitar todas as instâncias em que ela é usada e verificar se não quebrou nada.

Uma maneira de lidar com isso é criar várias interfaces que exponham partes do objeto de configuração necessárias para diferentes partes do aplicativo. Em vez de permitir que outras classes acessem o objeto de configuração, você passa instâncias de uma interface para as classes que precisam dela. Dessa forma, as partes do aplicativo que usam config dependem da menor coleção de campos na interface, e não da classe de configuração inteira. Finalmente, para testes de unidade, você pode criar uma classe personalizada que implemente a interface.

Se você quiser explorar essas ideias ainda mais, recomendo ler sobre SOLID princípios , especialmente a Interface Princípio de Segregação e o Princípio da Inversão de Dependência .

    
por 26.01.2011 / 21:34
fonte
6

Segreguei grupos de configurações relacionadas com interfaces.

Algo como:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

etc.

Agora, para um ambiente real, uma única classe implementará muitas dessas interfaces. Essa classe pode puxar de um banco de dados ou a configuração do aplicativo ou o que você tem, mas muitas vezes sabe como obter a maioria das configurações.

Mas segregar por interface torna as dependências reais muito mais explícitas e refinadas, e isso é uma melhoria óbvia para testabilidade.

Mas eu nem sempre quero ser forçado a injetar ou fornecer as configurações como uma dependência. Há um argumento que poderia ser feito que eu deveria, por coerência ou explicitação. Mas na vida real parece uma restrição desnecessária.

Então, eu usarei uma classe estática como fachada, fornecendo uma entrada fácil de qualquer lugar para as configurações e, dentro dessa classe estática, eu irei localizar a implementação da interface e obter a configuração.

Eu sei que a localização do serviço é um sinal positivo da maioria, mas vamos encarar isso, fornecer uma dependência através de um construtor é um peso, e às vezes esse peso é mais do que eu gostaria de suportar. O Service Location é a solução para manter a capacidade de teste por meio de programação para interfaces e permitir várias implementações, ao mesmo tempo em que fornece a conveniência (em situações cuidadosamente medidas e adequadamente poucas) de um ponto de entrada de singleton estático.

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Eu acho esse mix o melhor de todos os mundos.

    
por 26.01.2011 / 21:31
fonte
3

Sim, como você percebeu, um objeto de configuração global dificulta o teste de unidade. Ter um setter "secreto" para testes unitários é um hack rápido, que embora não seja legal, pode ser muito útil: ele permite que você comece a escrever testes unitários para refatorar seu código para um design melhor ao longo do tempo.

(Referência obrigatória: Trabalhando efetivamente com o código herdado . Ele contém esse e muitos outros truques inestimáveis para o teste de unidade código legado.)

Na minha experiência, o melhor é ter o mínimo possível de dependências na configuração global. O que não é necessariamente zero - tudo depende das circunstâncias. Ter algumas classes "organizadoras" de alto nível que acessam a configuração global e transmitem as propriedades reais de configuração para os objetos que criam e usam podem funcionar bem, desde que os organizadores façam apenas isso, por exemplo. não contêm muito código testável. Isso permite que você teste a maioria ou toda a funcionalidade importante testável da unidade do aplicativo, sem interromper completamente o esquema de configuração física existente.

Isso já está chegando perto de uma solução de injeção de dependência genuína, como o Spring for Java. Se você pode migrar para tal framework, tudo bem. Mas na vida real, e especialmente quando se lida com um aplicativo legado, muitas vezes a refatoração lenta e meticulosa em direção à DI é o melhor compromisso possível.

    
por 26.01.2011 / 21:24
fonte
2

Na minha experiência, no mundo Java, é para isso que o Spring é destinado. Ele cria e gerencia objetos (beans) com base em determinadas propriedades que são configuradas no tempo de execução e, por outro lado, são transparentes para seu aplicativo. Você pode ir com o arquivo test.config que Anon. menciona ou você pode incluir alguma lógica no arquivo de configuração que o Spring manipulará para definir propriedades com base em alguma outra chave (por exemplo, hostname ou ambiente).

No que diz respeito ao seu segundo problema, você pode contornar isso por alguma arquitetura, mas não é tão grave quanto parece. O que isto significa no seu caso é que você não teria, por exemplo, um objeto Config global que várias outras classes usem. Você acabou de configurar essas outras classes através do Spring e, em seguida, não tem nenhum objeto de configuração, porque tudo o que precisava de configuração foi feito através do Spring, então você pode usar essas classes diretamente no seu código.

    
por 26.01.2011 / 21:21
fonte
2

Esta questão é realmente sobre um problema mais geral que a configuração. Assim que eu leio a palavra "singleton" eu imediatamente pensei em todos os problemas associados a esse padrão, e não menos do que é pouca testabilidade.

O padrão Singleton é "considerado prejudicial". Significado, não é sempre a coisa errada, mas geralmente é. Se você está pensando em usar um padrão singleton para qualquer coisa , pare para considerar:

  • Você precisará subclassificar isso?
  • Você precisará programar para uma interface?
  • Você precisa executar testes de unidade contra ele?
  • Você precisará modificá-lo com frequência?
  • O seu aplicativo destina-se a suportar várias plataformas / ambientes de produção?
  • Você está um pouco preocupado com o uso da memória?

Se sua resposta for "sim" para qualquer uma dessas (e provavelmente várias outras coisas que eu não pensei), então você provavelmente não quer usar um singleton. As configurações geralmente precisam de muito mais flexibilidade do que um singleton (ou, no caso, qualquer classe sem instância) pode fornecer.

Se você quiser quase todos os benefícios de um singleton sem nenhuma dor, use uma estrutura de injeção de dependência como Spring ou Castle ou o que estiver disponível para seu ambiente. Dessa forma, você só precisará declará-lo uma vez e o contêiner fornecerá automaticamente uma instância para qualquer necessidade.

    
por 26.01.2011 / 21:27
fonte
0

Uma forma de lidar com isso em C # é ter um objeto singleton que bloqueia durante a criação da instância e inicializa todos os dados de uma só vez para uso por todos os objetos do cliente. Se esse singleton puder manipular pares de chave / valores, você poderá armazenar qualquer tipo de dado, incluindo chaves complexas, para uso por diversos clientes e tipos de clientes. Se você quiser mantê-lo dinâmico e carregar novos dados do cliente conforme necessário, poderá verificar a existência de uma chave principal do cliente e, se estiver faltando, carregar os dados desse cliente, anexando-os ao dicionário principal. Também é possível que o singleton de configuração primário possa conter conjuntos de dados do cliente em que múltiplos do mesmo tipo de cliente usam os mesmos dados, todos acessados por meio do singleton de configuração principal. Grande parte dessa organização de objetos de configuração pode depender de como seus clientes de configuração precisam acessar essas informações e se essas informações são estáticas ou dinâmicas. Eu usei ambas as configurações de chave / valor, bem como APIs de objeto específico, dependendo do que era necessário.

Um dos meus singletons de configuração pode receber mensagens para recarregar a partir de um banco de dados. Nesse caso, carrego em uma segunda coleção de objetos e bloqueio somente a coleção principal para trocar coleções. Isso impede o bloqueio de leituras em outros threads.

Se você estiver carregando arquivos de configuração, poderá ter uma hierarquia de arquivos. As configurações em um arquivo podem especificar quais dos outros arquivos serão carregados. Eu usei esse mecanismo com serviços do Windows em C # que possuem vários componentes opcionais, cada um com suas próprias configurações. Os padrões de estrutura de arquivos eram os mesmos entre os arquivos, mas foram carregados ou não com base nas configurações do arquivo principal.

    
por 26.01.2011 / 21:49
fonte
0

A classe que você está descrevendo está soando como um antipadrão de objeto de Deus exceto com dados em vez de funções. Isso é um problema. Você provavelmente deve ter os dados de configuração lidos e armazenados no objeto apropriado, enquanto apenas lê os dados novamente se for necessário por algum motivo.

Além disso, você está usando um Singleton por um motivo que não é apropriado. Singletons só devem ser usados quando a existência de vários objetos criar um estado ruim. Usar um Singleton nesse caso é inadequado, pois ter mais de um leitor de configuração não deve causar um estado de erro imediatamente. Se você tem mais de um, provavelmente está fazendo errado, mas não é absolutamente necessário que você tenha apenas um leitor de configuração.

Por fim, criar um estado global como esse é uma violação do encapsulamento, pois você está permitindo mais aulas do que o necessário para acessar os dados diretamente.

    
por 26.01.2011 / 22:27
fonte
0

Eu acho que se existem muitas configurações, com "aglomerações" de outras relacionadas, faz sentido dividi-las em interfaces separadas com as respectivas implementações - então, injetar essas interfaces onde necessário com DI. Você ganha testabilidade, menor acoplamento e SRP.

Eu fui inspirado neste assunto por Joshua Flanagan de Los Techies; ele escreveu um artigo no importa há algum tempo.

    
por 26.01.2011 / 23:06
fonte