2 puntos por GN⁺ 2025-03-12 | 1 comentarios | Compartir por WhatsApp
  • Aunque el código sea lógicamente excelente, puede volverse difícil de leer con el tiempo, y este texto atribuye ese cansancio a la complejidad visible
  • Halstead Complexity Metrics cuenta operadores y operandos para calcular Volume y Difficulty, y considera más difícil una implementación con más variables y operadores aunque haga lo mismo
  • La Cognitive Complexity de SonarSource evalúa la carga de condicionales, bucles, manejo de excepciones, combinaciones de operadores lógicos, recursión y goto, centrándose en la sintaxis abreviada, las interrupciones del flujo lineal y el control de flujo anidado
  • En el aspecto de las variables, el variable shadowing, los nombres parecidos, los rangos de vida largos y los patrones de uso poco familiares elevan el costo de seguir el flujo de datos para quien lee
  • Funciones pequeñas, patrones familiares, agrupación de cadenas largas, expresiones condicionales simples, uso limitado de goto, anidación superficial, nombres de variables distinguibles y rangos de vida cortos se convierten en criterios de legibilidad aplicables más allá del lenguaje y el formato

Límites y objetivo de las métricas de legibilidad de código

  • No existe una métrica única, ampliamente usada y consensuada para la legibilidad del código
  • El material de referencia disponible eran artículos académicos poco usados en la práctica o ideas cercanas a opiniones, así que hacía falta una herramienta de discusión más concreta para usar directamente en revisiones de código
  • El objetivo no era inventar una nueva métrica, sino reunir patrones visuales que cualquiera pudiera usar al hablar de si un código es fácil de leer
  • Las mediciones o ideas revisadas cumplían con las siguientes condiciones
    • Pueden aplicarse a fragmentos de código fuente o a una sola función
    • No se concentran solo en la complejidad esencial del algoritmo implementado, como ocurre con Cyclomatic Complexity, que es difícil de separar del algoritmo mismo
    • No se quedan únicamente en elementos superficiales de estilo como la longitud de los nombres de variables, los espacios, la indentación o la colocación de paréntesis

Halstead Complexity Metrics

  • Maurice Halstead propuso Halstead Complexity Metrics a fines de los años 70 para crear mediciones empíricas del código fuente
  • Esta métrica puede aplicarse más allá del lenguaje y la plataforma, y se enfoca en la forma en que está escrito el código más que en el algoritmo implementado en sí
  • Las mediciones clave son cuatro conteos basados en operadores y operandos
    • cantidad de operadores únicos n1
    • cantidad de operandos únicos n2
    • cantidad total de operadores N1
    • cantidad total de operandos N2
  • A partir de esto, Halstead construyó métricas relacionadas como length, volume y difficulty, e incluso intentó derivar cifras para estimar la cantidad de bugs incluidos en una implementación
  • Intuitivamente, cuantos más operadores haya, más interacciones potenciales hay que considerar; y cuantos más operandos haya, más difícil se vuelve entender las posibilidades del flujo de datos
  • Ejemplo en JavaScript

    • Incluso en una misma función para determinar si un número es par o impar, una implementación simple con if y return usa menos operadores y operandos
    • 4 operadores únicos, 7 operadores en total
    • 5 operandos únicos, 6 operandos en total
    • Volume 33.30, Difficulty 2.50
    • Una implementación que usa arreglos, Number, expresiones de comparación e índices tiene más operadores y operandos
    • 7 operadores únicos, 10 operadores en total
    • 9 operandos únicos, 12 operandos en total
    • Volume 71.35, Difficulty 3.75
    • La primera implementación se ve más simple incluso a simple vista, y los valores de Volume y Difficulty de Halstead lo respaldan
    • La desventaja es que no siempre está claro qué debe contarse como operator y operand en todos los lenguajes, así que para medir conviene elegir una herramienta o implementación específica y usarla de forma consistente
  • Patrones prácticos que deja Halstead

    • En general, las funciones pequeñas y con pocas variables son más fáciles de leer
    • Los operadores específicos del lenguaje o la sintaxis azucarada agregan carga extra a quien lee, así que conviene no abusar de ellos
    • Encadenar largamente componentes funcionales como map, reduce y filter, además de lambdas, iteradores y comprehensions, puede reducir la legibilidad aunque el resultado sea conciso
    • Estas cadenas largas pueden verse con más frecuencia en JavaScript y Rust, o en Python cuando el código se apoya demasiado en itertools

