Pequeñas funciones consideradas dañinas

En esta publicación, mi objetivo es:

- Arroja luz sobre algunos de los supuestos beneficios de las funciones pequeñas
 - Explique por qué personalmente creo que algunos de los beneficios realmente no funcionan tan bien como se anuncian.
 - Explique por qué las funciones pequeñas pueden resultar contraproducentes a veces.
 - Explicar los momentos en que creo que las funciones más pequeñas realmente brillan

Los consejos generales de programación distribuidos invariablemente parecen ensalzar la elegancia y la eficacia de las funciones pequeñas. El libro Clean Code, a menudo considerado por muchos como una biblia de programación, tiene un capítulo dedicado solo a las funciones, y el capítulo comienza con un ejemplo de una función verdaderamente terrible que también es larga. El libro continúa culpando a la duración de la función como su delito más grave, afirmando que:

No solo es (la función) larga, sino que tiene código duplicado, muchas cadenas extrañas y muchos tipos de datos y API extraños e inadvertidos. ¿Entiendes la función después de tres minutos de estudio? Probablemente no. Están pasando muchas cosas allí dentro de muchos niveles diferentes de abstracción. Hay cadenas extrañas y llamadas a funciones extrañas mezcladas con sentencias if doblemente anidadas controladas por banderas.

El capítulo reflexiona brevemente sobre las cualidades que harán que el código sea "fácil de leer y comprender" y "permita que un lector casual intuya el tipo de programa en el que vive", antes de declarar que hacer que la función sea más pequeña necesariamente logrará este propósito.

La primera regla de funciones es que deben ser pequeñas. La segunda regla de funciones es que deberían ser más pequeñas que eso.

La idea de que las funciones deberían ser pequeñas es algo que casi se considera demasiado sacrosanto como para cuestionarlo. A menudo se trota durante las revisiones de código, en discusiones de Twitter, charlas en conferencias, libros y podcasts sobre programación, artículos sobre mejores prácticas para refactorizar código, etc. Esta idea apareció nuevamente en mi línea de tiempo hace unos días en forma de este tweet:

Fowler, en su tweet, enlaza a su artículo sobre la duración de la función, donde continúa diciendo que:

Si tiene que hacer un esfuerzo para mirar un fragmento de código para descubrir lo que está haciendo, entonces debe extraerlo en una función y nombrar la función después de ese "qué".
Una vez que acepté este principio, desarrollé el hábito de escribir funciones muy pequeñas, generalmente de unas pocas líneas de largo [2]. Cualquier función de más de media docena de líneas de código comienza a olerme, y no es inusual que tenga funciones que son una sola línea de código [3].

Las virtudes de las funciones pequeñas se evangelizan con tanta frecuencia que hoy llegó a mi línea de tiempo nuevamente en forma de este tweet:

Algunas personas parecen estar tan enamoradas de pequeñas funciones que la idea de abstraer todas y cada una de las lógicas que puedan parecer incluso nominalmente complejas en una función separada es algo que se defiende apasionadamente.

He trabajado en bases de código heredadas de personas que habían internalizado esta idea hasta tal punto que el resultado final fue bastante infernal y completamente antitético a todas las buenas intenciones con las que se allanó el camino. En esta publicación, espero explicar por qué algunos de los beneficios tan promocionados no siempre resultan como uno espera y los momentos en que algunas de las ideas pueden resultar contraproducentes.

Supuestos beneficios de funciones más pequeñas

Por lo general, se explican varias razones para demostrar el mérito de las funciones más pequeñas.

Haz una cosa

La idea es simple: una función solo debe hacer una cosa y hacerlo bien. A primera vista, esto suena como una idea extremadamente sólida, en sintonía, incluso, con la filosofía de Unix.

El momento en que esto se vuelve turbio es cuando esta "una cosa" necesita ser definida. La "única cosa" puede ser cualquier cosa, desde una simple declaración de retorno a una expresión condicional a una pieza de cálculo matemático a una llamada de red. Como sucede, muchas veces esta "una cosa" significa una abstracción de un solo nivel de alguna lógica (a menudo comercial).

