Quais são os usos válidos das classes estáticas?

8

Percebi que quase toda vez que vejo programadores usando classes estáticas em linguagens orientadas a objetos como C #, eles estão fazendo errado. Obviamente, os principais problemas são o estado global e a dificuldade de trocar implementações em tempo de execução ou com zombarias / stubs durante os testes.

Por curiosidade, observei alguns dos meus projetos selecionando aqueles que foram bem testados e quando fiz um esforço para realmente pensar em arquitetura e design. Os dois usos das classes estáticas que encontrei são:

  • A classe de utilitário - algo que eu preferiria evitar se estivesse escrevendo esse código hoje,

  • O cache do aplicativo: usar a classe estática para isso é claramente errado. E se eu quiser substituí-lo por MemoryCacheou Redis?

Olhando para o .NET Framework, também não vejo nenhum exemplo de uso válido de classes estáticas. Por exemplo:

  • Fileclasse estática torna realmente doloroso mudar para alternativas. Por exemplo, e se alguém precisar alternar para Armazenamento Isolado ou armazenar arquivos diretamente na memória ou precisar de um provedor que suporte transações NTFS ou, pelo menos, consiga lidar com caminhos com mais de 259 caracteres ? Ter uma interface comum e várias implementações parece tão fácil quanto usar Filediretamente, com o benefício de não precisar reescrever a maior parte do código base quando os requisitos mudam.

  • ConsoleA classe estática torna os testes excessivamente complicados. Como garantir dentro de um teste de unidade que um método produz dados corretos para o console? Devo modificar o método para enviar a saída para um gravável Streamque é injetado em tempo de execução? Parece que uma classe de console não estática seria tão simples quanto uma classe estática, apenas sem todas as desvantagens de uma classe estática, mais uma vez.

  • Mathclasse estática também não é um bom candidato; e se eu precisar mudar para a aritmética de precisão arbitrária? Ou para alguma implementação mais nova e mais rápida? Ou a um esboço que dirá, durante um teste de unidade, que um determinado método de uma Mathclasse foi realmente chamado durante um teste?

No Programmer.SE, eu li:

Além dos métodos de extensão, quais são os usos válidos das classes estáticas, ou seja, casos em que a Injeção de Dependência ou os singletons são impossíveis ou resultarão em um design de qualidade inferior e maior extensibilidade e manutenção?

Arseni Mourzenko
fonte
no Groovy ? "Se a classe Groovy que você está testando chama um método estático em outra classe Groovy, você pode usar o ExpandoMetaClass que permite adicionar dinamicamente métodos, construtores, propriedades e métodos estáticos ..."
gnat
Isso não responde à sua pergunta (e é por isso que é um comentário) e provavelmente será uma opinião impopular, mas acho que você está procurando um nível de modularidade que o Java / C # simplesmente não pode fornecer (sem pular mais aros do que um animal de circo). Você já identificou que um método estático é uma dependência codificada e, geralmente, qualquer classe é uma dependência codificada.
Doval
1
Portanto, você pode colocar tudo atrás de uma interface e tratá-los como módulos, mas precisará de adaptadores para converter de uma interface para outra quando introduzir código estrangeiro ou quando desejar colar diferentes funções em uma interface ou selecionar um subconjunto das funções de uma interface. E você perde a capacidade de ter métodos binários úteis (pelo menos sem trapacear instanceof) porque, dadas duas instâncias, IFoonão há garantia de que elas sejam apoiadas pela mesma classe.
Doval
2
@ Magus Isso ou você levanta os braços, digamos YAGNI, e aceita que, se precisar de um tipo diferente de string, fará com que seu IDE mude todas as instâncias de com.lang.lib.Stringpara com.someones.elses.Stringem toda a base de código.
Doval

Respostas:

20

e se; e se; e se?

YAGNI

A sério. Se alguém quiser usar implementações diferentes de Fileou Mathou Consoleentão, poderá passar pela dor de abstrair isso. Você já viu / usou Java?!? As bibliotecas padrão Java são um bom exemplo do que acontece quando você abstrai as coisas por uma questão de abstração. Eu realmente preciso seguir três etapas para criar meu objeto Calendário ?

