Ir al contenido principal

Principios de spring

Spring nace como un marco de trabajo para Java, y alcanza una enorme popularidad. Sus principales pilares son:

  • Inversión del control.
  • Inyección de dependencias.
  • La programación por aspectos.

Inversión de control

Es un principio de diseño de software en el que el flujo de ejecución de un programa se invierte respecto a los métodos de programación tradicionales. En los métodos de programación tradicionales la interacción se expresa de forma imperativa haciendo llamadas a procedimientos o funciones. Tradicionalmente el programador especifica la secuencia de decisiones y procedimientos que pueden darse durante el ciclo de vida de un programa mediante llamadas a funciones.

En su lugar, en la inversión de control se especifican respuestas deseadas a sucesos o solicitudes de datos concretas, dejando que algún tipo de entidad o arquitectura externa lleve a cabo las acciones de control que se requieran en el orden necesario y para el conjunto de sucesos que tengan que ocurrir.

Como ejemplo, con la programación tradicional, la función principal de una aplicación podría hacer llamadas a funciones de una librería de menús para mostrar una lista de comandos disponibles y consultar al usuario para seleccionar uno. La librería devolvería la opción elegida como el valor de la llamada a la función, y la función principal usa este valor para ejecutar el comando asociado. Este estilo era común en las interfaces basadas en texto. Por ejemplo, un cliente de correo electrónico puede mostrar una pantalla con comandos para cargar correo nuevo, responder el correo actual, iniciar un correo nuevo, etc., y la ejecución del programa se bloqueará hasta que el usuario presione una tecla para seleccionar un comando.

Por otro lado, con la inversión de control, el programa se escribiría utilizando un marco de trabajo que conoce elementos comunes de comportamiento y gráficos, como sistemas de ventanas, menús, control del mouse, etc. El código personalizado "llena los espacios en blanco" para el marco, como proporcionar una tabla de elementos de menú y registrar una subrutina de código para cada elemento, pero es el marco el que monitorea las acciones del usuario e invoca la subrutina cuando se selecciona un elemento de menú.

La ventaja de la inversión de control es que el mismo código se podría inyectar en un marco que reciba los comandos como peticiones telnet remotas sin apenas cambios.

Inyección de dependencias

En POO, cuando las aplicaciones crecen, aumenta el número de clases existentes, y las relaciones entre las mismas. Si una clase debe instanciar a todos los objetos de los que depende se introduce una dependencia muy fuerte entre ellas. Además se complica enormemente la gestión de la configuración de los objetos instanciados:

  • Cual es la mejor manera de mantener la independencia de los objetos a pesar de requerir de la ayuda de otros. Cuanta mayor sea la dependencia, más difícil será aprovechar un objeto dado en un contexto distinto.
  • Cómo se puede hacer para centralizar la configuración de los diferentes objetos, y hacer que el modo de instanciación sea el mismo para todos.
  • Cómo podemos hacer para que la aplicación permita hacer cambios de sus configuraciones de manera sencilla.

Si dentro de una clase instanciamos los objetos que necesita estamos introduciendo una gran rigidez en el código; porque hacemos la clase se defina con un comportamiento y una configuración concreta. Es imposible cambiar la configuración de la dependencia sin modificar el código de la clase que lo necesita, con lo además es muy dificil conseguir separar la clase para volver a utilizarla con posterioridad.

La inyección de dependencias aprovecha que existe un contenedor (fruto de la inversión de control), para que las clases indiquen cuales son los servicios que necesitan. El contenedor cuando prepare las clases, se encargará de asignarles las referencias que necesita.

public class Cazador {
  private Arma arma;

  public Cazador() {
    arma = new Arma(5);
  }

  public boolean cazar(Presa presa) {
    return presa.resistencia < arma.fuerza;
  }
}

public class Arma { 
  public int fuerza;
}

public class Presa {
  public int resistencia;
}

En este ejemplo sencillo, no tenemos manera de mejorar el armamento del cazador sin modificar la propia clase del cazador.

Si lo cambiamos para aplicar inyección de dependencias, simplemente diremos que el cazador necesita un arma, y seré el contexto quien inicialice el arma que necesite para salir a cazar.

@Component
public class Cazador {
  @Autowired
  private Arma arma;
  public boolean cazar(Presa presa) {
    return presa.resistencia < arma.fuerza;
  }
}

@Component
public class Arma { 
  public int fuerza;
}

public class Presa {
  public int resistencia;
}

Programación de aspectos

