¿Qué hacer con un archivo fuente C ++ de 11000 líneas?


229

Entonces tenemos este enorme archivo fuente mainmodule.cpp (¿11000 líneas es enorme?) En nuestro proyecto y cada vez que tengo que tocarlo me da escalofríos.

Como este archivo es tan central y grande, sigue acumulando más y más código y no puedo pensar en una buena manera de hacer que realmente empiece a reducirse.

El archivo se usa y cambia activamente en varias (> 10) versiones de mantenimiento de nuestro producto, por lo que es realmente difícil refactorizarlo. Si tuviera que "simplemente" dividirlo, digamos para empezar, en 3 archivos, fusionar los cambios de las versiones de mantenimiento se convertirá en una pesadilla. Y también si divide un archivo con un historial tan largo y rico, el seguimiento y la verificación de los cambios antiguos en el SCChistorial de repente se vuelven mucho más difíciles.

Básicamente, el archivo contiene la "clase principal" (distribución y coordinación del trabajo interno principal) de nuestro programa, por lo que cada vez que se agrega una característica, también afecta a este archivo y cada vez que crece. :-(

¿Qué haría usted en esta situación? ¿Alguna idea sobre cómo mover nuevas funciones a un archivo fuente separado sin desordenar el SCCflujo de trabajo?

(Nota sobre las herramientas: Usamos C ++ con Visual Studio; Usamos AccuRevcomo SCCpero creo que el tipo de SCCrealmente no importa aquí; Usamos Araxis Mergepara hacer una comparación y fusión real de archivos)


15
@BoltClock: en realidad, Vim lo abrirá bastante rápido.
antes del

58
69305 líneas y contando. Un archivo en nuestra aplicación en el que mi colega arroja la mayor parte de su código. No pude resistir publicar esto aquí. No tengo a nadie en mi compañía para reportar esto.
Agnel Kurian

204204
No lo entiendo ¿Cómo puede el comentario "renunciar a ese trabajo" obtener tantos votos positivos? Algunas personas parecen vivir en un país de las hadas, donde todos los proyectos se escriben desde cero y / o usan 100% ágil, TDD, ... (ponga aquí cualquiera de sus palabras de moda).
Stefan

39
@Stefan: Cuando me enfrenté a una base de código similar, hice exactamente eso. No me gustaba pasar el 95% de mi tiempo trabajando en la base de código de 10 años, y el 5% en realidad escribiendo código. En realidad, era imposible probar algunos aspectos del sistema (y no me refiero a la prueba unitaria, me refiero a ejecutar el código para ver si funciona). No duré mi período de prueba de 6 meses, me cansé de pelear perdiendo batallas y escribiendo códigos que no podía soportar.
Binario Worrier

50
con respecto al aspecto del seguimiento del historial de dividir el archivo: use el comando de copia del sistema de control de versiones para copiar el archivo completo sin importar cuántas veces quiera dividirlo, y luego elimine todo el código de cada una de las copias que no desea en ese archivo Esto conserva el historial general, ya que cada uno de los archivos divididos puede rastrear su historial a través de la división (que se verá como una eliminación gigante de la mayoría de los contenidos del archivo).
rmeador

Respuestas:


86
  1. Encuentre algún código en el archivo que sea relativamente estable (que no cambie rápidamente y que no varíe mucho entre las ramas) y que pueda ser una unidad independiente. Mueva esto a su propio archivo, y para el caso a su propia clase, en todas las ramas. Debido a que es estable, esto no causará (muchas) fusiones "incómodas" que deben aplicarse a un archivo diferente del que se hicieron originalmente, cuando combina el cambio de una rama a otra. Repetir.

  2. Encuentre un código en el archivo que básicamente solo se aplica a un pequeño número de sucursales, y podría estar solo. No importa si está cambiando rápidamente o no, debido a la pequeña cantidad de ramas. Mueva esto a sus propias clases y archivos. Repetir.

Entonces, nos hemos librado del código que es el mismo en todas partes, y el código que es específico para ciertas ramas.

Esto te deja con un núcleo de código mal administrado: se necesita en todas partes, pero es diferente en cada rama (y / o cambia constantemente para que algunas ramas se ejecuten detrás de otras), y sin embargo, está en un solo archivo que estás tratando sin éxito de fusionarse entre ramas. Para de hacer eso. Ramifique el archivo de forma permanente , tal vez renombrándolo en cada rama. Ya no es "principal", es "principal para la configuración X". OK, entonces pierde la capacidad de aplicar el mismo cambio a múltiples ramas al fusionar, pero este es en cualquier caso el núcleo del código donde la fusión no funciona muy bien. Si de todos modos tiene que administrar manualmente las fusiones para lidiar con los conflictos, entonces no es una pérdida aplicarlas manualmente de forma independiente en cada rama.

Creo que te equivocas al decir que el tipo de SCC no importa, porque, por ejemplo, las habilidades de fusión de git son probablemente mejores que la herramienta de fusión que estás usando. Entonces, el problema central, "la fusión es difícil" ocurre en diferentes momentos para diferentes SCC. Sin embargo, es poco probable que pueda cambiar los SCC, por lo que el problema probablemente sea irrelevante.


En cuanto a la fusión: he mirado a GIT y he visto a SVN y he visto a Perforce y déjame decirte que nada de lo que he visto en ningún lado supera a AccuRev + Araxis por lo que hacemos. :-) (Aunque GIT puede hacer esto [ stackoverflow.com/questions/1728922/… ] y AccuRev no puede, todos tienen que decidir por sí mismos si esto es parte de la fusión o del análisis del historial).
Martin Ba

