2010-06-09 7 views
36

He estado trabajando con un código C de 10 años en mi trabajo esta semana, y después de implementar algunos cambios, fui al jefe y le pregunté si necesitaba algo más. Fue entonces cuando dejó caer la bomba. Mi siguiente tarea fue ir a través de las 7000 o más líneas y comprender más del código, y para modularizar el código de alguna manera. Le pregunté cómo le gustaría modularizar el código fuente y me dijo que comenzara a poner el viejo código C en las clases de C++.¿Cómo actualizar el código C antiguo?

Siendo un buen trabajador, asentí con la cabeza sí, y volví a mi escritorio, donde ahora me siento, preguntándome cómo en el mundo tomar este código y "modularizarlo". Ya está en 20 archivos fuente, cada uno con su propio propósito y función. Además, hay tres estructuras "principales". cada una de estas estructuras tiene más de 30 campos, muchos de ellos son otras estructuras más pequeñas. Es un completo desastre tratar de entender, pero casi todas las funciones del programa pasan un puntero a una de las estructuras y usan la estructura pesadamente.

¿Hay alguna manera limpia de calzar esto en clases? Estoy decidido a hacerlo si se puede hacer, simplemente no tengo idea de cómo comenzar.

+29

+1 Ese gran suspiro colectivo que acaba de escuchar fue la gran cantidad de usuarios de SO que trabajan con código heredado ... –

+7

O bien refactorizarlo en C bien estructurado, o reescribirlo en C++ idiomático. Intentar forzar a C malo a utilizar algún tipo de lenguaje híbrido le dará los problemas de mantenimiento de ambos idiomas, y pocos de los beneficios de ninguno de ellos. –

+0

Definitivamente hablaría con su jefe sobre la reescritura del programa –

Respuesta

35

En primer lugar, tiene la suerte de contar con un jefe que reconoce que la refacturación de códigos puede ser una estrategia de ahorro de costos a largo plazo.

