¿Cómo evito las refactorizaciones en cascada?

52

Tengo un proyecto. En este proyecto, quise refactorizarlo para agregar una característica, y he refactorizado el proyecto para agregar la característica.

El problema es que cuando terminé, resultó que tenía que hacer un cambio de interfaz menor para acomodarlo. Así que hice el cambio. Y luego, la clase consumidora no se puede implementar con su interfaz actual en términos de la nueva, por lo que también necesita una nueva interfaz. Ahora han pasado tres meses, y he tenido que solucionar innumerables problemas virtualmente no relacionados, y estoy resolviendo problemas que se planearon en un año o simplemente no se solucionarán debido a dificultades antes de que se compile. de nuevo.

¿Cómo puedo evitar este tipo de refactorizaciones en cascada en el futuro? ¿Es solo un síntoma de mis clases anteriores dependiendo demasiado el uno del otro?

Edición breve: en este caso, el refactor era la característica, ya que el refactor aumentó la extensibilidad de una pieza particular de código y disminuyó algunos acoplamientos. Esto significaba que los desarrolladores externos podían hacer más, que era la característica que quería ofrecer. Por lo tanto, el refactor original en sí no debería haber sido un cambio funcional.

Edición más grande que prometí hace cinco días:

Antes de comenzar este refactor, tenía un sistema donde tenía una interfaz, pero en la implementación, simplemente dynamic_cast a través de todas las implementaciones posibles que envié. Obviamente, esto significaba que no podía simplemente heredar de la interfaz, por una cosa, y en segundo lugar, que sería imposible que alguien sin acceso a la implementación implementara esta interfaz. Así que decidí que quería solucionar este problema y abrir la interfaz para el consumo público para que cualquiera pudiera implementarlo y que la implementación de la interfaz fuera todo el contrato requerido, obviamente una mejora.

Cuando estaba encontrando y matando con fuego todos los lugares donde había hecho esto, encontré un lugar que resultó ser un problema particular. Dependía de los detalles de implementación de todas las diversas clases derivadas y la funcionalidad duplicada que ya estaba implementada pero mejor en otro lugar. Podría haberse implementado en términos de la interfaz pública en lugar de reutilizar la implementación existente de esa funcionalidad. Descubrí que se requería una pieza particular de contexto para funcionar correctamente. En términos generales, la implementación previa de la llamada se parecía un poco a

for(auto&& a : as) {
     f(a);
}

Sin embargo, para obtener este contexto, necesitaba cambiarlo por algo más parecido

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

Esto significa que para todas las operaciones que solían ser parte de f , algunas de ellas deben formar parte de la nueva función g que opera sin contexto, y algunas de ellas deben hacerse de una parte del f ahora diferido. Pero no todos los métodos f call necesitan o desean este contexto, algunos de ellos necesitan un contexto distinto que obtengan por medios separados. Así que para todo lo que f termina llamando (lo cual es, en términos generales, bastante todo ), tuve que determinar qué contexto, si lo hubo, necesitaban, de dónde deberían obtenerlo, y cómo dividirlos del antiguo f en nuevo f y nuevo g .

Y así es como terminé donde estoy ahora. La única razón por la que seguí adelante es porque necesitaba esta refactorización por otras razones de todos modos.

    
pregunta DeadMG 11.01.2015 - 15:53

11 respuestas

69

La última vez traté de iniciar una refactorización con consecuencias imprevistas, y no pude estabilizar la compilación y / o las pruebas después de un día , me di por vencido y revertí el código base hasta el punto anterior al refactorización.

Luego, comencé a analizar lo que estaba mal y desarrollé un mejor plan para hacer la refactorización en pasos más pequeños. Así que mi consejo para evitar refactorizaciones en cascada es simplemente: saber cuándo parar , ¡no dejes que las cosas se salgan de control!

A veces, tiene que morder la bala y tirar un día completo de trabajo, definitivamente más fácil que tirar tres meses de trabajo. El día que pierdas no es completamente en vano, al menos has aprendido cómo no abordar el problema. Y, según mi experiencia, hay siempre posibilidades para realizar pasos más pequeños en la refactorización.

