Usando #ifdef para cambiar entre diferentes tipos de comportamiento durante el desarrollo

28

¿Es una buena práctica usar #ifdef durante el desarrollo para cambiar entre diferentes tipos de comportamiento? Por ejemplo, quiero cambiar el comportamiento del código existente, tengo varias ideas sobre cómo cambiar el comportamiento y es necesario cambiar entre diferentes implementaciones para probar y comparar diferentes enfoques. Por lo general, los cambios en el código son complejos y tienen influencia en diferentes métodos en diferentes archivos.

Normalmente presento varios identificadores y hago algo así

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

void bar()
{
    doSomething3();
#ifndef APPROACH3
    doSomething4();
#endif
    doSomething5();
#ifdef APPROACH2
    bar_approach2();
#endif
}

int main()
{
    foo();
    bar();
    return 0;
}

Esto permite cambiar rápidamente entre diferentes enfoques y hacer todo en una sola copia del código fuente. ¿Es un buen enfoque para el desarrollo o hay una mejor práctica?

Yury Shkliaryk
fuente
2
Como está hablando del desarrollo, creo que debe hacer lo que sea fácil de hacer para cambiar y experimentar con diferentes implementaciones. Esto se parece más a las preferencias personales durante el desarrollo, no a las mejores prácticas para resolver un problema específico.
Emerson Cardoso
1
Recomiendo usar el patrón de estrategia o el buen polimorfismo, ya que esto ayuda a mantener un único punto de conexión para el comportamiento conmutable.
pmf
44
Tenga en cuenta que algunos IDE no evalúan nada en #ifdefbloques si el bloque está desactivado. Nos encontramos con casos en los que el código puede quedar obsoleto fácilmente y no compilarse si no construye rutinariamente todas las rutas.
Berin Loritsch
Echa un vistazo a esta respuesta que le di a otra pregunta. Presenta algunas formas de hacer mucho #ifdefsmenos engorroso.
user1118321

Respuestas:

9

Preferiría usar ramas de control de versiones para este caso de uso. Eso le permite diferenciar entre las implementaciones, mantener un historial separado para cada una, y cuando haya tomado una decisión y necesite eliminar una de las versiones, simplemente descarta esa rama en lugar de pasar por una edición propensa a errores.

Karl Bielefeldt
fuente
gites especialmente experto en este tipo de cosas. Tal vez no tanto en el caso de svn, hgo de otros, pero todavía se puede hacer.
twalberg
Ese fue mi pensamiento inicial también. "¿Quieres jugar con algo diferente?" git branch!
Wes Toleman
42

Cuando sostienes un martillo, todo parece un clavo. Es tentador una vez que sabes cómo #ifdeffunciona usarlo como una forma de obtener un comportamiento personalizado en tu programa. Lo sé porque cometí el mismo error.

Heredé un programa heredado escrito en MFC C ++ que ya se usaba #ifdefpara definir valores específicos de la plataforma. Esto significaba que podía compilar mi programa para usarlo en una plataforma de 32 bits o en una plataforma de 64 bits simplemente definiendo (o en algunos casos no definiendo) valores macro específicos.

Entonces surgió el problema de que necesitaba escribir un comportamiento personalizado para un cliente. Podría haber creado una sucursal y crear una base de código separada para el cliente, pero eso habría hecho un infierno de mantenimiento. También podría haber definido los valores de configuración para que el programa los lea al inicio y utilizar estos valores para determinar el comportamiento, pero luego tendría que crear configuraciones personalizadas para agregar los valores de configuración adecuados a los archivos de configuración para cada cliente.

Me sentí tentado y cedí. Escribí #ifdefsecciones en mi código para diferenciar los diferentes comportamientos. No se equivoquen, no fue nada exagerado al principio. Se realizaron cambios de comportamiento muy menores que me permitieron redistribuir versiones del programa a nuestros clientes y no necesito tener más de una versión de la base de código.