He hecho esto muchas veces, es decir, convertir el código C antiguo a C++. Los beneficios pueden sorprenderlo. El código final puede ser la mitad del tamaño original cuando haya terminado, y mucho más simple de leer. Además, es probable que descubras bichos C difíciles en el camino. Estos son los pasos que tomaría en su caso. Los pasos pequeños son importantes porque no puede saltar de la A a la Z cuando refactoriza un cuerpo grande de código. Debe pasar por pasos pequeños e intermedios que quizás nunca se implementen, pero que puedan validarse y etiquetarse en cualquier RCS que esté utilizando.

  1. Crear un conjunto de regresión/prueba. Ejecutará el conjunto de pruebas cada vez que complete un lote de cambios en el código. Debería tener esto ya, y será útil para algo más que esta tarea de refactorización. Tómese el tiempo para hacerlo integral. El ejercicio de crear el banco de pruebas lo familiarizará con el código.
  2. Delegue el proyecto en su sistema de control de revisiones de su elección. Armado con un banco de pruebas y un parque infantil, tendrá la posibilidad de realizar grandes modificaciones al código. No tendrás miedo de romper algunos huevos.
  3. Haga que esos campos struct sean privados. Este paso requiere muy pocos cambios de código, pero puede tener un gran beneficio. Proceda un campo a la vez. Pruebe para hacer cada campo private (sí, o protegido), luego aísle el código que accede a ese campo. La conversión más sencilla y no intrusiva sería convertir ese código en friend function. Considera también hacer ese código un método. Convertir el código en un método es simple, pero también deberá convertir todos los sitios de llamadas. Uno no es necesariamente mejor que el otro.
  4. Reduzca los parámetros a cada función. Es poco probable que cualquier función requiera acceso a los 30 campos de la estructura pasada como argumento. En lugar de pasar toda la estructura, pase solo los componentes necesarios. Si una función, de hecho, parece requerir acceso a muchos campos diferentes de la estructura, este puede ser un buen candidato para convertirse en un método de instancia.
  5. Const-ify tantas variables, parámetros y métodos como sea posible. Un montón de código C antiguo no puede usar const generosamente. Al pasar de abajo hacia arriba (en la parte inferior del gráfico de llamadas), agregará garantías más sólidas al código, y podrá identificar los mutadores de los no mutadores.
  6. Reemplace los punteros con referencias donde sea sensible. El propósito de este paso no tiene nada que ver con ser más C++, por el simple hecho de ser más parecido a C++. El objetivo es identificar los parámetros que nunca son NULL y que nunca se pueden volver a asignar. Piense en una referencia como una afirmación en tiempo de compilación que dice, , este es un alias de un objeto válido y representa el mismo objeto en todo el ámbito actual.
  7. Reemplazar char* con std::string. Este paso debería ser obvio. Puede reducir drásticamente las líneas de código. Además, es divertido reemplazar 10 líneas de código con una sola línea.A veces puede eliminar funciones completas cuyo propósito fue realizar operaciones de cadenas C que son estándar en C++.
  8. Convertir matrices C a std::vector o std::array. De nuevo, este paso debería ser obvio. Esta conversión es mucho más simple que la conversión de char a std::string porque las interfaces de std::vector y std::array están diseñadas para coincidir con la sintaxis de la matriz C. Una de las ventajas es que puede eliminar esa variable adicional length que se transfiere a cada función junto con la matriz.
  9. Convertir malloc/free a new/delete. El objetivo principal de este paso es prepararse para futuras refactorizaciones. El simple hecho de cambiar el código C de malloc a new no le gana mucho directamente. Esta conversión le permite agregar constructores y destructores a esas estructuras y utilizar las herramientas de memoria automática incorporadas de C++.
  10. Reemplazar las operaciones de localizar new/delete con la familia std::auto_ptr. El propósito de este paso es hacer que su código sea seguro.
  11. Lanza excepciones siempre que los códigos de retorno se manejen burbujeando. Si el código C maneja errores buscando códigos de error especiales y luego devolviendo el código de error a su interlocutor, y así sucesivamente, borrando el código de error en la cadena de llamadas, entonces ese código C probablemente sea un candidato para usar excepciones. Esta conversión es realmente trivial. Simplemente throw el código de retorno (C++ le permite lanzar cualquier tipo que desee) en el nivel más bajo. Inserte una declaración try{} catch(){} en el lugar del código que maneja el error. Si no existe un lugar adecuado para manejar el error, considere envolver el cuerpo de main() en una declaración try{} catch(){} y registrarlo.

Ahora paso atrás y ver lo mucho que ha mejorado el código, sin convertir cualquier cosa a clases. (Sí, sí, técnicamente, sus estructuras ya son clases). Pero no ha arañado la superficie de OO, pero logró simplificar y solidificar enormemente el código C original.

¿Debería convertir el código para usar clases, con polimorfismo y un gráfico de herencia? Yo digo que no. El código C probablemente no tiene un diseño general que se preste a un modelo OO. Tenga en cuenta que el objetivo de cada paso anterior no tiene nada que ver con la incorporación de principios OO en su código C. El objetivo era mejorar el código existente imponiendo tantas restricciones de tiempo de compilación como sea posible, y eliminando o simplificando el código.

Un último paso.

Considere añadir puntos de referencia para que pueda mostrarlos a su jefe cuando haya terminado. No solo puntos de referencia de rendimiento. Comparar líneas de código, uso de memoria, número de funciones, etc.

+1

adora su primer punto: esto es imprescindible antes de que alguien acepte una asignación de portabilidad. – Syd

+0

Un gran consejo, todos excepto el # 6. No hay absolutamente ninguna verificación en tiempo de compilación con una referencia. Los punteros de Const (a diferencia de puntero-a-const) son a menudo mejores, ya que todos los problemas de duración y aliasing son más evidentes. –

