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