Introducción
Hace un tiempo, unos años, trabaje en una empresa donde tenían un producto para varios clientes, Y cada cliente, requería un nuevo requisito, o quitar alguno, esto generaba código del estilo:
// habilitar caracteristica del cliente A
#endif
Este tipo de código a lo largo, de miles de lineas de código, mezclado con las demás funcionalidades de los demás clientes, el código es más difícil de seguir que una demostración matemática (al menos para mí...)
Hace un tiempo, me tope con este problema, pero no quería extender la complejidad de los #if a lo largo de todas las clases del programa. Debía de encapsular esta complejidad en una clase.
Pero antes, veamos un ejemplo práctico.
Ejemplo práctico
Imaginemos, que tenemos un software que tiene las siguientes funcionalidades:
- Exportar sus datos a un fichero Txt (__TXT__EXPORT__ )
- Exportar sus datos a un fichero Excel (__EXCEL_EXPORT__)
- Visualización 2d (__2D_VIEW__ )
- Visualización 3d (__3D_VIEW__ )
- Exportación del modelo 3d generado a un fichero (__EXPORT_3D_MODEL__ ). Esta funcionalidad solo se activo si está activa la visualicación 2d y 3d.
La función drawOptionsInstallOldWay dibuja en pantalla las opciones instaladas de la forma #if. Y la función drawOptionsInstallNewWay las dibuja de otra forma que ya veremos.
El aspecto de la aplicación sería con las siguientes opciones activadas:
__TXT__EXPORT__, __2D_VIEW__,__3D_VIEW__
La parte izquierda se dibuja con el método drawOptionsInstallOldWay y la parte derecha (azul) con el método drawOptionsInstallNewWay .
Ahora, veamos cual de los dos métodos es más difícil de leer y de mantener a la larga.
private void drawOptionsInstallOldWay(Graphics g)
{
float y = 40.0f;
#if __TXT__EXPORT__
g.DrawString("Txt Export", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
#endif
#if __EXCEL_EXPORT__
g.DrawString("Excel Export", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
#endif
#if __2D_VIEW__
g.DrawString("2d View", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
#endif
#if __3D_VIEW__
g.DrawString("3d View", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
#endif
#if __3D_VIEW__ && __2D_VIEW__ // __EXPORT_3D_MODEL__
g.DrawString("Export 3d Model", SystemFonts.DefaultFont, Brushes.Black, 10.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
#endif
}
private void drawOptionsInstallNewWay(Graphics g)
{
float y = 40.0f;
if(RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_TXT__REQUERIMENT))
{
g.DrawString("Txt Export", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
}
if(RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT))
{
g.DrawString("Excel Export", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
}
if(RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_2D_REQUERIMENT))
{
g.DrawString("2d View", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
}
if (RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_3D_REQUERIMENT))
{
g.DrawString("3d View", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
}
if (RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GENERATE_3D_MODEL))
{
g.DrawString("Export 3d Model", SystemFonts.DefaultFont, Brushes.Blue, 200.0f, y);
y += SystemFonts.DefaultFont.Height + 2.0f;
}
}
Vemos que el método drawOptionsInstallNewWay no hace uso de #if, sino de llamadas a un objeto de tipo RequerimentsActivator, el cual, le responde si una funcionalidad o requisito está activo o no.
Con esto se consigue que la complejidad de los #if, está centralidad, encapsulada en la clase RequerimentActivator.
La clase RequerimentActivator
Esta clase sigue el patrón de diseño singleton. De esta forma, se podrá acceder desde cualquier parte del programa, pero, yo creo que las partes donde se debería de acceder, es desde el interfaz, pero no llamarla desde las clases del problema de la aplicación.
Es decir, hacer uso de esta clase, RequerimentActivator, solo desde el MainForm.cs o clases que deriven de la clase Form. Así, las clases del dominio del problema no estarán acopladas a esta clase.
Esta clase tiene un enumerado llamado OptionalRequeriments donde se enumeraran todas las opciones que se pueden activar y desactivar.
public enum OptionalRequeriments { EXPORT_EXCEL__REQUERIMENT = 0, EXPORT_TXT__REQUERIMENT = 1, GRAPHICS_2D_REQUERIMENT = 2,GRAPHICS_3D_REQUERIMENT = 3, GENERATE_3D_MODEL=4 };
int _MaxOptionalRequeriments = 5; //numero de opciones
Y el método iniRequeriments inicializa el array booleano _requeriments a true o a false dependiendo si un requisito se activo o no. Pero, ¿cómo?
Haciendo uso, ahora sí, de los #if.
#if __EXCEL_EXPORT__
_requeriments[(int)OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT] = true;
#endif
#if __TXT__EXPORT__
_requeriments[(int)OptionalRequeriments.EXPORT_TXT__REQUERIMENT] = true;
#endif
Sí por ejemplo el simbolo __EXCEL_EXPORT__ no está definido, entonces
_requeriments[(int)OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT] estaría a falso.
La función completa sería:
private void iniRequeriments()
{
_requeriments = new bool[_MaxOptionalRequeriments];
for (int i = 0; i < _MaxOptionalRequeriments; i++)
{
_requeriments[i] = false;
}
#if __EXCEL_EXPORT__
_requeriments[(int)OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT] = true;
#endif
#if __TXT__EXPORT__
_requeriments[(int)OptionalRequeriments.EXPORT_TXT__REQUERIMENT] = true;
#endif
#if __2D_VIEW__
_requeriments[(int)OptionalRequeriments.GRAPHICS_2D_REQUERIMENT] = true;
#endif
#if __3D_VIEW__
_requeriments[(int)OptionalRequeriments.GRAPHICS_3D_REQUERIMENT] = true;
#endif
#if __2D_VIEW__ && __3D_VIEW__
_requeriments[(int)OptionalRequeriments.GENERATE_3D_MODEL] = true;
#endif
}
En negrita he puesto el requisito 'generar modelo 3d a fichero'. Como este requisito depende de dos requisitos, no se define un simbolo de tipo __GENERATE_MODEL_3D__.
La gracia de esto, es que se puede establecer una jerarquía de requisitos. Requisitos que se activen o no, dependiendo de otros requisitos!!
Además, de luego, la facilidad para habilitar los menús, dependiendo de las funcionalidades activas. Como se ve abajo.
private void updateMenuFromRequerimentsNewWay()
{
txtToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_TXT__REQUERIMENT);
excelToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.EXPORT_EXCEL__REQUERIMENT);
View2dToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_2D_REQUERIMENT);
View3dToolStripMenuItem1.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GRAPHICS_3D_REQUERIMENT);
generateModel3DToolStripMenuItem.Enabled = RequerimentsActivator.GetInstance().IsRequerimentInstalled(Versions.RequerimentsActivator.OptionalRequeriments.GENERATE_3D_MODEL);
}
Conclusión
Creo que está forma de activar o desactivar los requisitos de un software, hace más mantenible a la larga el código fuente, ya que, encapsula la complejidad de los #if . Además, esto permite, aumentar la complejidad, pudiendo crear jerarquías de requisitos (activar un requisito cuando varios requisitos están activos, o pueden obedecer a una lógica booleana).
El código fuente se puede encontrar aquí.
Félix Romo
felix.romo.sanchezseco@gmail.com