Es justo: tal vez ya tenga la mejor herramienta disponible. La capacidad de Git de fusionar un cambio que ocurrió en el Archivo A en la rama X, en el Archivo B en la rama Y, debería facilitar la división de los archivos ramificados, pero presumiblemente el sistema que usa tiene las ventajas que le gustan. De todos modos, no estoy proponiendo que cambies a git, solo digo que SCC hace la diferencia aquí, pero aun así estoy de acuerdo contigo en que esto se puede descontar :-)
Steve Jessop

129

La fusión no será una pesadilla tan grande como lo será cuando obtenga el archivo 30000 LOC en el futuro. Entonces:

  1. Deja de agregar más código a ese archivo.
  2. Dividirlo.

Si no puede dejar de codificar durante el proceso de refactorización, puede dejar este archivo grande tal como está durante un tiempo al menos sin agregarle más código: ya que contiene una "clase principal" que podría heredar de él y mantener la clase heredada ( es) con funciones sobrecargadas en varios archivos nuevos y bien diseñados.


@ Martin: afortunadamente no has pegado tu archivo aquí, así que no tengo idea de su estructura. Pero la idea general es dividirlo en partes lógicas. Tales partes lógicas podrían contener grupos de funciones de su "clase principal" o podría dividirlo en varias clases auxiliares.
Kirill V. Lyadvinsky

3
Con 10 versiones de mantenimiento y muchos desarrolladores activos, es poco probable que el archivo se pueda congelar durante el tiempo suficiente.
Kobi

99
@ Martin, tiene un par de patrones GOF que harían el truco, una sola Fachada que mapea las funciones de mainmodule.cpp, alternativamente (he recomendado a continuación) crear un conjunto de clases de Comando que cada mapa se asigna a una función / característica de mainmodule.app. (He ampliado esto en mi respuesta.)
ocodo

2
Sí, totalmente de acuerdo, en algún momento debe dejar de agregarle código o, eventualmente, será 30k, 40k, 50k, el módulo principal de kaboom simplemente falla. :-)
Chris

67

Me parece que te enfrentas a una serie de olores de código aquí. En primer lugar, la clase principal parece violar el principio abierto / cerrado . También parece que está manejando demasiadas responsabilidades . Debido a esto, asumiría que el código es más frágil de lo necesario.

Si bien puedo entender sus inquietudes con respecto a la trazabilidad después de una refactorización, esperaría que esta clase sea bastante difícil de mantener y mejorar y que cualquier cambio que realice es probable que cause efectos secundarios. Supongo que el costo de estos supera el costo de refactorizar la clase.

En cualquier caso, dado que los olores del código solo empeorarán con el tiempo, al menos en algún momento el costo de estos superará el costo de la refactorización. Por su descripción, supongo que ha pasado el punto de inflexión.

Refactorizar esto debe hacerse en pequeños pasos. Si es posible, agregue pruebas automatizadas para verificar el comportamiento actual antes de refactorizar cualquier cosa. Luego seleccione pequeñas áreas de funcionalidad aislada y extráigalas como tipos para delegar la responsabilidad.

En cualquier caso, parece un gran proyecto, así que buena suerte :)


18
Huele mucho: huele a que el antipatrón Blob está en casa ... en.wikipedia.org/wiki/God_object . Su comida favorita es el código de espagueti: en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan

@jdehaan: Estaba tratando de ser diplomático al respecto :)
Brian Rasmussen

+1 De mí también, no me atrevo a tocar incluso el código complejo que he escrito sin pruebas para cubrirlo.
Danny Thomas

49

La única solución que he imaginado para tales problemas es la siguiente. La ganancia real por el método descrito es la progresividad de las evoluciones. No hay revoluciones aquí, de lo contrario estarás en problemas muy rápido.

Inserte una nueva clase de cpp sobre la clase principal original. Por ahora, básicamente redirigiría todas las llamadas a la clase principal actual, pero apuntaría a hacer que la API de esta nueva clase sea lo más clara y sucinta posible.

Una vez hecho esto, tiene la posibilidad de agregar nuevas funcionalidades en nuevas clases.

