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

43

Na maioria dos meus aplicativos, tenho um objeto singleton ou estático "config", encarregado de ler várias configurações do disco. Quase todas as classes o utilizam, 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 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 deseja testar com a mesma configuração que executa. Existem algumas soluções para isso:

  • Dê ao objeto de configuração um setter SOMENTE usado para teste, para que você possa passar em diferentes configurações.
  • Continue usando um único objeto de configuração, mas altere-o de um singleton para uma instância que você passe por todos os lugares onde for necessário. Em seguida, você pode construí-lo uma vez em seu aplicativo e uma vez em seus testes, com configurações diferentes.

Mas de qualquer maneira, você ainda tem um segundo problema: quase qualquer classe pode usar o objeto de configuração. Portanto, em um teste, você precisa definir a configuração da 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 chegar à conclusão de 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 qualquer lugar?

JW01
fonte
A configuração obtém suas configurações lendo um arquivo no disco, certo? Então, por que não apenas ter um arquivo "test.config" que possa ser lido?
Anon.
Isso resolve o primeiro problema, mas não o segundo.
JW01
@ JW01: Todos os seus testes precisam de uma configuração marcadamente diferente? Você precisará definir essa configuração em algum lugar durante o teste, não é?
Anon.
Verdadeiro. Mas se eu continuar usando um único pool de configurações (com um pool diferente para teste), todos os meus testes acabarão usando as mesmas configurações. E como eu posso querer testar o comportamento com configurações diferentes, isso não é o ideal.
JW01
"Portanto, em um teste, você precisa definir a configuração da classe que está sendo testada, mas também TODAS as suas dependências. Isso pode tornar seu código de teste feio." Tem um exemplo? Tenho a sensação de que não é a estrutura de todo o seu aplicativo conectada à classe de configuração, mas a estrutura da própria classe de configuração que está tornando as coisas "feias". A instalação de uma classe não deve configurar automaticamente suas dependências?
AndrewKS

Respostas:

35

Não tenho problemas com um único objeto de configuração e posso ver a vantagem de manter todas as configurações em um único local.

No entanto, o uso desse único objeto em qualquer lugar 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, poderá visitar todas as instâncias em que é usada e verificar se não quebrou nada.

Uma maneira de lidar com isso é criar várias interfaces que expõem 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 dele. Dessa forma, as partes do aplicativo que usam a configuração dependem da coleção menor de campos na interface, e não de toda a classe de configuração. Por fim, para teste de unidade, você pode criar uma classe personalizada que implementa a interface.

Se você deseja explorar essas idéias ainda mais, recomendo a leitura sobre os princípios do SOLID , particularmente o Princípio de Segregação de Interface e o Princípio de Inversão de Dependências .

Kramii Restabelecer Monica
fonte
7

Segrego 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 extrair de um banco de dados ou da configuração do aplicativo ou do que você possui, mas geralmente sabe como obter a maioria das configurações.

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

Mas .. nem sempre quero ser forçado a injetar ou fornecer as configurações como uma dependência. Pode-se argumentar que eu deveria, por consistência ou explicitação. Mas na vida real parece uma restrição desnecessária.

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

Eu sei que a localização do serviço diminui rapidamente a maioria, mas vamos ser sinceros, 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 testabilidade por meio da programação de interfaces e permitir várias implementações, enquanto ainda oferece a conveniência (em situações cuidadosamente medidas e adequadamente em poucas situações) de um ponto de entrada estático de singleton.

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

Acho essa mistura o melhor de todos os mundos.

quentin-starin
fonte
3

Sim, como você percebeu, um objeto de configuração global dificulta o teste de unidade. Ter um setter "secreto" para teste de unidade é um hack rápido, o que, embora não seja agradável, pode ser muito útil: permite começar a escrever testes de unidade, para que você possa refatorar seu código para um melhor design 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 código herdado de teste de unidade.)

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 de "organizador" de alto nível que acessam a configuração global e transmitem as propriedades de configuração reais aos objetos que eles criam e usam podem funcionar bem, desde que os organizadores façam apenas isso, por exemplo, eles próprios não contêm muito código testável. Isso permite que você teste a maioria ou todas as funcionalidades importantes testáveis ​​da unidade do aplicativo, sem interromper completamente o esquema de configuração física existente.

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