Tudo isso dito, não vou me sentar aqui e defender fortemente as classes estáticas. O estado estático é totalmente ruim (mesmo que eu ainda o use ocasionalmente para agrupar / armazenar em cache coisas). As funções estáticas podem ser ruins devido a problemas de simultaneidade e testabilidade.

Mas funções estáticas puras não são tão terríveis. Não são coisas elementares que não fazem sentido para fora abstrato vez que não há nenhuma alternativa sadia para a operação. Não são implementações de uma estratégia que pode ser estático, porque eles já estão captada através do delegado / functor. E, às vezes, essas funções não têm uma classe de instância a ser associada; portanto, classes estáticas são boas para ajudar a organizá-las.

Além disso, os métodos de extensão são um uso prático , mesmo que não sejam classes filosoficamente estáticas.

Telastyn
fonte
1
Parte da pergunta parece ter sido a reclamação de que, embora existam pessoas que dizem que há usos válidos para classes estáticas, elas não apoiam essa afirmação com exemplos. Esta resposta poderia ser melhorada se o fizesse.
Magus
@ Magus - os três exemplos dados na pergunta são de uso perfeito, embora o OP defenda o contrário.
Telastyn
Não acho que seja uma resposta ruim, apenas para que haja espaço para melhorias. Se os exemplos no OP são os únicos, tudo bem. Faria sentido dizer isso na resposta, em prol de futuras pesquisas. De qualquer maneira, ele tem o meu voto positivo.
Magus
1
Gostaria que as estruturas fornecessem um suporte um pouco melhor para o estado estático do thread, em vez de vê-lo como uma reflexão tardia. Algo como Consolerealmente não deve fazer parte do estado do sistema, mas também não deve fazer parte de um objeto que é repassado constantemente. Em vez disso, parece que faria mais sentido dizer que cada thread tem uma propriedade "console atual" que pode ser facilmente salva e restaurada se for necessário ter uma rotina que grava para Consoleusar outra coisa.
Supercat
1
@BenjaminHodgson - o design raramente é quantitativo. De qualquer forma, os métodos de extensão poderiam ser implementados de maneira diferente (pense na manipulação do protótipo JavaScript) e os usuários não seriam os mais sábios. O fato de serem funções estáticas é um detalhe de implementação, não uma parte intrínseca de sua natureza.
Telastyn
18

Em uma palavra simplicidade.

Quando você se dissocia demais, você recebe o olá mundo do inferno.

void main(String[] args) {
    TextOutputFactory outputFactory = new TextOutputFactory();
    OutputStream stream = outputFactory.CreateStdOutputStream();

    Encoding encoding = new EncodingFactory.CreateUtf8Encoding();
    stream.Encoding = encoding;

    SystemConstant constant = new SystemConstant();
    stream.LineEnding = constant.PlatformLineEnding;

    stream.FlushOnEachLine = true;

    String greeting = new FixedSizeInMemoryString(); // We may have to switch to a file based string later!"
    greeting.SetContent("Hello world");
    greeting.Initialize();

    stream.SendContent(greeting);
    stream.EndCurrentLine();

    ProcessCompletion completion = new ProcessCompletion();
    completion.Status = constant.SuccessStatus;
    completion.ExitProgram();
}

Mas ei, pelo menos não há configuração XML, o que é um toque agradável.

A classe estática permite otimizar o caso rápido e comum. A maioria dos pontos mencionados pode ser resolvida com muita facilidade, introduzindo sua própria abstração sobre eles ou usando coisas como Console.Out.

Laurent Bourgault-Roy
fonte
5
Isso me lembra o GNU Hello World, responsável por várias preocupações de localização.
Telastyn
1
-1 Por falta de configuração XML. +2 Para colocar em poucas palavras.
S1lv3r
1
O Hello World deve realmente ser generalizado para permitir cenários supraglobal. Quero dizer, posso estar cumprimentando outro mundo, afinal. Eventualmente. Precisaríamos determinar primeiro alguma forma comum de comunicação ... Primos em binário, enviados pelo sinal de onda portadora AM ... mas que frequência? Eu sei, o modo fundamental dos átomos de silício!
10

