Conversión de la fuente C a C++
refactoring legacy (11)
Esto es lo que haría:
- Como el código tiene 20 años, elimine el analizador de sintaxis / analizador y reemplácelo por uno de los códigos de C ++ basados en lex / yacc / bison más nuevos (o similares), etc., mucho más fácil de mantener y comprender. Más rápido para desarrollar también si tienes un BNF a mano.
- Una vez que esto se haya adaptado al código anterior, comience a envolver los módulos en clases. Reemplazar variables globales / compartidas con interfaces.
- Ahora lo que tienes será un compilador en C ++ (no del todo bien).
- Dibuje un diagrama de clases de todas las clases en su sistema y vea cómo se están comunicando.
- Dibuja otro usando las mismas clases y ve cómo deben comunicarse.
- Refactorice el código para transformar el primer diagrama en el segundo. (Esto podría ser complicado y complicado)
- Recuerde utilizar el código C ++ para todo el nuevo código agregado.
- Si le queda algo de tiempo, intente reemplazar las estructuras de datos una a una para usar el estándar STL o Boost.
¿Cómo haría para convertir una base de código C razonablemente grande (> 300K) y bastante madura en C ++?
El tipo de IC que tenemos en mente se divide en archivos que corresponden aproximadamente a módulos (es decir, menos granulares que una descomposición típica basada en clases OO), utilizando enlaces internos en lugar de funciones y datos privados, y enlaces externos para funciones y datos públicos. Las variables globales se utilizan ampliamente para la comunicación entre los módulos. Existe un conjunto de pruebas de integración muy extenso disponible, pero no hay pruebas de nivel de unidades (es decir, módulos).
Tengo en mente una estrategia general:
- Compila todo en el subconjunto C de C ++ y haz que funcione.
- Convierta los módulos en grandes clases, de modo que todas las referencias cruzadas estén delimitadas por un nombre de clase, pero dejando todas las funciones y datos como miembros estáticos, y haga que funcionen.
- Convierta clases enormes en instancias con constructores apropiados y referencias cruzadas inicializadas; reemplazar los accesos a miembros estáticos con accesos indirectos según corresponda; y haz que funcione
- Ahora, enfoque el proyecto como una aplicación OO mal factorizada, y escriba pruebas unitarias donde las dependencias son tratables, y descompóngase en clases separadas donde no lo están; el objetivo aquí sería pasar de un programa de trabajo a otro en cada transformación.
Obviamente, esto sería bastante trabajo. ¿Hay estudios de caso / historias de guerra en este tipo de traducción? Estrategias alternativas? ¿Otro consejo útil?
Nota 1: el programa es un compilador, y probablemente millones de otros programas confíen en que su comportamiento no cambia, por lo que la reescritura al por mayor no es una opción.
Nota 2: la fuente tiene casi 20 años y quizás tenga un 30% de abandono del código (líneas modificadas + líneas totales agregadas / anteriores) por año. En otras palabras, es muy mantenido y extendido. Por lo tanto, uno de los objetivos sería aumentar la capacidad de mantenimiento.
[En aras de la pregunta, suponga que la traducción a C ++ es obligatoria, y que dejarla en C no es una opción. El punto de agregar esta condición es eliminar las respuestas "déjalo en C".]
Echemos otra idea estúpida:
- Compila todo en el subconjunto C de C ++ y haz que funcione.
- Comience con un módulo, conviértalo en una gran clase, luego en una instancia y construya una interfaz C (idéntica a la que comenzó) fuera de esa instancia. Deja que el código C restante funcione con esa interfaz C.
- Refactorice según sea necesario, haciendo crecer el subsistema OO desde el código C un módulo a la vez, y elimine partes de la interfaz C cuando se vuelvan inútiles.
Escribiría clases de C ++ en la interfaz C. Si no se toca el código C, disminuirá la probabilidad de que se estropee y acelere el proceso de manera significativa.
Una vez que tenga su interfaz C ++ arriba; entonces es una tarea trivial copiar + pegar el código en sus clases. Como mencionaste, durante este paso es vital hacer pruebas unitarias.
Probablemente, dos cosas que debe considerar, además de cómo desea comenzar, son en qué quiere enfocarse y dónde desea detenerse .
Usted declara que hay un gran cambio de código, esto puede ser una clave para enfocar sus esfuerzos. Le sugiero que elija las partes de su código donde se necesita mucho mantenimiento, las partes maduras / estables aparentemente funcionan lo suficientemente bien, por lo que es mejor dejarlas tal como están, excepto, probablemente, para algunas cortinas con fachadas, etc.
El lugar donde desea detenerse depende de la razón por la que desea convertir a C ++. Esto difícilmente puede ser un objetivo en sí mismo. Si se debe a alguna dependencia de terceros, centre sus esfuerzos en la interfaz de ese componente.
El software en el que trabajo es una gran base de código antigua que se ha "convertido" de C a C ++ hace años. Creo que fue porque la GUI se convirtió a Qt. Incluso ahora, todavía parece un programa C con clases. Romper las dependencias causadas por los miembros de datos públicos, y refactorizar las grandes clases con métodos de monstruos de procedimiento en métodos más pequeños y las clases nunca realmente ha despegado, creo que por las siguientes razones:
- No es necesario cambiar el código que está funcionando y que no necesita ser mejorado. Al hacerlo, introduce nuevos errores sin agregar funcionalidad, y los usuarios finales no lo aprecian;
- Es muy, muy difícil hacer refactor de manera confiable. Muchos pedazos de código son tan grandes y tan vitales que la gente casi no se atreve a tocarlo. Tenemos un conjunto bastante extenso de pruebas funcionales, pero es difícil obtener suficiente información de cobertura del código. Como resultado, es difícil establecer si ya existen suficientes pruebas para detectar problemas durante la refactorización;
- El ROI es difícil de establecer. El usuario final no se beneficiará de la refactorización, por lo que debe ser un costo de mantenimiento reducido, que aumentará inicialmente porque al refactorizar introduce nuevos errores en código maduro, es decir, bastante libre de errores. Y la refacturación también será costosa ...
NÓTESE BIEN. Supongo que conoce el libro "Working effective with Legacy code"?
Qué pasa:
- Compilando todo en el subconjunto C de C ++ y ponlo a funcionar, y
- ¿Implementando un conjunto de fachadas que deja el código C inalterado?
¿Por qué es "obligatoria la traducción a C ++"? Puede envolver el código C sin el dolor de convertirlo en clases enormes y así sucesivamente.
Si tiene un proyecto pequeño o académico (digamos, menos de 10,000 líneas), una reescritura es probablemente su mejor opción. Puedes factorizarlo como quieras, y no tomará demasiado tiempo.
Si tiene una aplicación en el mundo real, le sugiero que la compile como C ++ (que generalmente significa principalmente reparar prototipos de funciones y similares), luego trabaje en la refactorización y el ajuste de OO. Por supuesto, no me suscribo a la filosofía de que el código necesita ser estructurado OO para ser un código C ++ aceptable. Haría una conversión pieza por pieza, reescribiendo y refactorizando como lo necesitaras (para funcionalidad o para incorporar pruebas unitarias).
Su lista se ve bien, pero le sugiero que revise el conjunto de pruebas primero y trate de hacerlo lo más ajustado posible antes de hacer cualquier codificación.
GCC se encuentra actualmente en transición media a C ++ desde C. Empezaron moviendo todo al subconjunto común de C y C ++, obviamente. Mientras lo hacían, agregaron advertencias a GCC por todo lo que encontraron, que se encuentra en -Wc++-compat
. Eso debería llevarte en la primera parte de tu viaje.
Para las últimas partes, una vez que realmente tienes todo compilando con un compilador C ++, me enfocaría en reemplazar cosas que tienen contrapartes idiomáticas de C ++. Por ejemplo, si usa listas, mapas, conjuntos, bitvectores, tablas hash, etc., que se definen con macros C, es probable que gane mucho moviéndolos a C ++. Del mismo modo con OO, es probable que encuentre beneficios en los que ya esté utilizando una expresión idiomática C OO (como struct inheritence), y donde C ++ le proporcione mayor claridad y un mejor tipo de verificación de su código.
Menciona que su herramienta es un compilador y que: "En realidad, la coincidencia de patrones, no solo la coincidencia de tipos, en el despacho múltiple sería aún mejor".
Es posible que desee echar un vistazo a maketea . Proporciona coincidencia de patrones para AST, así como la definición AST a partir de una gramática abstracta, y visitantes, transformadores, etc.
Su aplicación tiene mucha gente trabajando en ello y una necesidad de no estar roto. Si se toma en serio la conversión a gran escala a un estilo OO, lo que necesita son herramientas de transformación masiva para automatizar el trabajo.
La idea básica es designar grupos de datos como clases, y luego obtener la herramienta para refactorizar el código para mover esos datos a las clases, mover las funciones de solo esos datos a esas clases, y revisar todos los accesos a esos datos a las llamadas en las clases .
Puede hacer un preanálisis automático para formar clústeres de estadísticas para obtener algunas ideas, pero igual necesitará un ingeniero experto en aplicaciones para decidir qué elementos de datos se deben agrupar.
Una herramienta que es capaz de hacer esta tarea es nuestro kit de herramientas de reingeniería de software DMS . DMS tiene analizadores C fuertes para leer su código, captura el código C como árboles sintácticos abstractos del compilador (y, a diferencia de un compilador convencional) puede calcular los análisis de flujo a través de su 300K SLOC completo. DMS tiene un front-end de C ++ que puede usarse como el "back" end; uno escribe transformaciones que mapean sintaxis C a sintaxis C ++.
Una importante tarea de reingeniería de C ++ en un gran sistema de aviónica da una idea de cómo es usar DMS para este tipo de actividad. Consulte los documentos técnicos en www.semdesigns.com/Products/DMS/DMSToolkit.html, específicamente Reingeniería de modelos de componentes en C ++ a través de la Transformación automática de programas
Este proceso no es para los débiles de corazón. Pero que cualquiera que considere la refacturación manual de una aplicación grande ya no tiene miedo al trabajo duro.
Sí, estoy asociado con la compañía, siendo su principal arquitecto.
Habiendo comenzado prácticamente lo mismo hace unos meses (en un proyecto comercial de diez años, originalmente escrito con la filosofía "C ++ no es más que C con la struct
inteligente"), sugeriría utilizar la misma estrategia. Solía comer un elefante: tómalo un bocado a la vez. :-)
En la medida de lo posible, divídalo en etapas que se pueden hacer con efectos mínimos en otras partes. Construir un sistema de fachada, como sugirió Federico Ramponi , es un buen comienzo: una vez que todo tiene una fachada C ++ y se comunica a través de él, puede cambiar las partes internas de los módulos con la certeza justa de que no pueden afectar nada fuera de ellos.
Ya contamos con un sistema de interfaz C ++ parcial (debido a los esfuerzos de refactorización anteriores más pequeños), por lo que este enfoque no fue difícil en nuestro caso. Una vez que tuvimos todo comunicándonos como objetos C ++ (lo que tomó algunas semanas, trabajando en una rama de código fuente completamente separada e integrando todos los cambios a la rama principal tal como se aprobaron), era muy raro que no pudiéramos compilar totalmente versión de trabajo antes de irnos por el día.
El cambio aún no está completo: hicimos una pausa dos veces para los lanzamientos provisionales (apostamos por un lanzamiento puntual cada pocas semanas), pero está en camino y ningún cliente se ha quejado de ningún problema. Nuestra gente de QA solo ha encontrado un problema que yo también recuerdo. :-)