¿Cuáles son los usos válidos de las clases estáticas?

8

Noté que casi cada vez que veo programadores que usan clases estáticas en lenguajes orientados a objetos como C #, lo están haciendo mal. Los principales problemas son obviamente el estado global y la dificultad de intercambiar implementaciones en tiempo de ejecución o con simulacros / stubs durante las pruebas.

Por curiosidad, he visto algunos de mis proyectos seleccionando los que están bien probados y cuando hice un esfuerzo por pensar realmente en la arquitectura y el diseño. Los dos usos de las clases estáticas que he encontrado son:

  • La clase de utilidad, algo que preferiría evitar si escribiera este código hoy,

  • El caché de la aplicación: usar una clase estática para eso es simplemente incorrecto. ¿Qué pasa si quiero reemplazarlo por MemoryCacheRedis?

Al mirar .NET Framework, tampoco veo ningún ejemplo de uso válido de clases estáticas. Por ejemplo:

  • FileLa clase estática hace que sea realmente doloroso cambiar a alternativas. Por ejemplo, ¿qué sucede si uno necesita cambiar a Almacenamiento aislado o almacenar archivos directamente en la memoria o necesita un proveedor que pueda admitir transacciones NTFS o al menos poder manejar rutas de más de 259 caracteres ? Tener una interfaz común y múltiples implementaciones parece tan fácil como usarlo Filedirectamente, con el beneficio de no tener que reescribir la mayor parte del código base cuando cambian los requisitos.

  • ConsoleLa clase estática hace que las pruebas sean demasiado complicadas. ¿Cómo debo asegurarme dentro de una prueba unitaria de que un método genera datos correctos en la consola? ¿Debo modificar el método para enviar la salida a un objeto de escritura Streamque se inyecta en tiempo de ejecución? Parece que una clase de consola no estática sería tan simple como una estática, una vez más, sin todos los inconvenientes de una clase estática.

  • Mathla clase estática tampoco es un buen candidato; ¿Qué pasa si necesito cambiar a aritmética de precisión arbitraria? ¿O a alguna implementación más nueva y más rápida? ¿O a un trozo que dirá, durante una prueba de unidad, que un método dado de una Mathclase fue efectivamente llamado durante una prueba?

En Programmer.SE, he leído:

Aparte de los métodos de extensión, ¿cuáles son los usos válidos de las clases estáticas, es decir, casos en los que la inyección de dependencia o los singletons son imposibles o darán como resultado un diseño de menor calidad y una mayor extensibilidad y mantenimiento?

Arseni Mourzenko
fuente
en Groovy ? "Si la clase Groovy que está probando realiza llamadas a un método estático en otra clase Groovy, entonces podría usar ExpandoMetaClass, que le permite agregar dinámicamente métodos, constructores, propiedades y métodos estáticos ..."
gnat
Esto no responde a su pregunta (por eso es un comentario) y probablemente será una opinión impopular, pero creo que está buscando un nivel de modularidad que Java / C # simplemente no puede proporcionar (sin saltar a través de más aros que un animal de circo). Ya ha identificado que un método estático es una dependencia codificada y, en general, cualquier clase es una dependencia codificada.
Doval
1
Por lo tanto, podría poner todo detrás de una interfaz y tratarlos como módulos, pero luego necesitará adaptadores para convertir de una interfaz a otra cuando ingrese código externo o cuando desee unir diferentes funciones en una interfaz o seleccionar un subconjunto de las funciones de una interfaz. Y pierde la capacidad de tener métodos binarios útiles (al menos sin hacer trampa instanceof) porque en dos casos IFoono hay garantía de que estén respaldados por la misma clase.
Doval
2
@Magus Eso o tirar los brazos hacia arriba, por ejemplo YAGNI, y acepta que si alguna vez necesita un tipo diferente de cadena, sólo obtendrá su IDE para cambiar todos los casos de com.lang.lib.Stringque com.someones.elses.Stringen toda la base de código.
Doval

Respuestas:

20

y si; y si; ¿y si?

YAGNI

Seriamente. Si alguien quiere usar diferentes implementaciones de Fileo Matho Consoledespués pueden pasar por el dolor de la abstracción que de distancia. ¿Has visto / usado Java?!? Las bibliotecas estándar de Java son un buen ejemplo de lo que sucede cuando abstrae cosas por el bien de la abstracción. ¿ Realmente necesito seguir 3 pasos para construir mi objeto Calendario ?

Dicho todo esto, no me voy a sentar aquí y defender fuertemente las clases estáticas. El estado estático es completamente malo (incluso si todavía lo uso ocasionalmente para agrupar / almacenar cosas en caché). Las funciones estáticas pueden ser malas debido a problemas de concurrencia y capacidad de prueba.

Pero las funciones estáticas puras no son tan terribles. No son cosas elementales que no tienen sentido para salir abstracta ya que no hay alternativa cuerda para la operación. No son puestas en práctica de una estrategia que puede ser estática, porque ya están abstraídos por medio del delegado / funtor. Y a veces estas funciones no tienen una clase de instancia con la que asociarse, por lo que las clases estáticas están bien para ayudar a organizarlas.

Además, los métodos de extensión son de uso práctico , incluso si no son clases filosóficamente estáticas.