Por ejemplo, en una aplicación web, una operación CRUD como "crear usuario" puede ser "una cosa". Por lo general, al menos, crear un usuario implica crear un registro en la base de datos (y manejar cualquier error concomitante). Además, crear un usuario también puede requerir enviar un correo electrónico de bienvenida. Además, uno también podría querer activar un evento personalizado para un agente de mensajes como Kafka para alimentar este evento en varios otros sistemas.

Por lo tanto, un "nivel único de abstracción" no es solo un nivel único. Lo que he visto suceder es que los programadores que han aceptado completamente la idea de que una función debe hacer "una cosa" tienden a tener dificultades para resistir el impulso de aplicar el mismo principio de forma recursiva a cada función o método que escriben.

Por lo tanto, en lugar de una abstracción razonablemente hermética que pueda entenderse (y probarse) como una sola unidad, ahora terminamos con unidades aún más pequeñas que se han tallado para delinear todos y cada uno de los componentes de "la única cosa" hasta que esté completamente modular y completamente SECO.

La falacia de DRY

SECO y una propensión a hacer que las funciones sean lo más pequeñas posible no son necesariamente lo mismo, pero he observado que lo último lleva muchas veces a lo primero. DRY, en mi opinión, es un buen principio rector, pero muchas veces, el pragmatismo y la razón se sacrifican en el altar de una adhesión dogmática a DRY, especialmente por los programadores de la persuasión de Rails.

Raymond Hettinger, un desarrollador principal de Python, tiene una charla fantástica llamada Beyond PEP8: Mejores prácticas para un código hermoso e inteligible. Esta charla es imprescindible, no solo para los programadores de Python, sino para cualquier persona interesada en programar o para quién se gana la vida, porque incisivamente deja al descubierto las falacias de una adhesión dogmática a PEP8, que es la guía de estilo de Python que muchas linters implementan . Que el enfoque de la charla esté en PEP8 no es tan importante como las ricas ideas que uno puede extraer de la charla, muchas de las cuales son independientes del lenguaje.

Incluso si no mira toda la charla, debe mirar este minuto de la charla que dibuja una analogía terriblemente precisa a la llamada de sirena de DRY. Los programadores que insisten en SECAR la mayor cantidad de código posible corren el riesgo de no ver el bosque por los árboles.

Mi principal problema con DRY es que obliga a uno a abstracciones, anidadas y prematuras. En la medida en que es imposible abstraer perfectamente, lo mejor que podemos hacer es abstraer lo suficientemente bien en la medida de lo posible. Definir "suficientemente bien" es difícil y depende de una gran cantidad de factores.

En el siguiente diagrama, la palabra "abstracción" se puede usar indistintamente con "función". Por ejemplo, si asumimos la mejor manera de diseñar la capa de abstracción A, es posible que debamos considerar lo siguiente:

- la naturaleza de los supuestos que sustentan la abstracción A y la probabilidad (y durante cuánto tiempo) de que retengan agua
 - la medida en que las capas de abstracciones subyacentes a la abstracción A (abstracción X y abstracción Y), así como cualquier abstracción construida sobre la abstracción A (abstracción Z) son propensas a permanecer consistentes, flexibles, extensibles y correctas en su implementación y diseño
 - los requisitos y expectativas de cualquier abstracción futura (abstracción M) que pueda construirse sobre la abstracción A y cualquier abstracción que deba ser soportada debajo de A (abstracción N)

La abstracción A que desarrollamos inevitablemente estará sujeta a una reevaluación constante en el futuro y, con toda probabilidad, también a una invalidación parcial o incluso completa. La única característica general que nos ayudará en la modificación inevitable que se necesitaría es diseñar nuestra abstracción para que sea flexible.

SECAR el código en la mayor medida posible en este momento significaría privar a nuestro yo futuro de la flexibilidad para acomodar cualquier cambio que pueda ser necesario. Lo que realmente deberíamos optimizar es permitirnos suficiente margen para realizar los cambios inevitables que se requerirán tarde o temprano en lugar de optimizar el ajuste perfecto de inmediato.

