Padrão C # para manipular “funções livres” de maneira limpa, evitando classes estáticas de “bolsa de utilidade” no estilo Helper

9

Recentemente, eu estava revisando algumas classes estáticas da "bolsa de utilidade" do estilo Helper flutuando em torno de algumas grandes bases de código C # com as quais trabalho, basicamente como o seguinte trecho muito condensado:

// Helpers.cs

public static class Helpers
{
    public static void DoSomething() {}
    public static void DoSomethingElse() {}
}

Os métodos específicos que revi são

  • principalmente não relacionados entre si,
  • sem estado explícito persistido através de invocações,
  • pequeno e
  • cada um consumido por uma variedade de tipos não relacionados.

Edit: O item acima não pretende ser uma lista de supostos problemas. É uma lista de características comuns dos métodos específicos que estou revisando. É um contexto para ajudar as respostas a fornecer soluções mais relevantes.

Apenas para esta pergunta, vou me referir a esse tipo de método como um GLUM (método geral de utilitário leve). A conotação negativa de "triste" é parcialmente pretendida. Me desculpe se isso parece um trocadilho idiota.

Mesmo deixando de lado meu ceticismo padrão sobre GLUMs, não gosto das seguintes coisas sobre isso:

  • Uma classe estática está sendo usada apenas como um espaço para nome.
  • O identificador de classe estática é basicamente sem sentido.
  • Quando uma nova GLUM é adicionada, (a) essa classe "bolsa" é tocada sem um bom motivo ou (b) uma nova classe "bolsa" é criada (o que por si só não costuma ser um problema; o ruim é que a nova classes estáticas frequentemente repetem o problema de falta de relação, mas com menos métodos).
  • A meta-nomeação é inevitavelmente horrível, não-padrão e, normalmente, internamente inconsistentes, se é Helpers, Utilitiesou o que quer.

Qual é um padrão razoavelmente bom e simples para refatorar isso, de preferência abordando as preocupações acima e de preferência com o mínimo de toque possível?

Eu provavelmente deveria enfatizar: todos os métodos com os quais estou lidando não são pareados entre si. Não parece haver uma maneira razoável de decompô-los em sacos de métodos estáticos de granulação mais fina e ainda com vários membros.

William
fonte
11
Sobre o GLUMs ™: Acho que a abreviação de "leve" pode ser removida, pois os métodos devem ser leves seguindo as práticas recomendadas;)
Fabio
@ Fabio eu concordo. Incluí o termo como uma piada (provavelmente não muito engraçada e possivelmente confusa) que serve de abreviação para a redação desta pergunta. Eu acho que "leve" parecia apropriado aqui porque as bases de código em questão têm vida longa, herdadas e um pouco confusas, ou seja, existem muitos outros métodos (geralmente esses GUMs ;-) que são "pesados". Se eu tivesse mais orçamento, no momento, definitivamente adotaria uma abordagem mais agressiva e abordaria a segunda.
William

Respostas:

17

Eu acho que você tem dois problemas intimamente relacionados entre si.

  • Classes estáticas contêm funções logicamente não relacionadas
  • Os nomes de classes estáticas não fornecem informações úteis sobre o significado / contexto das funções contidas.

Esses problemas podem ser corrigidos combinando apenas métodos logicamente relacionados em uma classe estática e movendo os outros para sua própria classe estática. Com base no domínio do problema, você e sua equipe devem decidir quais critérios serão usados ​​para separar métodos em classes estáticas relacionadas.

Preocupações e possíveis soluções

Os métodos não têm relação entre si - problema. Combine métodos relacionados em uma classe estática e mova métodos não relacionados para suas próprias classes estáticas.

Os métodos sem estado explícito persistiram nas invocações - não é um problema. Métodos sem estado são um recurso, não um bug. O estado estático é difícil de testar de qualquer maneira e só deve ser usado se você precisar manter o estado através de invocações de método, mas existem maneiras melhores de fazer isso, como máquinas de estado e a yieldpalavra - chave.

