Mitos y leyendas sobre métodos estáticos

El Aquellarre

En los últimos años he notado que cada vez utilizo más los métodos estáticos cuando desarrollo en C# (seguramente en su primo Java me pasaría lo mismo). No es que los busque de forma intencionada, sencillamente hay muchas ocasiones en las que me resulta la opción más natural.

Esto no sería tan extraño si no fuese porque durante mucho tiempo viví bajo la impresión (bastante generalizada) de que los métodos estáticos son evil. No es ni la primera ni la última vez que evoluciona mi opinión con respecto a algo, y siempre está bien analizar los motivos.

Cuándo utilizo métodos estáticos

Ya he dicho que no es algo que busque explícitamente, y de hecho en mi código conviven sin problemas partes en las que se utilizan más métodos estático, dando lugar a un código más funcional (o procedural), con partes mucho más orientadas a objetos.

Lo único que me lleva a decidir entre un estilo u otro es la comodidad a la hora de implementar (y testear, y mantener, etc.) una funcionalidad. Hay cosas que (creo) se modelan mejor desde un punto de vista de objetos, y otras desde un punto de vista de funciones.

Porque eso es lo que son los métodos estáticos: funciones en un lenguaje en el que no tienes más remedio que empaquetar todo en clases.

Hace diez años, Steve Yegge escribió un excelente post sobre verbos y sustantivos en el que se trataba este problema.

Cuando utilizamos un lenguaje como C# o Java en el que todo tiene que estar necesariamente dentro de una clase, acabamos diseñando a partir de sustantivos. Cada clase es un sustantivo. Tienes un Customer. Y un Order. Y un OrderProcessor, que sólo tiene un método, Process. Hay cosas que encajan muy bien en la filosofía de nombres, como Customer/ y Order, y otras que chirrían más, como el OrderProcessor.

Ante esto podemos mantenernos «dentro de los cánones establecidos por el lenguaje y la OOP», o podemos revelarnos (muy poquito, tampoco nos engañemos) y asumir que parte de nuestro código es más sencillo y más bonito si lo modelamos directamente como funciones que, al final, se traducen en método estáticos.

En general, trato de evitar al máximo las clases que acaban en Manager, Service, Provider, Formatter, etc. Si puedo, aplico alguna de estas técnicas para pasar lógica hacia objetos con más peso, o intento convertirlos en método estáticos.

Por ejemplo, antes que tener esto:

public class OrderProcessor {
  public void ProcessAll(Order[] orders) {
    foreach (var order in orders)
      order.Process();
    DomainEvents.Raise(new OrderBatchProcessed(orders));
  }
}

Prefiero tener esto:

public class Order {
  // Todas las cosas que lleve un order

  public static void ProcessAll(Order[] orders) {
    foreach (var order in orders)
      order.Process();
    DomainEvents.Raise(new OrderBatchProcessed(orders));
  }
}

Si la clase Order llegase a crecer mucho, hay formas de tratar con clases grandes que también tienen sus ventajas.

Hay muchos escenarios en los que resulta cómodo recurrir a métodos estáticos.

En cuanto te alejas un poco del «dominio» (en el sentido más amplio de la palabra) de tu aplicación, y empiezas a necesitas funciones para convertir de un tipo de objeto a otro. O para formatear un objeto para mostrarlo en pantalla. O para serializarlo a XML o JSON de una manera específica.

Todo este tipo de operaciones suelen realizarse sin dependencias externas y son perfectos candidatos para implementar como métodos estáticos en lugar de colgarlos de alguna clase arbitraria solo para sentirte más OOP Master.

También suelo utilizarlos como fachadas para tratar con estado global. Es cierto que intento minimizar el estado global, pero a veces no queda más remedio que asumir la realidad, y tener una fachada estática para lanzar eventos de dominio, traducir recursos y cosas por el estilo ayuda a lidiar con él.

Cazando mitos

Muchos de los que piensan que los métodos estáticos son terribles acaban por crear objetos sin estado ni dependencias que contienen el método estático. Eso obliga a instanciar una clase para invocar un método que no depende para nada del resto de la clase.

¿Quién no ha visto este código en las típicas katas de TDD?

public class StringCalculator {
  public int Add(string numbers) {
    // Hacer algo con numbers 
  }
}

¿Por qué crear un clase como esa para tener dentro una función cuya salida depende únicamente de los parámetros de entrada?

Entre los argumentos que se usan para defender ese estilo está el de «las clases y métodos estáticos no son relamente orientación a objetos».

Francamente, hoy en día no veo eso como un argumento válido. Para mi esto no va de poder aplicar al código una etiqueta de «100% OOP Certified» (o, para el caso, «100% FP Certified»), sino de sacarle partido a las herramientas que tenemos. Entiendo el argumento en un contexto didáctico, donde estás intentando aplicar al máximo un paradigma, pero en un entorno real hay que ser más pragmático.

