Clean Architecture - Code Smells. Parte 2

Este artículo es la segunda parte de una serie que sobre code smells al implementar Clean Architecture que me he ido encontrando a lo largo del tiempo en primera persona o colaborando con otros equipos.

En la primera parte de la serie repasamos principalmente 3 problemas:

  • No alislar bien el dominio
  • Mala definición de los casos de uso
  • Devolver datos primitivos desde los repositorios

En esta segunda parte vamos a centrarnos en las entidades y los distintos tipos de reglas de negocio.

Entidades de dominio anémicas

El concepto de modelo de dominio anémico fue introducido por Martin fowler en su artículo AnemicDomainModel.

Una entidad de dominio se considera anémica cuando solo contiene datos y no contiene comportamiento.

Es un antipatrón que no es característico exclusivamente de Clean Architecture, sino de cuando se empieza a trabajar con arquitecturas orientadas al dominio en general.

Veamos un ejemplo de entidad anémica:

public class SupportTicket {
    private String reportBy;
    private String problem;
    private Status status;
    private Datetime date;

    public String getReportBy() {
        return date;
    }

    public void setReportBy(String reportBy) {
        this.reportBy = reportBy;
    }
    public String getProblem() {
        return problem;
    }

    public void setProblem(String problem) {
        this.problem = problem;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public Datetime getDate() {
        return date;
    }

    public void setDate(Datetime date) {
        this.date = date;
    }

}

Es un ejemplo muy simplificado de un entidad de dominio que representa un ticket de soporte.

Las características que tiene para que se considere anémica son las siguientes:

  • Por defecto todos los campos tienen getter y setter.
  • No existe ningún método que realice ningún tipo de lógica de negocio.
  • No existe constructor que encapsule que campos son obligatorios o asigne valores por defecto.
  • Depende de clases o componentes externos que no entre en un estado inconsistente.

La carencia de reglas de negocio en una entidad anémica provoca una serie de problemas:

  • Las reglas de negocio, que deben existir, se encuentran alojadas en otras clases como los casos de uso o incluso fuera del dominio.
  • Es común que debido a este problema se acaben duplicando dichas reglas de negocio.
  • Se complica el testing.
  • Se complican futuros refactors.

Tiene más sentido que las reglas de negocio empresarial esten ubicadas en las entidades de dominio.

Vamos a ver como podría ser una solución para que nuestra entidad deje de ser anémica.


public class SupportTicket {
    private String reportBy;
    private String problem;
    private Status status;
    private Date date;

    public SupportTicket (String reportBy, String problem ){
       this.reportBy = required(reportBy, "reportBy is required");
       this.problem = required(problem, "problem is required");   
       
       this.status = Status.OPEN;
       this.date = new Date();
    }

    public String getReportBy() {
        return reportBy;
    }

    public String getProblem() {
        return problem;
    }

    public int getStatus() {
        return status;
    }

    public Date getDate() {
        return date;
    }

    public changeProblem(String problem){
       if (this.status = Status.REPORTED)
          throw new ModifyReportedSupportTicketException ();
      
       this.problem = required(problem, "problem is required"); 
        
       this.date = new Date();

       this.status = Status.OPEN;
    }