En cuanto a las funcionalidades existentes, debe moverlas progresivamente en nuevas clases a medida que se vuelven lo suficientemente estables. Perderá la ayuda de SCC para este código, pero no se puede hacer mucho al respecto. Simplemente elige el momento adecuado.

Sé que esto no es perfecto, aunque espero que pueda ayudar, ¡y el proceso debe adaptarse a sus necesidades!

Información Adicional

Tenga en cuenta que Git es un SCC que puede seguir fragmentos de código de un archivo a otro. He escuchado cosas buenas al respecto, por lo que podría ayudar mientras avanza progresivamente su trabajo.

Git se construye alrededor de la noción de blobs que, si entiendo correctamente, representan fragmentos de archivos de código. Mueva estas piezas en diferentes archivos y Git las encontrará, incluso si las modifica. Aparte del video de Linus Torvalds mencionado en los comentarios a continuación, no he podido encontrar algo claro sobre esto.


Una referencia sobre cómo GIT hace eso / cómo lo hace con GIT sería muy bienvenida.
Martin Ba

@ Martin Git lo hace automáticamente.
Mateo

44
@ Martin: Git lo hace automáticamente, porque no rastrea archivos, rastrea contenido. En realidad, es más difícil en git simplemente "obtener el historial del único archivo".
Arafangion

1
@Martin youtube.com/watch?v=4XpnKHJAok8 es una charla donde Torvalds habla sobre git. Lo menciona más adelante en la charla.
Mateo

66
@ Martin, mira esta pregunta: stackoverflow.com/questions/1728922/…
Benjol


25

Déjame adivinar: ¿Diez clientes con conjuntos de características divergentes y un gerente de ventas que promueve la "personalización"? He trabajado en productos como ese antes. Tuvimos esencialmente el mismo problema.

Reconoce que tener un archivo enorme es un problema, pero aún más problemas son las diez versiones que debe mantener "actualizadas". Eso es mantenimiento múltiple. SCC puede facilitarlo, pero no puede hacerlo bien.

Antes de intentar dividir el archivo en partes, debe volver a sincronizar las diez ramas para que pueda ver y dar forma a todo el código a la vez. Puede hacer esto una rama a la vez, probando ambas ramas contra el mismo archivo de código principal. Para hacer cumplir el comportamiento personalizado, puede usar #ifdef y amigos, pero es mejor usar lo normal si / de lo contrario contra constantes definidas. De esta manera, su compilador verificará todos los tipos y probablemente eliminará el código objeto "muerto" de todos modos. (Sin embargo, es posible que desee desactivar la advertencia sobre el código muerto).

Una vez que solo hay una versión de ese archivo compartida implícitamente por todas las ramas, es más fácil comenzar con los métodos tradicionales de refactorización.

Los #ifdefs son principalmente mejores para las secciones donde el código afectado solo tiene sentido en el contexto de otras personalizaciones por rama. Se puede argumentar que estos también presentan una oportunidad para el mismo esquema de fusión de ramas, pero no se vuelvan locos. Un proyecto colosal a la vez, por favor.

A corto plazo, el archivo parecerá crecer. Esto esta bien. Lo que estás haciendo es unir cosas que necesitan estar juntas. Luego, comenzará a ver áreas que son claramente las mismas independientemente de la versión; estos se pueden dejar solos o refactorizados a voluntad. Otras áreas diferirán claramente según la versión. Tienes varias opciones en este caso. Un método es delegar las diferencias a objetos de estrategia por versión. Otra es derivar versiones de cliente de una clase abstracta común. Pero ninguna de estas transformaciones es posible siempre que tenga diez "consejos" de desarrollo en diferentes ramas.


2
Estoy de acuerdo en que el objetivo debería ser tener una versión del software, pero no sería mejor usar archivos de configuración (tiempo de ejecución) y no compilar la personalización del tiempo
Esben Skov Pedersen

O incluso "clases de configuración" para la compilación de cada cliente.
tc.

Me imagino que la configuración en tiempo de compilación o en tiempo de ejecución es funcionalmente irrelevante, pero no quiero limitar las posibilidades. La configuración en tiempo de compilación tiene la ventaja de que el cliente no puede hackear un archivo de configuración para activar funciones adicionales, ya que coloca toda la configuración en el árbol de origen en lugar de un código desplegable de "objeto textual". La otra cara es que tiende a AlternateHardAndSoftLayers si es tiempo de ejecución.
Ian

22

No sé si esto resuelve su problema, pero supongo que lo que quiere hacer es migrar el contenido del archivo a archivos más pequeños independientes entre sí (resumidos). Lo que también entiendo es que tienes alrededor de 10 versiones diferentes del software flotando y necesitas apoyarlas a todas sin estropear las cosas.

En primer lugar, no hay forma de que esto sea fácil y se resuelva solo en unos pocos minutos de lluvia de ideas. Las funciones vinculadas en su archivo son vitales para su aplicación, y simplemente cortarlas y migrarlas a otros archivos no salvará su problema.