O caso dos métodos estáticos:

var c = Math.Max(a, Int32.Parse(b));

é muito mais simples, mais claro e mais legível que isso:

IMathLibrary math = new DefaultMathLibrary();
IIntegerParser integerParser = new DefaultIntegerParser();
var c = math.Max(a, integerParser.Parse(b));

Agora adicione injeção de dependência para ocultar as instanciações explícitas e veja o one-liner crescer em dezenas ou centenas de linhas - tudo para dar suporte ao cenário improvável de você inventar sua própria Maximplementação, que é significativamente mais rápida que a do framework e você precisa para poder alternar de forma transparente entre as Maximplementações alternativas .

O design da API é uma troca entre simplicidade e flexibilidade. Você sempre pode introduzir camadas adicionais de indireção ( ad infinitum ), mas é necessário ponderar a conveniência do caso comum contra o benefício de camadas adicionais.

Por exemplo, você sempre pode gravar seu próprio wrapper de instância em torno da API do sistema de arquivos se precisar alternar de maneira transparente entre os provedores de armazenamento. Mas sem os métodos estáticos mais simples, todos seriam forçados a criar uma instância sempre que precisassem fazer algo simples como salvar um arquivo. Seria realmente uma troca ruim de design otimizar para um caso extremo extremamente raro e tornar tudo mais complicado para o caso comum. O mesmo ocorre com Console- você pode criar facilmente seu próprio wrapper se precisar abstrair ou zombar do console, mas muitas vezes você usa o console para soluções rápidas ou para fins de depuração, para que você queira apenas a maneira mais simples possível de produzir algum texto.

Além disso, embora "uma interface comum com várias implementações" seja legal, não há necessariamente uma abstração "correta" que unifique todos os provedores de armazenamento que você deseja. Você pode alternar entre arquivos e armazenamento isolado, mas outra pessoa pode alternar entre arquivos, banco de dados e cookies para armazenamento. A interface comum pareceria diferente e é impossível prever todas as abstrações possíveis que os usuários possam desejar. É muito mais flexível fornecer métodos estáticos como "primitivos" e permitir que os usuários construam suas próprias abstrações e invólucros.

Seu Mathexemplo é ainda mais dúbio. Se você deseja alterar para uma precisão de precisão arbitrária, presumivelmente precisará de novos tipos numéricos. Esta seria uma mudança fundamental de todo você programa e ter que mudar Math.Max()para ArbitraryPrecisionMath.Max()é certamente menos do que você se preocupa.

Dito isto, concordo que as classes estáticas com estado são, na maioria dos casos, más.

JacquesB
fonte
0

Em geral, os únicos lugares onde eu usaria classes estáticas são aqueles em que a classe hospeda funções puras que não cruzam os limites do sistema. Sei que o último é redundante, pois qualquer coisa que atravessa um limite provavelmente não é pura intenção. Eu chego a essa conclusão tanto pela desconfiança do estado global quanto pela facilidade de testar a perspectiva. Mesmo onde há uma expectativa razoável de que haverá uma única implementação para uma classe que ultrapasse os limites do sistema (rede, sistema de arquivos, dispositivo de E / S), eu escolho usar instâncias em vez de classes estáticas, porque a capacidade de fornecer uma simulação A implementação / fake nos testes de unidade é fundamental no desenvolvimento de testes de unidade rápidos, sem acoplamento a dispositivos reais. Nos casos em que o método é uma função pura, sem acoplamento a um sistema / dispositivo externo, acho que uma classe estática pode ser apropriada, mas apenas se não prejudicar a testabilidade. Acho que os casos de uso são relativamente raros fora das classes de estrutura existentes. Além disso, pode haver casos em que você não tem escolha - por exemplo, métodos de extensão em C #.

tvanfosson
fonte