Los grandes desarrolladores saben cuándo y cómo refactorizar el código de Rails

Publicado: 2022-03-11

Refactorizar a gran escala: ¿Por qué harías algo así?

Si no está roto, no lo arregles.

Es una frase bien conocida, pero como sabemos, la mayor parte del progreso tecnológico humano fue hecho por personas que decidieron arreglar lo que no estaba roto. Especialmente en la industria del software, se podría argumentar que la mayor parte de lo que hacemos es arreglar lo que no está roto.

Arreglar la funcionalidad, mejorar la interfaz de usuario, mejorar la velocidad y la eficiencia de la memoria, agregar características: todas estas son actividades para las cuales es fácil ver si vale la pena hacerlas, y luego nosotros, como desarrolladores experimentados de Rails, argumentamos a favor o en contra de dedicar nuestro tiempo a ellas. Sin embargo, hay una actividad, que en su mayor parte cae en un área gris: la refactorización estándar y, especialmente, la refactorización de código a gran escala.

El término refactorización a gran escala merece una explicación. Exactamente lo que se puede considerar "a gran escala" variará de un caso a otro, ya que el término es un poco vago, pero considero que cualquier cosa que afecte significativamente a más de unas pocas clases, o a más de un subsistema, y ​​su interfaz sea "grande". .” Por otro lado, cualquier refactorización de Rails que permanezca oculta detrás de la interfaz de una sola clase definitivamente sería "pequeña". Por supuesto, hay muchas áreas grises en el medio. Finalmente, confía en tu instinto, si temes hacerlo, entonces probablemente sea "grande".

La refactorización, por definición, no produce ninguna funcionalidad visible, nada que pueda mostrarle al cliente, ningún entregable. En el mejor de los casos, pueden producir pequeñas mejoras en la velocidad y el uso de la memoria, pero ese no es el objetivo principal. Se podría decir que el objetivo principal es el código con el que está satisfecho. Pero debido a que está reorganizando el código de tal manera que tiene consecuencias de gran alcance en toda la base de código, existe la posibilidad de que se desate el infierno y haya problemas. Por supuesto, de ahí proviene el pavor que mencionamos. ¿Alguna vez le presentó a alguien nuevo su base de código y después de que le preguntaron acerca de un código peculiarmente organizado, respondió con algo como:

Sí, este es un código heredado que tenía sentido en ese momento, pero las especificaciones cambiaron y ahora es demasiado costoso arreglarlo.

Tal vez incluso les diste una mirada muy seria y les dijiste que lo dejaran y no lo tocaran.

Al planificar cómo refactorizar el código de Rails, es posible que necesite un gráfico complejo para comenzar.

La pregunta, "¿Por qué querríamos hacerlo?" es natural y puede ser tan importante como hacerlo...

La pregunta, "¿Por qué querríamos hacerlo?" es natural y puede ser tan importante como el proceso de refactorización porque, con bastante frecuencia, hay otras personas a las que debe convencer para que le permitan dedicar su costoso tiempo a la refactorización. Entonces, consideremos casos en los que le gustaría hacerlo y los beneficios que se obtendrán:

Mejoras de rendimiento

Está satisfecho con la organización actual de su código desde el punto de mantenibilidad, pero sigue causando problemas de rendimiento. Es demasiado difícil optimizar la configuración actual y los cambios serían muy frágiles.

Solo hay una cosa que hacer aquí y eso es perfilarlo extensamente. Ejecute puntos de referencia y haga estimaciones sobre cuánto ganará y luego intente estimar cómo se traducirá en ganancias concretas. A veces incluso puede darse cuenta de que la refactorización de código propuesta no vale la pena. Otras veces tendrá datos duros y fríos para respaldar su caso.

Mejoras arquitectónicas

Tal vez la arquitectura esté bien pero algo desactualizada, o tal vez sea tan mala que te estremezcas cada vez que tocas esa parte del código base. Funciona bien y rápido, pero es una molestia agregar nuevas funciones. En ese dolor reside el valor comercial de la refactorización. El "dolor" también significa que el proceso de refactorización llevará más tiempo para agregar nuevas funciones, tal vez mucho más.