La dificultad de lectura según Cognitive Complexity

  • Cognitive Complexity, creada por SonarSource, es una métrica pensada para captar con mayor precisión la dificultad de lectura
  • La idea central de esta métrica tiene tres partes
    • La sintaxis abreviada que combina enunciados reduce la dificultad
    • La dificultad aumenta cada vez que se rompe el flujo lineal
    • El control de flujo anidado aumenta la dificultad
  • Aunque se critica que el nombre suena científico o como si fuera una métrica objetiva, en la práctica puede verse como una heurística útil
  • El problema de la densidad de la sintaxis abreviada

    • Una sintaxis abreviada como MyObj myObj = a?.myObj; es más corta y toma menos tiempo de lectura que una forma como if (a != null) { myObj = a.myObj; }
    • Sin embargo, los dos códigos pueden no ser realmente idénticos
    • En el primero, myObj pasa a ser a.myObj o null
    • En el segundo, myObj pasa a ser a.myObj o undefined
    • Incluso en lenguajes con verificación de tipos fuerte como TypeScript o Rust, eso solo reduce la posibilidad de omisiones, pero no garantiza que todos los casos se manejen correctamente
    • Si el soporte de verificación de tipos es débil, como en JavaScript común, es más probable que estos casos límite no se manejen
    • La sintaxis abreviada puede ser fácil de escribir y de leer, pero existe un trade-off entre brevedad y densidad
  • Elementos que rompen el flujo lineal

    • El código lineal sin condicionales es más fácil de recorrer rápidamente que el código con condicionales
    • Cognitive Complexity considera que aumentan la dificultad no solo los condicionales, los bucles y goto, sino también los macros condicionales, try/except, las secuencias de operadores lógicos y la recursión
    • switch se cuenta como un solo grupo, pero una cadena de else-if se considera más difícil con cada else-if adicional
    • Esto se debe a que cada rama de else-if puede hacer más de una comparación
    • Aun así, el fall-through en switch y un break faltante también pueden aumentar la dificultad de lectura
    • La dificultad cambia según se encadenen operadores lógicos iguales dentro de una condición o se mezclen &&, || y !
    • debug || verbose || consoleMode es una expresión condicional simple
    • debug || (verbose && consoleMode) es más difícil de leer por la mezcla de operadores
    • debug || !(verbose && consoleMode) es aún más compleja porque además incluye negación
  • Manejo de excepciones y goto

    • try/catch aumenta la dificultad en Cognitive Complexity, pero no considera que varios bloques catch sean más difíciles que uno solo, y try y finally se ignoran
    • El acto mismo de lanzar una excepción también puede añadir costo de lectura
    • Cuando el manejo de excepciones cruza límites entre funciones, la complejidad de las funciones involucradas queda entrelazada
    • Quien lee tiene que buscar dónde se captura esa excepción
    • goto normalmente se cuenta como un factor que aumenta la dificultad
    • Aun así, algunos expertos consideran útil una forma como goto out o goto done para liberar recursos y salir de la función en condiciones de error
    • En cambio, un goto que cruza límites de bucles de una forma que no puede expresarse con continue o break impone una carga grande, porque obliga a reconstruir un nuevo flujo de control