Nota al margen : parece que estás en una situación en la que tienes que decidir si estás dispuesto a sacrificar tres meses completos de trabajo y comenzar de nuevo con una nueva refactorización (y posiblemente más exitosa) plan. Puedo imaginar que no es una decisión fácil, pero pregúntese, ¿qué tan alto es el riesgo que necesita otros tres meses no solo para estabilizar la compilación, sino también para corregir todos los errores imprevistos que probablemente haya introducido durante su reescritura hiciste los últimos tres meses? Escribí "reescribir", porque supongo que eso es lo que realmente hiciste, no una "refactorización". No es improbable que pueda resolver su problema actual más rápido volviendo a la última revisión en la que se compila su proyecto y comience con una refactorización real (opuesta a "reescribir") nuevamente.

    
respondido por el Doc Brown 12.01.2015 - 08:30
53
  

¿Es solo un síntoma de mis clases anteriores dependiendo demasiado el uno del otro?

Claro. Un cambio que causa una gran cantidad de otros cambios es más o menos la definición de acoplamiento.

  

¿Cómo evito los refactores en cascada?

En el peor tipo de bases de código, un solo cambio continuará en cascada, lo que eventualmente hará que cambies (casi) todo. Parte de cualquier refactor donde hay un acoplamiento generalizado es aislar la parte en la que está trabajando. Debe refactorizar no solo donde su nueva función está tocando este código, sino donde todo lo demás toca ese código.

Por lo general, eso significa hacer algunos adaptadores para ayudar a que el código antiguo funcione con algo que se parece y actúa como el código antiguo, pero usa la nueva implementación / interfaz. Después de todo, si todo lo que hace es cambiar la interfaz / implementación pero dejar el acoplamiento no está ganando nada. Es pintalabios en un cerdo.

    
respondido por el Telastyn 11.01.2015 - 17:53
17

Parece que tu refactorización fue demasiado ambiciosa. Una refactorización se debe aplicar en pasos pequeños, cada uno de los cuales se puede completar en (digamos) 30 minutos (o, en el peor de los casos, a lo sumo al día) y deja el proyecto en condiciones de construcción y todas las pruebas aún pasan.

Si mantienes cada cambio individual mínimo, realmente no debería ser posible que una refactorización rompa tu compilación durante mucho tiempo. El peor de los casos es probablemente cambiar los parámetros a un método en una interfaz ampliamente utilizada, por ejemplo, para añadir un nuevo parámetro. Pero los cambios consiguientes de esto son mecánicos: agregar (e ignorar) el parámetro en cada implementación, y agregar un valor predeterminado en cada llamada. Incluso si hay cientos de referencias, no debería tardar ni un día en realizar una refactorización.

    
respondido por el Jules 11.01.2015 - 19:16
12
  

¿Cómo puedo evitar este tipo de refactor en cascada en el futuro?

Diseño de deseosos

El objetivo es un excelente diseño e implementación de OO para la nueva función. Evitar la refactorización también es un objetivo.

Comience desde cero y haga un diseño para la nueva función que es lo que desea tener. Tómese el tiempo para hacerlo bien.

Sin embargo, tenga en cuenta que la clave aquí es "agregar una característica". Las cosas nuevas tienden a dejarnos ignorar en gran medida la estructura actual del código base. Nuestro diseño de ilusiones es independiente. Pero luego necesitamos dos cosas más:

  • Refactorice solo lo suficiente para hacer una costura necesaria para inyectar / implementar el código de la nueva función.
    • La resistencia a la refactorización no debe impulsar el nuevo diseño.
  • Escriba una clase orientada al cliente con una API que mantenga la nueva función & Codez existente felizmente ignorantes el uno del otro.
    • Se transcribe para obtener objetos, datos y resultados de un lado a otro. Maldito sea el principio de menor conocimiento. No vamos a hacer nada peor que lo que ya hace el código existente.

Heurísticas, lecciones aprendidas, etc.

La refactorización ha sido tan simple como agregar un parámetro predeterminado a una llamada de método existente; o una sola llamada a un método de clase estática.

