Clean architecture

Siguiendo con artículos sobre lecturas que he realizado últimamente, hoy voy a presentar un ejemplo de código basado en la lectura de un estupendo libro del popular Uncle Bob (Robert C. Martin). Este libro no es ni más ni menos que Clean Architecture, obra donde su autor nos propone una arquitectura que bebe de otras arquitecturas que han surgido en los últimos años y cuya lectura resulta muy amena, ya que utiliza un estilo que recuerda más a una novela que a un texto puramente técnico.

No voy a resumir el libro, el cual desglosa un montón de buenas prácticas ilustrándolas con ejemplos de vivencias personales del autor, que lleva desde los años 70 desarrollando código. Invito a quien quiera profundizar en Clean Architecture a que lea el libro para tener una idea más  detallada de la propuesta de Uncle Bob. Simplemente me voy a limitar a explicar de manera breve las capas en las que se basa la arquitectura y a mostrar mi implementación de ejemplo. Estas capas son:

  • Capa de entidad: las entidades contienen las reglas puras de negocio.
  • Capa de casos de uso: los casos de uso contienen las reglas de negocio concreta para la aplicación.
  • Capa de adaptadores de interfaz: esta capa se encarga de transformar los datos que provienen de dependencias externas como bases de datos y Frameworks web a la manera más conveniente para las entidades y casos de uso.
  • Capa de Frameworks y drivers: capa que contiene las dependencias externas y donde escribimos muy poco código.

Entre estas capas hay una regla de oro que consiste en que no hay dependencias de las capas superiores en las inferiores, por ejemplo la capa de entidad no puede tener dependencia de la capa de casos de uso pero si lo contrario. De esta manera podemos tener la lógica desacoplado entre capas y podemos probarla de manera independiente, creando mocks de las dependencias. Si una capa superior necesita de una dependencia de una capa inferior se define un interfaz para obtenerla de manera que quede completamente desacoplada.

Para realizar un ejemplo sobre lo que yo he entendido de esta arquitectura (como yo la he asimilado, no digo que sea una implementación perfecta de lo que tenía en mente Uncle Bob), he elegido un sencillo desarrollo que consiste en el verbo POST de un CRUD de API Rest de una entidad extremadamente sencilla de usuario. El código está escrito en Java 8 con Sprint Boot.  Voy a explicar cómo he realizado el código que podéis encontrar en mi github.

    • Primero he creado la entidad User que está en el paquete entity. La entidad es muy sencilla para no complicar el ejemplo :
package com.coconutcode.user.entity;

import lombok.Getter;

import java.util.Optional;

@Getter
public class User {
    private String username;

    public User(String username) throws UsernameNotIncluded {
        if(Optional.ofNullable(username).isPresent()) {
            this.username = username;
        } else {
            throw new UsernameNotIncluded();
        }
    }
}

    • Después la clase que contiene el caso de uso de crear un usuario en el paquete usecases, un interfaz para definir la dependencia que realizará la persistencia del nuevo usuario (CreateUser) y otro para buscar si un usuario ya existe con ese nombre (GetUser) Nótese que al crear estos interfaces desacoplamos el caso de uso de la capa de persistencia, ya que en esta capa no hay ningún tipo de detalle de cómo se realizarán las consultas a la base de datos o con que Framework.
package com.coconutcode.user.usecases;

import com.coconutcode.user.entity.User;
import lombok.AllArgsConstructor;
import java.util.Optional;

@AllArgsConstructor
public class CreateUserUseCase {
    private CreateUser createUser;

    private GetUser getUser;

    public User createUser(User user) throws MandatoryValueNotIncludedException {
        if(userIncluded(user)) {
            if(usernameAlreadyUsed(user)) {
                throw new MandatoryValueNotIncludedException("Username already exists: " + user.getUsername());
            } else {
                return createUser.createUser(user);
            }
        } else {
            throw new MandatoryValueNotIncludedException("User not included");
        }
    }

    private boolean usernameAlreadyUsed(User user) {
        return getUser.getUser(user.getUsername()).isPresent();
    }

