OVal es un framework de validación para Java, que permite realizar comprobaciones a cualquier tipo de objetos y crear soluciones usando un enfoque de Diseño por Contrato. En este artículo vamos a ver brevemente qué es el Diseño por Contrato, y cómo utilizar OVal junto a Spring Framework para crear una infraestructura que facilite un Diseño por Contratos.
¿Qué es el Diseño por Contrato?
El Diseño Por Contrato (Design by Contract en inglés, también conocido por las siglas DbC) es una metodología para el diseño e implementación de aplicaciones y componentes popularizada por el lenguaje de programación Eiffel.
En Estados Unidos "Design by Contract" es una marca registrada, por lo que muchos desarrolladores lo llaman Programming by Contract (Programación por Contratos), o Contract Programming (Programación Contractual), o contract-first development (desarrollo con contrato primero).
La idea central de DbC es una metáfora sobre cómo interactuan los elementos de un sistema de software para colaborar entre si, basándose en obligaciones y beneficios mutuos. La metáfora proviene del mundo de los negocios, en donde un "cliente" y un "proveedor" firman un "contrato" que, por ejemplo, define:
- el proveedor debe brindar cierto producto (obligación) y tiene derecho a que el cliente le pague una cuota (beneficio).
- el cliente paga una cuota (obligación) y tiene derecho a obtener el producto (beneficio).
- ambas partes deben satisfacer ciertas obligaciones, como leyes y regulaciones, que se aplican a todos los contratos.
De manera similar, si una método en una clase brinda cierta funcionalidad, podría:
- imponer ciertas obligaciones que se garanticen por cualquier módulo cliente que la invoque: la precondición del método - una obligación del cliente, y un beneficio para el proveedor (el método en sí mismo), ya que lo libera de tener que gestionar casos por fuera de la precondición.
- garantizar cierto comportamiento de salida: la postcondición del método - una obligación del proveedor, y obviamente un beneficio para el cliente.
- mantener cierta propiedad o estado en el objeto, asumida al momento de la entrada y garantizada a la salida: las invariantes de la clase.
El contrato es la formalización de estres obligaciones y beneficios. Se podría resumir al Diseño por Contrato por estas tres preguntas que el diseñador del compomente debe preguntarse:
- ¿Qué espera el componente? (precondición)
- ¿Qué garantiza el componente? (postcondición)
- ¿Qué mantiene el componente? (invariante)
DbC considera que los contratos son cruciales para crear software correcto, y que deben ser parte del proceso de diseño del software. De hecho, DbC fometa escribir primero las asersiones y luego el código.
DbC en Java
Java no tiene soporte a nivel de lenguaje para hacer DbC (si bien los assert pueden usarse de forma muy limitada para algo parecido, no es recomendable). Es por esto que hay varias librerias externas que brindan utilidades para lograr declarar precondiciones, postcondiciones e invariantes, de distintas maneras.
La mayoría de los frameworks disponibles utilizan anotaciones en las clases, en las cuales se indican las condiciones que tienen que cumplirse antes de invocar al método, el valor de retorno del método, y las invariantes de la clase. Luego, utilizando AOP, estas anotaciones se traducen en código ejecutable en tiempo de ejecución.
A continuación vamos a ver una introducción a OVal, una librería que implementa DbC utilizando anotaciones. Intregraremos OVal a Spring para facilitar el uso de AOP.
Introducción a OVal
OVal es un framework pragmático y extensible para validar cualquier tipo de objetos Java (y no sólo JavaBeans).
Se pueden declarar restricciones usando anotaciones (@NotNull, @MaxLength, etc), POJOs y XML. También se pueden crear restricciones personalizadas, usando clases Java o expresiones en lenguajes de scripting, como JavaScript, Goovy, BeanShell, OGNL o MVEL.
Además de validaciones a nivel de atributos, OVal permite implementar el Diseño por Contratos, aplicando restricciones a métodos usando la técnica de AOP a través de AspectJ. De esta manera se pueden validar argumentos de un método en tiempo de ejecución.
Entonces, OVal permite:
- validar objetos de forma simple a demanda
- especificar restricciones para atributos de una clase, y para métodos getter
- validar objetos basándose en ciertas anotaciones de EJB3 JPA (como ser todos los campos que necesitan un valor no-nulo)
- configurar las restricciones a través de anotaciones, POJOs y/o archivos XML
- expresar restricciones usando lenguajes de scripting como Groovy, BeanShell y JavaScript
- crear restricciones personalizadas de forma simple
- desarrollar nuevos mecanismos para configurar restricciones
OVal y el Diseño por Contratos
OVal nos permite implementar un Diseño por Contratos de manera simple, ya que nos brinda mucha funcionalidad que caracteriza al DbC:
- especificar restricciones para los parámetros de un constructor que se verifican automáticamente cuando se invoca al constructor (precondición)
- especificar restricciones para los parámetros de un método que se verifican automáticamente cuando se invoca al método (precondición)
- requerir cierto estado en un objeto antes de invocar al método (precondición)
- forzar la validación del objeto luego de haber creado al objeto (invariante)
- forzar la validación del objeto antes/después de invocar un método del objeto (invariante)
- especificar restricciones para el valor de retorno de un método que se comprueba automáticamente luego de que se ejecuta el método (postcondición)
- requerir un estado determinado en el objeto luego de la invocación a un método (postcondición)
Los primeros pasos con OVal y Spring
Vamos a integrar OVal con Spring, de manera que se encargue de realizar validaciones a un bean cualquiera declarado en Spring. Para esto, los pasos a seguir serán:
- Crear un "servicio" de personas (una POJO Persona, una interfaz PersonaService, y una clase PersonaServiceImpl que implementa dicha interfaz).
- Configurar un bean de Spring para PersonaService.
- Configurar el interceptor de OVal e interceptar a nuestro PersonaService
- Agregar anotaciones a PersonaService para crear validaciones.
Elegimos hacer las tareas en este órden por cuestiones didácticas. Sin embargo, en una implementación real usando DbC, lo ideal sería comenzar escribiendo las validaciones de nuestro servicio, y luego el código del mismo.
¡Manos a la obra!
1. El "servicio" de personas
Vamos a crear entonces las 3 clases de negocio que nos importan. Primero, el POJO Persona, que será nuestro objeto de dominio.
public class Persona { private String nombre; private String apellido; public Persona(String nombre, String apellido) { this.nombre = nombre; this.apellido = apellido; } public Persona() {} //getters y setters a continuacion ... }
A continuación creamos la interfaz PersonaService que contiene los métodos de negocio.
public interface PersonaService { void guardarPersona(Persona p); Persona crearPersona(String nombre); }
Por último, una implementación del negocio con la clase PersonaServiceImpl. Más tarde agregaremos las restricciones de OVal a esta clase.
public class PersonaServiceImpl implements PersonaService { public void guardarPersona(Persona p) { System.out.println("Guardando persona: " + p); } public Persona crearPersona(String nombre) { if (nombre == null) { return null; } return new Persona(nombre, "Apellido no especificado"); } }
2. Configurar Spring para PersonaService
Vamos a crear el archivo application.xml de Spring en donde configuramos al bean PersonaService.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <!-- Nuestro servicio de Persona. --> <bean id="service.Persona" class="com.dosideas.oval.PersonaServiceImpl"/> </beans>
¡Listo! Con esto tenemos un servicio de Persona configurado en Spring como un bean normal. La magia de OVal comienza a continuación.
3. Configurar el interceptor de OVal en Spring
OVal necesita intercetpar las clases que tengan restricciones para poder realizar las verificaciones correspondientes. Al usar Spring se facilita mucho esta intercepción: basta con configurar el interceptor ya provisto por OVal y decirle cuáles beans serán verificados por OVal. Entonces, agregamos las siguientes líneas al archivo application.xml de Spring:
<bean id="ovalGuardInterceptor" class="net.sf.oval.guard.GuardInterceptor" /> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="proxyTargetClass" value="true" /> <property name="beanNames" value="service*" /> <property name="interceptorNames"> <list> <value>ovalGuardInterceptor</value> </list> </property> </bean>
Como vemos en el atributo beanNames, estamos interceptando todos los beans de Spring cuyo nombre comience con "service". Así, nuestra clase PersonaServiceImpl será interceptada por OVal y podrán ocurrir las comprobaciones. Claro, todavía nuestra clase no tiene ninguna verificación... así que vamos a agregarlas!
4. Agregar restricciones con OVal
OVal nos permite agregar restricciones usando anotaciones. Hay muchas anotaciones disponibles, entre ellas podemos destacar:
- @NotNull, que se aplica a los parámetros de un método. Verifica que dicho atributo no sea null al invocar el método.
- @Pre, que se aplica a los métodos. Contiene una expresión en algún lenguaje de scripting (por ejemplo, Groovy) que se evalúa antes de la ejecución del método (para verificar, por ejemplo, el valor de los parámetros o el estado del objeto).
- @Post, que se aplica a los métodos. Contiene una expresión en algún lenguaje de scripting que se evalúa después de la ejecución del método (para verificar, por ejemplo, el valor de retorno).
Hay varias anotaciones más disponibles.
Vamos entonces a agregarle varias validaciones a nuestra clase PersonaServiceImpl.
Uso de @NotNull
Como primera restricción haremos que el parámetro del método guardarPersona(Persona p) no sea null. Esto es muy sencillo:
public void guardarPersona(@NotNull Persona p) { ... }
Si intentamos invocar al método con un null, surgirá una excepción net.sf.oval.exception.ConstraintsViolatedException.
Uso de @Pre
Evidentemente la expresión @NotNull es muy útil, pero no alcanza para realizar validaciones más complejas. Por suerte tenemos la anotación @Pre, que nos permite crear toda una expresión en alguno de los lenguajes de scripting soportados. Esta expresión se evalúa antes de la invocación al método y después de las verificaciones a nivel de atributo. Si la evaluación de la expresión da falso, se lanzará una excepción net.sf.oval.exception.ConstraintsViolatedException.
Entonces, si queremos validar que el nombre y el apellido de la persona tampoco sean nulos agregamos:
@Pre(expr = "_args[0].nombre != null && _args[0].apellido != null", lang = "groovy") public void guardarPersona(@NotNull Persona p) { ... }
La variable _args representa un array de parámetros para el método. Sabemos que _args[0] no es null porque tenemos la anotación @NotNull. Si no usaramos la anotación @NotNull deberemos antes validar en la expresión que el parámetro no sea null.
Dentro de la expresión tenemos accesos a varias variables implícitas:
- _args[] es un array que representa a cada uno de los parámetros del método.
- _this es una referencia al objeto actual.
Uso de @Post
También es posible evaluar el valor de retorno de un método. Para esto utilizamos la anotación @Post, que se ejecuta después de la invocación del método (y antes de devolverle el valor al cliente que realizó la invocación). Esta anotación utiliza una expresión similar a la anotación @Pre. Si la evaluación de la expresión da falso, se lanzará una excepción java.lang.AssertionError.
Vamos a agregar una postcondición al método crearPersona(String nombre) para asegurar que el método nunca pueda retornar falso.
@Post(expr = "_returns != null", lang = "groovy") public Persona crearPersona(String nombre) { ... }
La variable implícita _returns representa al objeto de que se está retornando. Dentro de una expresión de @Post tenemos acceso a varias variables implícitas:
- _args[] es un array que representa a cada uno de los parámetros del método.
- _this es una referencia al objeto actual.
- _returns es una referencia al objeto de retorno.
Probando que todo funcione...
Y esto es todo. Con esos cambios nuestro PersonaService tendrá validaciones que se ejecutarán automáticamente cuando se invoquen los métodos. Creamos una prueba con JUnit muy simple para validar el comportamiento:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:application.xml" }) public class PersonaServiceTest { @Autowired private PersonaService personaService; @Test public void guardarPersona() { Persona p = new Persona("Invasor", "Zim"); personaService.guardarPersona(p); } @Test(expected=ConstraintsViolatedException.class) public void guardarPersona_personaNull() { personaService.guardarPersona(null); fail("Debería haber ocurrido una excepcion de validación"); } @Test(expected=ConstraintsViolatedException.class) public void guardarPersona_nombreNull() { Persona p = new Persona(null, "Zim"); personaService.guardarPersona(p); fail("Debería haber ocurrido una excepcion de validación"); } ... ... }
Conclusión
OVal es un framework para validación de objetos, que permite utilizar la técnica de Diseño por Contratos de manera muy simple y efectiva. OVal tiene muchas funciones y facilidades más de las que vimos hasta ahora, como ser control de invariantes, verificación de constructores y atributos, utilización de otros lenguajes de scripting y más. Pueden leer la documentación de OVal para aprender más sobre este interesante framework de validación.
Descargar proyecto de ejemplo
Pueden descargar el proyecto de ejemplo de OVal que contiene todas las clases que vimos en este artículo. El proyecto incluye todas las librerías y pruebas necesarias para ver funcionar a OVal.