Feeds:
Entradas
Comentarios

Archive for the ‘Single Responsability’ Category


Después del interludio con el spring io, vuelvo a la carga con el tema del diseño ágil de software. Como ya comenté, considero que dentro de las limitaciones de tiempo y dinero de un proyecto, hay que estar continuamente reestructurando el código con el objetivo de que sea fácil y barato cambiarlo cada vez que se produzca un cambio funcional, añadamos un nuevo requisito o necesitemos arreglar un bug. A estas reestructuraciones de código se les llama «refactors».

Un refactor es una transformación de código que no altera la funcionalidad de este, pero sí su estructura. La idea es hacer refactors que hagan que nuestro código sea más flexible y maleable para poder alterarlo de forma rápida. El truco está en saber si al aplicar un refactor estamos mejorando nuestro código o si simplemente lo estamos complicando ¿Qué criterios sigo yo para evaluar si un refactor es bueno o no? Básicamente sólo sigo los tres siguientes:

  • Que el código resultante sea más legible.  Como ya expliqué considero este el criterio más importante.
  • Que se respeten los principios DRY y «experto en información». Ya los explicaré en otro post
  • Que se respeten los principios SOLID. Entre este post y los siguientes pienso ir explorándolos.

¿Qué es el SOLID? SOLID es un acrónimo que consolida 5 principios: Single responsability, Open/Closed, Liskov substitution, Interface segregation y Dependency inversion. En este post me ocuparé del principio de única responsabilidad (Single responsability o SRP), o sea la S  de SOLID.

El principio de única responsabilidad (SRP) simplemente nos dice que ningún artefacto de código debe tener más de una única responsabilidad, y por lo tanto debe implementar una única funcionalidad. El principio aplica a sistemas, aplicaciones, frameworks, objetos y métodos. Obviamente según el artefacto al que se aplique, la amplitud del concepto de única responsabilidad cambia. Pero en este post nos vamos a centrar en cómo afecta a nuestra clases y objetos.

¿Cómo sabemos si un artefacto, como una clase o un método, respeta este principio? Es muy sencillo, simplemente  debemos preguntarnos a nosotros mismos sobre cuantas razones diferentes podemos tener para querer cambiar dicho artefacto. Un artefacto que cumple el principio de única responsabilidad sólo puede ser cambiado debido a una única razón. Si encontramos múltiples razones para cambiar un artefacto entonces no cumple dicho principio y debe ser refactorizado. A mi me gusta llamar a este principio, el principio del bombero/torero. El hecho de que el personaje de bombero/torero nos cause risa es que es totalmente ridículo que una misma persona realice tareas de dos profesiones tan diferentes al mismo tiempo. Si lo vemos absurdo en una persona, ¿porque nos quedamos tan tranquilos cuando vemos una clase que es un bombero/torero?

Para entender mejor este principio veamos un ejemplo. Suponed que tenemos una historia de usuario tal que sigue:

«Cuando se realiza una transferencia importante

Entonces se genera un mensaje con información sobre dicha transferencia

Y se notifica a las personas de interés

Para que den su aprobación y así evitar el fraude fiscal»

Hablando con el cliente me dice que la persona de interés es un auditor, y que había pensado en notificarle vía mail. Las transferencias importantes son aquellas por encima de 1000 euros (son un poco ratas). Cómo estoy codificando en JAVA y usando el famoso framework Zprin, el código que me sale es el siguiente:

public class AuditorTransferenciasMonetarias {
  // Inyección de dependencias con el famoso framework Zprin (TM)
  // Con este framework somos productivos de la leche
  private ZprinPropertySource systemConfiguration;
  private ZprinTemplateEngine templateEngine;
  private ZprinMailIt mailIt;