Creo que solo tienes estas opciones:

  1. No migres y quédate con lo que tienes. Posiblemente abandone su trabajo y comience a trabajar en software serio con un buen diseño además. La programación extrema no siempre es la mejor solución si está trabajando en un proyecto a largo plazo con fondos suficientes para sobrevivir a un choque o dos.

  2. Elabore un diseño de cómo le gustaría que se vea su archivo una vez que se haya dividido. Cree los archivos necesarios e intégrelos en su aplicación. Cambie el nombre de las funciones o sobrecarguelas para tomar un parámetro adicional (¿tal vez solo un simple booleano?). Una vez que tenga que trabajar en su código, migre las funciones que necesita para trabajar al nuevo archivo y asigne las llamadas de función de las funciones antiguas a las nuevas funciones. Aún debe tener su archivo principal de esta manera, y aún así poder ver los cambios que se le hicieron, una vez que se trata de una función específica, sabe exactamente cuándo se subcontrató, etc.

  3. Intente convencer a sus compañeros de trabajo con un buen pastel de que el flujo de trabajo está sobrevalorado y que necesita reescribir algunas partes de la aplicación para hacer negocios serios.


19

Exactamente este problema se maneja en uno de los capítulos del libro "Trabajando efectivamente con código heredado" ( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 ).


informit.com/store/product.aspx?isbn=0131177052 hace posible ver el TOC de este libro (y 2 capítulos de muestra). ¿Cuánto dura el capítulo 20? (Solo para tener una idea de lo útil que podría ser.)
Martin Ba

17
el capítulo 20 tiene 10,000 líneas de largo, pero el autor está resolviendo cómo dividirlo en trozos digeribles ... 8)
Tony Delroy

1
Son unas 23 páginas, pero con 14 imágenes. Creo que deberías obtenerlo, te sentirás mucho más seguro tratando de decidir qué hacer en mi humilde opinión.
Emile Vrijdags

Un excelente libro para el problema, pero las recomendaciones que hace (y otras recomendaciones en este hilo) comparten un requisito común: si desea refactorizar este archivo para todas sus sucursales, entonces la única forma de hacerlo es congelar archivo para todas las ramas y hacer los cambios estructurales iniciales. No hay forma de evitar eso. El libro describe un enfoque iterativo para extraer subclases de forma segura sin soporte de refactorización automática, creando métodos duplicados y delegando llamadas, pero todo esto es discutible si no puede modificar los archivos.
Dan Bryant

2
@Martin, el libro es excelente, pero depende en gran medida de la prueba, refactorización, ciclo de prueba, que puede ser bastante difícil de donde estás ahora. He estado en una situación similar y este libro fue el más útil que encontré. Tiene buenas sugerencias para el feo problema que tienes. Pero si no puede obtener un arnés de prueba de algún tipo en la imagen, todas las sugerencias de refactorización en el mundo no lo ayudarán.

14

Creo que sería mejor crear un conjunto de clases de comandos que se asignen a los puntos API de mainmodule.cpp.

Una vez que estén en su lugar, necesitará refactorizar la base de código existente para acceder a estos puntos API a través de las clases de comando, una vez hecho esto, puede refactorizar la implementación de cada comando en una nueva estructura de clases.

Por supuesto, con una sola clase de 11 KLOC, el código allí es probablemente muy acoplado y frágil, pero crear clases de comando individuales ayudará mucho más que cualquier otra estrategia de proxy / fachada.

No envidio la tarea, pero a medida que pasa el tiempo este problema solo empeorará si no se aborda.

Actualizar

Sugeriría que el patrón de Comando es preferible a una Fachada.

Es preferible mantener / organizar muchas clases de Comando diferentes en una Fachada (relativamente) monolítica. La asignación de una sola Fachada en un archivo 11 KLOC probablemente tendrá que dividirse en algunos grupos diferentes.

¿Por qué molestarse en tratar de descubrir estos grupos de fachadas? Con el patrón Command podrá agrupar y organizar estas clases pequeñas de forma orgánica, por lo que tiene mucha más flexibilidad.

Por supuesto, ambas opciones son mejores que el solo 11 KLOC y el archivo en crecimiento.


+1 una alternativa a la solución que propuse, con la misma idea: cambiar la API para separar el gran problema en pequeños.
Benoît

13

Un consejo importante: no mezcle refactorización y corrección de errores. Lo que desea es una versión de su programa que sea idéntica a la versión anterior, excepto que el código fuente es diferente.