Telastyn
fuente
1
Parte de la pregunta parece haber sido la queja de que si bien hay personas que dicen que hay usos válidos para las clases estáticas, no respaldan esa declaración con ejemplos. Esta respuesta podría mejorarse si lo hiciera.
Magus
@Magus: los 3 ejemplos dados en la pregunta son usos perfectamente buenos, aunque el OP argumenta lo contrario.
Telastyn
No creo que sea una mala respuesta, solo que podría haber margen de mejora. Si los ejemplos en el OP son los únicos, está bien. Tendría sentido decirlo en la respuesta, en aras de futuras búsquedas. De cualquier manera, tiene mi voto a favor.
Magus
1
Deseo que los marcos brinden un soporte algo mejor para el estado estático de subprocesos, en lugar de verlo como una ocurrencia tardía. Algo así Consolerealmente no debería ser parte del estado de todo el sistema, pero tampoco debería ser parte de un objeto que se pasa constantemente. En cambio, parece que tendría más sentido decir que cada subproceso tiene una propiedad de "consola actual" que se puede guardar y restaurar fácilmente si es necesario tener una rutina que escriba para Consoleusar otra cosa.
supercat
1
@BenjaminHodgson: el diseño rara vez es cuantitativo. De todos modos, los métodos de extensión podrían implementarse de manera diferente (piense en la manipulación del prototipo de JavaScript) y los usuarios no serían los más sabios. Que sean funciones estáticas es un detalle de implementación, no una parte intrínseca de su naturaleza.
Telastyn
18

En una palabra, simplicidad.

Cuando desacoplas demasiado, obtienes el hola mundo del infierno.

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();
}

Pero bueno, al menos no hay una configuración XML, lo cual es un buen toque.

La clase estática permite optimizar para el caso rápido y común. La mayoría de los puntos que mencionó se pueden resolver fácilmente introduciendo su propia abstracción sobre ellos o utilizando cosas como Console.Out.

Laurent Bourgault-Roy
fuente
55
Esto me recuerda a GNU Hello World que explica varias preocupaciones de localización.
Telastyn
1
-1 Por falta de configuración XML. +2 Por ponerlo en pocas palabras.
s1lv3r
1
Hello World realmente debería generalizarse para permitir escenarios supraglobal. Quiero decir, podría estar saludando a otro mundo, después de todo. Finalmente. Tendríamos que determinar primero alguna forma común de comunicación ... Primes en binario, enviados por la señal de onda portadora de AM ... pero ¿qué frecuencia? Lo sé, el modo fundamental de los átomos de silicio!
10

El caso de los métodos estáticos: Esto:

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

es mucho más simple, claro y legible que esto:

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

Ahora agregue la inyección de dependencia para ocultar las instancias explícitas, y vea que la línea única se convierte en decenas o cientos de líneas, todo para respaldar el escenario poco probable de que invente su propia Maximplementación, que es significativamente más rápida que la del marco y necesita para poder cambiar de forma transparente entre las Maximplementaciones alternativas .

El diseño API es una compensación entre simplicidad y flexibilidad. Puede siempre introducir capas adicionales de indirección ( hasta el infinito ), pero hay que sopesar la conveniencia de la causa común contra el beneficio de capas adicionales.

Por ejemplo, siempre puede escribir su propio contenedor de instancias alrededor de la API del sistema de archivos si necesita poder cambiar de forma transparente entre proveedores de almacenamiento. Pero sin los métodos estáticos más simples, todos se verían obligados a crear una instancia cada vez que tuvieran que hacer algo tan simple como guardar un archivo. Sería realmente una mala compensación de diseño optimizar para un caso extremo extremadamente raro, y hacer que todo sea más complicado para el caso común. Lo mismo con Console: puede crear fácilmente su propio contenedor si necesita abstraer o burlarse de la consola, pero a menudo la usa para soluciones rápidas o para depurar, por lo que solo desea la forma más simple posible de generar texto.

Además, si bien "una interfaz común con múltiples implementaciones" es genial, no necesariamente hay una abstracción "correcta" que unifique a todos los proveedores de almacenamiento que desee. Es posible que desee cambiar entre archivos y almacenamiento aislado, pero alguien más puede querer cambiar entre archivos, bases de datos y cookies para el almacenamiento. La interfaz común se vería diferente, y es imposible predecir todas las posibles abstracciones que los usuarios puedan desear. Es mucho más flexible proporcionar métodos estáticos como los "primitivos", y luego permitir a los usuarios construir sus propias abstracciones y envoltorios.

Tu Mathejemplo es aún más dudoso. Si desea cambiar a matemática de precisión arbitraria, presumiblemente necesitará nuevos tipos numéricos. Esto sería un cambio fundamental de que todo el programa, y tener que cambiar Math.Max()a ArbitraryPrecisionMath.Max()es sin duda el más pequeño de que las preocupaciones.

Dicho esto, estoy de acuerdo en que las clases estáticas con estado son en la mayoría de los casos malvadas.

JacquesB
fuente
0

En general, los únicos lugares donde usaría clases estáticas son aquellos donde la clase aloja funciones puras que no cruzan los límites del sistema. Me doy cuenta de que esto último es redundante, ya que cualquier cosa que cruza un límite probablemente no sea pura por intención. Llego a esta conclusión tanto por la desconfianza del estado global como por la facilidad de probar la perspectiva. Incluso cuando hay una expectativa razonable de que habrá una implementación única para una clase que cruza un límite del sistema (red, sistema de archivos, dispositivo de E / S), elijo usar instancias en lugar de clases estáticas debido a la capacidad de proporcionar un simulacro / la implementación falsa en pruebas unitarias es crítica en el desarrollo de pruebas unitarias rápidas sin acoplamiento a dispositivos reales. En los casos en que el método es una función pura sin acoplamiento a un sistema / dispositivo externo, creo que una clase estática puede ser apropiada, pero solo si no obstaculiza la capacidad de prueba. Encuentro que los casos de uso son relativamente raros fuera de las clases de framework existentes. Además, puede haber casos en los que no tenga otra opción, por ejemplo, métodos de extensión en C #.

tvanfosson
fuente