Cuando se desarrolla para un sistema complejo, cada vez es necesario añadirle más y más funcionalidades transversales que no tienen que ver con la lógica de negocio en si...

  • Guardar un log de los eventos para saber que ocurre.
  • Validar los datos recibidos en las peticiones
  • Abrir y liberar los recursos de bbdd o ficheros utilizados.
  • Detección de los errores durante los procesos.
  • Seguridad y control de acceso.

En la programación tradicional, añadir todas esas funcionalidades obliga a añadirlas como funciones a los métodos de neogico...

public class Cazador {
  @Autowired
  private Arma arma;
  @Autowired
  private Logger logger;
  @Autowired
  private Validator validator;
  @Autowried
  private Secured secured;
  public boolean cazar(Presa presa) {
    logger.log("Inicio de la caza");
    long time = System.currentTime();
    if( secured.isAllowed() ) {
      if( !validator.isValid(presa) ) {
        logger.error("La presa no es valida");
        throw new IllegalArgumentException();
      }
      boolean result = presa.resistencia < arma.fuerza;
      logger.log("Ha terminado la caza " + result + " y tardó " + (System.currentTIme() - time ) );
      return result;
    } else {
      logger.error("Usted no puede enviar al cazador de caza");
      throw new IllegalAccessException();
    }
  }
}
...

Si ahora te preguntan "¿Que hace el método cazar?"... es complicado llegar a encontrar el código de lo que realmente hace. Hay mucho código adicional para satisfacer necesidades externas a la propia caza en si que llegan el código del método haciendo muy dificil su lectura, y aumentando las probabilidades de error.

Si cambian los requisitos seguridad de la aplicación, y se pide que ante un error de permisos, además de lanzar una excepciónd e acceso ilegal se envíe un email al jefe de guardia... tendríamos que modificar todas las clases de negocio de la aplicación.

La programación de aspectos dice que toda esa lógica adicional se debe delegar a otras partes de la aplicación. Con spring es posible indicar que queremos que un método de una clase se ejecute antes o despues de los métodos de una clase objetivo.

@Component
public class Cazador {
  @Autowired
  private Arma arma;
  public boolean cazar(Presa presa) {
    return presa.resistencia < arma.fuerza;
  }
}

@Aspect
public class LogginAspect {
  @Arround
  public Object invoke(Callback callback) {
    logger.log("Inicio de la caza");
    long time = System.currentTime();
    Object result = callback.invoke();
    logger.log("Ha terminado la caza " + result + " y tardó " + (System.currentTIme() - time ) );
    return result;
  }
}

@Aspect
public class SecurityAspect {
  @Before
  public void before() {
    if( !allowed() ) {
      logger.error("Usted no puede enviar al cazador de caza");
      throw new IllegalAccessException();
    }
  }
}

@Aspect
public class ValidationAspect {
  @Before
  public void before(Callback callback) {
    if( !isValid(callback.getParams() ) ) {
        logger.error("La presa no es valida");
        throw new IllegalArgumentException();
    }
  }
}

En este caso no tenemos menos código, sin embargo si aumenta el número de clases de negocio, y a parte de un cazador, necesitamos un leñador, un rastreador, un pescador.... entonces si se verá una reducción importante en la cantidad de código.

Obviando la reducción de la cantidad de código, tenemos bien separado el código de la lógica de negocio, del código de validación de permisos, del código de logging, del código de validación de datos.

Clases dinámicas en Java

Gran parte de la magia que hay detras de spring, y de otros marcos de trabajo que funcionan usando la inversión de control en los que se utilizan metadatos para describir comportamientos (Hibernate también los utiliza); se debe al uso de clases Proxy dinámicas.

En Java existe una interfaz "InvocationHandler" con un método "invoke", que puede ser utilizada para crear clases "falsas":

A grandes rasgos lo que tenemos con Spring es:

Cazador cazador = (Cazador)Proxy.newInstance(
  DynamicProxyTest.class.getClassLoader(), 
  new Class[] { Cazador.class }, 
  new DynamicInvocationHandler() );
cazador.cazar(presa);

class DynamicInvocationHandler implements InvocationHandler {
  private Arma arma = new Arma(11);
  private Cazador inner = new Cazador();
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Callback call = new Callback(proxy, method, args);
    securityAspect.before();
    validationAspect.before(call);
    return loggingAspect.invoke(call);
  }
}

De hecho, si hacemos un Context.getBean(Cazador.class).getClass() no obtendremos un Cazador.class; obtendremos una Proxy class....

Comentarios