Y hay un beneficio que se puede obtener. Haga estimaciones del costo/beneficio de algunas características de muestra con y sin su gran refactorización propuesta. Explique que se aplicarán diferencias similares a la mayoría de las próximas características que tocarán esa parte del sistema ahora y para siempre en el futuro mientras se desarrolla el sistema. Sus estimaciones pueden ser incorrectas, ya que a menudo lo son en el desarrollo de software, pero sus proporciones probablemente estarán en el estadio de béisbol.

Poniéndolo al día

A veces, el código está inicialmente bien escrito. Estás muy contento con él. Es rápido, eficiente en memoria, fácil de mantener y bien alineado con las especificaciones. Inicialmente. Pero luego cambian las especificaciones, los objetivos comerciales cambian o aprende algo nuevo sobre sus usuarios finales que invalida sus suposiciones iniciales. El código aún funciona bien y aún está bastante contento con él, pero algo es incómodo cuando lo mira en el contexto del producto final. Las cosas se colocan en el subsistema ligeramente incorrecto o las propiedades se encuentran en la clase incorrecta, o tal vez algunos nombres ya no tengan sentido. Ahora están cumpliendo una función que, en términos comerciales, tiene un nombre completamente diferente. Sin embargo, todavía es muy difícil justificar cualquier tipo de refactorización de Rails a gran escala, ya que el trabajo involucrado estará a la misma escala que cualquiera de los otros ejemplos, pero los beneficios son mucho menos tangibles. Cuando lo piensas, ni siquiera es tan difícil mantenerlo. Solo tienes que recordar que algunas cosas son en realidad otra cosa. Solo debe recordar que A en realidad significa B y la propiedad Y en A en realidad se relaciona con C.

Y aquí radica el verdadero beneficio. En el campo de la neuropsicología hay muchos experimentos que sugieren que nuestra memoria a corto plazo o de trabajo es capaz de contener sólo 7+/-2 elementos, siendo uno de ellos el experimento de Sternberg. Cuando estudiamos un tema comenzamos con elementos básicos y, inicialmente, cuando pensamos en conceptos de nivel superior tenemos que pensar en sus definiciones. Por ejemplo, considere un término simple "contraseña SHA256 salada". Inicialmente, tenemos que mantener en nuestra memoria de trabajo las definiciones de "salado" y "SHA256" y tal vez incluso una definición de "función hash". Pero una vez que entendemos completamente el término, ocupa solo un espacio en la memoria porque lo entendemos intuitivamente. Esa es una de las razones por las que necesitamos comprender completamente los conceptos de nivel inferior para poder razonar sobre los de nivel superior. Lo mismo ocurre con los términos y definiciones específicos de nuestro proyecto. Pero si tenemos que recordar la traducción al significado real cada vez que discutimos nuestro código, esa traducción está ocupando otra de esas preciosas ranuras de memoria de trabajo. Produce carga cognitiva y hace que sea más difícil razonar a través de la lógica en nuestro código. A su vez, si es más difícil razonar, significa que hay una mayor posibilidad de que pasemos por alto un punto importante e introduzcamos un error.

Y no olvidemos los efectos secundarios más obvios. Existe una buena posibilidad de confusión cuando se discuten los cambios con nuestro cliente o cualquier persona que esté familiarizada con los términos comerciales correctos. Las personas nuevas que se unan al equipo deben familiarizarse tanto con la terminología comercial como con sus contrapartes en el código.

Creo que estas razones son muy convincentes y justifican el costo de la refactorización en muchos casos. Aún así, tenga cuidado, puede haber muchos casos extremos en los que debe usar su mejor juicio para determinar cuándo y cómo refactorizar.

En última instancia, la refactorización a gran escala es buena por las mismas razones por las que a muchos de nosotros nos gusta comenzar un nuevo proyecto. Miras ese archivo de origen en blanco y un mundo nuevo y valiente comienza a girar en tu mente. Esta vez lo harás bien, el código será elegante, estará bellamente diseñado y será rápido, robusto y fácilmente extensible, y lo más importante, será un placer trabajar con él todos los días. La refactorización, a pequeña y gran escala, le permite recuperar ese sentimiento, dar nueva vida a un código base antiguo y pagar esa deuda técnica.

Finalmente, es mejor si la refactorización está impulsada por planes para facilitar la implementación de una característica nueva determinada. En ese caso, la refactorización estará más enfocada y gran parte del tiempo dedicado a la refactorización también se recuperará de inmediato a través de una implementación más rápida de la función en sí.

