¿La mejor manera de analizar una clase grande antes de refactorizarla en clases más pequeñas?


8

Prefacio

Estoy no en busca de una manera de refactorizar una gran clase código espagueti, ese tema se ha tratado en otras preguntas.

Pregunta

Estoy buscando técnicas para comenzar a comprender un archivo de clase escrito por otro compañero de trabajo que abarca más de 4000 líneas y tiene un único método de actualización enorme que es más de 2000 líneas.

Eventualmente espero construir pruebas unitarias para esta clase y refactorizarlas en muchas clases más pequeñas que sigan DRY y el Principio de Responsabilidad Única.

¿Cómo puedo gestionar y abordar esta tarea? Me gustaría poder dibujar un diagrama de los eventos que ocurren dentro de la clase y luego pasar a la funcionalidad de abstracción, pero estoy luchando por obtener una vista de arriba hacia abajo de cuáles son las responsabilidades y dependencias de la clase.


Editar: en las respuestas que las personas ya han cubierto temas relacionados con los parámetros de entrada, esta información es para los recién llegados:

En este caso, el método principal de la clase no tiene parámetros de entrada y ejecuta un ciclo while hasta que se le indique que se detenga. El constructor tampoco toma parámetros.

Esto se suma a la confusión de la clase, sus requisitos y dependencias. La clase obtiene referencias a otras clases a través de métodos estáticos, singletons y alcanzando clases a las que ya tiene referencia.


1
¿Cómo es esto diferente de la mayoría de las tareas de refactorización? No planeas romper la interfaz, ¿verdad?
JeffO

@JeffO Creo que quiere una vista de 30000 pies, cómo manejar metodológicamente lo que está haciendo la clase y lo que hay que hacer; no "método de extracción", "variable de extracción", etc.
Thomas Junk

1
Con el tiempo, no construya pruebas unitarias. Comience a construir pruebas unitarias ahora y en las pruebas codifique cada bit de información que aprenda sobre la clase a medida que avanza. (Desafortunadamente, las pruebas pueden descubrir errores en la clase, lo que terminará agregando confusión a toda la imagen, pero aún es mejor que tratar de entender lo que está sucediendo sin ninguna prueba en su lugar.)
Mike Nakis


1
posible duplicado de descifrar código extranjero
mosquito

Respuestas:


10

Curiosamente, la refactorización es la forma más eficiente que he encontrado para comprender un código como este. No tienes que hacerlo limpiamente al principio. Puede hacer un análisis de refactorización de análisis rápido y sucio, luego revertirlo y hacerlo con más cuidado con las pruebas unitarias.

La razón por la que esto funciona es porque la refactorización realizada correctamente es una serie de pequeños cambios, casi mecánicos, que puede hacer sin comprender realmente el código. Por ejemplo, puedo ver un blob de código que se repite en todas partes y factorizarlo en un método, sin necesidad de saber cómo funciona. Podría darle un nombre tonto al principio como step1o algo así. Entonces me doy cuenta de eso step1y step7con frecuencia aparecen juntos, y lo factorizo. Entonces, de repente, el humo se ha disipado lo suficiente como para que realmente pueda ver el panorama completo y hacer algunos nombres significativos.

Para encontrar la estructura general, he encontrado que crear un gráfico de dependencia a menudo ayuda. Hago esto manualmente usando graphviz ya que he encontrado que el trabajo manual me ayuda a aprenderlo, pero probablemente hay herramientas automatizadas disponibles. Aquí hay un ejemplo reciente de una característica en la que estoy trabajando. He descubierto que esta también es una forma efectiva de comunicar esta información a mis colegas.

gráfico de dependencia


2
Maldición, desearía haber leído esto antes de mi entrevista de hoy ... Me dieron la tarea de refactorizar una clase, esto me habría ayudado mucho.
Grim

8