Anidación y forma de las funciones

  • Si los condicionales ya son difíciles de leer por sí mismos, los condicionales anidados lo son todavía más
  • Cognitive Complexity añade dificultad extra por cada nivel de anidación, además de la puntuación propia de condicionales y bucles
  • Esta idea también se conoce con nombres como “Level of Indentation” o “Bumpy Road”
  • Cuando la anidación supera dos niveles, la lectura se vuelve especialmente difícil, y el código con retornos tempranos que reduce la anidación se lee de forma más plana
  • Esta métrica no refleja directamente la longitud de la función, pero si todo lo demás es igual, una función larga exige más esfuerzo de lectura que una corta

Nombres de variables, rangos de vida y patrones familiares

  • Nombres distinguibles y descriptivos

    • Los nombres descriptivos son importantes para entender qué intenta hacer el código, mientras que los nombres redundantes o crípticos juegan en contra
    • El variable shadowing es riesgoso
    • Conviene evitar situaciones en las que quien lee tenga que seguir las reglas de alcance para distinguir qué variable se está usando
    • También conviene evitar identificadores visualmente parecidos
    • Nombres como i y j, o item e items, pueden causar errores porque se confunden fácilmente a simple vista
    • Un código que usa varias variantes del mismo nombre de variable dentro de una sola función, como node, _node y thisNode, tiene una forma propensa a bugs
  • Rangos de vida cortos para las variables

    • El live variable analysis observa el rango desde el primer punto en que una variable se usa hasta el último punto en que puede usarse
    • Si el rango de vida de una variable es largo, quien lee tiene que mantener en la cabeza más variables y más valores posibles
    • Declarar una variable justo antes de usarla puede acortar ese rango de vida frente a declararlas todas al inicio de la función
    • El peor caso es cuando una variable sigue viva a través de varias funciones y se usa en varios lugares
    • Si varias variables tienen en la práctica el mismo rango de vida, puede que un objeto sea más apropiado
    • Si un objeto no encaja, conviene minimizar la cantidad de funciones y líneas que hay que leer para entender esos valores
  • Cadenas largas y variables intermedias

    • El estilo de programación funcional acorta el rango de vida de las variables, pero las cadenas demasiado largas o el anidamiento de callbacks pueden volver más pesada la lectura
    • Dividir cadenas largas de funciones en grupos pequeños y usar variables intermedias o funciones auxiliares con buenos nombres puede reducir la carga cognitiva de quien lee
    • La versión con variables intermedias podría ser ligeramente menos eficiente
    • A menos que una herramienta de rendimiento indique que esa línea es realmente un cuello de botella, esas diferencias mínimas de eficiencia no importan
  • Reutilización de patrones de código familiares

    • Reutilizar código y formas de variables familiares facilita la lectura porque quien lee reconoce patrones que ya conoce
    • Esto se relaciona con el Principle of Least Surprise
    • Dentro de una base de código, conviene mantener consistentes las formas repetidas, como la manera de escribir condicionales
    • Cuando haga falta apartarse del patrón, la diferencia puede hacerse visible con nombres de variables o comentarios
    • Llevado al extremo, esto apunta a usar funciones plantilla o funciones genéricas para que quien lee no tenga que volver a reconocer patrones repetidos

8 patrones visuales para mejorar la legibilidad

  • Line/Operator/Operand count: las funciones más pequeñas y con menos variables u operadores son más fáciles de leer
  • Novelty: evitar la novedad en la forma de las funciones, los operadores y la sintaxis azucarada, y reutilizar los patrones comunes de la base de código
  • Grouping: dividir cadenas largas de funciones, iteradores o comprehensions en grupos lógicos usando funciones auxiliares o variables intermedias
  • Conditional simplicity: mantener las condiciones lo más cortas posible y, dentro de una misma condición, preferir secuencias del mismo operador lógico antes que mezclar operadores distintos
  • Gotos: no usar goto, salvo cuando siga un patrón específico de manejo de errores y la alternativa sea peor
  • Nesting: minimizar la lógica anidada y los grandes cambios de indentación; si se necesita anidación profunda, separarla en otra función en lugar de esconderla dentro de una función grande
  • Variable distinction: usar nombres de variables descriptivos y visualmente distinguibles, y evitar el variable shadowing
  • Variable liveness: mantener cortos los rangos de vida de las variables, especialmente cuando se extienden a través de límites de funciones

