Tengo un proyecto En este proyecto, deseé refactorizarlo para agregar una característica, y refactoré el proyecto para agregar la característica.
El problema es que cuando terminé, resultó que necesitaba hacer un pequeño cambio de interfaz para acomodarlo. Entonces 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 son tres meses después, y he tenido que solucionar innumerables problemas virtualmente no relacionados, y estoy buscando resolver problemas que fueron trazados durante un año a partir de ahora o simplemente enumerados como que no se solucionarán debido a la dificultad antes de que la cosa se compile de nuevo.
¿Cómo puedo evitar este tipo de refactorizaciones en cascada en el futuro? ¿Es solo un síntoma de que mis clases anteriores dependen demasiado entre sí?
Edición breve: en este caso, el refactor fue la característica, ya que el refactor aumentó la extensibilidad de un fragmento de código en particular y disminuyó el acoplamiento. Esto significaba que los desarrolladores externos podían hacer más, que era la característica que quería ofrecer. Entonces el refactor original en sí mismo no debería haber sido un cambio funcional.
Edición más grande que prometí hace cinco días:
Antes de comenzar este refactorizador, tenía un sistema donde tenía una interfaz, pero en la implementación, simplemente hice dynamic_cast
todas las implementaciones posibles que envié. Obviamente, esto significaba que no podía simplemente heredar de la interfaz, por un lado, y en segundo lugar, que sería imposible para cualquiera sin acceso de implementación implementar esta interfaz. Así que decidí que quería solucionar este problema y abrir la interfaz para el consumo público para que cualquiera pudiera implementarla y que implementar 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. Depende de los detalles de implementación de todas las diversas clases derivadas y la funcionalidad duplicada que ya se implementó pero mejor en otro lugar. Podría haberse implementado en términos de la interfaz pública y reutilizado la implementación existente de esa funcionalidad. Descubrí que requería un contexto particular para funcionar correctamente. En términos generales, la implementación previa de la llamada se parecía un poco
for(auto&& a : as) {
f(a);
}
Sin embargo, para obtener este contexto, necesitaba cambiarlo a 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 ellas f
, algunas de ellas deben formar parte de la nueva función g
que opera sin contexto, y algunas de ellas deben formar parte de la parte diferida ahora f
. Pero no todos los métodos f
llaman necesidad o quieren este contexto, algunos de ellos necesitan un contexto distinto que obtienen por medios separados. Entonces, para todo lo que f
termina llamando (que es, más o menos, casi todo ), tuve que determinar qué contexto, si es que necesitaban, de dónde deberían obtenerlo y cómo dividirlos de lo antiguo f
en lo nuevo f
y lo 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.
Respuestas:
La última vez que intenté comenzar 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 volví la base de código al punto anterior a la refactorización.
Luego, comencé a analizar lo que salió mal y desarrollé un mejor plan de cómo refactorizar en pasos más pequeños. Entonces, mi consejo para evitar las refactorizaciones en cascada es: saber cuándo parar , ¡no dejes que las cosas se salgan de tu control!
A veces tienes 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 pierdes no es completamente en vano, al menos has aprendido cómo no abordar el problema. Y según mi experiencia, siempre hay posibilidades de dar pasos más pequeños en la refactorización.
Nota al margen : parece estar en una situación en la que tiene que decidir si está dispuesto a sacrificar tres meses completos de trabajo y comenzar de nuevo con un nuevo plan de refactorización (y con suerte más exitoso). Me imagino 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 construcción, sino también para corregir todos los errores imprevistos que probablemente introdujo durante su reescritura que hizo en 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 donde se compila su proyecto y comience con una refactorización real (en lugar de "reescribir") nuevamente.
fuente
Seguro. Un cambio que causa una miríada de otros cambios es prácticamente la definición de acoplamiento.
En el peor tipo de bases de código, un solo cambio continuará en cascada, lo que eventualmente hará que cambie (casi) todo. Parte de cualquier refactor donde hay un acoplamiento generalizado es aislar la parte en la que está trabajando. Debe refactorizar no solo dónde está tocando este código su nueva función, sino dónde todo lo demás toca ese código.
Por lo general, eso significa hacer algunos adaptadores para ayudar al antiguo código a funcionar con algo que se ve y actúa como el antiguo código, pero que usa la nueva implementación / interfaz. Después de todo, si todo lo que haces es cambiar la interfaz / implementación pero dejar el acoplamiento no estás ganando nada. Es lápiz labial en un cerdo.
fuente
Parece que su refactorización fue demasiado ambiciosa. Se debe aplicar una refactorización en pequeños pasos, cada uno de los cuales se puede completar en (digamos) 30 minutos, o, en el peor de los casos, como máximo un día, y deja el proyecto edificable 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 construcción durante mucho tiempo. El peor de los casos probablemente sea cambiar los parámetros a un método en una interfaz ampliamente utilizada, por ejemplo, para agregar un nuevo parámetro. Pero los cambios consecuentes a partir 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 tomar ni un día realizar una refactorización de este tipo.
fuente
Diseño de ilusión
El objetivo es un excelente diseño e implementación de OO para la nueva característica. Evitar la refactorización también es un objetivo.
Comience desde cero y haga un diseño para la nueva característica 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 función". Las cosas nuevas tienden a dejarnos ignorar en gran medida la estructura actual de la base del código. Nuestro diseño de ilusiones es independiente. Pero luego necesitamos dos cosas más:
Heurística, 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 las clases existentes pueden ayudar a mantener la calidad del nuevo diseño con un riesgo mínimo absoluto.
La "estructura" lo es todo. La estructura es la realización del Principio de Responsabilidad Única; Diseño que facilita la funcionalidad. El código será breve y simple en toda la jerarquía de clases. El tiempo para el nuevo diseño se recupera durante las pruebas, el reelaboración y evitar la piratería a través de la jungla de códigos heredada.
Las clases de ilusiones se centran en la tarea en cuestión. En general, olvídate de extender una clase existente: solo estás induciendo la cascada de refactorización nuevamente y tienes que lidiar con la sobrecarga de la clase "más pesada".
Purgue cualquier remanente de esta nueva funcionalidad del código existente. Aquí, la funcionalidad de la nueva característica completa y bien encapsulada es más importante que evitar la refactorización.
fuente
Del libro (el maravilloso) Trabajar eficazmente con código heredado de Michael Feathers :
fuente
Parece que (especialmente de las discusiones en los comentarios) te has encerrado con reglas autoimpuestas que significan que este cambio "menor" es la misma cantidad de trabajo que una reescritura completa del software.
La solución tiene que ser "no hagas eso, entonces" . Esto es lo que sucede en proyectos reales. Como resultado, muchas API antiguas tienen interfaces feas o parámetros abandonados (siempre nulos), o funciones llamadas DoThisThing2 () que hacen lo mismo que DoThisThing () con una lista de parámetros completamente diferente. Otros trucos comunes incluyen el almacenamiento de información en globales o punteros etiquetados para pasar de contrabando una gran parte del marco. (Por ejemplo, tengo un proyecto donde la mitad de los búferes de audio contienen solo un valor mágico de 4 bytes porque eso fue mucho más fácil que cambiar cómo una biblioteca invoca sus códecs de audio).
Es difícil dar consejos específicos sin un código específico.
fuente
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 tiene un diseño con un acoplamiento muy alto; Debe leer acerca de los principios SÓLIDOS, que están formulados específicamente para abordar este tipo de problema en el diseño de software.
También recomendaría estos libros.
fuente
Lo más probable es que sí. Aunque puede obtener efectos similares con una base de código bastante agradable y limpia cuando los requisitos cambian lo suficiente
Además de detenerse para trabajar en código heredado, no me temo. Pero lo que puede hacer es usar 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í:
escriba el objetivo que desea lograr en una hoja de papel
haz el cambio más simple que te lleve en esa dirección.
compruebe si funciona utilizando el compilador y su conjunto de pruebas. Si continúa con el paso 7. de lo contrario, continúe con el paso 4.
en su nota de papel, las cosas que deben cambiar para que su cambio actual funcione. Dibuja flechas, de tu tarea actual, a las nuevas.
Revierta sus cambios Este es el paso importante. Es contrario a la intuición y duele físicamente al principio, pero como acabas de intentar algo simple, en realidad no es tan malo.
elija una de las tareas, que no tenga errores salientes (sin dependencias conocidas) y vuelva a 2.
cometer el cambio, tachar la tarea en el papel, elegir una tarea que no tenga errores salientes (sin dependencias conocidas) y volver 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.
fuente
Refactorizar es una disciplina estructurada, distinta de limpiar el código como mejor le parezca. Debe tener pruebas unitarias escritas antes de comenzar, y cada paso debe consistir en una transformación específica que usted sabe 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á los cambios que se deben aplicar y que pueden causar roturas. En ese caso, haga todo lo posible para implementar una compatibilidad de compatibilidad para la interfaz anterior que utiliza el nuevo marco. En teoría, el sistema debería funcionar igual que antes, y las pruebas unitarias deberían pasar. Puede marcar la compatibilidad de compatibilidad como una interfaz obsoleta y limpiarla en el momento más adecuado.
fuente
Como dijo @Jules, refactorizar y agregar características son dos cosas muy diferentes.
... pero de hecho, a veces necesitas cambiar el funcionamiento interno para agregar tus cosas, pero prefiero llamarlo modificar en lugar de refactorizar.
Ahí es donde las cosas se complican. Las interfaces están pensadas como límites para aislar la implementación de cómo se usa. Tan pronto como toque las interfaces, todo lo que esté a ambos lados (implementarlo o usarlo) también deberá cambiarse. Esto puede extenderse tanto como lo experimentaste.
Que una interfaz requiera un cambio suena bien ... que se extienda a otra implica que los cambios se extiendan aún más. Parece que alguna forma de entrada / datos requiere fluir por la cadena. ¿Es ese el caso?
Tu charla 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 permite modificar parte del sistema sin dañar el resto ... gracias a las interfaces.
... en realidad, la mejor manera de evitar modificaciones de código en cascada son precisamente buenas interfaces. ;)
fuente
Creo que generalmente no puedes a menos que estés dispuesto a mantener las cosas como están. Sin embargo, cuando situaciones como la suya, creo que lo mejor es informar al equipo y hacerles saber por qué debería realizarse una refactorización para continuar con un desarrollo más saludable. No iría y arreglaría las cosas solo. Lo hablaría durante las reuniones de Scrum (suponiendo que ustedes las tengan), y lo abordaría sistemáticamente con otros desarrolladores.
fuente