En este punto, el controlador de nuestra API empieza a hacer demasiadas cosas: recibe peticiones, extrae datos, los valida y los procesa, para finalmente montar la respuesta. Ser el encargado de recibir los datos no significa que tenga que gestionarlos ni implementar toda la lógica.

Siguiendo el Principio de Responsabilidad Única, y la Arquitectura por capas, es el momento de sacar del controlador la lógica de negocio y crear una nueva pieza con una responsabilidad más clara: un Service.

Acotando las responsabilidades del controlador

Nuestro controlador funciona, pero empieza a mezclar responsabilidades. En una API sencilla, puede no parecer un problema, pero es mejor comenzar a estructurar de forma limpia el código en este punto antes de que comience a crecer sin control.

El controlador actual es el siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RestController  
public class HelloController {  
  
    @GetMapping("/hello")  
    public ResponseEntity<HelloResponseDto> hello(@RequestParam(defaultValue = "World") String name) {  
        HelloResponseDto content = new HelloResponseDto(  
                "Hello " + name + " from Spring Boot!",  
                name,  
                Instant.now().toString()  
        );  
        return ResponseEntity.ok(content);  
    }  
  
}

Las responsabilidades de un controlador deberían ser:

  • Recibir peticiones HTTP
  • Extraer los datos
  • Delegar su tratamiento
  • Devolver una respuesta HTTP

En nuestro caso también se está encargando del procesado.

La capa Service

Una vez que el controlador ha recibido la petición y obtenido los datos, debe delegar su procesado. Los componentes encargados de gestionar la lógica de negocio y los casos de uso son los componentes de tipo Service, que se encuentran ubicados en la capa Service o Capa de negocio.

El servicio tratará los datos de la forma que marque el caso de uso y emitirá otro dato de respuesta que devolverá al controlador. De esta forma, no solo separamos código, sino que:

  • Conseguimos más claridad
  • Más facilidad para cambiar reglas
  • Mejor testabilidad
  • Tenemos una base para crecer

Controlador y servicio, trabajo en equipo

Para separar esa lógica de negocio y sacarla del controlador, necesitamos del Servicio, haciendo que se acoplen de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RestController  
public class HelloController {  
  
    private final HelloService helloService;  
  
    public HelloController(HelloService helloService) {  
        this.helloService = helloService;  
    }  
  
    @GetMapping("/hello")  
    public ResponseEntity<HelloResponseDto> hello(@RequestParam(defaultValue = "World") String name) {  
        return ResponseEntity.ok(helloService.buildHelloResponse(name));  
    }  
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Service  
public class HelloService {  
  
    public HelloResponseDto buildHelloResponse(String name) {  
        return new HelloResponseDto(  
                "Hello " + name + " from Spring Boot!",  
                name,  
                Instant.now().toString()  
        );  
    }  
}

Los que hemos hecho ha sido:

  • Declaración del HelloService en el controlador: de forma que pueda utilizarlo, pero no lo gestiona ni cambia su comportamiento (por eso se declara como final, marcando el sentido de la dependencia).
  • Inyección del HelloService por constructor: incluimos un constructor del controlador para que Spring pueda inyectar automáticamente el servicio, quitando esa responsabilidad al programador. Si una clase necesita algo para funcionar, debería declararlo en el constructor.
  • Nueva clase HelloService anotada con @Service: marca la clase para que Spring gestione su ciclo de vida y pueda inyectarse automáticamente en otros componentes. Pero no solo es una anotación técnica, también sirve para marcarlo como parte de la capa de negocio. Es una decisión de diseño.

Ahora es el Service el que se encarga de construir y devolver un objeto estructurado propio de la aplicación (HelloResponseDto). Es el que se encarga de construir la respuesta a partir de los datos.

Por otro lado, el controlador no necesita saber cómo se construye la respuesta, solo que puede pedirla e incorporarla a una respuesta HTTP bien formada. El controlador trabaja con conceptos HTTP (ResponseEntity, códigos de estado), mientras que el service trabaja con datos y lógica de la aplicación. Esto reduce el acoplamiento entre capas y permite evolucionar la lógica sin afectar al controlador.

Estructura final de paquetes

También conviene en este punto no solo organizar los componentes y sus responsabilidades, sino crear una estructura de paquetes acorde a esa estructura. El controlador quedará ubicado en un nuevo paquete controller, al igual que el servicio en el paquete service. La estructura final queda de la siguiente manera:

src/main/java/dev/juanfbermejo/SpringBootHelloWorld/
├── controller/
│   └── HelloController.java
├── dto/
│   └── HelloResponseDto.java
├── service/
│   └── HelloService.java
└── SpringBootHelloWorldApplication.java

Conclusiones

En este punto no estamos montando una arquitectura completa, pero sí dando el primer paso hacia una aplicación más mantenible. Con este cambio no solo hemos movido código de sitio, hemos empezado a separar responsabilidades de verdad: el controlador habla HTTP y delega en el service, que habla el lenguaje de la aplicación.