Las clases como code smell

Parece que la moda hoy en día es criticar la programación orientada a objetos y glosar las bondades de la programación funcional. Desde esa perspectiva, éste puede parecer un post partidista, pero no es esa mi intención. En serio. De hecho, muchas de las ideas que voy a explicar en el post ni siquiera se pueden considerar programación funcional, sino en todo caso programación procedural de toda la vida.

En realidad esto viene por la típica conversación en twitter en la que el límite de los 140 caracteres hace difícil explicar una idea:

Cada clase que creas en TypeScript es una oportunidad perdida de simplificar el código. Son un code smell que hay que justificar, no la opción por defecto.

Aunque originalmente estuviera pensando en TypeScript, diría que muchos de los argumentos son aplicables a cualquier lenguaje en el que no sea necesario encapsular todo en clases.

Obviamente, si estás utilizando un lenguaje como C# o Java, donde todo debe estar definido necesariamente dentro de una clase, no hay más que hablar. Podrás utilizar clases más o menos degeneradas (llegando al extremo de clases puramente estáticas), pero no podrás salir de ahí.

Sin embargo, si utilizas un lenguaje como Javascript, Python, TypeScript o C++, dispones de más opciones a la hora de organizar el código, y puedes crear clases, pero también puedes crear funciones sueltas, y puedes agrupar esas clases y esas funciones en módulos, que a su vez pueden tener estado propio… en fin, un mundo de posibilidades (para bien y para mal).

Dependiendo del lenguaje hay más o menos tendencia hacia el estilo «clasista», es decir, definir todo dentro de clases, o «modular», esto es, crear módulos que agrupan funciones. En el caso de Javascript hay una gran variedad de estilos (quizá con una ligera inclinación al modular hasta hace no mucho tiempo), pero en TypeScript se nota mucho la ascendencia javera y nettera de muchos de sus usuarios y la balanza se inclina hacia el estilo clasista. Demasiado, a mi parecer.

Pero, ¿por qué veo las clases como un code smell en este tipo de lenguajes? Por una cuestión de complejidad. Para mi uno de los factores más importantes a la hora de escribir código es intentar conseguir los objetivos habituales (calidad, mantenibilidad, etc.) de la forma más simple posible, y para ello nada mejor que utilizar los conceptos más simples que pueda.

Igual que no empiezo una aplicación intentando encajar todos los patrones de diseño que conozco y todas las librerías que he visto en Reddit, trato de no introducir complejidad accidental. Cuanto más simples sean los conceptos que estoy utilizando, más fácil será entenderlos y más sencillo será reutilizarlos en distintos contextos. Para ello, suelo intentar partir de las cosas más simples e ir escalando según lo necesito.

Valores y funciones

Lo más fácil que puedes definir, claro, es un valor constante. Si puedes modelar algo como un valor, estupendo porque no habrá mucho que razonar. Sólo necesitas conocer el valor y ya está.

Puede parecer una tontería, pero muchas veces nos dejamos llevar por el síndrome de la abstracción prematura, y lo que podría ser un simple valor, se convierte en una configuración externa que debe ser almacenada en una base de datos o un fichero de texto, y acaba introduciendo una complejidad innecesaria.

Desgraciadamente, sólo con valores constantes no puedes llegar demasiado lejos y siempre necesitamos realizar algún tipo de cálculo o proceso, por lo que el siguiente paso sería tener funciones.

Las funciones son simples. Si son puras, sin efectos colaterales, es especialmente fácil razonar con ellas, pero incluso con funciones impuras puedes generar código bastante simple y comprensible.

Cuando tienes un lenguaje en el que puedes tratar las funciones como valores y pasarlas como parámetro o devolverlas desde otras funciones, o puedes crear cierres lambda, son un mecanismo muy potente para modelar infinidad de escenarios, como veíamos al implementar un diccionario sólo con funciones.

Tener un código basado en funciones, en el que las dependencias de una función son otras funciones (y que pueden ser inyectadas como parámetros si lo necesitas), es llevar el principio de segregación de interfaces al extremo y favorece el principo de responsabilidad única. Por cierto, es curioso que dos de los principios SOLID, tan ligados a la OOP, se resuelvan mejor con funciones que con objetos.

