Mejores argumentos == Mejores funciones

Los argumentos juegan un papel muy importante en la legibilidad de una función. Cuando están implementados correctamente, muestran intención y proveen información sobre el comportamiento de la función. Esta es una de las razones por las que invertir tiempo en crear buenos argumentos puede ayudar a mejorar la calidad de tu código. Veamos cómo podemos sacar el máximo provecho de nuestros argumentos, o al menos asegurar que no se interpongan en el camino cuando intentamos crear buen software.

Mantén los argumentos < 2

Mientras menos argumentos tome una función, más fácil es de entender. Si has estado programando por un tiempo, estoy seguro de que has lidiado con bestias de argumentos: Esas horribles funciones que toman una enorme cantidad de argumentos sin relación aparente. Entender cómo todos esos argumentos trabajan juntos para hacer algo es una tarea muy difícil.

Lo que es peor es que en la mayoría de situaciones, esas funciones ni siquiera deberían existir. Usualmente, son una secuencia de pasos que puedes fácilmente empaquetar en funciones más pequeñas con uno o dos argumentos. Listas largas de argumentos son usualmente una señal de que una función está fallando en hacer solo una cosa, y hacerla bien. Si encuentras una función así, trata de rediseñarla partiendola en una serie de funciones más pequeñas y bien definidas.

Deshazte de todos los argumentos: las alegrías de las funciones niládicas

¿Cuál es el mejor número de argumentos que puede tomar una función?

Fácil, 0.

Los argumentos son difíciles para el cerebro: te obligan a considerar información adicional cuando tratas de entender qué hace una función. Los diferentes valores que pueden tomar los argumentos, el nivel de abstracción al que pertenecen y muchas otras cosas obstaculizan tus esfuerzos para entender una función. No tener argumentos te ahorra mucho trabajo cognitivo. Las funciones niládicas usualmente vienen en dos sabores:

  • Una función que dispara un evento, que cambia el estado del sistema o envía un mensaje a otra parte de la aplicación.
  • Una función que altera el estado del llamador, o devuelve alguna información sobre él, como toString o isEngineOff.

Las niladas no solo son fáciles de entender (las bien escritas, al menos), también son significativamente más fáciles de testear: verificar que la función funciona correctamente con diferentes combinaciones de argumentos es trivial si no recibes ninguno.

Si puedes resolver tu problema con una función niládica, hazlo.

Escribiendo buenas funciones monádicas

Después de 0, el mejor número de argumentos es 1. Las funciones monádicas (funciones con 1 argumento) son significativamente más difíciles de entender que funciones niládicas de complejidad similar. Aún así, las funciones que toman 1 argumento están totalmente bien, y tu proyecto probablemente contendrá una enorme cantidad de estas.

Las monádicas usualmente son una de las siguientes:

  • Una función que devuelve información sobre el argumento, como is_green(screen_background).
  • Una función que transforma el argumento en algo diferente y lo devuelve.
  • Una función que dispara un evento (igual que arriba).

Hay dos cosas importantes que considerar cuando trabajas con funciones monádicas.

La primera es evitar argumentos de salida. Una función tiene un argumento de salida cuando no devuelve ningún valor y muta el estado de la entrada. Por ejemplo, la función:

public void uppercasePersonName(Person person){
    person.fullName = person.fullName.toUpperCase();
}

En este caso, person es un argumento de salida, cuyo estado (fullName) se cambia. Las funciones con argumentos de salida pueden ser difíciles de entender y son un buen lugar para que se escondan bugs. Si es posible, prefiere funciones puras.

La segunda cosa a considerar es que, si es posible, deberías transformar monádicas en funciones niládicas. Piensa por ejemplo en la función add_reference, que agrega una referencia a un objeto article:

    add_reference(article)

Podemos refactorizar nuestro código para que los objetos article respondan add_reference:

    article.add_reference()

En muchas situaciones, esto también resulta en clases con mejores interfaces públicas.

Dos es compañía, tres es multitud

Más de 2 argumentos hace que una función sea significativamente más difícil de entender. Quieres que la definición de tu función se lea como prosa clara, pero entender cómo un nombre de función y 3 o 4 variables trabajan juntas puede ser desalentador. Cuando los argumentos no tienen una conexión obvia entre sí, entender la función se vuelve aún más difícil. Considera la siguiente función:

    # Entender esta función no es tan fácil, toma al menos un par de segundos hacerse una idea de qué hace
    def draw_cilinder(x, y, radius, height, renderer, texture)
    end

Por supuesto, no toda función con más de 2 argumentos es problemática. Imagina que estás trabajando en una aplicación de gráficos 3D, tarde o temprano necesitarás una función que reciba 3 argumentos para la posición de un punto en el espacio. Los argumentos tienen un orden natural y es fácil entender por qué pertenecen juntos.

Hay algunas formas de mejorar la legibilidad de una función que toma muchos argumentos:

Deshazte de las banderas

Recibir una bandera para determinar qué pasa en una función es algo que debes evitar. En su lugar, crea dos (o la cantidad necesaria de) funciones y llámalas según corresponda.

Una función que revisa una bandera para decidir qué debería estar haciendo está haciendo más de una cosa. Quieres que tus funciones hagan solo una cosa, y la hagan bien, así que deshazte de esas banderas.

Compacta argumentos en un solo objeto

A menudo, las funciones que toman muchos argumentos podrían ser un indicador de que tu programa necesita una nueva clase. En el ejemplo de una aplicación 3d, en lugar de recibir 3 argumentos para las posiciones de un punto en el espacio, podrías querer crear una clase Point y pasar eso como argumento en su lugar.

Agrupar argumentos juntos no debería ser abusado, podrías terminar con frankenobjetos hechos de piezas vagamente relacionadas. Piensa en abstracciones apropiadas y crea las clases necesarias.

Escribe mejores nombres

No siempre puedes reducir el número de argumentos. Cuando todo lo demás falla, darles nombres apropiados puede al menos hacer que sean mucho más fáciles de entender. Piensa en el siguiente fragmento:

  insert(article, epilogue)

  insert_epilogue_into_article(epilogue, article)

¿Cuál es más fácil de entender? Bueno, ambas funciones obviamente realizan una acción de inserción usando esos dos argumentos, pero encuentro la segunda menos ambigua. Está bien si es un poco verbosa, escribir es barato, así que inclínate del lado de la verbosidad.

Darle a las funciones un nombre que une una acción con los argumentos puede hacerlas mucho más fáciles de leer, así que ve y pruébalo.

Remueve dependencias del orden de argumentos usando argumentos con palabra clave

En los primeros días de un proyecto, cosas como el número de argumentos y su orden son muy propensas a cambiar. En esta etapa, quieres hacer tan fácil como sea posible experimentar y hacer cambios. Remover la fricción para hacer cambios te anima a mejorar el diseño y mover cosas, una forma de lograr esto es removiendo la dependencia sobre el orden de los argumentos.

Mira el siguiente constructor:

    def initialize(radius_in_mm, number_of_frets, fretboard_material, scale_length_in_imm)
        @radius = radius_in_mm
        @frets = number_of_frets
        @material = fretboard_material
        @scale_length = scale_length_in_imm
    end
    # ...
    Neck.new(254, 22, 'ebony', 647)

Si hacemos cambios en el orden (o número) de argumentos, necesitaremos actualizar cada lugar donde llamemos este constructor. Aunque no es difícil, hay fricción implícita en realizar este cambio. Podemos en su lugar usar argumentos con palabra clave así:

    def initialize(radius_in_mm:, number_of_frets:, fretboard_material:, scale_length_in_imm:)
        @radius = radius_in_mm
        @frets = number_of_frets
        @material = fretboard_material
        @scale_length = scale_length_in_imm
    end
    # ...
    Neck.new(radius_in_mm: 254,
             number_of_frets: 22,
             fretboard_material: 'ebony',
             scale_length_in_imm: 647)

Como habrás notado, este es un tradeoff: removimos la dependencia en el orden de los argumentos, pero ahora dependemos de sus nombres. En la mayoría de situaciones, este es un buen tradeoff, ya que los nombres de los argumentos son mucho más estables que su orden.

Para la mayoría de aplicaciones maduras, los argumentos con palabra clave son excesivos así que podrías decidir no usarlos. Para proyectos en etapas tempranas de desarrollo, es una forma poderosa de mantener el código flexible y responsivo.

¿No es demasiado, solo son argumentos?

Sí, sé que parece algo muy grande solo para humildes argumentos, pero puede ayudar a hacer tu código más legible. Cuando lees una función, las primeras cosas que ves son el nombre de la función, los argumentos (y si tienes suerte, el tipo de retorno). Darle a la función un buen nombre es obviamente importante, es una lección que todos los desarrolladores de software necesitan aprender: los buenos nombres son extremadamente valiosos.

Los argumentos son la pieza faltante para hacer que la definición de una función sea totalmente expresiva. Dedica un poco de tiempo y experimenta hasta obtener los resultados que necesitas. Un par de meses en el futuro, cuando olvides los detalles de tu implementación, estarás agradecido por los minutos extra que pasaste haciendo que tus definiciones de función sean más fáciles de entender.

Qué hacer a continuación:

  • Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que podrían encontrar útil esta información.
  • Puedes encontrar más información sobre crear buenos argumentos de función en el capítulo 3 de Clean Code, y en el capítulo 7 de Code Complete.
  • Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor).

Juan Luis Orozco Villalobos

¡Hola! Soy Juan, ingeniero de software y consultor en Budapest. Me especializo en computación en la nube e IA, y me encanta ayudar a otros a aprender sobre tecnología e ingeniería