Una forma podría ser comenzar a dividir la función / parte menos grande en su propio archivo y luego incluirlo con un encabezado (convirtiendo main.cpp en una lista de #incluidos, lo que suena a olor de código en sí mismo * No estoy un C ++ Guru sin embargo), pero al menos ahora está dividido en archivos).

A continuación, puede intentar cambiar todas las versiones de mantenimiento al "nuevo" main.cpp o sea cual sea su estructura. Nuevamente: no hay otros cambios o correcciones de errores porque rastrearlos es confuso como el infierno.

Otra cosa: por mucho que desee hacer un gran paso para refactorizar todo de una vez, puede morder más de lo que puede masticar. Tal vez solo elija una o dos "partes", introdúzcalas en todos los lanzamientos, luego agregue más valor para su cliente (después de todo, la refactorización no agrega valor directo, por lo que es un costo que debe justificarse) y luego elija otro una o dos partes

Obviamente, eso requiere cierta disciplina en el equipo para usar realmente los archivos divididos y no solo agregar cosas nuevas al main.cpp todo el tiempo, sino nuevamente, tratar de hacer un refactor masivo puede no ser el mejor curso de acción.


1
+1 para factorizar y #incluir de nuevo. Si hiciera esto para las 10 sucursales (un poco de trabajo allí, pero manejable) todavía tendría el otro problema, el de publicar cambios en todas sus sucursales, pero ese problema no ' Se han expandido (necesariamente). ¿Es feo? Sí, todavía lo es, pero podría aportar un poco de racionalidad al problema. Después de haber pasado varios años haciendo mantenimiento y servicio para un producto realmente muy grande, sé que el mantenimiento implica mucho dolor. Como mínimo, aprenda de ello y sirva de advertencia a los demás.
Jay

10

Rofl, esto me recuerda a mi antiguo trabajo. Parece que, antes de unirme, todo estaba dentro de un archivo enorme (también C ++). Luego lo han dividido (en puntos completamente aleatorios usando incluye) en aproximadamente tres (todavía archivos enormes). La calidad de este software fue, como era de esperar, horrible. El proyecto totalizó alrededor de 40k LOC. (que casi no contiene comentarios pero MUCHOS códigos duplicados)

Al final hice una reescritura completa del proyecto. Comencé rehaciendo la peor parte del proyecto desde cero. Por supuesto, tenía en mente una posible interfaz (pequeña) entre esta nueva parte y el resto. Luego inserté esta parte en el antiguo proyecto. No refactoré el código anterior para crear la interfaz necesaria, sino que lo reemplacé. Luego di pequeños pasos desde allí, reescribiendo el viejo código.

Tengo que decir que esto tomó aproximadamente medio año y no hubo desarrollo de la antigua base de código además de las correcciones de errores durante ese tiempo.


editar:

El tamaño se mantuvo en aproximadamente 40k LOC, pero la nueva aplicación contenía muchas más funciones y presumiblemente menos errores en su versión inicial que el software de 8 años. Una razón de la reescritura también fue que necesitábamos las nuevas características y era casi imposible introducirlas dentro del código anterior.

El software era para un sistema integrado, una impresora de etiquetas.

Otro punto que debo agregar es que, en teoría, el proyecto era C ++. Pero no era OO en absoluto, podría haber sido C. La nueva versión estaba orientada a objetos.


99
¡Cada vez que escucho "desde cero" en el tema sobre refactorización, mato a un gatito!
Kugel

He estado en una situación muy similar, aunque el ciclo principal del programa con el que tuve que lidiar fue solo ~ 9000 LOC. Y eso ya era bastante malo.
AndyUK

8

OK, por lo general, reescribir la API del código de producción es una mala idea como comienzo. Deben suceder dos cosas.

Primero, debe hacer que su equipo decida congelar el código en la versión de producción actual de este archivo.

Dos, debe tomar esta versión de producción y crear una rama que gestione las compilaciones utilizando directivas de preprocesamiento para dividir el archivo grande. Dividir la compilación usando las directivas de preprocesador JUST (#ifdefs, #includes, #endifs) es más fácil que recodificar la API. Definitivamente es más fácil para sus SLA y soporte continuo.

Aquí puede simplemente cortar las funciones que se relacionan con un subsistema en particular dentro de la clase y ponerlas en un archivo como mainloop_foostuff.cpp e incluirlo en mainloop.cpp en la ubicación correcta.

O

Una manera más lenta pero robusta sería diseñar una estructura de dependencias internas con doble indirección en la forma en que se incluyen las cosas. Esto le permitirá dividir las cosas y aún ocuparse de las codependencias. Tenga en cuenta que este enfoque requiere codificación posicional y, por lo tanto, debe combinarse con los comentarios apropiados.

Este enfoque incluiría componentes que se utilizan según la variante que está compilando.

La estructura básica es que su mainclass.cpp incluirá un nuevo archivo llamado MainClassComponents.cpp después de un bloque de declaraciones como el siguiente:

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

La estructura primaria del archivo MainClassComponents.cpp estaría allí para resolver las dependencias dentro de los subcomponentes de esta manera:

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

Y ahora para cada componente crea un archivo component_xx.cpp.

Por supuesto, estoy usando números, pero deberías usar algo más lógico basado en tu código.

El uso del preprocesador le permite dividir las cosas sin tener que preocuparse por los cambios de API, lo cual es una pesadilla en la producción.

Una vez que haya establecido la producción, puede trabajar en el rediseño.


Esto se parece a los resultados de la experiencia que funcionan pero inicialmente son dolorosos.
JBRWilkinson

En realidad, es una técnica que se utilizó en los compiladores de Borland C ++ para emular los usos de estilo Pascal para administrar archivos de encabezado. Especialmente cuando hicieron el puerto inicial de su sistema de ventanas basado en texto.
Elf King

8

Bueno, entiendo tu dolor :) He estado en algunos de esos proyectos también y no es bonito. No hay una respuesta fácil para esto.

Un enfoque que puede funcionar para usted es comenzar a agregar protecciones de seguridad en todas las funciones, es decir, verificar argumentos, condiciones previas / posteriores en los métodos, y luego agregar pruebas unitarias para capturar la funcionalidad actual de las fuentes. Una vez que tenga esto, estará mejor equipado para volver a factorizar el código porque aparecerá afirmaciones y errores que le alertarán si ha olvidado algo.

A veces, aunque hay momentos en que la refactorización solo puede traer más dolor que beneficio. Entonces puede ser mejor dejar el proyecto original y en un estado de pseudo mantenimiento y comenzar desde cero y luego agregar gradualmente la funcionalidad de la bestia.


4

No debe preocuparse por reducir el tamaño del archivo, sino por reducir el tamaño de la clase. Se reduce a casi lo mismo, pero te hace ver el problema desde un ángulo diferente (como sugiere @Brian Rasmussen , tu clase parece tener muchas responsabilidades).


Como siempre, me gustaría obtener una explicación para el voto negativo.
Björn Pollex

4

Lo que tienes es un ejemplo clásico de un antipatrón de diseño conocido llamado blob . Tómese un tiempo para leer el artículo que señalo aquí, y tal vez pueda encontrar algo útil. Además, si este proyecto es tan grande como parece, debe considerar algún diseño para evitar convertirse en código que no puede controlar.


4

Esta no es una respuesta al gran problema, sino una solución teórica a una parte específica del mismo:

  • Averigua dónde quieres dividir el archivo grande en subarchivos. Ponga comentarios en algún formato especial en cada uno de esos puntos.

  • Escriba un script bastante trivial que separe el archivo en subarchivos en esos puntos. (Quizás los comentarios especiales tienen nombres de archivo incrustados que el script puede usar como instrucciones sobre cómo dividirlo). Debe conservar los comentarios como parte de la división.

  • Ejecute el script Eliminar el archivo original.

  • Cuando necesite fusionar desde una rama, primero vuelva a crear el archivo grande al concatenar las piezas nuevamente, realice la fusión y luego vuelva a dividirlo.

Además, si desea preservar el historial de archivos SCC, espero que la mejor manera de hacerlo sea decirle a su sistema de control de origen que los archivos de piezas individuales son copias del original. Luego conservará el historial de las secciones que se guardaron en ese archivo, aunque, por supuesto, también registrará que las partes grandes fueron "eliminadas".


4

Una forma de dividirlo sin demasiado peligro sería echar un vistazo histórico a todos los cambios de línea. ¿Hay ciertas funciones que son más estables que otras? Puntos calientes de cambio si lo desea.

Si una línea no ha cambiado en algunos años, probablemente pueda moverla a otro archivo sin demasiada preocupación. Echaría un vistazo a la fuente anotada con la última revisión que tocó una línea determinada y veré si hay alguna función que pueda realizar.


Creo que otros propusieron cosas similares. Esto es breve y directo, y creo que este puede ser un punto de partida válido para el problema original.
Martin Ba

3

Wow, suena genial Creo que explicarle a tu jefe que necesitas mucho tiempo para refactorizar a la bestia vale la pena intentarlo. Si no está de acuerdo, dejar de fumar es una opción.

De todos modos, lo que sugiero es básicamente descartar toda la implementación y reagruparla en nuevos módulos, llamemos a esos "servicios globales". El "módulo principal" solo reenviará esos servicios y CUALQUIER código nuevo que escriba los usará en lugar del "módulo principal". Esto debería ser factible en un período de tiempo razonable (porque es principalmente copiar y pegar), no se rompe el código existente y puede hacerlo una versión de mantenimiento a la vez. Y si aún le queda tiempo, puede gastarlo refactorizando todos los módulos dependientes antiguos para que también utilicen los servicios globales.


3

Mis condolencias: en mi trabajo anterior me encontré con una situación similar con un archivo que era varias veces más grande que el que tenía que tratar. La solución fue:

  1. Escriba el código para probar exhaustivamente la función en el programa en cuestión. Parece que ya no tendrás esto en la mano ...
  2. Identifique algún código que pueda resumirse en una clase auxiliar / utilidades. No es necesario que sea grande, solo algo que no es realmente parte de su clase 'principal'.
  3. Refactorice el código identificado en 2. en una clase separada.
  4. Vuelva a ejecutar sus pruebas para asegurarse de que nada se haya roto.
  5. Cuando tenga tiempo, pase a 2. y repita según sea necesario para que el código sea manejable.

Las clases que cree en el paso 3. Las iteraciones probablemente crecerán para absorber más código que sea apropiado para su nueva función clara.

También podría agregar:

0: compre el libro de Michael Feathers sobre cómo trabajar con código heredado

Desafortunadamente, este tipo de trabajo es muy común, pero mi experiencia es que hay un gran valor en hacer que el código de trabajo pero horrible sea incrementalmente menos horrible mientras lo mantiene funcionando.


2

Considere formas de reescribir toda la aplicación de una manera más sensata. Tal vez reescriba una pequeña sección como prototipo para ver si su idea es factible.

Si ha identificado una solución viable, refactorice la aplicación en consecuencia.

Si todos los intentos de producir una arquitectura más racional fallan, entonces al menos usted sabe que la solución probablemente sea redefinir la funcionalidad del programa.


+1: reescríbelo en tu propio tiempo, aunque de lo contrario alguien podría escupir su tonto.
Jon Black

2

Mis 0.05 céntimos de euro:

Rediseñe todo el desorden, divídalo en subsistemas teniendo en cuenta los requisitos técnicos y comerciales (= muchas pistas de mantenimiento paralelas con una base de código potencialmente diferente para cada una, obviamente existe la necesidad de una alta modificabilidad, etc.).

Al dividirse en subsistemas, analice los lugares que más han cambiado y separe los de las partes que no cambian. Esto debería mostrarte los puntos problemáticos. Separe las partes más cambiantes en sus propios módulos (por ejemplo, dll) de tal manera que la API del módulo pueda mantenerse intacta y no necesite romper BC todo el tiempo. De esta manera, puede implementar diferentes versiones del módulo para diferentes ramas de mantenimiento, si es necesario, sin modificar el núcleo.

El rediseño probablemente necesitará ser un proyecto separado, tratar de hacerlo a un objetivo en movimiento no funcionará.

En cuanto al historial del código fuente, mi opinión: olvídalo del nuevo código. Pero mantenga el historial en algún lugar para que pueda consultarlo, si es necesario. Apuesto a que no lo necesitarás tanto después del comienzo.

Lo más probable es que necesite obtener la aceptación de la administración para este proyecto. Quizás pueda discutir con un tiempo de desarrollo más rápido, menos errores, mantenimiento más fácil y menos caos general. Algo parecido a "Habilitar de forma proactiva la viabilidad de mantenimiento y seguridad en el futuro de nuestros activos de software críticos" :)