Preparación

Asegúrese de que la cobertura de su prueba sea muy buena en todas las áreas del código base que es probable que toque. Si ve ciertas partes que no están bien cubiertas, primero dedique un tiempo a mencionar la cobertura de la prueba. Si no tiene ninguna prueba, primero debe dedicar tiempo a crear esas pruebas. Si no puede crear un conjunto de pruebas adecuado, concéntrese en las pruebas de aceptación y escriba tantas como pueda, y asegúrese de escribir pruebas unitarias mientras refactoriza. Teóricamente, puede hacer la refactorización del código sin una buena cobertura de prueba, pero requerirá que haga muchas pruebas manuales y que las haga con frecuencia. Tomará mucho más tiempo y será más propenso a errores. En última instancia, si la cobertura de su prueba no es lo suficientemente buena, el costo de realizar una refactorización de Rails a gran escala podría ser tan alto que, lamentablemente, debería considerar no hacerlo en absoluto. En mi opinión, ese es un beneficio de las pruebas automatizadas que no se enfatiza con suficiente frecuencia. Las pruebas automatizadas le permiten refactorizar con frecuencia y, lo que es más importante, ser más audaz al respecto.

Una vez que se haya asegurado de que la cobertura de su prueba sea buena, es hora de comenzar a planificar los cambios. Al principio no deberías estar haciendo ninguna codificación. Debe trazar aproximadamente todos los cambios involucrados y rastrear todas las consecuencias a través de la base de código, así como cargar el conocimiento sobre todo eso en su mente. Su objetivo es comprender exactamente por qué está cambiando algo y el papel que desempeña en el código base. Si te tropiezas cambiando cosas solo porque parece que necesitan ser cambiadas o porque algo se rompió y esto parece arreglarlo, es probable que termines en un callejón sin salida. El nuevo código parece funcionar, pero de forma incorrecta, y ahora ni siquiera puede recordar todos los cambios que ha realizado. En este punto, es posible que deba abandonar el trabajo que ha realizado en la refactorización de código a gran escala y, básicamente, ha perdido el tiempo. Así que tómese su tiempo y explore el código para comprender las ramificaciones de cada cambio que está a punto de realizar. Pagará generosamente al final.

Necesitarás una ayuda para el proceso de refactorización. Puede que prefieras otra cosa, pero a mí me gusta una simple hoja de papel en blanco y un bolígrafo. Comienzo escribiendo el cambio inicial que quiero hacer en la parte superior izquierda del papel. Luego empiezo a buscar todos los lugares afectados por el cambio y los anoto debajo del cambio inicial. Es importante aquí usar su juicio. En última instancia, las notas y los diagramas en el papel están ahí para usted, así que elija el estilo que mejor se adapte a su memoria. Escribo fragmentos de código cortos con viñetas debajo de ellos y muchas flechas que conducen a otras notas similares que indican cosas que dependen de él directamente (flecha completa) o indirectamente (flechas discontinuas). También anoto las flechas con marcas abreviadas como recordatorio de algo específico que noté en el código base. Recuerde, solo regresará a esas notas durante los próximos días mientras realiza los cambios planificados en ellas y está perfectamente bien usar recordatorios muy cortos y crípticos para que usen menos espacio y sean más fáciles de diseñar en el papel. . Algunas veces estaba limpiando mi escritorio meses después de una refactorización de Rails y encontré uno de esos papeles. Era un completo galimatías, no tenía ni idea de lo que significaba nada en ese papel, excepto que podría haber sido escrito por alguien que se había vuelto loco. Pero sé que ese papel era indispensable mientras trabajaba en el problema. Además, no crea que necesita escribir cada cambio. Puede agruparlos y rastrear los detalles de una manera diferente. Por ejemplo, en su documento principal, puede notar que necesita "cambiar el nombre de todas las ocurrencias de Ab a Cd" y luego puede rastrear los detalles de varias maneras diferentes. Puede escribirlos todos en una hoja de papel separada, puede planear realizar una búsqueda global para todas las apariciones una vez más, o simplemente puede dejar abiertos todos los archivos de origen donde se debe realizar el cambio en el editor de su elección. y haga una nota mental para revisarlos una vez que termine de mapear los cambios.