Problemas observados en bases de código reales

  • Las bases de código que más fatiga mental generaban combinaban varios antipatrones al mismo tiempo
  • En concreto, había funciones largas, mezcla de distintos elementos del lenguaje y muchas cadenas de funciones que debieron separarse en helpers
  • Como resultado, dentro de funciones grandes crecían al mismo tiempo la complejidad de anidación y los rangos de vida largos de las variables
  • Aunque la calidad del código y de su autor era alta, se encontraron uno o más bugs críticos
  • Uno de ellos era un bug fácil de ver, pero probablemente pasó desapercibido porque estaba en medio de una función larga y compleja, donde era difícil razonar sobre él
  • Como la persona que con más probabilidad leerá tu código dentro de un mes eres tú mismo, escribirlo de forma fácil de leer se conecta directamente con un costo real de trabajo

1 comentarios

 
GN⁺ 2025-03-12
Opiniones de Hacker News
  • Que el artículo dijera que las cadenas de map/reduce/filter perjudican la legibilidad cuando son largas o múltiples no se sostiene en absoluto con el resto del contenido
    Se siente como la típica queja de que algo es malo solo porque no te resulta familiar, y en cuanto te acostumbras un poco, muchas veces son mucho más fáciles de leer y escribir que las alternativas
    Por ejemplo, me cuesta imaginar un código más fácil de leer que books.filter(book => book.pageCount > 1000).map(book => book.author).distinct()
    Incluso como métrica de complejidad, este tipo de código casi siempre sale mejor que cualquier otro enfoque, y al menos hay que aprender lo básico de la programación funcional. No hace falta explicar mónadas, pero sí estar lo bastante familiarizado como para no menospreciar map y filter sin más

    • La redacción sí se siente innecesariamente filosa, pero el ejemplo al que apuntaba la frase citada no era una cadena corta como esa
      Yo diría que recién a partir de una cadena de unas 5 llamadas seguidas empieza a volverse difícil de leer, y el ejemplo del artículo tenía más o menos ese largo
      “multiple” se refería a casos con cadenas anidadas o donde cambia el tipo que se está manipulando, y ahí sí baja la velocidad de lectura
      Los componentes funcionales pueden ser elegantes, pero también se pueden usar en exceso
    • El ejemplo es solo un filtro conceptualmente simple sobre una sola lista
      Cuando la cadena se vuelve demasiado larga, las condiciones se complican y hay varias listas o variables, ya no es fácil entenderla de una sola pasada
      En un bucle procedimental puedes ponerle nombre a los resultados intermedios, y gracias a esos nombres puedes olvidarte de lo que hiciste antes y concentrarte en el siguiente paso
    • SELECT DISTINCT author FROM books WHERE pageCount > 1000;
    • El ejemplo está bien, pero creo que la queja original apuntaba más a cadenas realmente largas
      En lo personal, prefiero partir la cadena para ponerles nombre a los resultados intermedios, y usar los nombres de variables casi como si fueran comentarios
      var longBooks = books.filter(book => book.pageCount > 1000)
      var authorsOfLongBooks = longBooks.map(book => book.author).distinct()
    • Las soluciones en SQL que puso la gente también están bien, pero en Prolog también se podría escribir así
      ?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).
      Según la estructura de la base de datos en Prolog, también podría quedar más corto
      ?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).
  • Creo que el buen código tiene, en el fondo, una dimensión cualitativa y literaria bastante importante
    A los programadores o académicos acostumbrados al pensamiento matemático, que buscan respuestas cuantitativas, esto muchas veces les incomoda
    Me gustan tanto Dostoyevsky como Wodehouse, y ambos son excelentes, pero de maneras muy distintas
    Aunque programar no sea un campo tan abierto, sí he trabajado con buenos codebases que se sienten cualitativamente muy diferentes entre sí, y muchas veces toma tiempo agarrarle el estilo a un codebase, igual que cuando te acostumbras a la voz de un autor nuevo

    • Estoy 100% de acuerdo. Uno de los mejores elogios que he recibido sobre programación fue que el código se lee como una historia
      Quería decir que, al leer el archivo de arriba abajo, la narrativa fluía de forma natural por el orden de las funciones, y que la implementación declarativa parecía estar hablándole al lector
      Sigo el paradigma de programación funcional pura, y creo que este enfoque encaja mejor con un estilo más narrativo
      Como las dependencias/entradas de una función quedan limitadas a sus argumentos o a otras funciones puras, y la salida queda contenida solo en el tipo de retorno, es más fácil guiar la complejidad paso a paso
      A diferencia de otros paradigmas con complejidades como estado oculto, irónicamente creo que el paradigma más matemáticamente preciso también es el que mejor se adapta a un estilo más narrativo
    • Si tardas más de 5 segundos en leer y entender el objetivo de alto nivel de una función, para mí es mal código
      No importa la forma; si no puedes entender qué hace una función en un tiempo razonable sin mucha experiencia de desarrollo, entonces simplemente es mal código
    • Muchos patrones sintácticos que se consideran elegantes en realidad se sienten menos claros que las matemáticas
      Por ejemplo, el operador ternario del artículo, return n % 2 === 0 ? 'Even' : 'Odd';, se siente como si el cerebro humano tuviera que leerlo al revés, y parece más apto para que un compilador procese el árbol sintáctico que para una persona
      Un matemático humano lo escribiría como una función por partes: si n mod 2 = 0, entonces 'Even'; si no, 'Odd', y eso resulta mucho más claro
    • Por eso es importante el code review
      Ayuda a que los nuevos integrantes del equipo aprendan un estilo consistente, y también mantiene el estilo del equipo más o menos uniforme
      También vale la pena revisar el comentario sobre .editorconfig: https://news.ycombinator.com/item?id=43333011
      Reduce las discusiones sobre detalles de estilo en los pull requests
    • Tal vez por eso Literate Programming nunca logró consolidarse mucho
  • El artículo está bien, pero creo que pasó por alto la mutabilidad, que es el factor más agotador mentalmente al leer código
    Poder “fijar” el significado de una variable una sola vez mientras lees un método, y mantenerlo así mientras infieres el resto, es un gran regalo
    La comprensión de un método debería aumentar de 0% a 100% de forma monótona; no deberías tener que reiniciar el método en tu cabeza porque, en cierta iteración, te confundiste sobre cómo el cuerpo del bucle cambió el acumulador
    La verdadera razón por la que GOTO es dañino también está aquí. No es que sea difícil mover el puntero de instrucciones mental dentro de un método, sino que con GOTO es difícil saber el estado de las variables mutables

    • No estoy de acuerdo. Hay un espacio de información abstracto que el código modela, y el puntero de instrucciones mental tiene que moverse dentro de ese espacio
      Tanto las variables mutables como las inmutables pueden ayudar u obstaculizar ese movimiento, y depende de qué tan limpiamente corresponda el código a ese espacio
      Las variables inmutables tienen una pequeña ventaja táctica, porque no hay que preocuparse de que el valor cambie o cambie de una forma engañosa, pero por experiencia no es una ventaja lo bastante grande como para volverla una regla de “usa siempre inmutabilidad”
      A veces la mutabilidad permite expresar ese espacio de información de una manera mucho más limpia
    • La complejidad total no es solo un problema de mover el puntero de instrucciones desde un punto de inicio conocido
      Si lo ves no desde el punto de llamada sino desde la perspectiva de lo llamado, cuando alguien puede saltar a una línea específica no puedes rastrear qué pasó antes de eso. Pudo haber venido de cualquier parte, así que ya no basta un análisis local sino que hace falta análisis global del programa
      Si la mutabilidad fuera la verdadera fuente de la complejidad de GOTO, entonces if y los bucles for deberían tener el mismo problema
      Estoy de acuerdo en que la mutabilidad y el estado crean complejidad directamente, pero GOTO me parece una categoría completamente distinta y mucho más dañina
  • Un patrón que personalmente no me gusta es devolver enseguida en un if y dejar el resto como ruta predeterminada implícita
    if (n % 2 === 0) return "Even"; return "Odd"; es más corto, pero prefiero mucho más if ... return "Even"; else return "Odd";
    La razón es que el primero da una sensación de asimetría. "Even" y "Odd" son opciones simétricas, así que la versión con else me resulta más intuitiva

    • Yo lo escribiría como return (n % 2 === 0) ? "Even" : "Odd";, que tiene menos boilerplate y por eso es más fácil de leer
      Si es un lenguaje con operador ternario, debería poder reconocerse fácilmente
    • Las guard clauses / retornos tempranos tienden a mover el foco del desarrollador no hacia la ruta normal principal, sino hacia ir acotando el comportamiento de la función
      En mi experiencia, else añade anidación extra y hace más fácil que sigas evaluando condiciones límite o variables más allá del alcance inmediato de la ruta normal. Hay que cargar con todo ese contexto
    • Personalmente prefiero el primer estilo
      Puedes ver visualmente el retorno a un nivel de indentación justo debajo del nombre de la función, y si no hay salida temprana da la sensación de que hay un resultado garantizado
      Si el retorno queda enterrado más abajo, se siente algo raro
    • Yo pondría una línea en blanco para que return "Odd"; se vea separado del if, y si el lenguaje lo permite también añadiría llaves al cuerpo del if
      Hay casos en los que acepto else, pero por lo general es cuando hay efectos secundarios, y normalmente si refactorizas hasta eliminarlo queda más claro
      Es común que código complejo termine convertido en una secuencia de guards que va saliendo según importancia o costo de ejecución, y eso separa la lógica real de la función/método de las condiciones de terminación
    • La asimetría se vuelve evidente si refactorizas el código a un estilo de continuation/callback o a una mutación de estructuras de datos más compleja
      El primer estilo sigue fluyendo hacia abajo y termina ejecutando el segundo conjunto de instrucciones. return es un operador especial que rompe el flujo de control, así que el flujo de control general del primer estilo no captura correctamente la completitud de ambos casos
      En Rust idiomático, no se usa return salvo en casos excepcionales donde se rompe el flujo de control del método, así que el segundo ejemplo suele verse más a menudo sin sentencia de retorno
      En Python también se suele hacer retorno temprano al inicio cuando hay argumentos o estado inválidos, y luego el retorno en posición final es el valor de retorno real
      Por estas convenciones, si rompes una estructura if-else completa, el return indentado pasa a verse como si fuera un caso excepcional. Si sigues esta convención, naturalmente las sentencias return parecen redundantes salvo cuando interrumpen el flujo de control, y la convención de Rust se entiende. En todos los lenguajes, return es una sentencia equivalente a break
  • Tal vez solo me pase a mí, pero TypeScript a veces dificulta leer código
    Está bien si mantienes el modelo de datos razonablemente “atómico” y el desarrollador es disciplinado para declarar y documentar realmente los tipos
    Pero cuando los tipos empiezan a derivarse de otros tipos con utility types, y además se omiten tipos explícitos para apoyarse en la inferencia, todo se desarma muy rápido
    Se vuelve muy difícil rastrear de dónde salió un campo cuando estás en una pila profunda, como de 4 o 5 niveles de indirección entre tipos. Parte es inferencia, parte es explícita, parte son tipos derivados y además se mezclan alias de campos
    Con modelos de datos grandes y pilas de llamadas profundas, una forma como function checkDogs(dogs: Dog[]) { ... } sin tipo de retorno explícito se vuelve totalmente inutilizable y de verdad desesperante

    • Creo que las funciones probablemente deberían declarar el tipo de salida
      La razón principal es forzar que todas las rutas de retorno de esa función respeten ese tipo
      He visto muchas regresiones por agregar una condición nueva y hacer que una rama devolviera un tipo ligeramente distinto al de las demás
      Pero no creo que poner tipo en la declaración de variables tenga mucho valor
      En el ejemplo, const checkedDoggos = checkDogs([]) está bien, y dejar que checkedDoggos herede el tipo de la función está perfecto
      Estoy trabajando con una base de código donde el linter obliga a escribir const checkedDoggos: DogBreedAndSize[] = checkDogs([]), y es bastante ridículo y aporta muy poco
    • Aunque en TypeScript parte del tipo de retorno se infiera, igual prefiero tener aunque sea algo de información de tipos antes que JavaScript sin ninguna información de tipos
      En JavaScript no puedes estar seguro, así que terminas subiendo y bajando por la pila todo el tiempo y manteniéndolo en la cabeza
    • Mi política es agregar tipos solo cuando el compilador de TypeScript empieza a gritar
  • Hay que tener cuidado con la frase “las funciones pequeñas con pocas variables por lo general son fáciles de leer”.
    No me gusta que las discusiones sobre legibilidad se concentren solo en la legibilidad microscópica. Eso hace que el código se fragmente demasiado fácilmente bajo la suposición equivocada de que la legibilidad microscópica es más importante que la macroscópica.
    Este tipo de dogmatismo produce programadores que no ven el bosque por mirar solo los árboles, y termina generando código demasiado ineficiente o difícil de depurar.
    Los lenguajes de la familia APL están en el extremo opuesto, pero el punto óptimo real probablemente esté en algún punto intermedio y varíe mucho según la persona.

    • Sobre todo cuando se mezclan varios archivos, claramente existe un punto medio.
      En código que no conozco, después de ir a la definición 3 o 4 veces ya se vuelve pesado; quizá sea problema mío, pero me cuesta imaginar que la mayoría sea muchísimo mejor que yo en eso.
      En la cultura de .NET, y en especial en “clean architecture”, este problema es impactante. Cuando intentas modificar una funcionalidad o rastrear un problema, todo está repartido entre 4 capas y 15 archivos, y algunos archivos tienen más de 60% de puro keyword.
      No sé exactamente dónde trazar la línea, pero en general prefiero una sola función larga que pueda leerse de corrido siguiendo otras recomendaciones, antes que código tan fragmentado que obliga a hacer scroll arriba y abajo cada 5 líneas.
      Lo mismo pasa con tipos/clases: no hace falta poner en otro archivo un enum de 4 valores que solo se usa en este DTO.
  • Es un artículo interesante, pero no me deja satisfecho.
    Llega demasiado rápido a conclusiones y vuelve otra vez al terreno de la preferencia personal. Coincido con varios gustos, pero el artículo explícitamente intentaba ir más allá de eso.
    Decir “evita operadores o azúcar sintáctica específicos del lenguaje porque cargan al lector” no se desprende de las métricas. Si una función tiene 3 operadores distintos y un operador específico del lenguaje reemplaza esos tres de una sola vez, el “esfuerzo” de la función disminuye.
    Componentes como map/reduce/filter, si se usan bien, también reemplazan otros operadores y reducen el “volumen”, así que puede ir en cualquier dirección.
    El ejemplo de ?. parece un diagnóstico muy específico de JavaScript que apunta a su diseño de lenguaje difícil de leer. En muchos lenguajes null y undefined no están separados, por eso normalmente se le llama null-safe operator.
    “El sombreado de variables (variable shadowing) es terrible” y “un tiempo de vida largo te obliga a mantener más variables en la cabeza” pueden entrar en conflicto.
    En algunos contextos me gusta mucho el sombreado de variables, porque las saca del scope en vez de dejar accesible la instancia anterior.

  • Hay un plugin genial para VS Code llamado Highlight.
    Permite aplicar colores distintos al código con expresiones regulares personalizadas, y supongo que un uso común es poner //TODO en amarillo.
    Yo lo uso para atenuar logs, porque meter logs por todos lados agrega mucho ruido visual.
    La librería que mantengo escribe logs como this.logger?.info('Some logs here');, y a eso le aplico una transparencia de 0.4 para que quede en segundo plano.
    Sigue siendo visible, pero a primera vista resalta más la lógica real del negocio.
    La configuración se puede adaptar así: "highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }
    https://marketplace.visualstudio.com/items?itemName=fabiospa...

  • No estoy de acuerdo con la idea de que dividir cadenas largas de funciones o callbacks en grupos pequeños y usar variables con buenos nombres sea un poco menos eficiente.
    Las dos versiones pueden ser igual de eficientes.
    En ambos casos se asignan los mismos objetos, se guardan en el heap y luego los recoge el garbage collector. La diferencia de eficiencia depende del compilador.
    En la segunda versión, el compilador debería poder ver que cada variable solo se usa justo después de declararse, y tratar esos objetos como si estuvieran fuera de scope igual que en una llamada encadenada.

    • De acuerdo. Después de compilar, es muy probable que al compilador no le importe que le hayas puesto nombre al valor de retorno.
      Claro, asumiendo que lo dejas inferir el tipo de la variable.
      El costo que sí se ve en la práctica aparece cuando materializas valores intermedios explícitamente para verlos en el depurador. Por ejemplo, si lo conviertes en una lista, generas asignaciones que podrían haberse evitado y eso sí cuesta.
  • Me gusta el intento de cuantificar la “legibilidad”. Hace mucha falta un enfoque así.
    Ahora mismo, la definición más común de legibilidad se siente bastante cercana a “lo que a mí me resulta fácil de leer”.
    Quizá podrías mostrarle código a muchísima gente, medir el tiempo mientras eligen una oración que describa lo que hace, y así encontrar dimensiones reales de la legibilidad.
    Los problemas que más gente acierte en el menor tiempo promedio serían ejemplos de código fácil de leer en el mundo real, y más importante aún, podrían ayudar a identificar prácticas realmente difíciles de leer.
    Supongo que las personas encuestadas se agruparían por ejes como “años de experiencia programando” o “si entienden el paradigma X”, y los resultados también podrían desplazarse con el tiempo a medida que cambian las modas.

    • Una de las dificultades clave es que aprendemos a leer código.
      Lo que aprendiste a leer y escribir moldea lo que te parece fácil de leer.
      Influyen muchos factores: qué intentas hacer, con quién trabajas, qué sabías hacer antes de programar, qué otros lenguajes conoces, etc.
      Una vez que recoges lo más obvio, por ejemplo ir más allá de no poner nombres de variables arbitrarios, irrelevantes o engañosos, muchos problemas de “legibilidad” podrían terminar siendo en realidad cuestiones de construir consenso.
      Tal vez no exista una respuesta correcta que trascienda al grupo específico de programadores con el que quieres trabajar.
    • No le veo mucho valor.
      La legibilidad del código es parecida a la legibilidad de un idioma: normalmente se vuelve un problema para quien no conoce bien ese lenguaje, y con tiempo se resuelve.
      El problema real en programación es la complejidad del código, y eso no se puede juzgar solo con métricas de fragmentos individuales.
      El problema está en las relaciones entre funciones, no en las decisiones de implementación dentro del cuerpo de una función.
    • Ese enfoque es demasiado unidimensional.
      Averiguar qué hace el código suele ser fácil; lo difícil normalmente es modificarlo o agregar funcionalidades.
      Esa dificultad aparece porque se oculta cómo se conectan entre sí varios niveles de abstracción.