martes, 19 de agosto de 2008

Jaque.

Hace un par de días descubrí el muy prometedor Un Punto Azul Pálido. Empecé a leer hacia atrás, hasta darme con la entrada El objeto del negocio donde el autor despotrica contra los métodos de negocio "inútiles" al estilo public void almacenarCuenta(Cuenta c) { cuentaDao.almacenar(c); }

Donde se ve una capa de negocio inútil que hace de pasamanos entre el front-end y la capa de datos, sin aportar nada.

Alguna parte de la aplicación sobre la que trabajo está así. "Jaque", pensé, porque por un lado no podía dejar de estar de acuerdo con lo que había leído, pero por el otro mi orgullo, un poco herido, quería salir a defender la estructura que armamos (es un proyecto que comenzamos de cero absoluto) y que ha probado ser más que robusta, legible y escalable (en serio, lo ha probado).

Logré dejar el amor propio de lado (sentimiento dañino si los hay para un desarrollador, que debe estar siempre consciente de que **nunca** ha escrito código sin errores), pensé un rato y me dí cuenta de que en realidad el post critica algo que nosotros mismos ya nos hemos criticado y conversado, por lo que creo que es interesante contar un poco cómo es que sucedió eso y, sobre todo, advertir que si bien no está bien que esto suceda, podría ser peor.