Os métodos são pequenos - não é um problema. - Os métodos devem ser pequenos.

Cada método é consumido por uma variedade de tipos não relacionados - não é um problema. Você criou um método geral que pode ser reutilizado em muitos lugares não relacionados.

Uma classe estática está sendo usada apenas como um espaço para nome - não é um problema. É assim que métodos estáticos são usados. Se esse estilo de chamar métodos o incomodar, tente usar métodos de extensão ou a using staticdeclaração.

O identificador de classe estática é basicamente sem sentido - problema . Não faz sentido porque os desenvolvedores estão colocando métodos não relacionados nele. Coloque métodos não relacionados em suas próprias classes estáticas e forneça nomes específicos e compreensíveis.

Quando um novo GLUM é adicionado, (a) essa classe "bag" é tocada (tristeza do SRP) - não é um problema se você seguir a ideia de que apenas métodos relacionados à lógica devem estar em uma classe estática. SRPnão é violado aqui, porque o princípio deve ser aplicado a classes ou métodos. A responsabilidade única das classes estáticas significa conter métodos diferentes para uma "idéia" geral. Essa ideia pode ser um recurso ou uma conversão, ou iterações (LINQ), normalização ou validação de dados ...

ou com menos frequência (b) uma nova classe "bolsa" é criada (mais bolsas) - não é um problema. Classes / classes / funções estáticas - são as nossas ferramentas (desenvolvedores), que usamos para projetar software. Sua equipe tem alguns limites para o uso de aulas? Quais são os limites máximos para "mais malas"? A programação tem tudo a ver com contexto, se, para uma solução em seu contexto específico, você terminar com 200 classes estáticas, que são compreensivelmente nomeadas, logicamente colocadas em namespaces / pastas com hierarquia lógica - não é um problema.

A meta-nomeação é inevitavelmente horrível, não-padrão e, normalmente, internamente inconsistentes, se é Helpers, Utilities, ou qualquer outra coisa - problema. Forneça nomes melhores que descrevam o comportamento esperado. Mantenha apenas métodos relacionados em uma classe, que fornecem ajuda para uma melhor nomeação.

Fabio
fonte
Não é uma violação do SRP, mas claramente uma violação do Princípio Aberto / Fechado. Dito isto, eu geralmente encontrada a Ocp a ser mais problemas do que vale a pena
Paul
2
@Paul, o princípio Abrir / Fechar não pode ser aplicado a classes estáticas. Esse foi o meu argumento, que os princípios SRP ou OCP não podem ser aplicados a classes estáticas. Você pode aplicá-lo a métodos estáticos.
Fabio
Ok, houve alguma confusão aqui. A primeira lista de itens da pergunta não é uma lista de supostos problemas. É uma lista de características comuns dos métodos específicos que estou revisando. É um contexto para ajudar as respostas a fornecer soluções mais aplicáveis. Vou editar para esclarecer.
William
11
Re "classe estática como namespace", o fato de ser um padrão comum não significa que não é um anti-padrão. O método (que é basicamente uma "função livre" para emprestar um termo do C / C ++) dentro dessa classe estática de baixa coesão é a entidade significativa nesse caso. Nesse contexto específico, parece estruturalmente melhor ter um subdomínio Acom 100 classes estáticas de método único (em 100 arquivos) do que uma classe estática Acom 100 métodos em um único arquivo.
William
11
Fiz uma limpeza bastante importante na sua resposta, principalmente cosmética. Não sei ao certo qual é o seu último parágrafo; ninguém se preocupa com o teste de código que chama a Mathclasse estática.
21717 Robert Downey
5

Apenas adote a convenção de que classes estáticas são usadas como espaços para nome em situações como essa em C #. Os espaços para nome recebem bons nomes, assim como essas classes. O using staticrecurso do C # 6 também facilita a vida.