Con el tiempo, esto se convirtió en un infierno de mantenimiento de todos modos porque el programa ya no se comportó de manera consistente en todos los ámbitos. Si quería probar una versión del programa, necesariamente tenía que saber quién era el cliente. El código, aunque traté de reducirlo a uno o dos archivos de encabezado, estaba muy abarrotado y el enfoque de solución rápida que #ifdefproporcionaba significaba que tales soluciones se extendieron por todo el programa como un cáncer maligno.

Desde entonces aprendí mi lección, y tú también deberías. Úselo si es absolutamente necesario, y úselo estrictamente para cambios de plataforma. La mejor manera de abordar las diferencias de comportamiento entre los programas (y, por lo tanto, los clientes) es cambiar solo la configuración cargada al inicio. El programa se mantiene constante y se vuelve más fácil de leer y de depurar.

Neil
fuente
¿Qué pasa con las versiones de depuración, como "if debug, define variable x ..."? Parece que podría ser útil para cosas como el registro, pero también podría cambiar totalmente la forma en que funciona el programa cuando la depuración está habilitada y cuando no .
cuando
8
@snb Pensé en eso. Todavía prefiero poder cambiar un archivo de configuración y hacer que se registre con más detalle. De lo contrario, algo sale mal con el programa en producción, y no tiene forma de depurarlo sin reemplazar completamente el ejecutable. Incluso en situaciones ideales, esto es menos que deseable. ;)
Neil
Oh sí, sería mucho más ideal no tener que volver a compilar para depurar, ¡no pensé en eso!
cuando
9
Para un ejemplo extremo de lo que está describiendo, mire el segundo párrafo bajo el subtítulo "El problema de la mantenibilidad" en este artículo sobre por qué la EM llegó al punto en el que tuvieron que reescribir la mayor parte de su tiempo de ejecución C desde cero hace unos años. . blogs.msdn.microsoft.com/vcblog/2014/06/10/…
Dan Neely
2
@snb La mayoría de las bibliotecas de registro suponen un mecanismo de nivel de registro. Si desea que se registre cierta información durante la depuración, hágalo con un nivel de registro bajo ("Debug" o "Verbose" generalmente). Luego, la aplicación tiene un parámetro de configuración que le dice qué nivel registrar. Entonces la respuesta sigue siendo la configuración para este problema. Esto también tiene el enorme beneficio de poder activar este bajo nivel de registro en un entorno de cliente .
jpmc26
21

Temporalmente no hay nada de malo en lo que está haciendo (por ejemplo, antes del check-in): es una excelente manera de probar diferentes combinaciones de técnicas o de ignorar una sección de código (aunque eso habla de problemas en sí mismo).

Pero una palabra de advertencia: no guarde las ramas #ifdef, es un poco más frustrante que perder el tiempo leyendo lo mismo implementado de cuatro maneras diferentes, solo para descubrir cuál debería estar leyendo .

¡Leer sobre un #ifdef requiere esfuerzo, ya que debes recordar saltearlo! No lo hagas más difícil de lo que tiene que ser.

Use #ifdefs con la mayor moderación posible. En general, puede hacer esto dentro de su entorno de desarrollo para las diferencias permanentes , como las compilaciones Debug / Release, o para diferentes arquitecturas.

He escrito funciones de biblioteca que dependían de las versiones de biblioteca incluidas, que requerían divisiones #ifdef. Por lo tanto, a veces puede ser la única forma, o la más fácil, pero incluso así debería estar molesto por mantenerlos.

Erdrik Ironrose
fuente
1

Usar #ifdefs así hace que el código sea muy difícil de leer.

Entonces, no, no uses #ifdefs así.

Puede haber toneladas de argumentos por qué no usar ifdefs, para mí este es suficiente.

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

Puede hacer muchas cosas que puede hacer:

void foo()
{
    doSomething1();
    doSomething2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
}

void foo()
{
    doSomething1();
    doSomething2();
    foo_approach2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
    foo_approach2();
}

Todo depende de qué enfoques se definan o no. Lo que hace no está absolutamente claro a primera vista.

Pieter B
fuente