El requerimiento (absolutamente paperless) era crear un framework de trabajo, y al tiempo desarrollar la nueva versión de la aplicación principal de la empresa reescribiéndola desde 0. Cambio de tecnología (de .Net 1.1 parecido a VB 6 a .Net 2.0), de lenguaje (de VB a C#) de paradigma (de estructurado + orientado a formularios y eventos a orientado a objetos) y de entorno (de aplicación de escritorio a aplicación web para correr en un servidor de la intranet).

Entonces, teníamos que "copiar funcionalidad" (producir la misma salida que antes en pantalla y contra la base de datos) con la aplicación existente como única referencia. Ésta contaba con poca documentación y tenía el código enmarañado por el mantenimiento y el rápido crecimiento. Por otro lado el equipo estaba muy fresco, recién habíamos entrado a la empresa y no teníamos la más mínima idea de un negocio que para colmo de males es bastante particular.

Encima se sucedieron algunos cambios de asignaciones y finalmente de personal (todo tranquilo, nada terrible). Finalmente quedamos dos "arquitectos". Yo en realidad tirando un poco más para el lado de la aplicación o el negocio. Así que el otro empezó desde más abajo en la arquitectura, yo desde la capa de datos hacia arriba.

Cuestión que después de un tiempo la capa de datos quedó muy bien armada utilizando un generador de código, con la que creamos una clase encargada de cada tabla más algunos artilugios.

Llegamos al punto. El negocio. Nuestra agenda se basaba en reproducir "pantallas" de la aplicación anterior. Pero no teníamos un inventario de las funcionalidades que involucraban esas pantallas. En todo caso, esta forma de ver la aplicación (como un conjunto de pantallas independientes) se basaba en una filosofía que no era la nuestra (patrón MVC, con una controladora por funcionalidad, que puede vincular una o muchas páginas a uno o varios objetos de negocio).

Resumen: teníamos la estructura armada para el MVC, teníamos la estructura para la capa de datos, teníamos la estructura general del front-end armada... y faltaba diseñar la capa de negocio... de un negocio que no sabíamos muy bien de qué la venía.

Y ahí metí la pata. Sí, metí la pata. Con el generador de código creé una clase de negocio inicial a partir de cada tabla de la aplicación. Pasamanos. Un montón de pasamanos.

Tres errores conceptuales, por lo menos:

  • El principal: un negocio no se automatiza, se piensa, se diseña. Nadie nos salva de eso, hay que tener cuidado con las soluciones mágicas. El negocio es la parte de la aplicación en la que ninguna herramienta puede reemplazar a un ser humano, y en la que ninguna regla automática nos va a venir a salvar. Hay que relevar y entender el negocio antes de meter los dedos.
  • Vincular conceptualmente el negocio a la estructura de la base de datos. Es casi el mismo error que vincularlo conceptualmente al front-end. El negocio es el negocio, por eso es una capa separada. Si lo vinculamos a la base de datos caemos en la estructura pasamanos, si lo vinculamos al front-end caemos en la siempre terrible, enmarañada y cambiante lógica del usuario.
  • Reprodujimos una lógica que no era la nuestra (la del sistema viejo) sin conocerla a fondo. Nos "comimos" todos sus errores conceptuales al no diferenciar lo que estaba bien de lo que estaba mal. No vimos sus puntos fuertes. No utilizamos la experiencia. Copiamos.

Pero, dentro de todo, algunos aciertos:

  • Algo había que hacer, y se respetó la máxima de los principios de diseño: "más vale disperso que denso". Con el tiempo muchos pasamanos se fueron llenando de código: de validaciones, de relaciones, de métodos y clases complejas. Se creó mucho espacio vacío e inútil, pero la mayor parte de ese espacio terminó siendo esencial para colocar código que sabíamos que iba a estar pero que en ese momento no sabíamos cuál era. De esto adolecen los ejemplos en el código mencionado arriba. Ok, el método simplemente toma los datos y los graba a ciegas. ¿Pero no tiene que validar nada antes? ¿No tiene que presentar mensajes al usuario en caso de problemas de negocio ("El movimiento no se puede insertar porque el cliente no tiene saldo suficiente")? ¿No tiene que verificar que el estado del sistema sea válido para la operación? ¿No tiene que revisar los permisos del usuario actual? ¿Dónde está esta funcionalidad? Si la operación impacta en varias tablas ¿no tiene que iniciar una transacción? Creo que debería estar ahí. Supongo que el problema de la aplicación que describe el post de Improbable es que ese código no está, y por eso el método está vacío. ¿Dónde lo habrán puesto?
  • Con esta "salida" la capa de datos quedó oculta, inaccesible (internal de la librería de datos+negocio) y esto es importante. El front-end no debe acceder a datos directamente. El pasamanos actúa al menos como un encapsulador de la capa de datos donde podemos ubicar código de ser necesario. Si no hacemos nada es peor, ya que tendremos muchas páginas o elementos del front-end accediendo cada uno por su lado a los datos. Un cambio en la estructura de una tabla impactaría directamente en el front-end, y no habría un lugar donde frenar ese impacto.
  • Fue corregible, fue mantenible. Fuimos haciendo refactorizaciones. Borramos algunas clases. Fuimos metiendo unas en otras que se convirtieron en internas y así se presentaron objetos y métodos más complejos. En esto fue fundamental el uso del patrón MVC que frenó los cambios, que no impactaron en el front-end (que se desarrollaba en paralelo), detalle sutil que los hizo posibles. Creo que si hubiésemos vinculado el negocio al front-end hubiese sido imposible "bajar" la complejidad adquirida.
  • Representó en su momento lo poco que conocíamos del negocio: la estructura de la base de datos. Es importante que la estructura de un proyecto represente lo que los desarrolladores saben de él. Lo vuelve legible. A medida que aprendíamos más del negocio, lo íbamos modificando. Nunca tuvimos documentación funcional, y todavía (con varios módulos desarrollados) no nos perdemos. A veces no sabemos qué hay que hacer, pero por lo menos es fácil saber qué fue lo que hicimos.
  • No repetimos el error: cuando hubo que incorporar un nuevo módulo, hicimos el relevamiento correspondiente a partir del sistema viejo y repensamos y rediseñamos el negocio de acuerdo a las virtudes y a los errores que vimos. Es decir, aprovechamos la experiencia del sistema anterior, cosa que antes no habíamos hecho.

En resumen, lo que rescato:

Hay que pensar el negocio, no queda otra.

Pero si estamos apurados, es preferible crear objetos de más (pasamanos) que enmarañar todo.

Está bien salir del paso, pero no debería quedar así. Hay que ir depurando lo que sobra a medida que se dan los cambios.

Para finalizar, creo haber salido del Jaque aunque con alguna pieza de menos, veremos cómo sigue.

8 comentarios:

Improbable dijo...

Gracias por los adjetivos dedicados a mi blog.

Por cierto, como decía en el post, que aplicación no tiene lógica?. Mi punto es que un método que sea un pasamanos es una luz amarilla: o falta implementar lógica o estás inventando la rueda (por ejemplo, en un reporte no tiene sentido el pasamanos: se podría usar un fast lane reader)

O, si querés, el peor pecado: implementar un join en la capa de negocios, haciendo lo mismo que la base, pero peor, más lento y con más esfuerzo. :-)

