Patrón C # para manejar limpiamente las "funciones libres", evitando las clases estáticas de "bolsa de utilidad" estilo Helper

9

Recientemente estuve revisando algunas clases estáticas de "bolsa de utilidad" estilo Helper que flotan alrededor de algunas grandes bases de código C # con las que trabajo, básicamente cosas como el siguiente fragmento muy condensado:

// Helpers.cs

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

Los métodos específicos que he revisado son

  • en su mayoría no relacionados entre sí,
  • sin estado explícito persistió a través de invocaciones,
  • pequeño y
  • cada uno consumido por una variedad de tipos no relacionados.

Editar: lo anterior no pretende ser una lista de supuestos problemas. Es una lista de características comunes de los métodos específicos que estoy revisando. Es un contexto para ayudar a las respuestas a proporcionar soluciones más relevantes.

Solo por esta pregunta, me referiré a este tipo de método como GLUM (método de utilidad general ligero). La connotación negativa de "tristeza" es en parte intencionada. Lo siento si esto aparece como un juego de palabras tonto.

Incluso dejando de lado mi propio escepticismo predeterminado sobre GLUM, no me gustan las siguientes cosas sobre esto:

  • Una clase estática se está utilizando únicamente como un espacio de nombres.
  • El identificador de clase estática es básicamente sin sentido.
  • Cuando se agrega un nuevo GLUM, (a) esta clase de "bolsa" se toca sin razón alguna o (b) se crea una nueva clase de "bolsa" (que en sí misma no suele ser un problema; lo malo es que la nueva las clases estáticas a menudo solo repiten el problema de la falta de relación, pero con menos métodos).
  • El meta-denominación es ineludiblemente horrible, no estándar, y por lo general carece de coherencia interna, ya sea Helpers, Utilitieso lo que sea.

¿Cuál es un patrón razonablemente bueno y simple para refactorizar esto, preferiblemente abordando las preocupaciones anteriores y preferiblemente con un toque lo más ligero posible?

Probablemente debería enfatizar: todos los métodos con los que estoy tratando no están relacionados por pares entre sí. No parece haber una forma razonable de dividirlos en bolsas de métodos de clase estática de grano fino pero aún con múltiples miembros.

Guillermo
fuente
1
Acerca de GLUMs ™: Creo que la abreviatura de "ligero" podría eliminarse, ya que los métodos deberían ser ligeros siguiendo las mejores prácticas;)
Fabio
@Fabio estoy de acuerdo. Incluí el término como una broma (probablemente no muy graciosa y posiblemente confusa) que sirve como abreviatura de la redacción de esta pregunta. Creo que "ligero" parecía apropiado aquí porque las bases de código en cuestión son de larga duración, heredadas y un poco desordenadas, es decir, existen muchos otros métodos (generalmente estos GUMs ;-) que son "pesados". Si tuviera más (ehem, cualquier) presupuesto en este momento, definitivamente tomaría un enfoque más agresivo y abordaría este último.
William

Respuestas:

17

Creo que tienen dos problemas estrechamente relacionados entre sí.

  • Las clases estáticas contienen funciones lógicamente no relacionadas.
  • Los nombres de las clases estáticas no proporcionan información útil sobre el significado / contexto de las funciones contenidas.

Estos problemas se pueden solucionar combinando solo métodos relacionados lógicamente en una clase estática y moviendo los otros a su propia clase estática. Según el dominio del problema, usted y su equipo deben decidir qué criterios utilizará para separar los métodos en clases estáticas relacionadas.

Preocupaciones y posibles soluciones.

Los métodos no están relacionados entre sí - problema. Combine métodos relacionados en una clase estática y mueva métodos no relacionados a sus propias clases estáticas.

Los métodos no tienen un estado explícito persistente en las invocaciones , no es un problema. Los métodos sin estado son una característica, no un error. El estado estático es difícil de probar de todos modos, y solo debe usarse si necesita mantener el estado a través de las invocaciones de métodos, pero hay mejores formas de hacerlo, como máquinas de estado y la yieldpalabra clave.

Los métodos son pequeños , no hay problema. - Los métodos deben ser pequeños.

Cada uno de los métodos es consumido por una variedad de tipos no relacionados , no es un problema. Ha creado un método general que puede reutilizarse en muchos lugares no relacionados.

Una clase estática se está utilizando únicamente como un espacio de nombres , no es un problema. Así es como se utilizan los métodos estáticos. Si este estilo de métodos de llamada le molesta, intente usar métodos de extensión o la using staticdeclaración.

El identificador de clase estático es básicamente sin sentido - problema . No tiene sentido porque los desarrolladores están poniendo métodos no relacionados en él. Coloque métodos no relacionados en sus propias clases estáticas y asígneles nombres específicos y comprensibles.

Cuando se agrega un nuevo GLUM, (a) esta clase de "bolsa" se toca (tristeza SRP) , no es un problema si sigue la idea de que solo los métodos relacionados lógicamente deberían estar en una clase estática. SRPno se viola aquí, porque el principio debe aplicarse para clases o métodos. La responsabilidad única de las clases estáticas significa contener diferentes métodos para una "idea" general. Esa idea puede ser una característica o una conversión, o iteraciones (LINQ), o normalización de datos, o validación ...

o con menos frecuencia (b) se crea una nueva clase de "bolsa" (más bolsas) , no es un problema. Clases / clases / funciones estáticas: son nuestras herramientas (de desarrolladores) que utilizamos para diseñar software. ¿Su equipo tiene algunos límites para usar las clases? ¿Cuáles son los límites máximos para "más bolsas"? La programación tiene que ver con el contexto, si para la solución en su contexto particular, termina con 200 clases estáticas que, lógicamente, se colocan en espacios de nombres / carpetas con jerarquía lógica, no es un problema.