Primero, una palabra de precaución (aunque no sea lo que está preguntando) antes de responder a su pregunta: una pregunta tremendamente importante es por qué¿Quieres refactorizar algo? El hecho de que mencione "compañero de trabajo" plantea la pregunta: ¿Hay alguna razón comercial para el cambio? Especialmente en un entorno corporativo, siempre habrá códigos heredados e incluso nuevos códigos con los que no estará satisfecho. El hecho es que no hay dos ingenieros que resuelvan un problema dado con la misma solución. Si funciona y no tiene implicaciones de rendimiento horribles, ¿por qué no dejarlo así? El trabajo de características generalmente es mucho más beneficioso que la refactorización porque no le gusta particularmente una implementación. Dicho esto, hay algo que decir sobre la deuda técnica y la limpieza del código que no se puede mantener. Mi única sugerencia aquí es que ya has hecho la distinción y has comprado para hacer los cambios, de lo contrario es probable que te preguntendespués de haberse hundido en un gran esfuerzo y probablemente obtendrá mucho más (y comprensible) retroceso.


Antes de abordar una refactorización, siempre quiero asegurarme de tener una comprensión firme de lo que estoy cambiando. Generalmente, utilizo dos métodos para abordar este tipo de cosas: registro y depuración. Esto último se ve facilitado por la existencia de pruebas unitarias (y pruebas del sistema si tienen sentido). Como no tiene ninguno, le recomiendo encarecidamente que escriba pruebas con cobertura de sucursal completa para asegurarse de que sus cambios no creen cambios de comportamiento inesperados. Las pruebas unitarias y el paso por el código lo ayudan a obtener una comprensión de bajo nivel del comportamiento en un entorno controlado. El registro le ayuda a comprender mejor el comportamiento en la práctica (esto es especialmente útil en situaciones multiproceso).

Una vez que consiga una mejor comprensión de todo lo que está pasando a través de depuración y registro, a continuación, voy a empezar la lectura de códigos. En este punto, generalmente tengo una mejor comprensión de lo que está sucediendo y sentarme y leer 2k LoC debería tener mucho más sentido en ese punto. Mi intención aquí es más para obtener una vista de extremo a extremo y asegurarme de que no haya ningún tipo de casos extraños que las pruebas unitarias que he escrito no estén cubriendo.

Luego pensaré en el diseño y redactaré uno nuevo. Si la compatibilidad con versiones anteriores es importante, me aseguraré de que la interfaz pública no cambie.

Con una comprensión firme de lo que está sucediendo, las pruebas y un nuevo diseño nuevo en la mano, lo someteré a revisión. Una vez que el diseño haya sido examinado por al menos dos personas (espero que estén familiarizadas con el código), comenzaré a implementar los cambios.

Espero que no sea tan dolorosamente obvio y que ayude.


1
Tu respuesta es realmente útil para mí. En realidad, no había pensado en producir resultados de registro de la clase actual primero, sin embargo, hay algunos problemas: no creo que pueda probar esta clase por unidad. Toda su funcionalidad principal ocurre en un método sin parámetros de entrada, este método recoge la mayor parte de su estado de otras clases que no se pasan, su constructor tampoco toma argumentos, se accede a sus dependencias a través de singletons. Su sugerencia es hacer una prueba unitaria primero y luego comenzar a rediseñar, ¿cómo me adapto si resulta que es probable que se necesite un rediseño para realizar la prueba unitaria?
sydan

Una nota adicional sobre su palabra de precaución. Entiendo esto, he visto a personas hablar sobre cómo a menudo es mejor dejar estas cosas en paz, sin embargo, la clase todavía se está desarrollando y los nuevos desarrolladores están comenzando a trabajar con ella. Con nuevas modificaciones y sin pruebas unitarias, no hay forma de entender cómo un cambio afecta a la clase. Ya se me asignó la tarea de abstraer una característica de la clase y completé esta tarea, pero esto ha hecho poca diferencia en el tamaño gigantesco del problema y creo que aún queda mucho por hacer.
sydan

1
@sydan: ¿Puedes burlarte de la interacción con otras clases? Sé que esto es más fácil en algunos idiomas que en otros. Si eso no es posible, ¿tal vez las pruebas del sistema para comenzar?
Demian Brecht

Interesante, ¿por qué el downvote?
Demian Brecht

También estoy interesado
sydan

4

No hay una receta general, pero algunas reglas generales ( suponiendo un lenguaje estáticamente tipado pero que realmente no debería importar):

1) Echa un vistazo a la firma del método. Se da a conocer lo que pasa en y es de esperar que viene a cabo . Desde el estado general de esta clase de dios , supongo que este es un primer punto de dolor . Supongo que se usa más de un parámetro.

2) Utilice la función de búsqueda de su editor / EDI para determinar Exit-Points (por lo general un retorno se utiliza statement_)

