Adiós, código limpio (2020)
(overreacted.io)- A través de la experiencia de reescribir el código de redimensionamiento de un editor gráfico enfocándose en eliminar duplicación, y luego tener que revertirlo, muestra que la obsesión por el código limpio puede perjudicar la facilidad real para hacer cambios
- La implementación existente repetía fórmulas por manejador para Rectangle, Oval, Header y TextBlock, y la nueva implementación intentaba eliminar la duplicación separando dirección y figura para combinarlas
- Justo después de la refactorización, el tamaño del código se redujo a la mitad y los puntos de cambio quedaron concentrados en un solo lugar, pero si después se necesitaban comportamientos especiales por figura o por manejador, la abstracción podía terminar siendo un obstáculo
- Un problema mayor que el juicio técnico fue haber hecho check-in directamente en master sin consultar al colega que había escrito el código original, lo que puede dañar la construcción de confianza del equipo
- La “limpieza” no es un objetivo, sino solo una herramienta para lidiar con sistemas complejos; más importante que la forma del código es si el equipo puede responder al cambio en conjunto
Repetición en el código de redimensionamiento de un editor gráfico
- En el lienzo de un editor gráfico había código para redimensionar figuras como rectángulos y óvalos mediante pequeños manejadores en los bordes
- La implementación funcionaba, pero se repetían las fórmulas para calcular posición y tamaño según la figura y la dirección del manejador
Rectangletenía métodos comoresizeTopLeft,resizeTopRight,resizeBottomLeft,resizeBottomRightOvaltenía métodos comoresizeLeft,resizeRight,resizeTop,resizeBottomHeaderyTextBlocktambién tenían sus propios métodos de redimensionamiento
- Si el usuario presionaba Shift, también había que manejar la conservación de la proporción durante el redimensionamiento, lo que agregaba todavía más fórmulas
Abstracción para eliminar duplicación
- La repetición parecía darse en dos ejes
- Se repetía entre manejadores de la misma dirección:
Oval.resizeLeft()yHeader.resizeLeft()son ambos acciones de arrastrar el manejador izquierdo - Se repetía entre métodos dentro de la misma figura: varios métodos de
Ovaltratan todos con óvalos
- Se repetía entre manejadores de la misma dirección:
- La nueva estructura intentaba dividir el código en
DirectionsyShapespara combinar comportamientos por dirección y comportamientos por figura- En
Directionsestabantop,left,bottom,right - En
ShapesestabanOval,Rectangle - Se combinaban manejadores y figuras con
createHandle,createBox
- En
- Con este enfoque se creaban configuraciones de manejadores como
fourCorners,fourSides,twoSides, y se usaban para generarRectangle,Oval,Header,TextBlock
La ilusión justo después de refactorizar
- Después de la refactorización, el tamaño total del código se redujo a la mitad y la duplicación parecía haber desaparecido
- Se asumía que, al cambiar el comportamiento de una dirección o figura específica, bastaría con modificar un solo lugar en vez de varios métodos
- Hizo check-in del cambio en master tarde por la noche, pensando que había desenredado limpiamente el código desordenado de un colega
El problema que apareció al día siguiente
- Al día siguiente, su jefe le pidió revertir el cambio, y en ese momento le resultó difícil aceptarlo porque creía que el nuevo código era más limpio que el anterior
- Con el tiempo, vio que ese juicio era correcto por dos razones
- Había reescrito el código y hecho check-in sin hablar con la persona que lo había escrito originalmente
- A cambio de reducir la duplicación, había sacrificado la facilidad para cambiar requisitos
- Un equipo de ingeniería sano debe seguir construyendo confianza, y una reescritura grande del código de un colega puede golpear con fuerza la colaboración
El costo de una abstracción equivocada
- Más adelante hicieron falta muchos casos especiales y comportamientos para varios manejadores de varias figuras
- En la nueva estructura de abstracción, acomodar esos requisitos podría haber vuelto el código varias veces más complejo
- En cambio, la estructura original “desordenada” tenía cada método separado, por lo que era relativamente fácil aplicar esos cambios
- Eliminar duplicación en el código no es gratis: al ganar una capacidad, se puede perder otra
Repensar la “limpieza”
- Muchos desarrolladores pueden pasar por una etapa de obsesión con el “código limpio” y la eliminación de duplicación
- Cuando uno no tiene confianza en el código que escribe, es fácil sentirse orgulloso de cosas medibles como reglas estrictas de lint, convenciones de nombres, estructura de archivos o ausencia de duplicación
- Al aprender a crear abstracciones, cada vez que se ve código repetido puede surgir la tentación de creer que la abstracción es una virtud y aplicarla
- Hay que pensar en profundidad con qué resultados reales de ingeniería se conectan esas sensaciones de “limpio” o “sucio”
- No solo importa cómo se ve el código, sino cómo el código cambia y se modifica junto con las personas del equipo
Cuándo soltar el código limpio
- El código limpio no es un objetivo, sino un intento de entender sistemas complejos
- Puede servir como mecanismo de defensa que orienta cuando no hay certeza sobre el impacto que tendrá un cambio en la base de código
- Al principio, puede ser placentera la experiencia de simplificar código complejo extrayendo funciones o refactorizando clases
- Pero no hay que quedarse ahí y convertirse en un “fanático del código limpio”
- Que el código limpio sirva de guía, pero en algún momento hay que soltarlo
1 comentarios
Opiniones de Hacker News
La duplicación de código a veces está bien, pero eso no prueba que el código limpio sea malo.
Parece que se fueron un poco de más con el refactoring; si solo hubieran extraído a una función esas “10 líneas de matemáticas repetidas”, habría quedado más limpio.
El compañero que no convirtió esas 10 líneas en una función tampoco hizo lo correcto, así que yo habría rechazado el PR, pero no lo habría reescrito personalmente.
Reescribir el PR de otra persona es un insulto rápido que cierra la posibilidad de discutir, y muchas veces uno no conoce el contexto en el que se escribió ese código.
Si no hay estándares de codificación, el PR se debe revisar y rechazar; si se deja que se haga commit con la idea de “el código limpio no importa”, en pocos meses se vuelve una base de código en la que nadie quiere trabajar.
Lo que hay que aprender aquí no es sobre código limpio, sino sobre comunicación y consideración.
También se elimina la oportunidad de que el autor original aprenda cómo el código de baja calidad realmente hace perder tiempo.
Aunque el enfoque de código limpio fuera mejor, el punto central aquí no era el código, sino que desperdició su propio tiempo sin dar feedback útil.
Lo mejor habría sido que el autor original elevara el estándar y que la otra persona trabajara en algo que sí necesitara tocar. Es decir, la respuesta correcta era una revisión de código.
Estas personas muchas veces, en vez de resolver problemas reales, resuelven cosas que no son problemas y hasta crean problemas futuros.
En una primera implementación, lo correcto suele ser no abstraer un fragmento tan pequeño como esas 10 líneas. El código que hace exactamente esa tarea es fácil de modificar después, y abstraer 10 líneas por un beneficio no comprobado tiene poco sentido.
Clean Code no importa en absoluto, y la mayoría de reglas o “principios” parecidos tampoco importan. Más bien son cosas a las que se aferran quienes quieren justificar estructuras tipo panal que van acumulando con el tiempo en la base de código.
El resultado suele ser código demasiado abstraído, difícil de cambiar, lento y difícil de entender.
Si se necesita un principio, basta con fijarse en expresar con claridad y facilidad de cambio lo que el programa realmente hace: transformación de datos.
La mayoría de los principios SOLID no ayudan a ver qué es la transformación de datos ni cómo ocurre, o incluso lo dificultan, así que por lo general sirven de poco para entender programas reales.
Si se mira el refactoring propuesto, esas 10 líneas en realidad no eran idénticas, solo parecidas.
La fórmula para redimensionar la esquina superior izquierda de un rectángulo y la fórmula para cambiar la esquina inferior derecha de una elipse son distintas. Por fuera se ven parecidas, pero en algunos lugares aparece
-en vez de+.En este caso la complejidad está en la geometría misma, y “abstraerla” solo mueve el desorden a otro lugar; no lo elimina de raíz.
Podría existir alguna abstracción matemática ingeniosa, como un sistema numérico especial, pero si no está incorporada en el lenguaje, es muy probable que el costo de implementar operaciones especiales sobre enteros básicos supere el beneficio.
El “código limpio” necesita un rebranding.
En más de 25 años de carrera he visto una resistencia interminable a los patrones de diseño, la abstracción, la eliminación de duplicación, etc.
Las razones que siempre aparecen son que la abstracción hace el código más complejo, que no hay tiempo para escribir código limpio, o que todo es cuestión de gustos y opiniones.
Pero el objetivo del código limpio es hacer que el código sea más simple y más fácil de mantener cuando cambian los requisitos.
El valor del software está en que puede cambiar con el tiempo. Si no fuera así, usaríamos circuitos fijos, que son mucho más fáciles y baratos de implementar y mantener.
Si el refactoring no logró ese objetivo, y el jefe puede argumentar de forma convincente que el código quedó más difícil de mantener ante futuros cambios de requisitos, entonces ese código no era “limpio”.
No haberlo discutido con el autor original es un problema de procedimiento y etiqueta, fuera del alcance del código limpio. Que alguien haya actuado de forma grosera y unilateral no elimina el valor del código limpio.
El famoso libro Clean Code presenta “sin duplicación” como condición de un sistema bien diseñado, y trata la duplicación como el principal enemigo del trabajo extra, el riesgo adicional y la complejidad innecesaria.
Según esa definición, eliminar duplicación se vuelve sinónimo de simplificar, y eso es incorrecto.
Eliminar duplicación es introducir una dependencia. Si esa dependencia modela bien el problema, puede ser una buena abstracción y una simplificación; si no, solo es compresión disfrazada de abstracción.
Como referencia, esa cita corresponde a la mención de Simple Design de Kent Beck que aparece en Clean Code.
Uno es la sensación subjetiva de que cierto código es bueno; el otro es seguir el libro Clean Code y sus lineamientos relacionados.
Cada vez más personas sienten que esas dos cosas difieren bastante. Seguir algunas pautas del libro puede producir código que en realidad no es limpio en absoluto.
En un mundo ideal, todos querríamos escribir código limpio, pero cada vez menos personas sienten que Clean Code sea una buena forma de alcanzar ese objetivo.
En especial, la abstracción excesiva y la eliminación de duplicación pasaron a verse, para mucha gente, como fuentes de código desordenado.
A veces la deuda técnica está codificada en los propios requisitos de producto, así que no se puede arreglar con refactoring ni con buenas prácticas; solo se puede reducir el dolor.
El objetivo del código limpio y los resultados reales de practicarlo son muy distintos. La gente, aun teniendo los requisitos enfrente, por lo general no puede predecir el estado futuro del código.
El código limpio invita a la terrible pregunta “¿y si…?”, y eso puede convertirse fácilmente en una caja de Pandora que posterga el lanzamiento indefinidamente.
Casi siempre es mejor programar en función de lo que se sabe, y que alguien con experiencia en el dominio lidere la arquitectura.
Muchas de las buenas abstracciones ya están en bibliotecas. Por supuesto, lamentablemente no siempre es así.
Una mala abstracción devora tiempo, y el código también es una forma de comunicarse con otras personas del equipo, así que importa mucho más cómo reciben esa comunicación los compañeros que mi propia forma de pensar.
Un colega escribió mucho código copiando y pegando, y tú lo refactorizaste después del commit
El colega se quejó con el jefe, el jefe te regañó, y terminaste concluyendo que la próxima vez dejarías la suciedad en la base de código
Hay una lección que aprender aquí, pero eso no significa que copiar y pegar sea mejor que escribir una función
La refactorización debió haberse subido como un cambio separado, etiquetando al autor original, para que fuera revisada
Dicho eso, que el colega lo escalara al jefe en vez de hablarlo directamente, y que el jefe pidiera revertirlo, es una señal bastante mala
Las respuestas posibles serían “No, porque…” o, rara vez, “Tienes razón, lo hago mañana”
Sin escándalo, todos evitan el problema y quizá alguien aprende algo
Pero el autor añadió un contexto importante: intercambió la posibilidad de cambios en los requisitos por una reducción de duplicación, y más adelante cada manejador de figura terminó necesitando muchos casos especiales y comportamientos propios
Esto muestra la diferencia entre DRY accidental y DRY esencial
Muchas veces las bibliotecas A y B parecen coherentes entre sí y por eso el código queda igual, pero en realidad puede ser una coincidencia
Aunque un wrapper alrededor de lib A y otro alrededor de lib B contengan el mismo código, si envuelven cosas completamente distintas, abstraerlos produce malos resultados
Lo mismo pasa con “15”. Si “todas las listas siempre deben mostrar 15 elementos”, tiene sentido abstraer el 15; pero si son “las 15 canciones principales de la página de inicio” y “los 15 comentarios predeterminados debajo de una canción”, agrupar ese 15 es una peor decisión
La segunda señal de alerta es que la refactorización en sí tampoco era una buena refactorización. Le falta modularidad, sigue acoplada y solo redujo un poco la duplicación
Puede sonar como “El código que escribiste era malo, así que lo reescribí. ¿Algún problema?”
La mejor solución probablemente sería pedir una explicación en forma de “revisión tardía”, proponer una solución y esperar que el autor original no simplemente la ignore
El punto central es reconocer que el código cambia en el futuro. Los requisitos cambian y el código relacionado también
Si creas una abstracción que limita esa capacidad de cambio, perjudicas la capacidad de tu yo futuro para adaptar la base de código
En especial, cuando invertiste mucho tiempo y esfuerzo en una abstracción, te apegas emocionalmente a ella y puedes terminar perdiendo más tiempo intentando encajar los cambios en una solución elegante
KISS es lo correcto
Este caso parece más bien el problema de “a nadie le gustan los presumidos”
Ocurre a menudo, sobre todo cuando un desarrollador junior todavía no aprendió que el trabajo no es solo entre él y la computadora, sino entre él, la computadora y otras personas
Una versión más limpia probablemente habría sido aceptada si hubiera tenido una conversación real con el autor original y hubieran acordado una mejor forma
El título del texto parece más bien aprendí a trabajar en equipo que “adiós, código limpio”
El código más complejo pudo haber sido percibido como una amenaza, y la persona como un competidor al que había que contener mediante la cadena de mando
Si eres junior, debes saber que esto depende del contexto. En muchos lugares de trabajo, la simplicidad importa más que la flexibilidad; se evitan las características confusas del lenguaje, y se prefieren las formas de trabajar que implican pasar mucho tiempo frente al teclado y lenguajes así
En cambio, las filosofías que valoran el aprendizaje continuo, la artesanía del software y la reflexión sobre el proceso chocan por completo con esos entornos
Los intentos de mejorar no son populares entre quienes no crecen
Ambas son decisiones de vida válidas. El software puede ser un lugar cómodo para trabajar 7 horas e irse a casa, y también puede ser un mundo de exploración donde los únicos límites son la imaginación y el intelecto
Si quieres descubrir cosas nuevas, ten cuidado con pasar décadas junto a personas que temen lo desconocido
Dicho más brevemente: si tus incentivos no están alineados con los de tus colegas, considera irte
Este texto tiene razones para ser querido durante mucho tiempo, y les abrió los ojos a muchos desarrolladores que caen en la misma trampa
Incluso si trabajas solo, puedes sobrediseñar una abstracción limpia y luego quedar atrapado por esa abstracción en el futuro
El problema es que los cambios se commitean directamente a la rama main sin revisión
Todo cambio de código debería tener revisión de pares, ya sea con programación en pareja o con un PR tradicional, para evitar escaladas como esta
El jefe fue un mal jefe. En lugar de regañar, debió haber notado que faltaba un buen proceso
En un equipo con cultura de revisión de código, si un novato demasiado entusiasta intentara algo así, un colega le habría dicho “gracias, pero esto no va” o “buena idea, probemos hacerlo así”
Por separado, creo que no se debería refactorizar código sin una buena razón. Incluso el código malo puede ser código que casi nunca haya que modificar
El mejor momento para refactorizar es cuando te das cuenta de que necesitas cambiar el comportamiento, pero la estructura existente lo vuelve demasiado difícil
Lamentablemente, parece que aquí se aprendió la lección equivocada
No entiendo por qué todos solo hablan de qué debería haber hecho él en su lugar. Esta es una lección suficientemente válida
En finanzas se trabaja todo el tiempo con productos que son algo parecidos, pero no exactamente iguales. Las opciones sobre acciones y las opciones de divisas son ambas opciones, tienen precio de ejercicio y se valoran con alguna versión de Black-Scholes, pero en la práctica son enormemente distintas
La tentación de alguien recién incorporado de querer unificar muchos comportamientos comunes es grande, y a menudo terminamos en la posición del jefe del texto original, diciéndole a un desarrollador joven que se calme y mantenga las cosas separadas
No se trata de despedirse del código limpio, sino de que hay que evitar la sobreabstracción para poder mantener código realmente limpio
Lo que hacen los manejadores genéricos son transformaciones afines, es decir, traslación, rotación, escalado y reflexión; si el escalado es uniforme, es una transformación de similitud. Está garantizado que eso funciona con cualquier figura
El verdadero problema es que no debes abstraer algo que no conoces lo suficiente. Especialmente si no sabes qué posibilidades pueden aparecer en el futuro
Por ejemplo, si crees que las elipses son solo círculos y elipses, te vas a sorprender bastante cuando aparezca el primer oval “real”
Como el autor no distingue activamente entre código sucio y código “no limpio”, los demás intentan hacer esa distinción
DRY tiene valor, pero la abstracción tiene costos. Siempre es una concesión
Una de las razones por las que me gustan lenguajes como Haskell es que el maximalismo de abstracción está instalado a nivel del lenguaje, no a nivel de codificación
Todos están de acuerdo sobre qué debe hacer Applicative. El desafío se mueve más a la derecha en la tubería que va de la idea al código: encontrar el Applicative adecuado para el problema
La mayoría de los patrones de diseño orientados a objetos tienen equivalentes naturales y bien definidos en este entorno, y ayudan a reducir las abstracciones específicas de cada proyecto
Para principiantes, el costo inicial de aprender el lenguaje es alto, y a menudo pasa que abres otro código Haskell y hay unas 12 extensiones del compilador que nunca habías visto y que tienes que entender
Aun así, como esas extensiones del compilador luego te las puedes llevar a todos los proyectos, lo veo como una ganancia neta
Para expertos, la desventaja es que hace falta un doctorado para tener una intuición de cuánta RAM va a consumir cats
Si un código usa Applicative o Monad, esas leyes te permiten saber cómo se comporta
Los patrones de diseño orientados a objetos no tienen esas leyes. Son conceptos o patrones definidos de forma laxa, abiertos a interpretaciones y personalizaciones individuales, pero no tienen leyes concretas que digan exactamente qué son ni qué comportamiento o salida deberías esperar
Pero no puedo evitar decir que, en ejemplos como el del blog, seguir el camino del código limpio muchísimas veces terminó haciendo que mi yo del futuro se disparara en el pie
Si más tarde tienes que estudiar la abstracción para volver a entenderla, esa abstracción no es beneficiosa
Donde la orientación a objetos encaja de forma más natural es en widgets de GUI e implementaciones de editores
La regla que integra las demás reglas de la orientación a objetos es no preguntes, ordena
En este ejemplo, los manejadores se compartirían de una forma conocida, y habría puntos de control específicos por figura. Si cambia un manejador de la caja, se podría llamar a
updateControlPoints(changedHandle), y si cambia un punto de control, se podría llamar aupdateBoxHandles(changedControlPoint)También serían posibles otras separaciones limpias, pero yo lo haría así
Pero no se suele llegar a un acuerdo sobre qué biblioteca de parsing usar, si usar
Data.Text.StrictoData.Text.Lazy, cuánto estilo point-free usar, si usar.y>>>u otros combinadores, o qué extensiones del lenguaje usarMe gusta Haskell, pero sufre problemas parecidos a otros lenguajes. La gente realmente no logra ponerse de acuerdo sobre la mejor forma de hacer las cosas
Obtienes el sistema de tipos de Haskell y rendimiento predecible
Bastaba con mover solo la parte matemática repetida a una función aparte y llamarla desde las funciones de redimensionamiento
Las propias funciones de redimensionamiento parecían repetidas porque constituían la interfaz, así que el refactor que las eliminaba estaba mal
Pero las matemáticas que realizaban esas funciones eran un objetivo totalmente válido para ordenar el código
El resultado es un montón de funciones pequeñas y puras, pero no es un problema porque son fáciles de probar y refactorizar
Hay muchas formas y patrones para resolver el mismo problema, pero desarmar una interfaz existente que otras personas usan no es un buen punto de partida
La ventaja de la abstracción es que puedes hacer el interior más flexible sin perjudicar el resto
Un buen primer paso es, como dijiste, crear por debajo funciones matemáticas más flexibles y dejar lo demás como está
Luego puedes hablar más a fondo con el equipo sobre cómo comunicar que la interfaz ofrece una fachada clara mientras la implementación queda abstraída
Lo que se describe aquí se siente como una mala abstracción
El problema real es que la gente diseña basándose en apenas dos o tres ejemplos, sin comprobar si es lo suficientemente general como para admitir usos futuros.
Hay que abstraer de abajo hacia arriba, no de arriba hacia abajo.
No hace falta crear un único punto donde todo ocurra y que maneje combinaciones
2^n.Aun así se puede reducir la repetición, cambiar en un solo punto y conservar la capacidad de hacer modificaciones a medida cuando sea necesario.
Por eso no basta con decir “evita las malas abstracciones” o “haz que admita usos futuros”.
En general, nadie puede predecir el futuro.
Las abstracciones deben usarse con extremo cuidado incluso cuando parecen correctas. Es mejor ser extremadamente escéptico.
Si tocas algo en el código matemático, afecta a todo.
En algún punto se vuelve más fácil dejar algo de código duplicado, señalar las similitudes con pistas en la estructura del código o con comentarios y, con el tiempo, destacar en comentarios las diferencias clave.
El enfoque de código limpio para este tipo de código no maneja bien los requisitos cambiantes.
Al final es fácil terminar con un montón de funciones auxiliares que contienen varias rutas de código según desde dónde se llamen.
A veces esto se puede abstraer y, como algunos ya habrán notado, la oración anterior describe el polimorfismo orientado a objetos.
Pero ¿cómo podrías conocer de antemano los requisitos exactos? Intentar hacer “código limpio” en una etapa tan temprana solo termina en malas abstracciones o sobrediseño.
El enfoque correcto es no hacer nada, anotar los posibles problemas y esperar hasta saber más sobre el dominio del problema.
No me preocupa demasiado el código un poco duplicado pero fácil de entender, como en el ejemplo original.
Pero sí me preocupa si tengo que corregir el mismo issue en tres lugares o agregar código de una nueva funcionalidad en tres lugares.
En ese momento empiezo a pensar si puedo empujar de alguna manera el código que hay que cambiar o agregar hacia una ubicación común.
La gente lee sobre patrones de diseño o DRY y, como está aprendiendo, por lo general los aplica mal y luego suele culpar a las fuentes originales.
Rob Pike dijo alguna vez que una de las razones por las que Go terminó siendo así es que “una copia pequeña es mejor que una dependencia pequeña”.
Cuanto más programo, más entiendo lo que quiso decir.
La repetición en sí no es el problema. Lo importante es entender la unidad que estás leyendo ahora y que quizá vas a modificar.
DRY a menudo significa agregar otra abstracción, y si esa abstracción no es lo suficientemente ortogonal, ahora tienes que considerar dos unidades en lugar de una, así que empeoraste la situación.