+0

+1 para una respuesta reflexiva y útil, respaldada por la experiencia. – djna

3

Con "solo" 7000 líneas de código C, probablemente será más fácil volver a escribir el código desde cero, sin siquiera intentar comprender el código actual.

Y no hay una manera automática de hacer o incluso ayudar a la modularización y refactorización que usted prevé.

7000 LOC may sonido me gusta mucho, pero mucho de esto será un texto repetitivo.

12

En primer lugar, decirle a su jefe que no estás continuando hasta que tenga:

http://www.amazon.com/Refactoring-Improving-Design-Existing-Code/dp/0201485672

y en menor medida:

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

En segundo lugar, no hay manera de modularización código por calcetín en la clase C++. Esta es una gran tarea y debe comunicarle a su jefe la complejidad de refactorizar el código altamente procesal.

Todo se reduce a hacer un pequeño cambio (método de extracción, método de movimiento a clase, etc.) y luego probar: no hay atajos con esto.

me siento su dolor, aunque ...

+1

+1 para el libro Plumas. Está lleno de consejos para preservar la cordura. –

2

tratar de ver si se puede simplificar el código antes de cambiar a C++. Básicamente, creo que solo quiere que convierta funciones en métodos de clase y convierta estructuras en miembros de datos de clase (si no contienen punteros de función, si lo hacen, conviértelos a métodos reales). ¿Puedes ponerte en contacto con el (los) codificador (es) original (es) de este programa? Podrían ayudarlo a lograr un entendimiento, pero principalmente buscaría ese código que es el "motor" de todo y basaría el nuevo software a partir de ahí. Además, mi jefe me dijo que a veces es mejor simplemente reescribir todo, pero el programa existente es una muy buena referencia para imitar el comportamiento de tiempo de ejecución de. Por supuesto, los algoritmos especializados son difíciles de recodificar. Una cosa que puedo asegurar es que si este código no es lo mejor que podría ser, entonces tendrá muchos problemas más adelante. Me gustaría ir a su jefe y promover el hecho de que debe volver a hacer desde cero partes del programa. Acabo de estar allí y estoy muy feliz de que mi supervisor me haya dado la posibilidad de reescribir. Ahora la versión 2.0 está a años luz de la versión original.

+0

+1 Estoy de acuerdo con modularizar el código antes de convertirlo a C++ –

+1

Mejor aún, modularlo y dejarlo en C, a menos que haya una buena razón para cambiar el idioma. –

20

En realidad, 7000 líneas de código no es mucho. Para una cantidad tan pequeña de código, una reescritura completa puede estar en orden. Pero, ¿cómo se llamará este código? Presumiblemente, los llamadores esperan una C API? ¿O no es esto una biblioteca?

De todos modos, reescriba o no, antes de comenzar, asegúrese de tener un conjunto de pruebas que pueda ejecutar fácilmente, sin intervención humana, en el código existente. Luego, con cada cambio que realice, ejecute las pruebas en el nuevo código.

+2

no se trata solo de la cantidad de líneas de código, sino de su longitud y complejidad. Depende de lo que esté haciendo. 7000 líneas de código de 1 fuente pueden ser equivalentes a 70 000 de otra. –

+7

+1 para la prueba. – Bill

+1

@Neil: el libro de Plumas que David recomendó habla extensamente sobre "código de cubierta", que implica determinar el comportamiento actual con pruebas unitarias, y luego confirmar que los cambios en el código todavía superan las pruebas. Excelente consejo! –

4

Seguramente se puede hacer, la pregunta es ¿a qué costo? Es una tarea enorme, incluso para 7K LOC. Su jefe debe entender que tomará mucho tiempo, mientras que usted no puede trabajar en nuevas características brillantes, etc. Si no comprende completamente esto, y/o no está dispuesto a apoyarlo, no tiene sentido comenzar.

