2014/07/06
Ejemplo de TDD, uso de tags para reportes de ocurrencias de eventos
En esta ocasión relataré un ejemplo de refactorización de clases, acompañado por la práctica de TDD en una aplicación de seguridad informática. No proveeré código y las entidades y atributos estarán parcialmente ofuscados, en parte por discreción y en parte como recurso didáctico de simplificar el modelo presentado.
El objetivo de la transformación tiene que ver con la generación de reportes estadísticos sobre la Ocurrencia de Eventos.
El modelo desarrollado y usado durante varios meses es bastante simple y razonable:
Área
|
Ocurrencia - Evento - Tag - Grupo de Tags por ejemplo Plataformas(windows,linux,mac
|
Provincia
El tag es... eso, un tag. Lo que ocurre es que sabemos que Áreas van a haber y que Provincias hay, pero más importante es que sabemos que nos interesan las Áreas y las Provincias. Otras categorías como Plataforma (windows, linux, mac) van apareciendo, pero no son tan interesantes como para convertirse en entidades. Para no estar cambiando el modelo correteando tras los caprichitos de las necesidades del negocio, existen los Grupos de Tags (Plataforma) con sus tags (windows, linux, mac). Tambien podría ser grupo BBDD con mysql, postqresql, mssql, oracle.
Hasta ahi vamos bárbaro, la operatoria como seda, hasta que vamos a generar reportes.
Quiero estadísticas de Ocurrencias de Eventos por Área, por Provincia, por Área y Provincia y para el otro lado. Ah, y las quiero por Plataformas, Color y Sabor.
Lo primero que uno hace es entonces cada combinación y el sistema nunca está terminado.
No me da la cabeza ni conozco tanto de reporting y encima no puedo estar incorporando módulos extra.
Tuve entonces una serie de ideas reveladoras:
Aunque es más sencillo hacer el reporte según una dimensión tipo entidad como Área o Empresa que la de Grupo de Tags, está última luego es más reutilizable
El anidamiento es más fácil con los Grupos que con las entidades.
Si a esto le sumamos que la interfaz de selección de dimensiones es generable automáticamente a partir de los Grupos, todo indicaría que en términos de reporte estadístico fue un error no poner toda la información en Tags y Grupos. De todos modos, las entidades son más fáciles de manejar en términos de nulidad y cardinalidad. Si no me entendiste, pensá en como implementar un ABM de Provincias donde estas estan representadas mediante Tags y Grupos de Tags en comparación con app/console.php doctrine:generate:crud (en symfony o equivalente en tu framework habitual).
Habiendo decidido mantener esta suerte de modelo dual, procedí a implementar el reporte, primero con una dimensión, luego dos y finalmente tres.
El algoritmo consiste en iterar sobre todas las Ocurrencias de una fecha e ir acumulando en una estructura tipo árbol, que expresa a recursividad del problema.
Aquí se puede apreciar parte del código de testeo en php:
private function buildExpectedAreas($t1,$t2,$t3,$t4) {
return array(
'total'=>$t1,
'agrupacion'=>array(
'areas' => array(
'entidades'=>array(
'contable'=> array(
'total'=>$t2,
'agrupacion'=>array()
),
'rrhh'=> array(
'total'=>$t3,
'agrupacion'=>array()
),
'logistica'=>array(
'total'=>$t4,
'agrupacion'=>array()
),
)
)
)
);
}
Para una consulta de dos dimensiones, por ejemplo Areas y Plataformas, reemplazá cada una de las tres lineas que dice 'agrupacion'=>array() por
'agrupacion'=>array(
'plataformas' => array(
'entidades'=>array(
'windows'=> array(
'total'=>$$$,
'agrupacion'=>array()
),
'unix'=> array(
'total'=>$$$,
'agrupacion'=>array()
),
'mainframe'=>array(
'total'=>$$$,
'agrupacion'=>array()
),
)
)
);
Si quisieras una tercera dimensión hay que repetir con la nueva agrupación nueve veces.
Primero implementé el reporte para una y dos dimensiones sobre Grupos.
Ahora bien, ¿qué hacer con Área y Provincia, que ya existen como entidades?
Mi primera idea fue transformarlas en Grupos con sus Tags en el modelo, pero por algo las queríamos como entidades, va a pegar fuerte en la interfaz y en las validaciones, como por ejemplo que Provincia sólo puede haber una y Áreas varias asociadas a un mismo Evento.
La segunda fue, en el momento de generar el reporte, inicio una transacción y genero los Grupos y Tags a partir de Área y Provincias. Tras el reporte va un rollback y no pasó nada.
Al final opté por una variación de la segunda: a cada Evento que el ORM me ha traido, le agrego los Tags necesarios tras haber creado los Grupos necesarios, sin persistir luego.
Problemas de performance no hay, ya que la generación de reportes es de baja frecuencia y concurrencia.
TDD acompañó todo el proceso, aunque no soy muy ortodoxo, sobre todo con el Timely de FIRST[1], Por lo general codeo primero y cuando alcanza una masa crítica hago los test y recién ahí entro en el ciclo test->fail->code->pass->refactor.
Asi que escribí unas pocas lineas y enganché con TDD. Con el siguiente awk se puede obtener las lineas que tenían en cada commit los archivos de código y test. Los AJUSTES se deben a que estas clases ya existían. CORTE es el primer commit. git lola es git log --graph --decorate --pretty=oneline --abbrev-commit --all
#!/bin/bash
CORTE=42ed122
ARCHIVO1=Lib/Codigo.php
ARCHIVO2=Tests/Lib/CodigoTest.php
git lola | cut -b 3-9 | awk -e '
BEGIN {
ARCHIVO1="'$ARCHIVO1'"
ARCHIVO2="'$ARCHIVO2'"
AJUSTE_CODIGO=346
AJUSTE_TEST=368
set +o posix
}
/.*/ {
cmd1 ="git show "$0":"ARCHIVO1" | wc -l "
cmd2 ="git show "$0":"ARCHIVO2" | wc -l "
command cmd1 | getline LINEAS_CODIGO
command cmd2 | getline LINEAS_TEST
LINEAS_CODIGO -= AJUSTE_CODIGO
LINEAS_TEST -= AJUSTE_TEST
print LINEAS_CODIGO "\t" LINEAS_TEST
}
/.*'$CORTE'.*/ {
exit 0;
}
'
La salida de esto va a tu hoja de cálculo favorita y se vé así:
Finalmente, apareció un requisito nuevo que me produjo un momento de iluminación y me llevó a la solución definitiva.
El requisito tiene que ver con algo que no había mencionado antes, que es el factor multiplicativo. Si un Evento tiene múltiples Empresas, Provincias o Plataformas, las Ocurrencias deben multiplicarse, por ejemplo:
Evento: detección de ataque xss
Áreas: contabilidad
Provicia: Tucumán
Plataformas: mac, windows, linux
En este caso cada Ocurrencia del Evento vale por tres. Si no te cierra bien por qué contabilidad tiene xss en Tucumán no te preocupes, tiene que ver con el enmascaramiento.
El requisito consiste en que si AHORA hago la estadística de Enero, me tiene que dar lo mismo que me dió en ENERO. Si mientras han cambiado las relaciones de "detección de ataque xss", como que en Área se agregue RRHH, la cuenta me daría 6 en Enero. Hay entonces que desnormalizar y en el momento en que se crea la Ocurrencia calcular los factores y guardarlos dentro de la Ocurrencia.
Cuando lo implemente, probablemente tire buena parte del código, pero los test me quedan aprovechables al cien por ciento. Sólo hay que agregar unos pocos que creen unos datos y calculen la estadística para ese momento. Luego, avacen en el tiempo, modifiquen algunas relaciones y vuelvan a calcular para ese momento y los resultados deberán ser los mismos.
Mi conclusión de esta experiencia es que de no haber contado con los tests no habría podido implementar nada, pues la verdad es que la complejidad del código resultante es un poco más grande que lo que mi mente puede abarcar a la vez. Además, ante ese nuevo requisito, no pierdo tanto trabajo pues la inversión está tanto en el código que pueda perder como en los tests que conservaré.
[1] FIRST:
F fast
I independient
R repeatable
S self
T timely
Suscribirse a:
Enviar comentarios (Atom)
No hay comentarios:
Publicar un comentario