Un «problema» de las funciones es que necesitan recibir como parámetro aquellos datos sobre los que van a trabajar, y eso puede acabar dando lugar a código repetitivo. Es uno de los casos en los que solemos estar tentados de introducir una clase, pero en realidad se puede resolver muy bien con aplicación parcial sin necesidad de introducir ningún concepto nuevo.

Por ejemplo, si tenemos que hacer peticiones a un servidor, puede parecer incómodo tener que estar pasando la ruta en cada petición y es tentador acaba con una clase como ésta para encapsular ese pequeño trozo de estado (la ruta):

class HttpClient {
  constructor(path) {
    this.path = path;
  }
  get(params) {
    // hacer una petición al path con params
    // y devolver una promesa con el resultado
  }
}

Eso parece más cómodo que tener una simple función get(path, params) y andar pasando el path cada vez, pero lo cierto es que si tuviéramos esa función y quisiéramos fijar path lo tendríamos fácil:

function get(path, params) {
  // hacer petición y devolver promesa
}

const getFrom = (path) => (params) => get(path, params);

const getCustomer = getFrom('/api/customer');

getCustomer({id: 10}).then(...);

Este tipo de técnicas nos permiten llegar muy lejos sin necesidad de introducir elementos más complejos, pero hay veces que se pueden quedar cortas.

Módulos con estado

Las funciones que veíamos en el punto anterior podemos agruparlas en módulos por una cuestión meramente organizativa y prácticamente no habrá cambiado nada, pero hay un momento en que los modulos sí añaden complejidad.

Cuando empezamos a tener varias funciones que dependen de un estado común, aunque podemos aplicar técnicas como la anterior para tratar el estado como parámetros de la función, puede llegar un punto en que sea demasiado incómodo o que necesitemos garantizar que realmente se está usando el mismo estado.

Pensad, por ejemplo, en la típica implementación de un log. Normalmente tendrás varias funciones, logInfo, logWarning, logError, que guardarán o no información en el log dependiendo del nivel de detalle configurado.

Este nivel de detalle podríamos pasarlo como parámetro a cada función, pero obliga a gestionar esa información desde fuera del log, y responsabiliza a sus clientes de saber cómo obtener el nivel actual del log y pasarlo de un sitio a otro. Si nos ponemos cabezotas podemos hacerlo igual que en el caso anterior, pero empieza a parecer menos práctico.

¿Necesitamos recurrir a una clase? Todavía no.

En realidad, nos basta con aprovechar el concepto de módulo que existe en Javascript. Podemos tener un módulo que exporte las funciones que necesitamos, y que encapsule (e incluso oculte, si queremos), el estado del que están dependiendo las funciones (en este ejemplo, el nivel de detalle del log).

Cualquiera que quiera usar las funciones podrá importar el módulo y utilizarlas, sin preocuparse de cómo están gestionando su estado interno.

Este escenario no está libre de problemas y pierde parte de la flexibilidad de utilizar funciones más simples.

Resulta más difícil razonar sobre el comportamiento de las funciones porque hay más acoplamiento entre las funciones y un estado que, posiblemente, ni siquiera es observable desde el mundo exterior.

Además, puede ser más complicado reemplazar las dependencias de los clientes que están usando el módulo, porque no es sencillo cambiar un módulo por otro. Existen cosas como proxyquire que se utilizan para testing, pero no es lo más agradable del mundo.

Para solventarlo siempre puedes hacer que las partes del código en las que necesitas más flexibilidad reciban como parámetro las funciones que van a utilizar, y pasarle las funciones del módulo o las que necesites. Vamos, inyección de dependencias en estado puro.

Si lo piensas un poco, esta forma de agrupar funciones en módulos encapsulando parte de estado se parece mucho a otro (¿anti?)patrón de diseño en programación orientada a objetos: el singleton.

Es verdad que tienen muy mala fama, pero al final hay un montón de cosas en una aplicación que son singletons, ya sean explícitos o camuflados en la configuración de contenedor de inversión de control. Echadle un ojo a cualquier framework MVC de moda de C# y Java y encontraréis infinidad de singletons, especialmente en toda la parte de «fontanería» de la aplicación.

Clases

¿En qué momento se hacen necesarias las clases? Cuando necesitamos parametrizar módulos.

Volvamos un momento al caso del log. Al introducir el módulo, evitábamos a todos los clientes del logger tener que andar pasado como parámetro el nivel de log y, sobre todo, tener que estar mantiendo ese valor y obteniéndolo de algún sitio.

