- 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
- cantidad de operadores únicos
- 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
ifyreturnusa menos operadores y operandos - 4 operadores únicos, 7 operadores en total
- 5 operandos únicos, 6 operandos en total
Volume33.30,Difficulty2.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
Volume71.35,Difficulty3.75- La primera implementación se ve más simple incluso a simple vista, y los valores de
VolumeyDifficultyde 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
- Incluso en una misma función para determinar si un número es par o impar, una implementación simple con
-
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,reduceyfilter, 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 comoif (a != null) { myObj = a.myObj; } - Sin embargo, los dos códigos pueden no ser realmente idénticos
- En el primero,
myObjpasa a sera.myObjonull - En el segundo,
myObjpasa a sera.myObjoundefined - 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
- Una sintaxis abreviada como
-
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 switchse cuenta como un solo grupo, pero una cadena deelse-ifse considera más difícil con cadaelse-ifadicional- Esto se debe a que cada rama de
else-ifpuede hacer más de una comparación - Aun así, el fall-through en
switchy unbreakfaltante 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 || consoleModees una expresión condicional simpledebug || (verbose && consoleMode)es más difícil de leer por la mezcla de operadoresdebug || !(verbose && consoleMode)es aún más compleja porque además incluye negación
-
Manejo de excepciones y goto
try/catchaumenta la dificultad en Cognitive Complexity, pero no considera que varios bloquescatchsean más difíciles que uno solo, ytryyfinallyse 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
gotonormalmente se cuenta como un factor que aumenta la dificultad- Aun así, algunos expertos consideran útil una forma como
goto outogoto donepara liberar recursos y salir de la función en condiciones de error - En cambio, un
gotoque cruza límites de bucles de una forma que no puede expresarse concontinueobreakimpone 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
iyj, oitemeitems, 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,_nodeythisNode, 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
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
mapyfiltersin másYo 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
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;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()?- 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
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
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
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 personaUn 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 claroAyuda 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=43333011Reduce las discusiones sobre detalles de estilo en los pull requests
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
GOTOes 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 conGOTOes difícil saber el estado de las variables mutablesTanto 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
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, entoncesify los buclesfordeberían tener el mismo problemaEstoy de acuerdo en que la mutabilidad y el estado crean complejidad directamente, pero
GOTOme parece una categoría completamente distinta y mucho más dañinaUn patrón que personalmente no me gusta es devolver enseguida en un
ify dejar el resto como ruta predeterminada implícitaif (n % 2 === 0) return "Even"; return "Odd";es más corto, pero prefiero mucho másif ... 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 conelseme resulta más intuitivareturn (n % 2 === 0) ? "Even" : "Odd";, que tiene menos boilerplate y por eso es más fácil de leerSi es un lenguaje con operador ternario, debería poder reconocerse fácilmente
En mi experiencia,
elseañ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 contextoPuedes 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
return "Odd";se vea separado delif, y si el lenguaje lo permite también añadiría llaves al cuerpo delifHay casos en los que acepto
else, pero por lo general es cuando hay efectos secundarios, y normalmente si refactorizas hasta eliminarlo queda más claroEs 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
El primer estilo sigue fluyendo hacia abajo y termina ejecutando el segundo conjunto de instrucciones.
returnes 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 casosEn Rust idiomático, no se usa
returnsalvo 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 retornoEn 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-elsecompleta, elreturnindentado pasa a verse como si fuera un caso excepcional. Si sigues esta convención, naturalmente las sentenciasreturnparecen redundantes salvo cuando interrumpen el flujo de control, y la convención de Rust se entiende. En todos los lenguajes,returnes una sentencia equivalente abreakTal 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 desesperanteLa 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 quecheckedDoggosherede el tipo de la función está perfectoEstoy 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 pocoEn JavaScript no puedes estar seguro, así que terminas subiendo y bajando por la pila todo el tiempo y manteniéndolo en la cabeza
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.
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 lenguajesnullyundefinedno 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
//TODOen 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.
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.
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.
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.
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.