AcP dijo...

De acuerdo en todo... pero... Me quedo pensando con eso de "el peor pecado: implementar un join en la capa de negocios"... es como una frase que me parece que está bien pero en algún lado me hace ruido... tengo que desarrollarlo.

Improbable dijo...

Perfecto, espero tus comentarios al respecto. Siempre es bueno ver si uno tiene que terminar cambiando de opinión :-)

Cerebrado dijo...

Permiteanme inmiscuirme desde bambalinas.
Si el problema no es la perfomance, y se mejora la legibilidad o la consistencia del modelo, por qué no hacer un join a nivel de negocio? (obviamente, si la lógica simplificada lo requiere).
Un burdo ejemplo: Mi cliente quiere ver las ventas de dos bases de datos distintas. Podría resolverlo a nivel de base de datos... pero el sentido común (al que trato de seguir cuando puedo) me dice que debo hacerlo en el negocio.-

Improbable dijo...

Por qué?


1. Es más dificil, vas a necesitar unas cuantas horas para hacerlo bien, y el cliente va a pagar dos veces por lo mismo: por tus horas y por lo que ya pagó cuando compró el motor de base de datos. Si vas a implementar un join, vas a tener que tener en cuenta
- Ordenamientos
- Límites de memoria

Tenés dos opciones: hacer un join por merge o hacer un nested loop join (siempre podés implementarte un algoritmo para un hash join :-) ). En el primer caso vas a tener que asegurarte que siempre tendrás memoria (es decir, confiar en la suerte) o hacer un algoritmo que use almacenamiento secundario. En el segundo, además de la performance, que total no importa la arregla el DBA :-), no vas a poder ordenar por tu seguna tabla, salvo que pidas el set de datos completo (y de nuevo con la memoria)

Más aún, en la segunda opción (el 'nested loop') que pasa si es un inner join y tenés ordenamiento sobre la primera tabla (además de paginado) cuando te das cuenta que un registro de la primera tabla no debiera formar parte del conjunto de resultado pero ya lo seleccionaste y ya paginaste?

Si el tipo quiere ver datos de dos bases distintas y son homogeneas, yo lo resolvería a nivel de base de datos. Si son heterogeneas, bueno, ahí si voy a tener que hacer algo para juntar yo los datos, pero sería mi última opción, y solo en este caso

Cerebrado dijo...

Me refería a bases de datos heterogéneas :P

AcP dijo...

Con ánimo de trolear un poco les dejo una entrada que acaba de compartir Cerebrado en su reader.
No stored procedures

Improbable dijo...

Cerebrado, por bases heterogeneas entiendo algo más que de diferente fabricante.


Andrés,Estoy de acuerdo en que no hay mayores motivos para usar stored procedures si se van a llamar desde código de un appl. server.

Sin embargo, la independencia de la base de datos es una quimera (si la base de datos más extendida ni siquiera comparte el nivel de aislamiento de transacciones con el resto), y, salvo que uno haga un producto (en tal caso, diría, mejor hacer una capa de acceso específica para cada base), no es deseable.

El mito de la independencia de la base de datos es un post que tengo pendiente.