Como @David ya sugirió, el libro de refabricación es obligatorio.

De su descripción, parece que una gran parte del código ya es "métodos de clase", donde la función obtiene un puntero a una instancia de estructura y funciona en esa instancia. Por lo tanto, podría convertirse con bastante facilidad en código C++. Por supuesto, esto no hará que el código sea mucho más fácil de comprender o mejor modularizado, pero si este es el principal deseo de su jefe, se puede hacer.

Tenga en cuenta también que esta parte de la refactorización es un proceso mecánico bastante simple, por lo que podría realizarse de forma bastante segura sin pruebas unitarias (con edición de hiperageo, por supuesto). Pero para cualquier otra cosa, necesitas pruebas unitarias para asegurarte de que tus cambios no rompan nada.

4

Es muy poco probable que se obtenga algo con este ejercicio. El buen código C ya es más modular de lo que normalmente puede ser C++: el uso de punteros a estructuras permite que las unidades de compilación sean independientes en el mismo modo que lo hace pImpl en C++; en C no es necesario exponer los datos dentro de una estructura exponer su interfaz. Así que si a su vez cada función C

// Foo.h 
typedef struct Foo_s Foo; 
int foo_wizz (const Foo* foo, ...); 

en una clase de C++ con

// Foo.hxx 
class Foo { 
    // struct Foo members copied from Foo.c 
    int wizz (...) const; 
}; 

se le han reducido la modularidad del sistema en comparación con el código C - cada cliente de Foo ahora necesita reconstruir su caso las funciones de implementación privadas o las variables miembro se agregan al tipo Foo.

Hay muchas cosas que las clases en C++ le dan, pero la modularidad no es una de ellas.

Pregúntele a su jefe cuáles son los objetivos comerciales que se logran con este ejercicio.

Nota sobre la terminología:

Un módulo en un sistema es un componente con una interfaz bien definida que puede ser reemplazado con otro módulo con la misma interfaz sin afectar al resto del sistema. Un sistema compuesto de tales módulos es modular.

Para ambos idiomas, la interfaz de un módulo es por convención un archivo de encabezado. Considere string.h y string como la definición de las interfaces para los módulos simples de procesamiento de cadenas en C y C++. Si hay un error en la implementación de string.h, se instala una nueva libc.so. Este nuevo módulo tiene la misma interfaz, y todo lo que se vincule dinámicamente con él se beneficia inmediatamente de la nueva implementación. Por el contrario, si hay un error en el manejo de cadenas en std::string, entonces cada proyecto que lo use debe ser reconstruido.C++ introduce una gran cantidad de acoplamiento en los sistemas, que el lenguaje no hace nada para mitigar; de hecho, los mejores usos de C++ que explotan completamente sus características a menudo están mucho más unidos que el código C equivalente.

Si intentas hacer C++ modular, normalmente terminas con algo como COM, donde cada objeto tiene que tener tanto una interfaz (una clase base virtual pura) como una implementación, y sustituyes una indirecta por una plantilla eficiente generada código.

Si no le importa si su sistema está compuesto por módulos reemplazables, entonces no necesita realizar acciones para hacerlo modular, y puede usar algunas de las características de C++ como clases y plantillas que , adecuado aplicado, puede mejorar la cohesión dentro de un módulo. Si su proyecto es producir una única aplicación enlazada estáticamente, entonces no tiene un sistema modular, y puede permitirse no preocuparse en absoluto por la modularidad. Si desea crear algo como anti-grain geometry, que es un bello ejemplo del uso de plantillas para acoplar diferentes algoritmos y estructuras de datos, entonces necesita hacer eso en C++, bastante bien, nada más difundido es tan poderoso.

Tenga mucho cuidado con lo que su gerente quiere decir con 'modularización'.

