Ya saben que soy partidario de explorar el tener el modelo de lo que estas armando en memoria. Luego viene la persistencia, y quizás esa persistencia vaya a una base de datos relacional. Pero no hay que estar desde el comienzo limitados a tener una base de datos a consultar con SQL o un ORM. Si trabajamos ágilmente también podemos diferir esas decisiones sin afectar al proyecto.
Lo que escribo hoy, se refiere a algo que encontre hace un tiempo, en un proyecto privado (así que no puedo dar mucho detalle). Y no era código de una aplicación, sino mas bien, de un utilitario de transformación de datos que se ejecuta diariamente (un ETL).
Lo que importa es que en el código me encuentro con un dominio en memoria (yo había recomendado tempranamente ese camino, no participé de la implementación inicial). Y con un for como (disfrazo el código, era paracido a éste):
for (var invoice in domain.Invoices) invoice.Customer = domain.Customers.Where(c => c.Id == invoice.CustomerId).FirstOrDefault();
En realidad eran dos, y repetidos en dos lugares distintos del código. Pero lo importante a ver es que lo de arriba funciona bien. El problema aparece cuando se ejecuta sobre datos reales. En el proceso que se ejecutaba diariamente, los clientes eran, digamos, 100000, y las facturas 600000. El ciclo de arriba TARDABA MAS DE UNA HORA: el Where se ejecutaba 600000 veces y cada evaluación del mismo recorría en promedio 50000 clientes.
Cuando llegó a mí el proyecto, en una de las revisiones, veo ese ciclo, y lo refactoricé a:
for (var invoice in domain.Invoices) if (domain.CustomersById.ContainsKey(invoice.CustomerId)) invoice.Customer = domain.CustomersById[invoice.CustomerId];
Es decir, a mantener un diccionario por Id de los clientes. El proceso completo pasó a tardar menos de 10 minutos, y haces, menos de 5 minutos (y el ciclo de arriba es prácticamente “instantáneo” cuando antes duraba más de una hora). Se siguen recorriendo 600000 facturas, pero ahora hay un “lookup” rápido, en vez de un Where de LINQ.
¿Estaba mal el primer “approach”? NO, ESTA BIEN, funciona. Pero luego, no es lo suficientemente rápido para lo que necesitamos. El tener el proceso corriendo más de una hora afectaba a otros procesos. Y también afectaba algo de su lógico interna. La cuestión que se puede mejorar con el segundo “approach”, y así se hizo.
Siguiendo una frase atribuida a Kent Beck: "make it works, make it right, make it fast”. En este caso, lo de “fast” quedó para lo último. No hay que optimizar prematuramente. Pero cuando LUEGO DE MEDIR (no antes) se ve que algo no funciona como esperábamos, se puede plantear el refactor o reimplementación para mejorar velocidades.
En otro caso, tenía llamadas a una API REST, digamos con un dato a dar de alta. Como había que dar de alta a veces 20 relaciones, se llamaba a la API 20 veces. Cuando se vió (y se midió) que eso tenía su costo en tiempo, pude refactorizar a enviar 1,2 o 3 mensajes con más información, en vez de veinte mensajes. Pero esa refactorización tuvo su costo: hubo que programarla (con un algoritmo del que estoy particularmente orgulloso, agrupando dinámicamente datos ;-). Por fortuna, todo estaba escrito en este caso con TDD, así que sólo se tardó un par de horas (y un buen desayuno pensando el algoritmo ;-).
En cambio, la mejora de más arriba (el implementar un diccionario y cambiar el ciclo) no fue tan fácil. Si bien había tests en el proyecto, no fue armado TODO con TDD, donde cada línea de código de producción aparece por algún test que se haya escrito. Sólo una parte estaba con TDD, y el resto eran mas bien test unitarios con mocks que trataban casos con pocos datos. Por ejemplo, tuve que revisar donde agregar los clientes al diccionario, porque se agregaban clientes en varios lugares del código, en lugar de tener un domain.AddCustomer o algo similar (eso sería el “make it right” de Beck, pero nunca pude llegar a eso en este caso). Creo recordar que a propósito, implementé el ciclo mal, corrí los tests, y daban todos en verde. Nunca se había ejercitado esa parte del programa. Así que tuve que compilar, copiar a algún ambiente con datos, correr el programa, revisar el resultado, etc, etc…
Moraleja:
– Optimizar sólo cuando sea necesario
– Programen con TDD
– Programen con TDD
– Programen con TDD
– 😉
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com