La mejor abstracción es una abstracción que se optimiza para ser lo suficientemente buena, no perfecta. Esa es una característica, no un error. Comprender esta naturaleza muy destacada de las abstracciones es la clave para diseñar las buenas.

Alex Martelli, el inventor de la frase tipeo de pato y famoso Pythonista, tiene una famosa charla titulada The Tower Of Abstraction, y las diapositivas bien merecen una lectura.

La fabulosa Rubyist Sandi Metz tiene una famosa charla llamada All The Little Things, donde postula que "la duplicación es mucho más barata que la abstracción incorrecta" y, por lo tanto, "prefiere la duplicación sobre la abstracción incorrecta".

Las abstracciones, en mi opinión, nunca pueden ser completamente "correctas" o "incorrectas" ya que la línea que separa "correctas" de "incorrectas" es intrínsecamente borrosa y cambia constantemente. Nuestra abstracción "perfecta" artesanal cuidadosamente hecha a mano es solo un requisito comercial o informe de error lejos de ser enviado al estado de "incorrecto".

Creo que ayuda ver las abstracciones como un espectro como se muestra en el diagrama que vimos anteriormente en esta publicación. Un extremo de este espectro se optimiza para la precisión, donde cada último aspecto de nuestro código debe ser exactamente preciso. Esto ciertamente tiene una buena cantidad de beneficios, pero no sirve para diseñar buenas abstracciones, ya que se esfuerza por lograr una alineación perfecta. El otro extremo de este espectro optimiza la imprecisión y la falta de límites. Si bien esto permite una flexibilidad máxima, considero que este extremo es propenso a otros inconvenientes.

Como con la mayoría de las otras cosas, "el ideal" se encuentra en algún punto intermedio. No hay un medio feliz para todos. El "ideal" también varía dependiendo de una gran cantidad de factores, tanto programáticos como interpersonales, y el sello distintivo de una buena ingeniería es poder identificar en qué parte del espectro se encuentra este "ideal" para cualquier contexto dado, así como constantemente reevaluar y recalibrar este ideal. A veces, dado un contexto, realmente existe la mejor manera de hacer algo. Pero este contexto puede cambiar en cualquier momento y también esta "mejor manera".

El nombre del juego

Hablando de abstracción, una vez que se ha decidido qué abstraer y cómo, es importante darle un nombre.

Y nombrar cosas es difícil.

Se considera algo obvio en la programación que dar nombres más largos y descriptivos es algo bueno, tanto que algunos incluso abogan por reemplazar los comentarios en código con una función que lleve el nombre del comentario. La idea aquí es que cuanto más descriptivo sea un nombre, mejor será la encapsulación. No es raro encontrar bases de código llenas de nombres como LikeThisWhichMakesItVeryDifficultToRead.

Es probable que esto vuele en el mundo de Java, donde la verbosidad es la norma, pero nunca he encontrado un código con nombres tan largos y fáciles de leer. Lo que podría haber sido, digamos, 4–5 líneas de código ahora está guardado en una función que lleva un nombre extremadamente largo. Cuando leo el código, ver aparecer una palabra tan detallada de repente me detiene cuando intento procesar todas las sílabas diferentes en el nombre de esta función, trato de adaptarlo al modelo mental que he estado construyendo hasta ahora y luego decidir si investigar o no la función en mayor detalle saltando a su definición y leyendo la implementación.

Sin embargo, el problema con las "funciones pequeñas" es que la búsqueda de funciones pequeñas termina engendrando aún más funciones pequeñas, todas las cuales tienden a recibir nombres extremadamente detallados en el espíritu de hacer que el código se auto documente y evite los comentarios.

Como resultado, la sobrecarga cognitiva del procesamiento de los nombres detallados de funciones (y variables), mapeándolos en el modelo mental que he estado construyendo hasta ahora, decidiendo en qué funciones profundizar más y cuáles escanear, y uniendo el rompecabezas para Descubrir el "panorama general" se vuelve bastante difícil.

Las funciones más pequeñas hacen que el programador requiera escribir más funciones, lo que requiere que se les ocurran más nombres para esas funciones. Personalmente, encuentro palabras clave, construcciones y modismos ofrecidos por el lenguaje de programación mucho más fácil desde una perspectiva visual en comparación con mirar variables personalizadas o nombres de funciones. Por ejemplo, cuando leo un bloque if-else, rara vez paso algún ciclo mental procesando las palabras clave if o elseif, pero paso mi tiempo entendiendo el flujo lógico del programa.