Así es como comenzaría a abordar el problema al menos.


2

Comience por agregarle comentarios. Con referencia a dónde se llaman las funciones y si puede mover las cosas. Esto puede hacer que las cosas se muevan. Realmente necesita evaluar qué tan frágil se basa el código. Luego mueva partes comunes de funcionalidad juntas. Pequeños cambios a la vez.



2

Algo que me parece útil hacer (y lo estoy haciendo ahora, aunque no a la escala que enfrenta), es extraer métodos como clases (refactorización de objetos de método). Los métodos que difieren entre las diferentes versiones se convertirán en diferentes clases que se pueden inyectar en una base común para proporcionar el comportamiento diferente que necesita.


2

Encontré esta oración como la parte más interesante de tu publicación:

> El archivo se usa y cambia activamente en varias (> 10) versiones de mantenimiento de nuestro producto, por lo que es muy difícil refactorizarlo

Primero, recomendaría que utilice un sistema de control de fuente para desarrollar estas versiones de mantenimiento 10+ que admitan la ramificación.

En segundo lugar, crearía diez ramas (una para cada una de sus versiones de mantenimiento).

¡Ya puedo sentirte encogido! Pero o su control de fuente no funciona para su situación debido a la falta de características, o no se está utilizando correctamente.

Ahora a la rama en la que trabaja, refactorícela como mejor le parezca, seguro sabiendo que no alterará las otras nueve ramas de su producto.