Cuando mapee las consecuencias de su cambio inicial, por la naturaleza de su gran escala, lo más probable es que identifique cambios adicionales que tengan consecuencias adicionales. Repita el análisis para ellos también, anotando todos los cambios dependientes. Dependiendo del tamaño de los cambios, puede escribirlos en la misma hoja de papel o elegir una nueva en blanco. Una cosa muy importante que debe intentar y hacer mientras mapea los cambios es tratar de identificar los límites donde realmente puede detener los cambios de ramificación. Desea limitar la refactorización al conjunto de cambios redondeado y sensato más pequeño. Si ve un punto en el que puede detenerse y dejar el resto como está, hágalo incluso si ve que debe refactorizarse, incluso si está relacionado en concepto con sus otros cambios. Termine esta ronda de refactorización de código, pruebe minuciosamente, implemente y regrese por más. Debe buscar activamente esos puntos para mantener manejable el tamaño de los cambios. Por supuesto, como siempre, haga una llamada de juicio. Muy a menudo llegué a un punto en el que podía cortar el proceso de refactorización agregando algunas clases de proxy para hacer un poco de traducción de la interfaz. Incluso comencé a implementarlos cuando me di cuenta de que serían tanto trabajo como llevar la refactorización un poco más lejos hasta el punto en que habrá un punto de "parada natural" (es decir, casi no se necesita código proxy). Luego retrocedí, revirtiendo mis últimos cambios y refactorizando. Si todo suena un poco como mapear un territorio inexplorado es porque siento que lo es, excepto que los mapas de territorio son solo bidimensionales.

Ejecución

Una vez que haya hecho su preparación de refactorización, es hora de ejecutar el plan. Asegúrese de que su concentración esté alta y asegure un entorno libre de distracciones. A veces llego a desconectar por completo la conexión a Internet en este punto. La cuestión es que, si te has preparado bien, tienes un buen conjunto de notas en el papel a tu lado, ¡y tu concentración aumentará! A menudo puede moverse muy rápido a través de los cambios de esta manera. En teoría, la mayor parte del trabajo se hizo de antemano, durante la preparación.

Una vez que esté refactorizando el código, preste atención a los fragmentos de código extraños que hacen algo muy específico y pueden parecer un código incorrecto. Tal vez sea un código incorrecto, pero con bastante frecuencia en realidad están manejando un caso de esquina extraño que se descubrió mientras investigaba un error en producción. Con el tiempo, a la mayoría del código de Rails le crecen "pelos" o "verrugas" que manejan errores extraños de casos de esquina, por ejemplo, un código de respuesta extraño aquí que tal vez sea necesario para IE6 o una condición allí que maneja un error de tiempo extraño. No son importantes para el panorama general, pero siguen siendo detalles significativos. Idealmente, se cubren explícitamente con pruebas unitarias, si no, trate de cubrirlas primero. Una vez tuve la tarea de migrar una aplicación de tamaño mediano de Rails 2 a Rails 3. Estaba muy familiarizado con el código, pero estaba un poco complicado y había que tener en cuenta muchos cambios, así que opté por la reimplementación. En realidad, no fue una reimplementación real, ya que casi nunca es un movimiento inteligente, pero comencé con una aplicación de Rails 3 en blanco y refactoricé segmentos verticales de la aplicación anterior en la nueva, utilizando aproximadamente el proceso descrito. Cada vez que terminaba un corte vertical, revisaba el antiguo código de Rails, observaba cada línea y verificaba dos veces que tuviera su contraparte en el nuevo código. Básicamente, estaba seleccionando todos los "pelos" del código anterior y replicándolos en la nueva base de código. Al final, el nuevo código base abordó todos los casos de esquina.

Asegúrese de realizar pruebas manuales con la suficiente frecuencia. Lo obligará a buscar "interrupciones" naturales en el proceso de refactorización que le permitirán probar una parte del sistema y le dará la confianza de que no rompió nada que no esperaba romper en el proceso. .

Envuélvelo

Una vez que haya terminado de refactorizar su código de Rails, asegúrese de revisar todos los cambios por última vez. Mire la diferencia completa y revísela. Muy a menudo, notará cosas sutiles que pasó por alto al comienzo de la refactorización porque no tenía el conocimiento que tiene ahora. Es un buen beneficio de la refactorización a gran escala: obtienes una imagen mental más clara de la organización del código, especialmente si no lo escribiste originalmente.

