domingo, agosto 22, 2010

Validación en Entity Framework

Metodologías e ideologías

Debo comenzar confesando que la moda domain-driven me parece ridícula. Es cierto que la programación orientada a objetos es estupenda… y que el modelo relacional también lo es. También es cierto que, al mezclarlos, se produce un desajuste que en inglés llaman impedance mismatch. Pero los intentos de "resolver" el problema mediante la metodología domain-driven consisten, básicamente, en cargarse todas las bondades del modelo relacional, de la arquitectura cliente-servidor y, de paso, la mayoría de los avances aparecidos y probados en estos cuarenta años de bases de datos relacionales.
No es éste el lugar para explicar todo lo que tengo en contra del DD, por lo que me limitaré a un solo argumento. ¿Existe alguna diferencia básica, digamos, entre una aplicación "de negocios" y una aplicación, digamos, como Word o un sistema operativo? Claro que la hay: el dinamismo de las llamadas reglas de negocio. Usted puede escribir una clase para un procesador de texto y atiborrarla de reglas sobre el tratamiento de las mayúsculas. Probablemente, dentro de cinco años esas reglas seguirán siendo igual de malas o apropiadas. En cambio, las reglas para el funcionamiento de una aplicación de negocios cambian constantemente.Este tío cree en el domain-driven... Si usted enchufa esas reglas como parte de métodos de las clases "de negocio", tendrá que contratar a un nerd granujiento a jornada completa, para que cada vez que el dueño del negocio se cambie los gayumbos, cambie la programación de las clases afectadas, recompile y reinstale el sistema.
Por supuesto, esto es lo que todo nerd desea: un trabajo fijo a jornada completa, ya sea cambiando detallitos insignificantes de la clase Order o "tuneando" el núcleo de su instalación personal de Linux, que le deje tiempo para conectarse al Facebook y para jugar a dragones y mazmorras. Es comprensible. Otra cosa es que sea racional o eficiente. O bueno para alguien excepto para el propio nerd.

La cabra y el monte

Sí, se parece probablemente a algún compañero de trabajoLas modas, al final, visten a personas reales. Quiero decir, que si al artista se le olvidó la ranura en el pantalón para hacer pis, el portador de la prenda ya se las arreglará para orinar; eso, o reventará. Por mucho domain-driven que haya infectado la cabecita del pobre programador, en cuanto necesite hierba, la cabra irá al monte… o reventará, ya sabe. ¿Un ejemplo? Teóricamente, en el mundo ideal de nuestros guitar heroes con acné, cada clase debería ocuparse de su propia validación. Pero, ¡oh, qué penita!, los programadores reales que trabajan con Entity Framework han adquirido la costumbre de acumular validaciones dentro del código del método SaveChanges (hay varias formas de conseguirlo).
¿Es eso bueno, o malo? Hay de todo. Esperar a SaveChanges para comprobar que una fecha de nacimiento no pertenece al siglo XII, por ejemplo, es una soberana estupidez. Pero si la regla depende del estado de una entidad, no le queda más remedio que enredar con el administrador del estado: la pureza domain-driven exige que este estado sea externo a la entidad. Por ese motivo, era tan complicado utilizar entidades con servicios WCF en la versión anterior de .NET. Y por ese motivo, gracias al cielo, Microsoft ha terminado ciscándose en la pureza y regalándonos self-tracking entities en la nueva versión. Viva lo impuro cuando resuelve problemas.
¿Y qué dice la sacrosanta teoría sobre todo esto? En honor a la verdad, la Programación Orientada a Objetos dice que, en este caso particular, la centralización es mala. Y tiene razón… si se tiene en cuenta el contexto sobre el que se desarrolla esta metodología: un mundo en el que las principales extensiones a una aplicación se producen principalmente mediante la incorporación de nuevas entidades. Pero este no es el escenario característico de una aplicación de ventas, de préstamos o de Bolsa, donde es más probable que aparezcan nuevas reglas para los viejos objetos que nuevos objetos con nuevas reglas. El impacto de la modificación de las nuevas reglas es mayor si el código que las controla está disperso a lo largo y ancho de los ficheros individuales de clases de entidades.
Y hay más: observe que estas reglas de negocio suelen referirse al estado observable de las entidades de negocio. Traducido a código fuente: las reglas de negocio, por lo general, afectan a propiedades públicas de estas clases. Al fin y al cabo, estas reglas son establecidas por analistas funcionales… o por el propio jefe del cotarro; es decir, por gente que no tiene por qué enredarse con protected, private e internal.

Cabalgando la ola

