lunes, 10 de marzo de 2014

Arquitecturas débilmente acopladas.

Conseguir una arquitectura cuyos módulos tienen un acoplamiento débil no consiste sólo en crear una interfaz por cada clase existente y consumirla. Voy a intentar reproducir el proceso mental que yo sigo para llegar a este objetivo. Espero que esto sirva, a futuros lectores, para ayudarles a "cambiar el chip" a la hora de diseñar los módulos de un sistema de información.

A lo bruto

Un ejemplo típico es el acoplamiento entre un servicio y un repositorio. Tenemos un repositorio que implementa persistencia en base de datos y dos servicios; uno usado por los gestores normales del sistema y otro usado por un administrador. Los usuarios normales sólo actualizan información de perfiles y el administrador da de alta.
Creamos el repositorio de persistencia con todas las operaciones típicas:
Module Repositorios



 public class DBRepositorio

  {

    public void deleteUsuario(UsuarioEntidad usuarioSuprimido)

    {

    }



    public void insertUsuario(UsuarioEntidad nuevoUsuario)

    { 

    }



    public void updateUsuario(UsuarioEntidad nuevosDatosUsuario)

    {

    }



  }
Y creamos los servicios (omitiendo todo el código de negocio, seguridad, etc para más claridad)
ModuleServicios

{



    using Repositorios;



   public class GestorServicio

  {

    private DBRepositorio miRepositorio;



    public GestorServicio(DBRepositorio repositorio)

    {

      miRepositorio = repositorio;

    }



    public void modificarDatosUsuario(UsuarioEntidad nuevosDatosUsuario)

    {

      miRepositorio.updateUsuario(nuevosDatosUsuario);

    }

  }



  public class AdministradoServicio

  {

    private DBRepositorio miRepositorio;



    public AdministradoServicio(DBRepositorio repositorio)

    {

      miRepositorio = repositorio;

    }



    public void RegistraUsuario(UsuarioEntidad nuevoUsuario)

    {

      miRepositorio.InsertUsuario(nuevoUsuario);

    }



  }

}
Aquí se puede apreciar como el servicio incluye una dependencia al módulo de repositorios y como lo utiliza para hacer su trabajo.

Pregunta: ¿Hay que modificar el módulo de servicios si se necesita utilizar ahora un repositorio basado en ficheros XML en vez de Base de Datos?
Respuesta: Pues sí. Hay que modificar el atributo de los servicios para que sea una referencia a la nueva clase XMLRepositorio y puede que hasta los métodos del nuevo repositorio se llamen diferente.

Intentando ser sutil

Esto es un alto acoplamiento entre módulos. Para intentar solucionarlo extraemos la interfaz de la clase DBRepositorio y creamos el repositorio XML implementando esa nueva interfaz.

module Repositorios

interface IRepositorio
{

  void DeleteUsuario(UsuarioEntidad usuarioSuprimido);

  void InsertUsuario(UsuarioEntidad nuevoUsuario);

  void updateUsuario(UsuarioEntidad nuevosDatosUsuario);

}

public class DBRepositorio : IRepositorio

{

  public void DeleteUsuario(UsuarioEntidad usuarioSuprimido)

  {

  }

  public void InsertUsuario(UsuarioEntidad nuevoUsuario)

  {

  }

  public void updateUsuario(UsuarioEntidad nuevosDatosUsuario)

  {

  }

}

public class XMLRepositorio : IRepositorio

{

  public void DeleteUsuario(UsuarioEntidad usuarioSuprimido)

  {

  }

  public void InsertUsuario(UsuarioEntidad nuevoUsuario)

  {
  }

  public void updateUsuario(UsuarioEntidad nuevosDatosUsuario)

  {

  }

}

Module Servicios

public class GestorServicio

{

  private IRepositorio miRepositorio;

  public GestorServicio(IRepositorio repositorio)

  {

    miRepositorio = repositorio;

  }

  public void modificarDatosUsuario(UsuarioEntidad nuevosDatosUsuario)

  {

    miRepositorio.updateUsuario(nuevosDatosUsuario);

  }

}

public class AdministradoServicio

{
  private IRepositorio miRepositorio;

  public AdministradoServicio(IRepositorio repositorio)

  {

    miRepositorio = repositorio;

  }

  public void RegistraUsuario(UsuarioEntidad nuevoUsuario)

  {

    miRepositorio.InsertUsuario(nuevoUsuario);

  }

}

Un poquito mejor ¿verdad? Ahora no tenemos que cambiar los servicios, simplemente recibir una instancia de un repositorio u otro. Con esta interfaz, declarada en el módulo de repositorios, los repositorios están diciendo "esto es lo que soy capaz de hacer, así es como me tienes que utilizar"; el cómo lo hace viene en cada implementación de la interfaz por parte de los repositorios.