Interrumpir mi flujo de razonamiento con aVeryVeryLongFuncNameAndArgList es una interrupción discordante. Esto es especialmente cierto cuando la función que se llama es en realidad una línea que se puede insertar fácilmente. Los cambios de contexto son caros, ya sean cambios de contexto de CPU o un programador que tiene que cambiar mentalmente el contexto mientras lee el código.

El otro problema con el exceso de funciones pequeñas, especialmente las que tienen nombres muy descriptivos y poco intuitivos, es que la base de código ahora es más difícil de buscar. Una función llamada createUser es fácil e intuitiva de usar, algo así como renderPageWithSetupsAndTeardowns (un nombre destacado como un ejemplo brillante en el libro Clean Code), por el contrario, no es el nombre más fácil de recordar o el más buscable. Muchos editores también realizan una búsqueda difusa de la base de código, por lo que tener demasiadas funciones con prefijos similares también es más probable que obtenga un número extraño de resultados durante la búsqueda, lo cual no es ideal.

Pérdida de localidad

Las funciones pequeñas funcionan mejor cuando no tenemos que saltar los límites de archivos o paquetes para encontrar la definición de una función. El libro Clean Code propone algo llamado The Stepdown Rule para este fin.

Queremos que el código se lea como una narrativa de arriba hacia abajo. Queremos que todas las funciones sean seguidas por aquellas en el siguiente nivel de abstracción para que podamos leer el programa, descendiendo un nivel de abstracción a la vez a medida que leemos la lista de funciones. Yo llamo a esto la regla de Stepdown.

Esto suena muy bien en teoría, pero rara vez lo he visto funcionar bien en la práctica. En cambio, lo que he visto casi invariablemente es la pérdida de localidad a medida que se agregan más funciones al código.

Supongamos que comenzamos con tres funciones, A, B y C, cada una de las cuales se llama (y ergo read) una tras otra. Nuestras abstracciones iniciales se basaron en ciertos supuestos, requisitos y advertencias, todos los cuales investigamos y razonamos asiduamente durante el tiempo del diseño inicial.

Muy pronto, digamos que tenemos un nuevo requisito emergente o un caso límite que no habíamos previsto o una nueva restricción que debemos atender. Necesitamos modificar la función A ya que "la única cosa" que encapsula ya no es válida (o nunca fue válida para empezar y ahora tenemos que rectificarla). De acuerdo con lo que hemos leído en Clean Code, decidimos que la mejor manera de lidiar con esto es, bueno, crear más funciones que oculten los nuevos requisitos desordenados que han surgido.

Un par de semanas después de haber realizado este cambio, si nuestros requisitos cambian una vez más, es posible que necesitemos crear aún más funciones para encapsular todos los cambios adicionales necesarios.

Enjuague y repita, y hemos llegado exactamente al problema que Sandi Metz describe en su publicación en The Wrong Abstraction. La publicación continúa diciendo que:

El código existente ejerce una poderosa influencia. Su misma presencia argumenta que es correcta y necesaria. Sabemos que el código representa un esfuerzo invertido, y estamos muy motivados para preservar el valor de este esfuerzo. Y, desafortunadamente, la triste verdad es que cuanto más complicado e incomprensible es el código, es decir, cuanto más profunda es la inversión para crearlo, más presión tenemos para retenerlo (la "falacia del costo hundido").

Si bien esto podría ser cierto cuando el mismo equipo que trabajó originalmente en la base de código continúa manteniéndolo, he visto lo contrario cuando nuevos programadores (o gerentes) toman posesión de la base de código. Lo que comenzó con buenas intenciones ahora se ha convertido en un código de espagueti que seguramente ya no está limpio, y ahora la necesidad de "refactorizar" o, a veces, incluso reescribir el código se vuelve aún más tentador.