Péter Török
fonte
Eu realmente gostei da sua abordagem e não sugeriria que a idéia de um levantador fosse mantida em "segredo" ou considerada um "truque rápido" para esse assunto. Se você adota a postura de que os testes de unidade são um importante usuário / consumidor / parte do seu aplicativo, ter test_atributos e métodos não é mais tão ruim, certo? Concordo que a solução genuína para o problema é empregar uma estrutura de DI, mas em exemplos como o seu, ao trabalhar com código legado ou outros casos simples, o código de teste não alienante se aplica de maneira limpa.
Filip Dupanović 26/01
1
@kRON, esses são truques imensamente úteis e práticos para tornar testável a unidade de código herdada, e também os estou usando extensivamente. No entanto, idealmente, o código de produção não deve conter nenhum recurso introduzido apenas para fins de teste. A longo prazo, é para onde estou me refatorando.
Péter Török
2

Na minha experiência, no mundo Java, é para isso que o Spring se destina. Ele cria e gerencia objetos (beans) com base em certas propriedades definidas em tempo de execução e, de outra forma, é transparente para o 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, nome do host ou ambiente).

No que diz respeito ao seu segundo problema, você pode contornar isso por meio de alguma pesquisa, mas não é tão grave quanto parece. O que isso 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 possui nenhum objeto de configuração, porque tudo o que era necessário para a configuração passou pelo Spring, para que você possa usar essas classes diretamente no seu código.

Justin Beal
fonte
2

Esta pergunta é realmente sobre um problema mais geral que a configuração. Assim que li a palavra "singleton", pensei imediatamente em todos os problemas associados a esse padrão, entre os quais a baixa testabilidade.

O padrão Singleton é "considerado prejudicial". Ou seja, nem sempre é a coisa errada, mas geralmente é. Se você já pensou em usar um padrão singleton para algo , pare para considerar:

  • Você precisará subclassificar?
  • Você precisará programar para uma interface?
  • Você precisa executar testes de unidade contra ele?
  • Você precisará modificá-lo com frequência?
  • Seu aplicativo é destinado a suportar múltiplas plataformas / ambientes de produção?
  • Você está um pouco preocupado com o uso de memória?

Se sua resposta for "sim" a alguma dessas perguntas (e provavelmente a várias outras coisas em que não pensei), provavelmente você não deseja usar um singleton. As configurações geralmente precisam de muito mais flexibilidade do que um singleton (ou, nesse caso, qualquer classe instável) pode fornecer.

Se você deseja 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 o seu ambiente. Dessa forma, você só precisará declará-lo uma vez e o contêiner fornecerá automaticamente uma instância para o que for necessário.

Aaronaught
fonte
0

Uma maneira de lidar com isso no C # é ter um objeto singleton que trava 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 dados, incluindo chaves complexas, para uso por muitos clientes e tipos de clientes diferentes. 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 ausente, carregar os dados para esse cliente, anexando-o ao dicionário principal. Também é possível que o singleton de configuração principal 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 configurações de chave / valor, bem como APIs de objetos específicos, dependendo de qual era necessário.

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

Se estiver carregando de arquivos de configuração, você pode ter uma hierarquia de arquivos. As configurações em um arquivo podem especificar qual dos outros arquivos carregar. 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 nos arquivos, mas foram carregados ou não com base nas configurações do arquivo principal.

Tim Butterfield
fonte
0

A classe que você está descrevendo parece 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 lê os dados apenas se forem necessários por algum motivo.

Além disso, você está usando um Singleton por um motivo que não é apropriado. Singletons devem ser usados ​​apenas quando a existência de vários objetos criar um estado ruim. O uso de 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ê tiver mais de um, provavelmente está fazendo algo errado, mas não é absolutamente necessário que você tenha apenas um leitor de configuração.

Finalmente, a criação de um estado global é uma violação do encapsulamento, pois você permite que mais classes do que o necessário para acessar diretamente os dados.

indyK1ng
fonte
0

Penso que, se houver muitas configurações, com "grupos" de configurações relacionadas, faz sentido dividi-las em interfaces separadas com as respectivas implementações - em seguida, injete essas interfaces quando necessário com o DI. Você ganha testabilidade, menor acoplamento e SRP.

Eu me inspirei nesse assunto por Joshua Flanagan, do Los Techies; ele escreveu um artigo sobre o assunto há algum tempo.

Grant Palin
fonte