Ocultación y transferencia de complejidad en la "optimización de la mantenibilidad"
Cuando la complejidad simplemente se traslada de funciones grandes a la jerarquía de clases, la configuración y las cadenas de llamadas, el sistema generalmente no es más fácil de mantener.
Cuando muchos equipos realizan “optimización de la capacidad de mantenimiento”, el primer paso es desarmar el código.
Una función de 300 líneas se dividió en 12 clases; se cambió un proceso con muchas ramas a “estrategia + fábrica + configuración”; una lógica que podía entenderse siguiendo la llamada se transformó en eventos, suscriptores, tablas de reglas y varios directorios de apariencia limpia.
De hecho, el código está menos lleno y un solo archivo es más corto. Al revisar, incluso le da a la gente la sensación de “esto es avanzado”.
Pero mi opinión es: **Muchas de las llamadas optimizaciones de mantenibilidad no reducen la complejidad, solo cambian la complejidad de parcialmente visible a dispersa, inestable y oculta. ** El resultado más común de este tipo de cambio es que el problema es más difícil de localizar, el cambio es más difícil de evaluar y es más difícil de entender para las personas nuevas.
El núcleo de la mantenibilidad siempre ha sido: **Cuando los requisitos cambian, ocurren errores en línea y surgen condiciones límite, ¿puede el equipo ver rápidamente las limitaciones reales y realizar modificaciones seguras dentro de un alcance limitado? **
La complejidad no desaparecerá debido a la división, simplemente permanecerá en otro lugar.
Una situación común es que la intuición de “mantenibilidad” sea demasiado visual.
Se sienten incómodos cuando ven funciones grandes, se sienten atrasados cuando ven muchos si/si no, e instintivamente quieren destrozarlos cuando ven múltiples juicios de negocios metidos en una clase. Así que la lógica compleja se dividió en muchos archivos delgados, las ramas condicionales se tradujeron a niveles de objetos, las reglas de negocios se trasladaron a configuraciones y se agregaron una pequeña interfaz y nombres, y la superficie del código quedó inmediatamente más limpia.
El problema es que, aunque las 300 líneas de código originales son feas, la complejidad al menos está repartida en el escritorio. Al leer de arriba a abajo, puede ver cómo se conectan las condiciones de la rama, el estado compartido, el manejo de excepciones y los resultados finales.
Una vez que se descompone la complejidad, la situación cambia:
- Debes recorrer 7 archivos a lo largo de la cadena de llamadas para saber dónde se cambió finalmente un campo;
- Debe comprender la interfaz, la clase de implementación, la lógica de registro y el ensamblaje en tiempo de ejecución al mismo tiempo para confirmar qué rama está tomando;
- A primera vista, parece que las reglas de negocio están en el código y la mitad de los resultados están en YAML, la mitad en la base de datos y la otra mitad en una tabla de mapeo generada al inicio.
La complejidad no ha disminuido, pero ha pasado de “un poco cansador al leer” a “mucho más lento a la hora de localizar problemas”.
El costo de mantenimiento generalmente se liquida tres meses después, cuando alguien corrige la lógica incorrecta, falla en línea y soluciona el problema del enlace.
El error de juicio más común de los equipos es confundir “limpieza parcial” con “mantenibilidad general”
Este tipo de error de juicio es común porque muchos beneficios de la refactorización parecen ser reales en el corto plazo.
Por ejemplo, una función que contiene múltiples ramas de procesamiento de pedidos se puede cambiar a la siguiente estructura:
Handler h = handlerFactory.get(order.type());
h.validate(order);
h.price(order);
h.persist(order);
h.notify(order);
Este código ciertamente parece más limpio que una larga lista de ramas.
Pero la verdadera pregunta es:
- Cómo decidir qué implementación utilizar para
handlerFactory; - ¿Existe algún requisito previo compartido entre
validate/price/persist/notify; - Si se permite una deriva de comportamiento entre diferentes implementaciones;
- ¿Debería realizarse un determinado cambio de requisitos en un lugar, cuatro lugares o una docena de lugares?
Si estos problemas no están restringidos, entonces este tipo de “estructura elegante” a menudo simplemente reescribe las diferencias comerciales que originalmente se escribieron explícitamente en if/else en diferencias implícitas dispersas en la jerarquía de clases.
Desde una perspectiva de revisión, se vuelve más limpio; Desde una perspectiva de mantenimiento, se vuelve más dependiente del contexto.
**La mantenibilidad se refiere a si es más fácil responder a todo el sistema “¿Dónde afectará este cambio?” **
Lo que realmente determina los costes de mantenimiento suelen ser cuatro cosas
Prefiero utilizar las siguientes cuatro preguntas para juzgar si una refactorización hace que el sistema sea más mantenible.
1. Cuando ocurre el problema, ¿la ruta de ubicación es más corta?
Al informar en línea un problema de que “ciertos tipos de pedidos ocasionalmente emiten cupones duplicados”, lo más valioso es si el ingeniero puede encontrar rápidamente: dónde están las condiciones de juicio, dónde está la protección idempotente y dónde se activan los efectos secundarios.
Si después de la división, la ruta de solución de problemas cambia de “ver una función” a “ver la definición de la interfaz, encontrar la clase de implementación, verificar el ensamblaje, rastrear eventos y cambiar la configuración”, entonces el costo de mantenimiento en realidad aumentará.
2. Cuando los requisitos cambian, ¿el alcance de la modificación es más convergente?
Una buena abstracción mantiene los cambios enfocados. La mala abstracción permite que el cambio se propague.
El peor tipo de refactorización es dividir la lógica en múltiples responsabilidades en la superficie. De hecho, cada vez que cambian los requisitos, deben cambiarse simultáneamente: definición de reglas, registro de fábrica, configuración predeterminada, muestras de prueba y puntos de monitoreo. El archivo se ha hecho más pequeño, pero el área de cambio se ha hecho más grande.
Este tipo de sistema parece modular, pero en realidad es más frágil, porque cada vez que haces un cambio, debes apostar a que no te has perdido ningún rincón.
3. ¿Las limitaciones se están volviendo más visibles en lugar de ocultas?
La razón por la que mucha lógica empresarial es difícil es que en la superficie parece que el código es feo, pero en realidad está más cerca de eso y tiene muchos requisitos previos:
- Este estado sólo puede ir de A a B, no directamente a C;
- Este campo sólo puede ser modificado por determinados tipos de clientes;
- Esta acción debe tener éxito con otro efecto secundario.
Si después de la refactorización, estas restricciones ya no aparecen en un solo lugar, sino que están dispersas en múltiples clases, anotaciones, configuraciones u oyentes, entonces existe el riesgo de amnesia.
4. ¿La retroalimentación de la prueba se acerca más al comportamiento real?
Muchas “optimizaciones de mantenimiento” conducirán convenientemente a un conjunto de pruebas únicas que son fáciles de escribir porque cada clase es más pequeña y se burlan de las dependencias.
Sin embargo, el aumento del número de pruebas individuales no significa que el sistema sea más fácil de mejorar.
Si la prueba sólo puede demostrar que “esta clase devolverá el valor esperado en el mundo simulado” pero no puede cubrir las relaciones de ensamblaje, el estado compartido y las restricciones de tiempo en el proceso real, entonces se trata más de proteger la estructura que de proteger el comportamiento.
Un malentendido común: para eliminar si/si no, reescriba las diferencias comerciales en un sistema de tipos
Por supuesto, if/else puede estar mal escrito, pero “eliminar if/else” no es un objetivo en sí mismo.
He visto muchos sistemas que originalmente tenían sólo dos o tres ramas claras y una semántica empresarial muy estable. Sin embargo, para seguir el patrón de diseño correcto, se dividieron en interfaces de políticas, clases base abstractas, centros de registro y puntos de extensión. Medio año después, el número de tipos aumentó de 3 a 9, pero a quienes llamaban se les hizo cada vez más difícil juzgar qué diferencias eran diferencias comerciales reales y cuáles eran simplemente diferencias estructurales remanentes de la evolución histórica.
En muchos casos, tener muchas ramas no significa que se deba adoptar el modelo de objetos; sólo significa que aquí hay criterio empresarial. Lo primero que hay que hacer es distinguir cuáles de estos juicios son ejes estables de cambio y cuáles son sólo bifurcaciones condicionales en un mismo proceso.
Si se trata solo de unos pocos juicios condicionales en un proceso, forzarlos a estar “orientados a objetos” probablemente simplemente reescribirá las condiciones que se pueden ver de un vistazo en varias capas de distribución de métodos.
**Ocultar la condición en polimorfismo no hará que la condición desaparezca, solo hará que el lector se dé cuenta de su existencia más adelante. **
Otro malentendido común: tratar la configuración como una papelera de reciclaje compleja
Otro enfoque que es particularmente fácil de confundir con “más fácil de mantener” es configurar reglas comerciales tanto como sea posible.
Las razones suelen ser muy buenas: no es necesario cambiar el código en el futuro, la operación es configurable y la expansión es más flexible.
Pero la configuración no es intrínsecamente más barata, simplemente traslada la complejidad del tiempo de compilación al tiempo de ejecución.
Una vez que la configuración de una regla comienza a asumir demasiada responsabilidad, pueden surgir rápidamente estos problemas:
- Existe una relación de prioridad y cobertura entre configuraciones, pero no hay un lugar en el sistema donde se puedan ver completamente;
- Los escenarios afectados por un cambio sólo pueden verificarse en línea;
- Los valores de configuración legales no significan una semántica correcta; los errores quedarán expuestos en tiempo de ejecución;
- La revisión del código se convierte en “No puedo entender lo que significa este JSON”.
Si una regla cambia con frecuencia, pero el cambio aún requiere juicio de ingeniería, pruebas de vinculación y planes de reversión, entonces es esencialmente un problema de código y no se convertirá repentinamente en un elemento de mantenimiento de bajo costo solo porque esté escrito en la configuración.
Un costo común de la configuración excesiva es que “ya nadie se atreve a tocar el sistema”.
Contraejemplo: algunas abstracciones harán que el sistema sea más fácil de mantener
Tampoco se puede decir “No abstraigas, no dividas”.
Hay situaciones en las que la abstracción no sólo vale la pena, sino que es necesaria.
Por ejemplo:
- Hacer frente a ejes de cambio estables y claros, como diferentes backends de almacenamiento, diferentes canales de pago y diferentes protocolos de serialización;
- Estos cambios realmente necesitan ser reemplazados en tiempo de ejecución, en lugar de simplemente “posibles extensiones más adelante” imaginarias;
- Cada implementación puede cumplir con el mismo conjunto de fuertes restricciones, en lugar de tener aparentemente la misma interfaz pero en realidad tener una semántica diferente;
- Los límites del equipo también siguen límites abstractos, y se pueden desarrollar y probar diferentes módulos de forma independiente.
El valor de la abstracción en este punto es que realmente reduce la fricción de cambios futuros.
De manera similar, si una función larga es responsable de la verificación de parámetros, la toma de decisiones comerciales, la orquestación de efectos secundarios y la compensación de excepciones, generalmente es correcto dividirla en varios pasos con límites claros. La premisa es que después del desmontaje, la columna vertebral del proceso aún es visible y las limitaciones clave no están ocultas.
Así que la pregunta siempre ha sido: una vez completada la demolición, la complejidad queda contenida o simplemente se transfiere a otros rincones cognitivos. **
Un método de juicio más práctico: primero observe las modificaciones más comunes en el futuro, no mire primero la limpieza estructural actual
Si sospecho que una “refactorización de mantenibilidad” es sólo un pulido estructural, normalmente comienzo haciendo tres preguntas muy prácticas:
- La próxima vez que el producto cambie este requisito, ¿cuáles son los cambios más probables para los ingenieros?
- Si algo sale mal en línea la próxima vez, ¿qué enlace debería mirar primero la persona de turno?
- Si una nueva persona asume el control, primero debe comprender las reglas comerciales o la estructura del marco.
Si las respuestas a estas tres preguntas se vuelven más complicadas, entonces existe una alta probabilidad de que esta refactorización no mejore la mantenibilidad.
La mantenibilidad es para costos de modificación futuros, no para las capturas de pantalla del código actual.
Resumen
La razón por la que muchas “optimizaciones de mantenimiento” son peligrosas es que en la superficie parecen completamente inútiles, pero en realidad está mucho más cerca de que sean demasiado fáciles de parecer parcialmente correctas.
Hay más clases, las funciones se han acortado, el directorio se ha vuelto más ordenado y el proceso de revisión se ha vuelto más fluido. Pero el costo real de mantenimiento proviene de la comprensión, el posicionamiento, la modificación y la verificación, no de la pulcritud visual.
Entonces mi sugerencia es muy simple: ** No desmantele la lógica compleja hasta que sea invisible, primero desmantele la lógica compleja hasta que pueda cambiarse. **
Si una refactorización simplemente traslada la complejidad del archivo actual a la cadena de llamadas, la capa de configuración y la capa de abstracción, generalmente no mejora la capacidad de mantenimiento, sino que simplemente retrasa el dolor de la siguiente solución de problemas.
What to read next
Want more posts about Maintainability?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #Engineering Practice?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home