Los métodos de extensión en clases existentes pueden ayudar a mantener la calidad del nuevo diseño con un riesgo mínimo absoluto.

"Estructura" lo es todo. La estructura es la realización del Principio de Responsabilidad Única; Diseño que facilita la funcionalidad. El código se mantendrá corto y simple hasta la jerarquía de clases. El tiempo para el nuevo diseño se completa durante las pruebas, el trabajo y la evitación de la piratería a través de la jungla de códigos heredados.

Las clases de reflexiones se centran en la tarea en cuestión. En general, olvídese de extender una clase existente: solo está induciendo la cascada de refactor de nuevo & Tener que lidiar con los gastos generales de la clase "más pesada".

Purgue cualquier resto de esta nueva funcionalidad del código existente. Aquí, la funcionalidad de una nueva característica completa y bien encapsulada es más importante que evitar la refactorización.

    
respondido por el radarbob 11.01.2015 - 22:53
9

De (el maravilloso) libro Trabajando Efectivamente con el Código Legado por Michael Feathers :

  

Cuando rompes dependencias en el código heredado, a menudo tienes que suspender un poco tu sentido de la estética. Algunas dependencias se rompen limpiamente; Otros terminan pareciendo menos ideales desde el punto de vista del diseño. Son como los puntos de incisión en la cirugía: es posible que quede una cicatriz en su código después de su trabajo, pero todo lo que esté debajo puede mejorar.

     

Si más adelante puede cubrir el código alrededor del punto donde rompió las dependencias, también puede curar esa cicatriz.

    
respondido por el Kenny Evitt 13.01.2015 - 05:25
6

Parece que (especialmente de las discusiones en los comentarios) te has encajado con reglas autoimpuestas que significan que este cambio "menor" es la misma cantidad de trabajo que una reescritura completa del software.

La solución debe ser "no hagas eso, entonces" . Esto es lo que sucede en proyectos reales. Muchas API antiguas tienen interfaces feas o parámetros abandonados (siempre nulos) como resultado, o funciones llamadas DoThisThing2 () que hace lo mismo que DoThisThing () con una lista de parámetros completamente diferente. Otros trucos comunes incluyen información de ocultación en elementos globales o punteros etiquetados para pasar de contrabando una gran parte del marco. (Por ejemplo, tengo un proyecto en el que la mitad de los buffers de audio solo contienen un valor mágico de 4 bytes porque era mucho más fácil que cambiar la forma en que una biblioteca invocaba sus códecs de audio).

Es difícil dar consejos específicos sin un código específico.

    
respondido por el pjc50 12.01.2015 - 18:04
3

Pruebas automatizadas. No necesita ser un fanático de TDD, ni necesita una cobertura del 100%, pero las pruebas automatizadas son las que le permiten realizar cambios con confianza. Además, parece que tienes un diseño con un acoplamiento muy alto; debe leer acerca de los principios de SOLID, que están formulados específicamente para abordar este tipo de problema en el diseño de software.

También recomendaría estos libros.

  • Trabajar de manera efectiva con el código heredado , Feathers
  • Refactorización , Fowler
  • Software orientado a objetos en crecimiento, guiado por pruebas , Freeman y Pryce
  • Código limpio , Martin
respondido por el syrion 11.01.2015 - 16:26
3
  

¿Es solo un síntoma de mis clases anteriores dependiendo demasiado el uno del otro?

Probablemente sí. Aunque puede obtener efectos similares con una base de código bastante agradable y limpia cuando los requisitos cambian lo suficiente

  

¿Cómo puedo evitar este tipo de refactorizaciones en cascada en el futuro?

Aparte de dejar de trabajar en el código heredado, no puedes tener miedo. Pero lo que puede hacer es utilizar un método que evite el efecto de no tener una base de código de trabajo durante días, semanas o incluso meses.