También hay quien piensa que los métodos estáticos son más difíciles de testear, pero realmente no es así. De hecho, en el ejemplo anterior es (marginalmente) más sencillo testear StringCalculator.Add que new StringCalculator().Add y hay escenarios en los que la introducción de un método estático construido de la forma adecuada puede simplificar mucho la forma de testear, como explicaba en este post sobre cómo refactorizar tests unitarios para eliminar dependencias. Esa es una técnica que utilizo muy frecuentemente y me gusta mucho para aislar la parte que quiero testear de lo que no considero interesante. La clave para que algo sea fácil de testear no es si es estático o no, sino el tipo de dependencias que tiene y su relación con ellas.

En realidad, cuanto se habla de que los métodos estáticos son complicados de testear no es por el propio método estático: es por cómo testear aquello que depende del método estático. Normalmente cuando pasa es porque quieres (o necesitas) aislarte de la implementación real del método estático.

Si es porque consideras que los tests unitarios sólo pueden abarcar una clase y hay que aislarlos de todas sus dependencias, te invito a que reconsideres tu postura sobre qué constituye la unidad en un test unitario.

Si aun así no quieres invocar el método estático porque accede a recursos externos o es más lento, siempre tienes varias alternativas. Llegado el caso, podrías encapsular el método estático en una clase y usar esa clase como dependencia de lo que estás testeando. Yo no haría esto de forma especulativa, pero si fuera necesario refactorizar hacia ello es fácil. De todas formas, la solución más sencilla es inyectar el método estático como dependencia de lo que necesites testear.

Puedes pasar de esto:

public void SomeBigProcess() {
  var numbers = numberProvider.GetNumbers();
  var result = StringCalculator.Add(numbers);
  var mailBody = $"Result is {result}";
  mailSender.Send(mailBody);
}

A esto:

public void SomeBigProcess() {
  SomeBigProcess(StringCalculator.Add);
}

public void SomeBigProcess(Func<string, int> add) {
	var numbers = numberProvider.GetNumbers();
	var result = add(numbers);
	var mailBody = $"Result is {result}";
	mailSender.Send(mailBody);
}

Ahora tienes un método al que le puedes inyectar el método estático y testearlo como más te guste. Incluso en este caso es algo más fácil de testear que teniendo un StringCalculator porque en lugar de necesitar un mock/fake/stub, puedes crearte al vuelo al función que vas a usar en el test.

Algunos incluso podrían decir que no usar un método estático es un problema de eficiencia porque obliga a instanciar un objeto StringCalculator para nada y eso mete presión al GC. Vale, puede ser, pero no es mi caso. Es sumamente raro que tenga un código en el que me preocupe tanto el consumo de memoria y las pausas del GC como para que eso sea relavante.

Otro mito que arrastran los métodos estáticos es que no son seguros en un entorno multihebra.

Esta confusión puede venir motivada por el uso de variables estáticas. En un entorno multihebra, tener estado mutable estático es una receta segura para dolores de cabeza. Tenemos un punto global, del que dependen todas las hebras, que puede ser modificado por cualquiera y hace que las hebras queden acopladas entre si temporalmente (dependiendo de en qué orden se ejecuten y modifiquen el estado global, el resultado será uno u otro).

Sin embargo, cuando hablamos de métodos, este problema no existe. A menos que accedan a variables estáticas pero, claro, en ese caso da igual que el método lo sea o no: el problema es la variable, como veíamos antes.

Un método estático que depende sólo de sus parámetros de entrada es completamente seguro en un entorno multihebra. En realidad, es lo más seguro que puede haber porque no accede a estado global y ni siquiera accede a estado local a la instancia de la clase, puesto que no pertenece a ninguna instancia.

El problema es que, mal utilizados, los métodos estáticos suelen ir acompañados de variables estáticas y acaban dando lugar a situaciones un tanto desastrosas. Bueno, en realidad, mal utilizado, casi todo puede dar lugar a situaciones desastrosas.

Conclusión

Mezlar métodos estáticos (funciones) y clases no debería ser algo tan raro. A fin de cuentas, los que hayan programado en Javascript o Python estan acostumbrados a ello y seguro que han podido comprobar las ventajas de disponer de más herramientas a la hora de modelar la solución a un problema.

Parte de los problemas que se han atribuido tradicionalmente a los métodos estáticos son perfectamente salvable, y otros directamente no son problemas.

En mi opinión, parte del recelo hacia ellos viene de la época en la que la OOP no estaba tan extendida, había mucha gente que venía de programacíon estructurada clásica (C, Visual Basic), y se no se aprovechaban las características de los lenguajes orientados a objetos: seguían programando todo con funciones y estructuras de datos. No es que eso sea malo, pero cuando llevabas eso a un lenguaje que no estaba pensado para ello, y además lo hacía alguien sin experiencia, el resultado era catastrófico (todos hemos visto código así).