Si es posible, haga que un colega lo revise también. Ni siquiera tiene que estar particularmente familiarizado con esa parte exacta del código base, pero debe tener una familiaridad general con el proyecto y su código. Tener un nuevo par de ojos en los cambios puede ayudar mucho. Si no puede lograr que otro desarrollador los mire, tendrá que pretender ser uno usted mismo. Duerme bien por la noche y revísalo con una mente fresca.

Si no tiene control de calidad, también tendrá que usar ese sombrero. De nuevo, tómate un descanso y distánciate del código y luego regresa para realizar pruebas manuales. Acabas de pasar por el equivalente a entrar en un gabinete de cableado eléctrico desordenado con un montón de herramientas y arreglarlo todo, posiblemente cortando y volviendo a cablear cosas, por lo que se debe tener un poco más de cuidado de lo habitual.

Finalmente, disfrute de los frutos de su trabajo considerando todos los cambios planificados que ahora serán mucho más limpios y fáciles de implementar.

¿Cuándo no lo harías?

Si bien hay muchos beneficios al realizar una refactorización a gran escala con regularidad para mantener el código del proyecto actualizado y de alta calidad, sigue siendo una operación muy costosa. También hay casos en los que no sería aconsejable:

La cobertura de su prueba es pobre

Como se mencionó: una cobertura de prueba muy pobre podría ser un gran problema. Use su propio criterio, pero podría ser mejor a corto plazo centrarse en aumentar la cobertura mientras trabaja en nuevas funciones y realiza tantas refactorizaciones localizadas a pequeña escala como sea posible. Eso lo ayudará mucho una vez que decida dar el paso y ordenar partes más grandes de la base de código.

La refactorización no está impulsada por una nueva función y la base de código no ha cambiado en mucho tiempo

Usé el tiempo pasado en lugar de decir "el código base no cambiará" a propósito. A juzgar por la experiencia (y por experiencia me refiero a equivocarse muchas veces), casi nunca puede confiar en sus predicciones sobre cuándo será necesario cambiar una determinada parte del código base. Por lo tanto, haga lo siguiente mejor: mire al pasado y asuma que el pasado se repetirá. Si algo no se ha cambiado durante mucho tiempo, probablemente no necesite cambiarlo ahora. Espera a que llegue ese cambio y trabaja en otra cosa.

Estás presionado por el tiempo

El mantenimiento es la parte más costosa del ciclo de vida del proyecto y la refactorización lo hace menos costoso. Es absolutamente necesario que cualquier negocio utilice la refactorización para reducir la deuda técnica y abaratar el mantenimiento futuro. De lo contrario, corre el peligro de entrar en un círculo vicioso en el que cada vez es más caro añadir nuevas funciones. Espero que sea evidente por qué eso es malo.

Dicho esto, la refactorización a gran escala es muy, muy impredecible cuando se trata de cuánto tiempo llevará, y no debe hacerlo a medias. Si por alguna razón interna o externa tiene poco tiempo y no está seguro de poder terminar dentro de ese plazo, es posible que deba abandonar la refactorización. La presión y el estrés, especialmente del tipo inducido por el tiempo, conducen a un nivel más bajo de concentración, que es absolutamente necesario para la refactorización a gran escala. Trabaje para obtener más "compra" de su equipo para reservar tiempo para ello y busque en su calendario un período en el que tendrá tiempo. No es necesario que sea un tramo continuo de tiempo. Por supuesto, tendrá otros problemas que resolver, pero esos descansos no deberían durar más de uno o dos días. Si es así, tendrá que recordar su propio plan porque comenzará a olvidar lo que aprendió sobre el código base y exactamente dónde se detuvo.

Conclusión

Espero haberte dado algunas pautas útiles y haberte convencido de los beneficios, y me atrevo a decir necesidad, de realizar refactorizaciones a gran escala en ciertas ocasiones. El tema es muy vago y, por supuesto, nada de lo que se dice aquí es una verdad definitiva y los detalles variarán de un proyecto a otro. Traté de dar consejos que, en mi opinión, son de aplicación general, pero como siempre, considere su caso particular y use su propia experiencia para adaptarse a sus desafíos específicos. ¡Buena suerte refactorizando!