Ahora se podría argumentar que, hasta cierto punto, esto es inevitable. Y tendrían razón. De lo que rara vez hablamos es de lo importante que es escribir código que tendrá una muerte elegante. He escrito en el pasado acerca de lo importante que es hacer que el código sea operacionalmente fácil de desmantelar, pero esto es aún más cierto cuando se trata de la base de código en sí.

Con demasiada frecuencia, los programadores piensan que el código es "inactivo" solo si se elimina o ya no se usa o si el servicio en sí está fuera de servicio. Si comenzamos a pensar en el código que escribimos como algo que muere cada vez que agregamos un nuevo git commit, creo que podríamos estar más incentivados para escribir código que sea fácil de modificar. Al pensar en cómo abstraer, es de gran ayuda ser consciente del hecho de que el código que estamos construyendo probablemente solo esté a unas pocas horas de morir (ser modificado). Por lo tanto, la optimización para facilitar la modificación del código tiende a funcionar mejor que tratar de construir narrativas descendentes del tipo propuesto en Clean Code.

Contaminación de clase

Las funciones más pequeñas también conducen a clases más grandes o simplemente a un mayor número de clases en lenguajes que admiten la programación orientada a objetos. En el caso de un lenguaje como Go, he visto que esta tendencia conduce a interfaces más grandes (combinadas con el doble golpe de contaminación de la interfaz) o una gran cantidad de paquetes pequeños.

Esto exacerba la sobrecarga cognitiva involucrada en el mapeo de la lógica de negocios a las abstracciones que hemos creado. Cuanto mayor sea el número de clases / interfaces / paquetes, más difícil será "asimilarlo todo" de una sola vez, lo que hace cero para justificar el costo de mantenimiento de estas diversas clases / interfaces / paquetes que hemos construido.

Menos argumentos

Los defensores de funciones más pequeñas también tienden casi siempre a defender que se pasen menos argumentos a la función.

El problema con menos argumentos de función es que uno corre el riesgo de no hacer explícitas las dependencias. En idiomas con herencia como Ruby, esto lleva a funciones que dependen de muchos estados globales y singletons. Definitivamente he visto clases de Ruby con 5-10 métodos pequeños, todos los cuales típicamente hacen algo muy trivial y toman quizás uno o dos parámetros como argumentos. También he visto que muchos de ellos mutan el estado global compartido o confían en los singletons que no se les pasan explícitamente, lo cual es un antipatrón si alguna vez hubo uno.

Además, cuando las dependencias no son explícitas, las pruebas se vuelven mucho más complicadas, con la sobrecarga de configurar y desmantelar el estado antes de que se pueda ejecutar cada prueba individual dirigida a nuestras funciones pequeñas.

Difícil de leer

Esto ya se ha dicho antes, pero vale la pena reiterarlo: una explosión de funciones pequeñas, especialmente funciones de una línea, hace que la base de código sea excesivamente difícil de leer. Esto perjudica especialmente a aquellos para quienes el código debería haber sido optimizado en primer lugar: los recién llegados.

Hay varios sabores de los recién llegados a una base de código. Usuarios nuevos en el lenguaje o marco en uso, usuarios nuevos en el dominio, usuarios nuevos en la organización o el equipo, y también en algunos casos, los autores originales de la base de código que vuelven a trabajar en ella después de un tiempo.

Una buena regla general, en mi experiencia, ha sido tener en cuenta a alguien que pueda verificar varias de las categorías de "nuevas" mencionadas anteriormente. Hacerlo me ayuda a reevaluar mis suposiciones y repensar los gastos generales que podría estar imponiendo inadvertidamente a alguien nuevo que leerá el código por primera vez. De lo que me he dado cuenta es que este enfoque en realidad conduce a un código mucho mejor y más simple de lo que podría haber sido posible de lo contrario.

El código simple no es necesariamente el código más fácil de escribir, y rara vez es el código SECO. Se necesita una gran cantidad de pensamiento cuidadoso, atención al detalle y cuidado para llegar a la solución más simple que sea correcta y fácil de razonar. Lo más llamativo de una simplicidad tan duramente ganada es que se presta a ser fácilmente comprendida por los programadores antiguos y nuevos, para todas las posibles definiciones de "antiguo" y "nuevo".