Pregunta: ¿Pueden los usuarios o los administradores utilizar los repositorios para tareas que no le corresponden ?
Respuesta: Pues sí. Resulta que la clase GestorServicio podría llamar al método IRepositorio.InsertUsuario o IRepositorio.DeleteUsuario. A la clase AdministradoServicio le pasa lo mismo, puede invocar métodos que nunca debería. 

A cada uno lo suyo

Esto es contraproducente. Uno de los objetivos de la arquitectura de software orientada al dominio es reflejar las restricciones del sistema de información (las que sean posibles a través de la arquitectura) para que sea mas fácil e intuitivo modelar los procesos.

Puesto que son las clases de módulo de Servicios las que saben lo que cada una va a necesitar de los repositorios, es lógico que sean ellas las que definan las interfaces de lo que necesitan. ¿Como va a saber el módulo de Repositorios que funcionalidades debe exponer si el módulo de repositorios es (o debería ser) totalmente ignorante de los requisitos funcionales de Gestores y Administradores? Como no lo sabe pues lo que hace es exponer todas las funcionalidades y eso nos ayuda a desacoplar los módulos y a dividir y encapsular responsabilidades.

Si la clase GestorServicio sólo debe actualizar información entonces esta clase debe utilizar una interfaz que sólo exponga métodos de actualizar información. Si la clase AdministraServicio sólo debe insertar nuevos elementos entonces se debe utilizar una interfaz que sólo exponga métodos de insertar información.

No es la clase utilizada la que debe exponer una interfaz, son los consumidores los que deben especificar que interfaz debe implementar una clase si quiere ser consumida. En vez de un "esto es lo que soy capaz de hacer para ti" debe ser un "esto es lo que debes ser capaz de hacer si quieres ser usado por mi". Es la clase consumidora la que debe especificar la interfaz que deben implementar las clases que quieran ser consumidas puesto que ésta es la que tiene que mantener consciencia sobre el proceso a modelar por el sistema de información.

//Module Repositorios

using Servicios;

public class DBRepositorio :Servicios.IRepositorioForGestor, Servicios.IRepositorioForAdministrador
{
  public void DeleteTuplaUsuario(UsuarioEntidad usuarioSuprimido)

  {

  }

  public void InsertTuplaUsuario(UsuarioEntidad nuevoUsuario)

  {

  }

  public void updateTuplaUsuario(UsuarioEntidad nuevosDatosUsuario)

  {

  }
}

public class XMLRepositorio :Servicios.IRepositorioForGestor, Servicios.IRepositorioForAdministrador
{
  public void DeleteUsuario(UsuarioEntidad usuarioSuprimido)
  {
  }

  public void InsertUsuario(UsuarioEntidad nuevoUsuario)

  {
  }

  public void updateUsuario(UsuarioEntidad nuevosDatosUsuario)

  {

  }

}

//Module Servicios

interface IRepositorioForGestor

{
  void updateUsuario(UsuarioEntidad nuevosDatosUsuario);

}

public class GestorServicio

{
  private IRepositorioForGestor miRepositorio;

  public GestorServicio(IRepositorioForGestor repositorio)

  {

    miRepositorio = repositorio;

  }

  public void modificarDatosUsuario(UsuarioEntidad nuevosDatosUsuario)
  {

    miRepositorio.updateUsuario(nuevosDatosUsuario);

  }

}

interface IRepositorioForAdministrador
{

  void InsertUsuario(UsuarioEntidad nuevoUsuario);

}

public class AdministradoServicio
{
  private IRepositorioForAdministrador miRepositorio;

  public AdministradoServicio(IRepositorioForAdministrador repositorio)
  {
    miRepositorio = repositorio;
  }

  public void RegistraUsuario(UsuarioEntidad nuevoUsuario)
  {
    miRepositorio.InsertUsuario(nuevoUsuario);
  }
}

Ahora sí que tenemos el módulo de servicios totalmente desacoplado de ninguna dependencia del repositorio. El repositorio tiene la dependencia mínima necesaria para dar la cara (a través de implementaciones de interfaces) a los diferentes servicios que existan o puedan existir en un futuro.


PD: Mientras redactaba esta entrada me he encontrado un gráfico muy clarificador, realizado por Jeffrey Palermo, de cómo se establecen las capas de una arquitectura para cumplir el objetivo de un acoplamiento débil. Se aprecia como la parte central de la arquitectura (core), que es la que tiene el conocimiento de los procesos del sistema de información, y por tanto de las operaciones que tienen que consumir para realizarlos, es la que define los contratos que se deben cumplir; y cómo la parte más externa son módulos ignorantes de los procesos del dominio que se modela; los cuales son fácilmente reemplazables por diferentes implementaciones.



No hay comentarios:

Publicar un comentario