  public void transferenciaRealizada(Transferencia transferencia) {
    if(this.esTransferenciaImportante(transferencia)) {
      String auditor=this.obtenerDireccionMailAuditor();
      String mensaje=this.componerMensajeAviso(transferencia);
      ConexionMail conexionMail=null;
      try {
        conexionMail=this.abrirConexionMail();
        conexionMail.enviar(new Mail().to(auditor).withBody(mensaje));
      } finally {
        if(conexionMail!=null)
          conexionMail.cerrar();
      }
    }
  }
  private boolean esTransferenciaImportante(Transferencia transferencia) {
    return transferencia.importe()>1000;
  }
  private String obtenerDireccionMailAuditor() {
    return this.systemConfiguration.getProperty("auditor.email");
  }
  private String componerMensajeAviso(Transferencia transferencia) {
    return this.templateEngine.getTemplate("aviso-transferencia-importante.ztpl").execute(transferencia);
  }
  private MailConnection abrirConexionMail() {
    return this.mailIt.openConnectionTo(this.systemConfiguration.getProperty("servers.mail"));
  }
}

Buen código, al usar el framework soy muy productivo y hago la clase super rápido, es más me quito de encima problemas complejos como el envío de mails, el acceso a propiedades configurables, o el tener que hacerme un motor de plantillas 😛 Sólo tiene un problema, que no cumple el SRP (maldito SOLID, quién lo habrá inventado). SRP nos dice que la clase sólo debe tener una responsabilidad y la única manera en la que esta debería tener que cambiarse es cuando hay alguna alteración en dicha responsabilidad. En este caso la responsabilidad viene definida por la historia de usuario, así que mientras esta no cambie de forma significativa, entonces no deberíamos vernos forzados a cambiar la implementación. Obviamente el código descrito arriba no cumple esta propiedad. Vayamos por partes.

Suponed que el criterio para clasificar a una transferencia como importante cambia. Nos veríamos forzados a tocar nuestra clase, aunque realmente nuestra historia de usuario es la misma. Para evitarlo alteramos nuestro código:

public class AuditorTransferenciasMonetarias {
  // ....
  public void transferenciaRealizada(Transferencia transferencia) {
    if(transferencia.esImportante()) {
      String auditor=this.obtenerDireccionMailAuditor();
      String mensaje=this.componerMensajeAviso(transferencia);
      ConexionMail conexionMail=null;
      try {
        conexionMail=this.abrirConexionMail();
        conexionMail.enviar(new Mail().to(auditor).withBody(mensaje));
      } finally {
        if(conexionMail!=null)
          conexionMail.cerrar();
      }
    }
  }
  // ....
}

Como vemos ahora la clase Transferencia tiene un método que nos indica si es importante o no. Si el criterio de importancia cambia nos da igual, ya que este método nos protege de dicho cambio. Si el cliente decide que quiere un motor de reglas parametrizable en runtime mediante un backoffice, el afectado es Transferencia no la clase que estamos construyendo.

Otro motivo de cambio ajeno a nuestra responsabilidad es que la definición de «personas interesadas» cambie. De repente queremos notificar no sólo a un auditor, sino a varios y además al director de la sucursal. Para protegernos de ello veamos el siguiente cambio:

public class AuditorTransferenciasMonetarias {
  // ....
  // Ahora tenemos un colaborador de "negocio"
   // que está al mismo nivel de abstracción que esta clase
  private DirectorioEmpleados directorioEmpleados;

  public void transferenciaRealizada(Transferencia transferencia) {
    if(transferencia.esImportante()) {
      String mensaje=this.componerMensajeAviso(transferencia);
      ConexionMail conexionMail=null;
      try {
        conexionMail=this.abrirConexionMail();
        conexionMail.enviar(rellenarDestinatarios(new Mail().withBody(mensaje)));
      } finally {
        if(conexionMail!=null)
          conexionMail.cerrar();
      }
    }
  }
  private Mail rellenarDestinatarios(Mail mail) {
    for(Empleado interesado: this.interesadosEnTransferenciasImportantes())
      mail=mail.to(interesado.email());
    return mail;
  }
  private List<Empleado> interesadosEnTransferenciasImportantes() {
    return this.directorioEmpleados.empleadosConRol(Empleado.INTERESADOS_TRANSFERENCIAS_IMPORTANTES);
  }
  // ....
}

Ahora gracias a la interface DirectorioEmpleados, podemos buscar a todos los empleados que necesitamos notificar. Para ello buscamos por rol. La asociación empleado/rol se puede definir aparte, e incluso ser dinámica y cambiar en runtime. Observad que hemos añadido un colaborador que está al mismo nivel de abstracción de la clase que estamos codificando, y que el código ahora se parece un poco más a la definición de la historia de usuario.