¿Y si necesitamos disponer simultáneamente en la aplicación de varios loggers? Es posible que, dependiendo del area de la aplicación, tengamos distintos loggers con distintos niveles de detalle (o si el log fuese más complicado, con distintos destinos para escribir la información).

En ese punto, nuestro módulo se empieza a quedar corto. Ya no hay una sóla configuración para las funciones de log, sino que queremos mantener en paralelo varias configuraciones, y queremos los clientes puedan utilizar una u otra de forma transparente para ellos, tan sólo accediendo al log adecuado.

Ahora sí tiene sentido introducir una clase. Una clase no deja de ser una forma de parametrizar esos módulos que describíamos antes, permitiéndonos crear distintas instancias del módulo, cada una de ellas con su propio estado. Hemos pasado de tener un singleton en la aplicación, a tener distintas instancias de ese componente cada una con su propia configuración.

Las clases solucionan en parte el problema que veíamos en los módulos por la dificultad de reemplazarlos. La dependencia no es entre el cliente y la clase sino entre el cliente y la instancia, por lo que si podemos pasar otra instancia que se comporte como queramos, habremos resuelto el problema.

En lenguajes como C# o Java esto suele requerir un interface o una clase derivada, pero en Javascript con su duck typing o en TypeScript con su tipado estructural, es trivial hacer el reemplazo y de hecho muchas veces ni siquiera es necesario crear un clase explícitamente y basta con crear un objeto anónimo con la estructura adecuada.

A cambio, introducen otro tipo de complejidad, ya que nos obliga a instanciarlas, y eso implica controlar de alguna forma el ciclo de vida de los objetos y el flujo de referencias de un sitio a otro.

En el caso de log, cuando sólo teníamos un módulo, cualquiera podía acceder a él y sabía que tendría la configuración adecuada. Al introducir una clase, si quiero logear algo, ¿de dónde saco la instancia? ¿Creo una nueva? ¿Qué configuración le pongo? ¿Cómo garantizo que todos los puntos de la aplicación que tienen que usar un mismo logger lo están usando, y no creando cada uno el suyo con distintos parámetros?

Toda esta flexibilidad que ganamos al poder parametrizar módulos, implica una mayor carga cognitiva a la hora de utilizarlos, por lo que hay que tenerlo en cuenta.

Conclusión

Un code smell es sólamente eso: código que huele raro. No quiere decir que esté mal, pero quiere decir que es código que necesita una justificación.

Igual que puedo resistir el olor de las clases grandes o acabar con clases degeneradas que sólo contienen métodos estáticos, no tengo ningún problema en utilizar clases en lenguajes como Javascript o TypeScript, pero tienen ser justificables.

Cuando el lenguaje lo permite, prefiero empezar por implementar las cosas de la forma más sencilla posible, e ir escalando en función de las necesidades que van surgiendo, en lugar de utilizar directamente las construcciones más complejas que tengo disponibles.

2 comentarios en “Las clases como code smell

  1. Cual seria el reemplazo o el equivalente en programación funcional a la necesidad de utilizar clases o ? Por ejemplo, en lenguajes como Clojure o Haskell ,que ofrecen poco o nulo soporte al concepto de clases.
    Si se quiere llevar el paradigma funcional hasta sus limites, sin recurrir a OOP, ¿Es posible lograr algo como estos «módulos parametrizados» ? o ¿ En ciertos escenarios la programación funcional es inferior a la OOP ? En algunos blogs en visto opiniones que expresan que la PF no permite llegar tan lejos como con la OOP ¿Sera esto cierto ? Estos temas me generan mucha inquietud e interés, por lo cual estaré muy atento a lo que usted piensa sobre el tema.

  2. Edwin, en la programación funcional «pura» no existen las clases, pero si existen tipos de datos, incluso en lenguajes como Haskell se podría decir que los tipos de datos son incluso mas potentes que en la POO, solo que tienes que pensar en hacer las cosas de forma distinta a en la POO, por ejemplo, dejar de pensar en clases como tal y empezar a pensar en funciones (y otros conceptos propios de la PF). La PF permite llegar a donde sea, la única limitante es que tan bien te manejes con sus conceptos.

Comentarios cerrados.