Me preocuparía un poco que tengas tanto en tu función main ().

En cualquier proyecto que escriba, usaría main () solo para realizar la inicialización de los objetos principales, como una simulación o un objeto de aplicación, estas clases es donde debe continuar el trabajo real.

También inicializaría un objeto de registro de aplicaciones en main para su uso global en todo el programa.

Finalmente, en main también agrego código de detección de fugas en bloques de preprocesador que aseguran que solo esté habilitado en las compilaciones DEBUG. Esto es todo lo que agregaría a main (). ¡Main () debe ser corto!

Tu dices eso

> El archivo contiene básicamente la "clase principal" (despacho y coordinación de trabajo interno principal) de nuestro programa

Parece que estas dos tareas podrían dividirse en dos objetos separados: un coordinador y un despachador de trabajo.

Cuando los divide, puede estropear su "flujo de trabajo SCC", pero parece que cumplir estrictamente con su flujo de trabajo SCC está causando problemas de mantenimiento de software. Deshazte de él ahora y no mires atrás, porque tan pronto como lo arregles, comenzarás a dormir tranquilo.

Si no puede tomar la decisión, luche con uñas y dientes con su gerente por ello, su aplicación debe ser refactorizada y, por lo que parece, ¡mal! ¡No aceptes un no por respuesta!