Si cada archivo ya tiene "su propio propósito y función" y "cada función en el programa pasa un puntero a una de las estructuras", entonces la única diferencia en cambiarlo en clases sería reemplazar el puntero a la estructura con el puntero implícito this. Eso no tendría ningún efecto en la modularización del sistema, de hecho (si la estructura solo se define en el archivo C en lugar del encabezado) reducirá la modularidad.

+2

"El buen código C ya es más modular de lo que normalmente puede ser C++" - Eso no tiene sentido para mí. Sin duda, si agrupa los métodos junto con la misma responsabilidad en una clase, está mejorando la modularidad. –

+2

Ha confundido la modularidad con el acoplamiento. –

+0

@Neal ¿a quién te diriges? –

19

Este calzador en C++ parece ser arbitrario, pregúntele a su jefe por qué necesita eso, descubra si puede cumplir el mismo objetivo con menos dolor, vea si puede crear un subconjunto de la nueva manera menos dolorosa, luego vaya y demo a su jefe y recomiende que siga el camino menos doloroso.

+6

+1 para plantear la pregunta: ¿cuál es el objetivo real del ejercicio? –

+1

Esperaría que el objetivo del ejercicio sea atacar este problema: "Es un lío completo para tratar de entender". – Ether

5

Supongo que el pensamiento aquí es que el aumento de la modularidad aislará fragmentos de código, de modo que se faciliten los cambios futuros. Confiamos en cambiar una pieza porque sabemos que no puede afectar otras piezas.

veo dos escenarios de pesadilla:

  1. Has muy bien estructurado el código C, se va a transformar fácilmente a las clases de C++. En cuyo caso, probablemente ya sea bastante modular, y probablemente no hayas hecho nada útil.
  2. Es un nido de ratas de cosas interconectadas. En ese caso, será muy difícil desenredarlo. El aumento de la modularidad sería bueno, pero va a ser un trabajo largo y duro.

Sin embargo, tal vez haya un medio feliz. ¿Podría haber piezas de lógica que sean importantes y conceptualmente aisladas, pero que actualmente son frágiles debido a la falta de ocultación de datos, etc. (Sí, el bien C no sufre de esto, pero no tenemos eso, de lo contrario nos iríamos bien? solo).

Sacar una clase para poseer esa lógica y sus datos, encierra esa pieza podría ser útil. Si es mejor hacerlo con C o C++ es cuestionable. (El cínico en mí dice: "Soy un programador de C, ¡gran C++ es una oportunidad de aprender algo nuevo!")

Entonces: lo trataría como un elefante para ser comido. En primer lugar, decida si se debe comer, el mal elephent no es divertido, la C bien estructurada debería dejarse en paz. Segundo, encuentra un primer bocado apropiado. Y me haría eco de los comentarios de Neil: si no tienes un buen conjunto de pruebas automáticas, estás condenado.

5

Creo que un mejor enfoque podría ser totalmente reescribir el código, pero debería preguntarle a su jefe para qué propósito lo quiere "para comenzar a poner el antiguo código C en clases de C++". Debe solicitar más detalles

2

He leído este artículo que se titula "Hacer mal código bueno" de http://www.javaworld.com/javaworld/jw-03-2001/jw-0323-badcode.html?page=7. Está dirigido a usuarios de Java, pero creo que todas sus ideas, nuestra bonita aplicación a su caso. Aunque el título hace que suene "me gusta", es solo por código incorrecto, creo que el artículo es para ingenieros de mantenimiento en general.

Para resumir Dr.Las ideas de Farrell, él dice:

  1. Comience con las cosas fáciles.
  2. Fijar los comentarios
  3. Fijar el formato
  4. convenciones proyecto complementario
  5. Escribir automatizado pruebas
  6. romper grandes archivos/funciones
  7. código
  8. reescritura que no entienden

I piense después de seguir los consejos de todos los demás esto podría ser un buen artículo para leer cuando tenga algo de tiempo libre.

¡Buena suerte!

Cuestiones relacionadas