    private boolean userIncluded(User user) {
        return Optional.ofNullable(user).isPresent();
    }
}

    • Después la clase que hace de adaptador con el Framework de base de datos que contiene el detalle sobre cómo persistir los datos (implementa los interfaces CreateUser y GetUser) y la clase que hace de adaptador con el Framework web que tiene el detalle de cómo presentar la información vía API Rest. Todo esto está en el paquete adapter:

package com.coconutcode.user.adapter.persistence;

import com.coconutcode.user.entity.User;
import com.coconutcode.user.usecases.CreateUser;
import com.coconutcode.user.usecases.GetUser;
import com.coconutcode.user.external.database.UserRepository;
import lombok.val;

import java.util.Optional;

public class UserPersistenceAdapter implements CreateUser, GetUser {
    private final UserRepository userRepository;

    public UserPersistenceAdapter(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public User createUser(User user) {
        val newUser = userRepository.save(new com.coconutcode.user.adapter.persistence.model.UserData(user.getUsername()));
        return new User(newUser.getUsername());
    }

    @Override
    public Optional getUser(String username) {
        val optionalUser = userRepository.findByUsername(username);
        return optionalUser.map(user -> new User(user.getUsername()));
    }
}
package com.coconutcode.user.adapter.presenter;

import com.coconutcode.user.entity.User;
import com.coconutcode.user.usecases.CreateUserUseCase;
import com.coconutcode.user.adapter.presenter.model.UserView;

public class UserPresenterAdapter {
    private final CreateUserUseCase createUserUseCase;

    public UserPresenterAdapter(CreateUserUseCase createUserUseCase){
        this.createUserUseCase = createUserUseCase;
    }

    public UserView createUser(UserView user) {
        return new UserView(createUserUseCase.createUser(new User(user.getUsername())));
    }
}

    • Por último en la capa de framework y drivers cuyo paquete he llamado external que contiene los detalles concretos de los frameworks. Al tener desacoplados los detalles de implementación podriamos cambiar por ejemplo el tipo de base de datos que se utiliza sin afectar a los casos de uso y a las entidades:
package com.coconutcode.user.external.database;

import com.coconutcode.user.adapter.persistence.model.UserData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public interface UserRepository extends JpaRepository{
    Optional findByUsername(String username);
}

package com.coconutcode.user.external.rest;

import com.coconutcode.user.adapter.presenter.UserPresenterAdapter;
import com.coconutcode.user.adapter.presenter.model.UserView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserRestController {
    private static final String USER_PATH = "/user";

    @Autowired
    private UserPresenterAdapter userPresenterAdapter;

    @RequestMapping(value = USER_PATH, method = RequestMethod.POST)
    public ResponseEntity create(@RequestBody UserView user) {
        try {
            return createOkResponse(userPresenterAdapter.createUser(user));
        } catch (Exception exception){
            return createBadRequestResponse(exception);
        }
    }

    private ResponseEntity createBadRequestResponse(Exception exception) {
        return new ResponseEntity(exception.getMessage(), HttpStatus.BAD_REQUEST);
    }

    private ResponseEntity createOkResponse(@RequestBody UserView userView) {
        return new ResponseEntity(userView, HttpStatus.OK);
    }
}

Como podéis ver en github cada capa tiene pruebas unitarias donde las otras capas están mockeadas. Se podrían añadir más casos de uso sin necesidad de modificar el existente y diferentes adaptadores para cubrir otros requisitos funcionales sin afectar a casos de uso y entidades que contienen la lógica de negocio importante.

Resumiendo, en mi opinión, esta arquitectura nos permite realizar una organización mucho más racional y siguiéndola conseguimos un acoplamiento bajo entre paquetes, lo que es interesante porque generalmente estamos centrados en tener acoplamiento bajo a nivel de clase pero descuidamos la organización del código a un nivel superior. También creo que esta arqutectura asi como otras que son populadres en los últimos años (DDD, hexagonal, etc) pone el foco en dejar el modelo libre de dependencias externas de tal manera que pueda contener la lógica importante para la aplicación y pueda ser probado bien unitariamente.

Deja un comentario