    public void markAsReported(){
       this.status = Status.REPORTED;
    }
}

La idea es tener todas las reglas de negocio empresarial relacionadas con ticket de soporte dentro de la entidad de dominio que lo representa.

Lo primero sería tener un constructor donde proteger nuestras invariantes, es decir, validar campos obligatorios o asignar valores por defecto.

Solo debemos poder obtener información desde fuera de nuestra entidad de aquello que tenga sentido y solo deberíamos permitir editar equello que este relacionado con acciones que pueda realizar el usuario.

Por este motivo no tiene sentido tener setters públicos para cada campo sino métodos relacionados con las acciones que se pueden realizar.

En nuestro ejemplo un usuario no debería poder modificar la fecha del ticket una vez creado o el campo estado directamente porque debería ser open por defecto, si exponemos ese campo desde fuera nos podrían pasar un estado inconsistente.

El valor de los campos fecha y estado, en nuestro ejemplo, son una consecuencia de acciones sobre la entidad, como es su creación o la modificación del texto del problema a reportar, por lo tanto no deberían ser editables desde fuera mediante un setter público sino solo internamente mejorando la consistencia de nuestra entidad.

En este caso desde el método changeProblem se modifica más de un campo de la entidad como son el texto del problema, la fecha y el estado del ticket.

Además en caso de ser un ticket reportado, el método changeProblem lanza una exceción indicando que no se permite esta acción cuando el estado es reportado.

Lo que suele suceder cuando tenemos entidades anémicas es que esto nos lleva a comenter otros code smells.

Mezclar reglas de negocio empresarial y de aplicación

Cuando trabajamos con Clean Architecture hablamos de dos tipos de reglas de negocio.

  • Reglas de Negocio empresarial, son reglas de negocio que operan sobre los datos del dominio y por lo tanto deben estar ubicadas junto a los datos de dominio, es decir, en las entidades. Esto nos proporciona además los beneficios que ya hemos visto anteriormente en el ejemplo de entidades anémicas.

  • Reglas de Negocio de aplicación, son reglas que de negocio que están relacionadas con el proceso de automatización de una aplicación. Este tipo de reglas se encuentran ubicadas en los casos de uso, que son las clases que coordinan las acciones de los casos de uso del sistema.

Rescatemos un ejemplo conocido del artículo de la primera parte de la serie para entender mejor los que son las reglas de negocio de aplicación:

En Spotify existe una preferencia de usuario "Offline mode" que si esta activada no se puede escuchar ni buscar música online sin wifi, en esas circunstancias solo funciona con datos locales.

Cuando realizamos una búsqueda, tendremos un caso de uso SearchUseCase, este caso de uso necesita saber si la preferencia "Offline mode" esta activada y si hay conexión para realizar la búsqueda llamando a una API o a la base de datos local.

En este caso de uso de ejemplo, necesitamos coordinar una serie de validaciones previas, relacionadas con la automatización del proceso, para saber donde hay que realizar la búsqueda. Por lo tanto estamos hablando de reglas de negocio de aplicación.

public class SearchUseCase ... {  

   ...

   public void execute(String searchText){

      Settings settings = settingsRepository.getSettings();
      
      if (connectivity.getNetworkType() != TYPE_WIFI && settings.isOfflineMode){
         List<Song> results = songRepository.get(policy.CACHE_FIRST, searchText);
      } else {
         List<Song> results = songRepository.get(policy.NETWORK_FIRST, searchText);
      }  

      ...  
   }
}

En ocasiones cuando tenemos entidades de dominio anémicas acabamos mezclando reglas de negocio de aplicación y empresarial.

Esto nos puede lleva a dos tipos de problemas:

  • Duplicación de las reglas empresariales en diferentes casos de uso, en una aplicación es normal trabajar con el mismo concepto de dominio, como por ejemplo un email, usuario, pedido en diferentes casos de uso como crear, guardar, realizar pago. Si no ubicamos las reglas de regocio empresarial correctamente en entidades, es una posibilidad que acabemos duplicándolas incluyendo futuros dolores de cabeza en nuestro código.

  • Invocar un caso de uso desde otro caso de uso, si ubicamos las reglas de negocio empresarial en los casos de uso y somos conscientes de que no queremos duplicarlas, lo que suele ocurrir es que acabaremos invocando un caso de uso desde otro. Esto nos añade un acoplammiento que puede ser un problema si los casos de uso evolucionan de forma independiente y no compatible como suele ser habitual.

Conclusiones

Empezar a utilizar Clean Architecture como otras arquitecturas orientadas al dominio no es fácil y se cometen errores.

En esta segunda entrega de la serie nos hemos centrado en problemas que pueden surgir al diseñar nuestras entidades de dominio y hemos aprendido a identificar los diferentes tipos de reglas de negocio que existen y a no mezclarlas.

Recursos relacionados