Otro motivo por el cual podemos vernos obligados a cambiar esta clase, es que ahora los mensajes no se envíen por mail, sino por SMS. O peor, ¡ que se envíen a la vez por SMS, Mail, y chat a la vez ! Como no queremos que nos pillen con eso (que me conozco al cliente, que cambia de opinión constantemente) volvemos a «mejorar» el código:

public class AuditorTransferenciasMonetarias {
  // ...
  private SistemaMensajeriaCorporativa mensajero;

  public void transferenciaRealizada(Transferencia transferencia) {
    if(transferencia.esImportante()) {
      this.mensajero
            .enviar(
              new Mensaje()
                   .a(this.interesadosEnTransferenciasImportantes())
                   .conContenido(this.componerMensajeAviso(transferencia))
      );
    }
  }
  // ....
}

Esta vez hemos introducido el sistema de mensajería corporativa, que nos permite enviar un mensaje de forma abstracta a varios empleados. Ya está, ya no hay más motivos de cambio… no perdón nos queda otro motivo y gordo. Hablando con nuestro amigo el «friki», nos cuenta que el framework Zprin ya no es el acabose y que ha salido el framework XprinGio, y que a Zprin se queda sin soporte en menos de dos años. Nos olemos la tostada, en cuanto se entere el arquitecto jefe del temita nos manda cambiar de framework, o peor, si se entera el jefe de proyecto que sólo nos quedan dos años de soporte, toma migración. Hay que protegerse a toda costa de tan temida migración:

public class AuditorTransferenciasMonetarias {
  private DirectorioEmpleados directorioEmpleados;
  private SistemaMensajeriaCorporativa mensajero;
  private PlantillasCorporativas almacenDePlantillas;

  public void transferenciaRealizada(Transferencia transferencia) {
    if(transferencia.esImportante()) {
      this.mensajero
            .enviar(
              new Mensaje()
                   .a(this.interesadosEnTransferenciasImportantes()
                   .conContenido(this.componerMensajeAviso(transferencia))
      );
    }
  }
  private Documento componerMensajeAviso(Transferencia transferencia) {
    return this.almacenDePlantillas.usandoPlantilla("avisos/transferencia-importante").crearDocumentoCon(transferencia);
  }
  private List<Empleado> interesadosEnTransferenciasImportantes() {
    return this.directorioEmpleados.empleadosConRol(Empleado.INTERESADOS_TRANSFERENCIAS_IMPORTANTES);
  }
}

Esta vez hemos eliminado todo dependencia con Zprin, y tenemos un motor de plantillas abstracto. Obviamente cuando implementemos el motor de plantillas, o el sistema de mensajería nos acoplaremos a Zprin, pero en una posible migración, sólo tendremos que cambiar estos servicios y no todas las clases de negocio. Otro cambio es que ahora la plantilla genera un Documento en vez de un String. Además la clase Documento sabrá serializarse en cualquier formato que se necesite por parte del mecanismo de mensajería (String, XML, HTML, JSON, PDF…) lo que permite tener sistemas de mensajería más complejos.

Como se observa, si seguimos el principio de única responsabilidad nuestros artefactos tendrán una alta cohesión y por lo general un bajo acoplamiento, ya que quedan pocos métodos muy relacionados entre si y muy cortos. Pero lo mejor es que se tenderá a que la tasa de cambios de un único artefacto baje, y que éstos cambios sean más localizados, ya que sólo se va a necesitar modificarlo ante un único tipo de cambio al existir una relación uno a uno entre responsabilidad y clase. Obviamente este era un ejemplo de código algo artificial, y seguro que le sacais pegas, pero creo que podéis captar la idea.

Pero no es oro todo lo que reluce, más adelante en esta serie veremos el principio DRY y el «experto en información» y como ambos se terminan pegando con el principio de única responsabilidad, con lo que se produce un conflicto grave. Para colmo, el principio de segregación de interfaces nos puede complicar también la vida. Armonizar todos estos principios, es el caballo de batalla del diseño orientado a objetos. De hecho, respetar cada principio por separado es sencillo, lo difícil es juntarlos todos, ya que veréis que es como si unos principios contradijeran a otros ¿Terminará siendo la OO en realidad un paradigma fracasado y contradictorio?

Read Full Post »