Ese método se llama "Método Mikado" y funciona así:

  1. escriba el objetivo que desea lograr en un pedazo de papel

  2. realice el cambio más simple que lo lleve en esa dirección.

  3. verifique si funciona con el compilador y su conjunto de pruebas. Si continúa con el paso 7. de lo contrario, continúe con el paso 4.

  4. en su papel, anote las cosas que deben cambiar para que su cambio actual funcione. Dibuja flechas, desde tu tarea actual, hasta las nuevas.

  5. Revierta sus cambios Este es el paso importante. Es contra intuitivo y le duele físicamente al principio, pero como acabas de probar algo simple, en realidad no es tan malo.

  6. seleccione una de las tareas, que no tenga errores de salida (no se conocen dependencias) y vuelva a 2.

  7. confirme el cambio, tache la tarea en el papel, elija una tarea que no tenga errores de salida (no se conocen dependencias) y vuelva a 2.

De esta manera tendrá una base de código de trabajo en intervalos cortos. Donde también puedes fusionar los cambios del resto del equipo. Y tiene una representación visual de lo que sabe que todavía tiene que hacer, esto ayuda a decidir si desea continuar con el esfuerzo o si debe detenerlo.

    
respondido por el Jens Schauder 14.01.2015 - 12:25
2

Refactoring es una disciplina estructurada, distinta de limpiar el código como mejor le parezca. Debe tener pruebas de unidad escritas antes de comenzar, y cada paso debe consistir en una transformación específica que sepa que no debe hacer cambios en la funcionalidad. Las pruebas unitarias deben pasar después de cada cambio.

Por supuesto, durante el proceso de refactorización, naturalmente descubrirá cambios que deben aplicarse y que pueden causar roturas. En ese caso, haga todo lo posible por implementar una compatibilidad de compatibilidad para la interfaz antigua que usa el nuevo marco. En teoría, el sistema todavía debería funcionar como antes, y las pruebas de la unidad deberían pasar. Puede marcar la compatibilidad de compatibilidad como una interfaz obsoleta y limpiarla en un momento más adecuado.

    
respondido por el 200_success 12.01.2015 - 08:04
2
  

... Refactorice el proyecto para agregar la función.

Como se dijo en @Jules, Refactorizar y agregar características son dos cosas muy diferentes.

  • La refactorización consiste en cambiar la estructura del programa sin alterar su comportamiento.
  • Agregar una característica, por otro lado, está aumentando su comportamiento.

... pero de hecho, a veces necesitas cambiar el funcionamiento interno para agregar tus cosas, pero prefiero llamarlo modificación en lugar de refactorización.

  

Necesitaba hacer un cambio de interfaz menor para acomodarlo

Ahí es donde las cosas se complican. Las interfaces se consideran bounderies para aislar la implementación de cómo se usa. Tan pronto como toque las interfaces, también tendrá que cambiar todo lo que esté en cada lado (implementándolo o usándolo). Esto puede extenderse hasta donde lo hayas experimentado.

  

entonces la clase consumidora no se puede implementar con su interfaz actual en términos de la nueva, por lo que también necesita una nueva interfaz.

Que una interfaz requiera un cambio, suena bien ... que se propague a otra implica que los cambios se extienden aún más. Parece que alguna forma de entrada / datos requiere que fluya por la cadena. ¿Es ese el caso?

Tu conversación es muy abstracta, por lo que es difícil de entender. Un ejemplo sería muy útil. Por lo general, las interfaces deben ser bastante estables e independientes entre sí, lo que hace posible modificar parte del sistema sin dañar el resto ... gracias a las interfaces.

... en realidad, la mejor manera de evitar las modificaciones de código en cascada son precisamente las interfaces buenas . ;)

    
respondido por el dagnelies 12.01.2015 - 18:30
-1

Creo que normalmente no puedes a menos que estés dispuesto a mantener las cosas como son. Sin embargo, cuando se trata de situaciones como la suya, creo que lo mejor es informar al equipo y hacerles saber por qué se debe hacer algo de refactorización para continuar con un desarrollo más saludable. Yo no iría y arreglaría las cosas yo solo. Lo hablaría durante las reuniones de Scrum (asumiendo que ustedes las tienen) y lo abordaría sistemáticamente con otros desarrolladores.

    
respondido por el Tarik 14.01.2015 - 04:09

Lea otras preguntas en las etiquetas