Nomes de classe malcheirosos Helpersindicam que os métodos devem ser divididos em classes estáticas melhor nomeadas, se possível, mas uma de suas suposições enfatizadas é que os métodos com os quais você está lidando são "não relacionados aos pares", o que implica dividir para uma nova classe estática por método existente. Provavelmente tudo bem, se

  • existem bons nomes para as novas classes estáticas e
  • você julga que ele realmente beneficia a manutenção do seu projeto.

Como um exemplo artificial, Helpers.LoadImagepode se tornar algo parecido FileSystemInteractions.LoadImage.

Ainda assim, você pode acabar com sacos de método de classe estática. Algumas maneiras de isso acontecer:

  • Talvez apenas alguns (ou nenhum) dos métodos originais tenham bons nomes para suas novas classes.
  • Talvez as novas classes posteriormente evoluam para incluir outros métodos relevantes.
  • Talvez o nome da classe original não fede e esteja realmente bem.

É importante lembrar que esses pacotes de métodos de classe estática não são muito incomuns em bases de código C # reais. Provavelmente não é patológico ter alguns pequenos .


Se você realmente vê uma vantagem em ter cada método semelhante à "função livre" em seu próprio arquivo (o que é bom e vale a pena se você honestamente julgar que ele realmente beneficia a capacidade de manutenção do seu projeto), considere transformar essa classe estática em vez de estática classe parcial, empregando o nome da classe estática semelhante a como você usaria um espaço para nome e consumindo-o via using static. Por exemplo:

estrutura do projeto fictício usando esse padrão

Program.cs

namespace ConsoleApp1
{
    using static Foo;
    using static Flob;

    class Program
    {
        static void Main(string[] args)
        {
            Bar();
            Baz();
            Wibble();
            Wobble();
        }
    }
}

Foo / Bar.cs

namespace ConsoleApp1
{
    static partial class Foo
    {
        public static void Bar() { }
    }
}

Foo / Baz.cs

namespace ConsoleApp1
{
    static partial class Foo
    {
        public static void Baz() { }
    }
}

Flob / Wibble.cs

namespace ConsoleApp1
{
    static partial class Flob
    {
        public static void Wibble() { }
    }
}

Flob / Wobble.cs

namespace ConsoleApp1
{
    static partial class Flob
    {
        public static void Wobble() { }
    }
}
William
fonte
-6

Geralmente você não deve ter ou exigir nenhum "método geral de utilidade". Na sua lógica de negócios. Dê outra olhada e coloque-os em um local adequado.

Se você está escrevendo uma biblioteca de matemática ou algo assim, eu sugeriria, embora eu normalmente os odeie, Métodos de Extensão.

Você pode usar os Métodos de extensão para adicionar funções aos tipos de dados padrão.

Por exemplo, digamos que eu tenho uma função geral MySort () em vez de Helpers.MySort ou adicionando um novo ListOfMyThings.MySort Eu posso adicioná-lo a IEnumerable

Ewan
fonte
O ponto é não dar mais um "olhar duro". O objetivo é ter um padrão reutilizável que possa limpar facilmente uma "bolsa" de utilitários. Eu sei que os utilitários são cheiros. A questão afirma isso. O objetivo é isolar sensivelmente essas entidades de domínio independentes (mas muito co-localizadas) por enquanto e ir o mais fácil possível no orçamento do projeto.
William
11
Se você não possui "métodos de utilidade geral", provavelmente não está isolando partes da lógica do resto de sua lógica o suficiente. Por exemplo, até onde eu sei, não há nenhuma função de associação de caminho de URL de uso geral no .NET, então, muitas vezes, acabo escrevendo uma. Esse tipo de problema seria melhor como parte da biblioteca padrão, mas toda lib padrão está faltando alguma coisa . A alternativa, deixando a lógica repetida diretamente dentro do seu outro código, o torna muito menos legível.
jpmc26
11
@Ewan que não é uma função, é uma assinatura de uso geral delegado ...
TheCatWhisperer
11
@ Ewan Esse era o ponto principal do TheCatWhisperer: as classes de utilidade são um problema em torno de ter que colocar métodos em uma classe. Que bom que você concorda.
jpmc26