El meta-denominación es ineludiblemente horrible, no estándar, y por lo general carece de coherencia interna, ya sea ayudantes, Utilidades, o lo que sea - un problema. Proporcione mejores nombres que describan el comportamiento esperado. Mantenga solo los métodos relacionados en una clase, que proporcionan ayuda para una mejor denominación.

Fabio
fuente
No es una violación de SRP, pero claramente una violación de Principio Abierto / Cerrado. Dicho esto, generalmente he encontrado que Ocp es más problemático de lo que vale la pena
Paul
2
@Paul, el principio Abrir / Cerrar no se puede aplicar para clases estáticas. Ese fue mi punto, que los principios SRP u OCP no pueden aplicarse para clases estáticas. Puede aplicarlo para métodos estáticos.
Fabio
De acuerdo, hubo cierta confusión aquí. La primera lista de elementos de la pregunta no es una lista de supuestos problemas. Es una lista de características comunes de los métodos específicos que estoy revisando. Es un contexto para ayudar a las respuestas a proporcionar soluciones más aplicables. Lo editaré para aclarar.
William
1
Re "clase estática como espacio de nombres", el hecho de que sea un patrón común no significa que no sea un antipatrón. El método (que es básicamente una "función libre" para tomar prestado un término de C / C ++) dentro de esta clase estática particular de baja cohesión es la entidad significativa en este caso. En este contexto particular, parece estructuralmente mejor tener un subespacio de nombres Acon 100 clases estáticas de método único (en 100 archivos) que una clase estática Acon 100 métodos en un solo archivo.
William
1
Hice una limpieza bastante importante en su respuesta, principalmente cosmética. No estoy seguro de qué trata su último párrafo; a nadie le preocupa cómo prueban el código que llama a la Mathclase estática.
Robert Harvey
5

Simplemente abrace la convención de que las clases estáticas se usan como espacios de nombres en situaciones como esta en C #. Los espacios de nombres obtienen buenos nombres, al igual que estas clases. La using staticcaracterística de C # 6 también hace la vida un poco más fácil.

Los nombres de clase malolientes como Helpersseñal de que los métodos deberían dividirse en clases estáticas mejor nombradas, si es posible, pero una de sus suposiciones enfatizadas es que los métodos con los que está tratando no están relacionados por pares, lo que implica dividirlos en Una nueva clase estática por método existente. Eso probablemente está bien, si

  • hay buenos nombres para las nuevas clases estáticas y
  • juzgas que realmente beneficia la mantenibilidad de tu proyecto.

Como un ejemplo artificial, Helpers.LoadImagepodría convertirse en algo así FileSystemInteractions.LoadImage.

Aún así, podría terminar con bolsas de métodos de clase estática. Algunas formas en que esto puede suceder:

  • Quizás solo algunos (o ninguno) de los métodos originales tengan buenos nombres para sus nuevas clases.
  • Quizás las nuevas clases evolucionen más tarde para incluir otros métodos relevantes.
  • Tal vez el nombre original de la clase no olía mal y en realidad estaba bien.

Es importante recordar que estas bolsas de métodos de clase estática no son terriblemente infrecuentes en las bases de código reales de C #. Probablemente no sea patológico tener algunos pequeños .


Si realmente ve una ventaja en tener cada método similar a "función libre" en su propio archivo (lo cual está bien y vale la pena si honestamente juzga que realmente beneficia la mantenibilidad de su proyecto), podría considerar hacer una clase estática en lugar de una estática clase parcial, empleando el nombre de la clase estática de forma similar a como lo haría con un espacio de nombres y luego consumiéndolo a través de using static. Por ejemplo:

estructura del proyecto ficticio utilizando este patrón

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() { }
    }
}
Guillermo
fuente
-6

En general, no debe tener ni requerir ningún "método de utilidad general". En su lógica de negocios. Eche otro vistazo y colóquelos en un lugar adecuado.

Si estás escribiendo una biblioteca de matemáticas o algo así, te sugiero, aunque normalmente los odio, los métodos de extensión.

Puede usar los métodos de extensión para agregar funciones a los tipos de datos estándar.

Por ejemplo, digamos que tengo una función general MySort () en lugar de Helpers.MySort o agregando una nueva ListOfMyThings.MySort, puedo agregarla a IEnumerable

Ewan
fuente
El punto es no tomar otra "mirada dura". El punto es tener un patrón reutilizable que pueda limpiar fácilmente una "bolsa" de utilidades. Sé que las utilidades son olores. La pregunta dice esto. El objetivo es aislar de manera sensata estas entidades de dominio independientes (pero muy ubicadas) por el momento e ir lo más fácil posible en el presupuesto del proyecto.
William
1
Si no tiene "métodos de utilidad general", probablemente no esté aislando las piezas lógicas del resto de su lógica lo suficiente. Por ejemplo, hasta donde sé, no hay una función de unión de ruta de URL de propósito general en .NET, por lo que a menudo termino escribiendo una. Ese tipo de cruft sería mejor como parte de la biblioteca estándar, pero a cada lib estándar le falta algo . La alternativa, dejar la lógica repetida directamente dentro de su otro código, lo hace mucho menos legible.
jpmc26
1
@Ewan que no es una función, es una firma delegada de propósito general ...
TheCatWhisperer
1
@Ewan Ese fue el punto de TheCatWhisperer: las clases de utilidad son un error al tener que poner métodos en una clase. Me alegra que estés de acuerdo.
jpmc26