La centralización, por lo tanto, no siempre es mala; digamos, entonces, que aceptamos que una parte de las reglas de negocio se implementan como parte del mecanismo de sincronización de los cambios con la base de datos. Sin embargo, ¡seguimos necesitando al nerd! Para cada cambio en las reglas, sigue siendo necesario recompilar la aplicación… y redistribuirla.
No sólo eso: imagine que su aplicación la utilizan varias empresas. O incluso, que la utiliza una sola empresa, pero que ésta tiene sucursales en distintos países, con diferentes legislaciones e idiosincrasias. Cada vez que en China cambien las reglas de descuentos para sujetadores, en la sucursal de Groenlandia tendrán que reinstalar el maldito software de ventas.
¿Y si movemos esas reglas fuera de la aplicación? Hablo de ubicar las reglas en un fichero de texto, de manera que la aplicación lo lea, digamos que al iniciarse. El contenido del fichero sería traducido a código ejecutable (eso es muy sencillo, con toda la tecnología que ofrece .NET) que se ejecutaría como parte del método SaveChanges del contexto.
Será más sencillo si muestro un ejemplo de reglas. La sintaxis concreta que he utilizado imita a la de C#, pero está claro que las variaciones son posibles:
class Order
{
on insert
Created = DateTime.Now;
on update
Modified = DateTime.Now;
rule MustPaidBeforeShipping is
Paid || !ShipDate.HasValue;
}
La explicación:
  1. Las reglas se definen por clase. Esto es así por la forma en la que puede tener lugar la validación durante OnSavingChanges: el componente que ejecuta el script recorrerá los ObjectStateEntry modificados en el contexto. Para cada uno de ellos, se elegirá el conjunto de reglas a ejecutar o verificar de acuerdo al tipo de entidad asociado, y luego, al estado de la entidad.
  2. Es recomendable que se respete la herencia. Si una regla se define en una clase base, debe respetarse también en las posibles clases derivadas, siempre que una de estas no la anule.
  3. Como mínimo, se deben soportar dos tipos de reglas: verificaciones y asignaciones a propiedades. En el ejemplo, hay dos asignaciones a propiedades, separadas según el estado de la entidad, y una sola verificación, que al no mencionar el estado, se activará tanto para inserciones como para actualizaciones.
  4. Cada regla tiene un identificador asociado. El escenario típico de estas validaciones será un proceso WCF remoto. Imagine que asociamos un mensaje de error directamente con la regla: ¿en qué idioma debería estar? Un servicio de capa intermedia debe evitar atarse a un idioma concreto, porque puede estar siendo utilizado por clientes configurados para idiomas diferentes. De todos modos, debería ser posible sustituir el identificador por un literal de cadena, para casos sencillos en que no se prevé la traducción a otros idiomas.
  5. Las expresiones trabajarían, principalmente, sobre entidades aisladas, por lo que no habría que preocuparse por extensiones del tipo LINQ: si usted quiere verificar que el total de una orden no sea superior a un valor, por ejemplo, prográmelo donde realmente corresponde, es decir, en la base de datos. Entity Framework no le puede ofrecer garantías, llegados a este punto, de que el grafo de entidades asociados a la entidad modificada, esté completamente cargado en memoria. Sólo piense, por ejemplo, lo difícil que es verificar la unicidad de una columna en un proceso cliente, sin tener todas las entidades en memoria y sin lanzar una consulta adicional al servidor.
Sobre las expresiones soportadas: en principio, no haría falta complicarlas con índices y corchetes, por las razones antes expuestas. Las expresiones básicas serían constantes y "rutas de objetos" (object paths). Estas rutas siempre comenzarían por una propiedad de la clase que define la regla, o por una clase o espacio de nombres. En nuestro ejemplo, ShipDate.HasValue comienza por una propiedad de la clase Order, pero DateTime.Now es, evidentemente, una propiedad estática de una conocida clase. Este tipo de sintaxis es muy fácil de implementar y traducir.
Las expresiones, que son el núcleo de nuestro sistema, se traducirían como árboles de expresiones LINQ, que se pueden compilar a código nativo dinámicamente. En esto ya tengo también bastante experiencia: el sistema es sencillo y muy eficiente (lo he utilizado en filtros a la medida para un servidor RTD para Excel, implementado dentro de una aplicación de servicios de Windows).

En resumen

Se trata solamente, en esta etapa, de una propuesta: crear un componente que, a partir de un script externo, se ocupe de determinadas validaciones e inicializaciones dentro del Entity Framework. Es fácil señalar el método SaveChanges como el punto donde se insertaría el componente de validación. Probablemente existan más puntos donde se pueda actuar.
En principio, el script con las reglas puede estar ubicado en un fichero de texto. Esto sería apropiado si las aplicaciones clientes accediesen a las entidades a través de un servicio WCF. Sería este servicio quien centralizaría el manejo del script y determinaría las reglas a cumplir. Pero sería sencillo extender el mecanismo, en ausencia de una capa intermedia, ubicando las reglas en la base de datos, para simplificar el cambio de las mismas.
Por supuesto, escribo esto, en este momento, para recabar sugerencias.

Etiquetas: , ,