Cuando soy "nuevo" en una base de código, si tengo la suerte de saber el idioma y / o el marco que se está utilizando, el mayor desafío para mí es comprender la lógica comercial o los detalles de implementación. Cuando no soy tan afortunado y me enfrento a la desalentadora tarea de maniobrar a través de una base de código escrita en un idioma extraño para mí, el mayor desafío que enfrento es caminar por la cuerda floja entre comprender lo suficiente del lenguaje o el marco en para poder dar sentido a lo que está haciendo el código sin tener que atravesar una madriguera de conejo y al mismo tiempo poder aislar la "única cosa" de interés que necesito entender para hacer el progreso necesario para mover el código proyecto a la siguiente etapa.

Lo que realmente espero durante los momentos en que me aventuro en un territorio desconocido es hacer el menor número de saltos mentales y cambios de contexto mientras trato de encontrar la respuesta a una pregunta dada. Invertir tiempo y pensar en facilitar las cosas para el futuro mantenedor o consumidor del código es algo que tendrá una gran recompensa, especialmente para proyectos de código abierto. Esto es algo que desearía haber hecho mejor antes en mi carrera y es algo que tengo muy presente en estos días.

Módulos poco profundos y clasitis

Una de las ideas más poderosas propuestas en el libro A Philosophy of Software Design (que se publicó después de la publicación inicial de esta publicación) es la de los módulos profundos y poco profundos.

Los mejores módulos son aquellos que brindan una funcionalidad potente pero tienen interfaces simples. Utilizo el término profundo para describir tales módulos. La profundidad del módulo es una forma de pensar sobre el costo versus el beneficio. Los beneficios proporcionados por un módulo es su funcionalidad. El costo de un módulo (en términos de complejidad del sistema) es su interfaz. La interfaz de un módulo representa la complejidad que el módulo impone al resto del sistema: cuanto más pequeña y simple es la interfaz, menos complejidad introduce. Los mejores módulos son aquellos con el mayor beneficio y el menor costo.
Los mejores módulos son profundos: tienen mucha funcionalidad oculta detrás de una interfaz simple. Un módulo profundo es una buena abstracción porque solo una pequeña fracción de su complejidad interna es visible para sus usuarios. Un módulo superficial es aquel cuya interfaz es relativamente compleja en comparación con la funcionalidad que proporciona. No es más sencillo pensar en la interfaz que pensar en la implementación completa. Si el método se documenta correctamente, la documentación será más larga que el código del método.
Desafortunadamente, el valor de las clases profundas no es muy apreciado hoy en día. La sabiduría convencional en la programación es que las clases deben ser pequeñas, no profundas. A los estudiantes se les enseña que lo más importante en el diseño de la clase es dividir las clases más grandes en clases más pequeñas. A menudo se da el mismo consejo sobre los métodos: "Cualquier método más largo que N líneas debe dividirse en varios métodos" (N puede ser tan bajo como 10). Este enfoque da como resultado una gran cantidad de clases y métodos poco profundos, que se suman a la complejidad general del sistema.
El extremo del enfoque de "las clases deberían ser pequeñas" es un síndrome que llamo clasitis, que se deriva de la visión errónea de que "las clases son buenas, por lo que más clases son mejores". La clasitis puede dar lugar a clases que son individualmente simples, pero produce enorme complejidad de las interfaces acumuladas. También tiende a dar como resultado un estilo de programación detallado de todas las repeticiones para cada clase.

Un argumento en contra que he escuchado es que una clase con una gran cantidad de métodos pequeños puede ser profunda si la interfaz pública que ofrece solo se compone de un puñado de métodos. Si bien esto puede ser cierto, no invalida el hecho de que la clase sigue siendo bastante complicada para un recién llegado a la implementación.

¿Cuándo tienen sentido las funciones más pequeñas?

A fin de cuentas, creo que las pequeñas funciones tienen absolutamente su utilidad, especialmente cuando se trata de pruebas.

Red de E / S

Esta no es una publicación sobre cómo escribir mejor las pruebas funcionales, de integración y unitarias para una gran cantidad de servicios. Sin embargo, cuando se trata de pruebas unitarias, la forma en que se prueba la E / S de red es, bueno, no realmente probarla.