Según tengo entendido, el problema es este: si muerde la bala y refactoriza, ya no puede llevar parches entre las versiones. SCC podría estar perfectamente configurado.
peterchen

@peterchen: exactamente el problema. Los SCC se fusionan en el nivel de archivo. (Combinaciones de 3 vías) Si mueve el código entre archivos, deberá comenzar a manipular manualmente los bloques de código modificado de un archivo a otro. (La característica GIT que alguien mencionó en otro comentario es buena para la historia, no para fusionarse hasta donde puedo decir)
Martin Ba

2

Como lo ha descrito, el problema principal es diferenciar entre la división previa y la división posterior, la combinación de correcciones de errores, etc. Herramienta a su alrededor. No tomará tanto tiempo codificar un script en Perl, Ruby, etc. para eliminar la mayor parte del ruido de la división previa y la concatenación de la división posterior. Haga lo que sea más fácil en términos de manejo del ruido:

  • eliminar ciertas líneas antes / durante la concatenación (por ejemplo, incluir protectores)
  • eliminar otras cosas de la salida diff si es necesario

Incluso podría hacerlo para que siempre que haya un registro, se ejecute la concatenación y tenga algo preparado para diferenciarse de las versiones de un solo archivo.


2
  1. ¡No vuelvas a tocar este archivo y el código otra vez!
  2. Tratar es como algo con lo que estás atrapado. Comience a escribir adaptadores para la funcionalidad codificada allí.
  3. Escriba código nuevo en diferentes unidades y hable solo con adaptadores que encapsulan la funcionalidad del monstruo.
  4. ... si solo uno de los anteriores no es posible, abandone el trabajo y obtenga uno nuevo.

2
+/- 0 - en serio, ¿dónde viven ustedes que recomendarían renunciar a un trabajo basándose en detalles técnicos como este?
Martin Ba

1

"El archivo contiene básicamente la" clase principal "(distribución y coordinación de trabajo interno principal) de nuestro programa, por lo que cada vez que se agrega una característica, también afecta a este archivo y cada vez que crece".

Si ese gran INTERRUPTOR (que creo que existe) se convierte en el principal problema de mantenimiento, puede refactorizarlo para usar el diccionario y el patrón de Comando y eliminar toda la lógica del interruptor del código existente al cargador, que llena ese mapa, es decir:

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();

2
No, no hay un gran cambio en realidad. La frase es lo más cerca que puedo llegar a describir este desastre :)
Martin Ba

1

Creo que la forma más fácil de rastrear el historial de la fuente al dividir un archivo sería algo como esto:

  1. Haga copias del código fuente original, utilizando los comandos de copia que conservan el historial que proporciona su sistema SCM. Probablemente necesitará enviar en este punto, pero aún no es necesario informarle a su sistema de compilación sobre los nuevos archivos, por lo que debería estar bien.
  2. Eliminar el código de estas copias. Eso no debería romper el historial de las líneas que mantienes.

"utilizando los comandos de copia que conservan el historial que proporciona su sistema SCM" ... algo malo que no proporciona
Martin Ba

Demasiado. Eso solo parece una buena razón para cambiar a algo más moderno. :-)
Christopher Creutzig

1

Creo que lo que haría en esta situación es morder la bala y:

  1. Averigua cómo quería dividir el archivo (según la versión de desarrollo actual)
  2. Ponga un bloqueo administrativo en el archivo ("¡Nadie toca mainmodule.cpp después de las 5pm del viernes!"
  3. Pase su fin de semana largo aplicando ese cambio a las> 10 versiones de mantenimiento (de la más antigua a la más reciente), hasta la versión actual incluida.
  4. Elimine mainmodule.cpp de todas las versiones compatibles del software. Es una nueva era: no hay más mainmodule.cpp.
  5. Convencer a la Administración de que no debe admitir más de una versión de mantenimiento del software (al menos sin un gran contrato de soporte $$$). Si cada uno de sus clientes tiene su propia versión única ... yeeeeeshhhh. Agregaría directivas de compilación en lugar de tratar de mantener más de 10 tenedores.

El seguimiento de los cambios antiguos en el archivo se resuelve simplemente con su primer comentario de registro que dice algo como "dividir desde mainmodule.cpp". Si necesita volver a algo reciente, la mayoría de las personas recordará el cambio, si es dentro de 2 años, el comentario les dirá dónde buscar. Por supuesto, ¿qué tan valioso será retroceder más de 2 años para ver quién cambió el código y por qué?

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.