De lo que sabe, qué necesita la función para las pruebas y qué espera a cambio .

Entonces, una primera prueba simple sería llamar a la función con los parámetros necesarios y esperar que el resultado no sea nulo . Eso no es mucho, pero es un punto de partida.

A partir de eso, podría entrar en un círculo hermenéutico (un término acuñado por HG Gadamer, un filósofo alemán). El punto es: ahora tiene una comprensión rudimentaria de la clase y actualiza esta comprensión con nuevos conocimientos detallados y tiene una nueva comprensión de toda la clase.

Esto combinado con el método científico : hacer suposiciones y ver si se cumplen.

3) Tome un parámetro y mire, en qué parte de la clase se transforma de alguna manera:

Por ejemplo, estás haciendo Java como yo, por lo general hay getter y setter que puedes buscar. SearchPattern $objectname. (o $objectname\.(get|set)si estás haciendo Java)

Ahora podría hacer más suposiciones sobre lo que hace el método.

Rastree solo los parámetros de entrada ( primero ) cada uno a través del método. Si es necesario, haga algunos diagramas o tablas , donde escriba cada cambio en cada una de las variables.

A partir de eso, puede escribir más pruebas, describiendo el comportamiento del método. Si tiene una comprensión aproximada de cómo se transforma cada parámetro de entrada a lo largo del método, comience a experimentar : pase nulo para un parámetro o entrada extraña . Haga suposiciones, verifique el resultado y varíe los aportes y las suposiciones.

Si hace esto una vez, tiene una "tonelada" de pruebas que describen el comportamiento de su método.

4) En el siguiente paso, buscaría dependencias : ¿qué necesita el método además de su entrada para funcionar correctamente ? ¿Hay posibilidades de reducir o reestructurar esos? Cuantas menos dependencias tengas, más claro verás los puntos, dónde hacer las primeras divisiones.

5) A partir de ahí, puede recorrer todo el camino de refactorización con patrones de refactorización y refactorización a patrones.

Aquí un buen Vid: GoGaRuCo 2014- El método científico de resolución de problemas Se trata de la resolución de problemas, pero útil para una metodología general de comprensión de cómo funciona algo .

Usted menciona que la función llamada no tiene parámetros de entrada : en este caso especial, primero trataría de identificar las dependencias y refactorizarlas a parámetros, para que pueda intercambiarlas como desee.


Hola Thomas, gracias por tu respuesta, como la de Demian, es muy útil y un buen enfoque metódico. Creo que comenzaré a avanzar por esta ruta combinando el registro que sugiere Demian con la variedad de entradas que usted ha sugerido. Lamentablemente, el método que ejecuta esta clase no tiene parámetros de entrada. Es una tarea de reloj que ejecuta un ciclo while eterno, parece que las dependencias se toman a través de singletons, clases estáticas y luego alcanzando las clases para agarrar a otros.
sydan

La primera etapa para establecer las entradas de métodos probablemente será la más difícil para mí, ya que no hay forma de saber qué variables usa la clase son entradas, constantes, salidas, variables temporales o variables de depuración aleatorias.
sydan

Oh, eso suena divertido! Tal vez, saltas directamente hacia él y sigues el primer objeto que encuentras. El círculo entonces comienza como se describe;)
Thomas Junk

Esa puede ser la mejor ruta ... ¿un 'ctrl-f' para la variable más utilizada parecería un buen punto de partida? ¿Debo editar mi pregunta original para mencionar la falta de parámetros de entrada y la estructura de métodos?
sydan

1
Me gusta lo que mencionaste en tu edición.
sydan

4

Usted menciona un updatemétodo de 2000 líneas .

Empezaría a leer este método de arriba a abajo. Cuando encuentre un conjunto de declaraciones que se pertenecen entre sí, es decir, comparten una responsabilidad (por ejemplo, crear un usuario), extraiga estas declaraciones en una función.

De esta manera, no cambia la forma en que funciona el código, pero usted

  • acortar este gran método
  • obtener una mejor comprensión de este método
  • preparar el método para futuras refactorizaciones

Repita estos pasos para otros métodos.


1
Este proceso cuando se realiza de manera no destructiva (solo leer, notar, planificar) a veces se denomina revisión de código . El libro de Steven C. McConnell - Code Complete - tiene una bonita plantilla de lista de verificación para esta ocasión
xmojmr
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.