No soy un gran fanático de las burlas. Los simulacros tienen varias deficiencias. Por un lado, los simulacros son una simulación artificial de algún resultado. Los simulacros son tan buenos como nuestra imaginación y nuestra capacidad de predecir los diversos modos de falla que podría encontrar nuestra aplicación. También es muy probable que los simulacros se desincronicen del servicio real que reemplazan, a menos que uno pruebe minuciosamente cada simulacro contra el servicio real. Los simulacros también funcionan mejor cuando hay una sola instancia de cada simulacro en particular y cada prueba usa el mismo simulacro.

Dicho esto, los simulacros siguen siendo prácticamente la única forma en que uno puede probar en unidad ciertas formas de E / S de red. Vivimos en una era de microservicios y subcontratación de la mayoría (si no todas) de las preocupaciones que no son fundamentales para nuestro producto principal a un proveedor. Una gran parte de la funcionalidad principal de una aplicación ahora involucra una llamada de red o cinco, y la mejor manera de probar unitariamente algunas de estas llamadas es burlándose de ellas.

En general, encuentro que limitar el área de superficie de los simulacros a la menor cantidad de código para funcionar mejor. Una llamada API a un servicio de correo electrónico para enviar a nuestro usuario recién creado un correo electrónico de bienvenida requiere una conexión HTTP. Aislar esta solicitud a la función más pequeña posible nos permite burlarnos de la menor cantidad de código en las pruebas. Normalmente, esta debería ser una función con no más de 1–2 líneas para realizar la conexión HTTP y devolver cualquier error junto con la respuesta. Lo mismo se aplica al publicar un evento en Kafka o al crear un nuevo usuario en la base de datos.

Pruebas basadas en propiedades

Para algo que puede proporcionar una cantidad tan enorme de beneficios con tan poco código, las pruebas basadas en propiedades están lamentablemente infrautilizadas. Desarrollado por la biblioteca QuickCheck de Haskell y ganando adopción en otros idiomas como Scala (ScalaCheck) y Python (Hipótesis), las pruebas basadas en propiedades permiten generar una gran cantidad de entradas que coinciden con algunas especificaciones para una prueba determinada y afirman que la prueba pasa por Todos y cada uno de estos casos.

Muchos marcos de pruebas basados ​​en propiedades tienen como objetivo funciones y, como tal, tiene sentido aislar cualquier cosa que pueda estar sujeta a pruebas basadas en propiedades a una sola función. Encuentro esto especialmente útil al probar la codificación o decodificación de datos, probar el análisis JSON o msgpack, etc.

Conclusión

La intención de esta publicación no era argumentar que DRY ni las funciones pequeñas son inherentemente malas (incluso si el título lo sugirió falsamente). Solo que tampoco son inherentemente buenos.

El número de funciones pequeñas en una base de código o la longitud promedio de la función en sí misma no es una métrica para presumir. Hay una charla de PyCon en 2016 llamada onelineizer sobre un programa de Python del mismo nombre que puede convertir cualquier programa de Python (incluido él mismo) en una sola línea de código. Si bien esto es una conferencia divertida y fascinante, sería bastante tonto escribir código de producción en el mismo asunto.

El consejo antes mencionado se aplica universalmente, no solo a Go. A medida que la complejidad de los programas que creamos ha aumentado considerablemente y las limitaciones con las que trabajamos se han vuelto más proteicas, corresponde a los programadores adaptar su pensamiento en consecuencia.

La programación de la ortodoxia, desafortunadamente, sigue estando fuertemente influenciada por los libros escritos durante una era en la que la programación orientada a objetos y los patrones de diseño reinaban supremamente. Muchas de estas ideas y mejores prácticas ampliamente difundidas hasta ahora han quedado sin respuesta durante décadas y requieren una reconsideración, especialmente porque el panorama de la programación y los paradigmas han evolucionado enormemente en los últimos años.

Eliminar viejos tropos no solo es flojo, sino que también adormece a los programadores con una falsa sensación de tranquilidad que no pueden permitirse.