A día de hoy me gustaría pensar que no es tan necesario estar con el palo encima de la gente y que tenemos criterio suficiente para poder usar las herramientas que tenemos a nuestra disposición y sacarles partido. Tal vez estemos pegando a los monos equivocados.

8 comentarios en “Mitos y leyendas sobre métodos estáticos

  1. Estando en esencia de acuerdo contigo en no evitar religiosamente los metodos estaticos, que pueden ser utiles en muchos casos, tampoco creo que haya que olvidar que en su dia se les vio una serie de problemas que se intentaron solucionar, tal vez de manera erronea, por la oop.
    Los metodos estaticos te obligan a ser explicito, ya que toda la informacion debe estar en los parametros. Ademas te obliga a separar los datos del codigo que debe tratarlos. Por otro lado el polimorfismo es muy limitado y esta cerrado (no se pueden crear interfaces abstractas sobre ellos). Si todo esto no supone un problema puedes usarlos pero, en general, son mas rigidos y soportan peor las necesidades de modificaciones y extensiones. Sobre todo en cuanto a su uso por el codigo cliente ya que dejar todo explicito le obliga a saber mas «cosas» del codigo que usa, cosas que tal vez cambien en una forma que no deberia «enterarse». Vamos que el codigo que usa metodos estaticos suele estar mas acoplado de lo que puede estar usando otros elementos.
    Pero es verdad que en muchos casos esa complicacion y ese desacople no son necesarios.

  2. En cuanto al estado, la encapsulacion te permite tener estado local mutable (por temas de rendimiento o comodidad) y exponer un api inmutable al exterior, por lo que a todos los efectos, si tienes cuidado con la concurrencia, es como si tuvieras una estructura de datos inmutable (tipo lo que hace clojure con http://clojure.org/reference/transients).
    Si estudiamos un lenguaje como haskell, en el que todos son funciones y los datos estan separados del codigo vemos que:
    1.- Necesitaron de un mecanismo de polimorfismo estatico extensible (type classes)
    2.- Se recomienda que no se exporten los constructores de datos y se usen «constructores inteligentes», o sea, que se encapsule en el modulo los datos y el codigo y se impida el uso de los datos independiente.
    3.- Aun asi se ve que hay modulos del estilo Data.Text.Internal.Encoding.Utf16
    Data.Text.Internal.Encoding.Utf32
    https://hackage.haskell.org/package/text
    marcando por convencion que son modulos que no deberian ser usados directamente

  3. Estoy de acuerdo en lo que comentas, y por eso decía que hay partes que me resulta más natural modelar con un estilo más «class based OOP», y otras con un estilo más funcional (o incluso procedural).

    El tema de la explicitud es siempre un arma de doble filo. Es verdad que provoca un acoplamiento mayor, pero también es cierto que hay veces que facilita la comprensión y el testeo.

    Con el estado mutable, si lo encapsulas en un objeto (o en un módulo) acotas el número de puntos desde el que se puede mutar y se simplifica la gestión, pero sigues teniendo ahí una parte complicada. Has metido el polvo debajo de la alfombra, pero sigue habiendo polvo y hay que tener cuidado por si se sale :-)

  4. Creo que diseñar el código orientado a objetos no tiene nada de malo , al igual que diseñar código funcional, la realidad es que se desarrolla simplemente para seguir un patron, aunque lo que esta debajo de los patrones son los principios, es decir , principios –> patrones –> frameworks, en este caso nos quedamos con los patrones y los principios, solo hay que sacarle el jugo a los 2 estilos de programación y tener la justificación de uno y otro.

    No se que tan malo sea, aveces yo meto metodos utilitarios, en su mayoria estaticos, en una clase que se llama Util y ya.

    Buen post xD

  5. Utilizo en exceso métodos estáticos, clases estáticas para fachada BLL que l uego llama a DAL. Desde el webforms se llama a esa fachada.

    Innumerables clases (static) Helper: Session, Cookies, Cache, Serialization, … y muchas más de insfraestructura

  6. Cambiando un poco de tema. También ando pasándome a Webpack y ya lo tengo bastante encaminado. La mejor documentación que he encontrado (con mucha diferencia) es esta:

    https://survivejs.com/webpack/introduction/

    A destacar especialmente la configuración que ofrece para separar el fichero «webpack.config.js»:

    http://survivejs.com/webpack/developing-with-webpack/splitting-configuration/

    También es muy interesante el HMR (Hot Module Replacement). Esto de refrescar partes de la página sin recargarla entera es gloria bendita.

    Reconozco que era bastante reacio a adoptar Webpack pero ofrece una potencia enorme. Es mucho más que un generador de «bundles», se puede calificar perfectamente como «task runner».

  7. Donde estén los helpers (estáticos) que se quite todo los demás…

    public class A {}
    public static class AHelper {
    public static string ToStr(this A src) {
    return (string)src;
    }
    }

Comentarios cerrados.