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.
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:
De manera similar, si una método en una clase brinda cierta funcionalidad, podría:
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:
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.
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.
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:
OVal nos permite implementar un Diseño por Contratos de manera simple, ya que nos brinda mucha funcionalidad que caracteriza al DbC:
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:
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!
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");
}
}
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.
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!
OVal nos permite agregar restricciones usando anotaciones. Hay muchas anotaciones disponibles, entre ellas podemos destacar:
Hay varias anotaciones más disponibles.
Vamos entonces a agregarle varias validaciones a nuestra clase PersonaServiceImpl.
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.
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:
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:
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"); } ... ... }
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 ejemploPueden 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.