Saltar al contenido principal

Capítulo 1: La Capa Service en Detalle

Introducción​

La capa Service es el elemento central de la arquitectura de 3 capas. Hace el enlace entre el controlador (que gestiona las solicitudes HTTP) y el repository (que accede a los datos). Aquí es donde reside la lógica de negocio de su aplicación.

Objetivos del Capítulo​

Al final de este capítulo, usted será capaz de:

  • Comprender el rol y las responsabilidades de la capa Service
  • Separar correctamente las preocupaciones entre las capas
  • Implementar una capa Service profesional
  • Utilizar la inyección de dependencias correctamente
  • Aplicar buenas prácticas

Arquitectura de 3 Capas​

Visión General​

Responsabilidades de Cada Capa​

CapaResponsabilidadesEjemplos
ControllerRecepción HTTP, Validación, Conversión, Respuestas@RestController, @GetMapping
ServiceLógica de negocio, Orquestación, Transacciones, Validación de reglas@Service, Cálculos
RepositoryAcceso a datos, Consultas BD, Mapeo de entidades@Repository, JpaRepository

¿Qué es la Lógica de Negocio?​

Definición​

La lógica de negocio (business logic) es el conjunto de reglas y procesos que definen el comportamiento de su aplicación.

Ejemplos de Lógica de Negocio​

❌ NO es lógica de negocio:

// Esto es acceso a datos
productRepository.findById(id);

// Esto es gestión HTTP
return ResponseEntity.ok(product);

✅ Lógica de negocio:

// Verificar el stock antes de crear una orden
if (product.getStock() < order.getQuantity()) {
throw new InsufficientStockException();
}

// Calcular el precio total con impuestos
double totalPrice = order.getSubtotal() * (1 + TAX_RATE);

// Aplicar una promoción
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9; // 10% de descuento
}

Creación de una Capa Service​

Ejemplo Básico​

package com.example.demo.service;

import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

private final ProductRepository productRepository;

// Inyección por constructor (RECOMENDADO)
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

// CREATE
public Product createProduct(Product product) {
// Lógica de negocio: validación
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}

// Llamada al repository
return productRepository.save(product);
}

// READ - Todos
public List<Product> getAllProducts() {
return productRepository.findAll();
}

// READ - Por ID
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found with id: " + id));
}

// UPDATE
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);

// Lógica de negocio: validación
if (productDetails.getPrice() < 0) {
throw new IllegalArgumentException("Le prix ne peut pas être négatif");
}

// Actualización de campos
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());

return productRepository.save(product);
}

// DELETE
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}

Puntos Clave​

  1. @Service: Marca la clase como una capa Service
  2. Inyección por constructor: Más limpio y testeable que @Autowired en los campos
  3. Validación de negocio: Verificación de reglas antes del acceso a datos
  4. Gestión de errores: Lanzar excepciones apropiadas

Inyección de Dependencias​

Tres Métodos de Inyección​

1. Inyección por Constructor (RECOMENDADA)​

@Service
public class ProductService {

private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}

Ventajas:

  • ✅ Inmutabilidad (final)
  • ✅ Fácil de testear
  • ✅ Obligatoria (error de compilación si falta)
  • ✅ Recomendado por Spring

2. Inyección por Campo (NO RECOMENDADA)​

@Service
public class ProductService {

@Autowired
private ProductRepository productRepository;
}

Desventajas:

  • ❌ Difícil de testear
  • ❌ Dependencias ocultas
  • ❌ No es inmutable

3. Inyección por Setter (RARAMENTE USADA)​

@Service
public class ProductService {

private ProductRepository productRepository;

@Autowired
public void setProductRepository(ProductRepository productRepository) {
this.productRepository = productRepository;
}
}

Separación de Preocupaciones​

Principio SOLID: Responsabilidad Única​

Cada capa debe tener UNA sola responsabilidad.

❌ Mal Ejemplo: Todo en el Controller​

@RestController
@RequestMapping("/api/products")
public class ProductController {

@Autowired
private ProductRepository productRepository;

@PostMapping
public Product create(@RequestBody Product product) {
// Lógica de negocio en el controller (¡MAL!)
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}

// Acceso directo al repository (¡MAL!)
return productRepository.save(product);
}
}

Problemas:

  • ❌ Lógica de negocio en el controller
  • ❌ Difícil de testear
  • ❌ Difícil de reutilizar
  • ❌ Violación del principio de responsabilidad única

✅ Buen Ejemplo: Separación Clara​

// Controller: Solo gestión HTTP
@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

@PostMapping
public Product create(@RequestBody Product product) {
return productService.createProduct(product);
}
}

// Service: Lógica de negocio
@Service
public class ProductService {

private final ProductRepository productRepository;

public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

public Product createProduct(Product product) {
// Validación de negocio
if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}

// Llamada al repository
return productRepository.save(product);
}
}

// Repository: Acceso a datos
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring genera automáticamente la implementación
}

Ejemplo Avanzado: Orquestación​

La capa Service puede coordinar varios repositories:

@Service
public class OrderService {

private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final CustomerRepository customerRepository;

public OrderService(OrderRepository orderRepository,
ProductRepository productRepository,
CustomerRepository customerRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.customerRepository = customerRepository;
}

public Order createOrder(Long customerId, Long productId, int quantity) {
// 1. Verificar que el cliente existe
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new RuntimeException("Customer not found"));

// 2. Verificar que el producto existe
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));

// 3. Verificar el stock
if (product.getStock() < quantity) {
throw new InsufficientStockException("Stock insuffisant");
}

// 4. Calcular el precio total
double totalPrice = product.getPrice() * quantity;

// 5. Aplicar un descuento si el cliente es premium
if (customer.isPremium()) {
totalPrice = totalPrice * 0.9;
}

// 6. Crear la orden
Order order = new Order();
order.setCustomer(customer);
order.setProduct(product);
order.setQuantity(quantity);
order.setTotalPrice(totalPrice);

// 7. Disminuir el stock
product.setStock(product.getStock() - quantity);
productRepository.save(product);

// 8. Guardar la orden
return orderRepository.save(order);
}
}

Gestión de Transacciones​

Anotación @Transactional​

@Service
public class OrderService {

@Transactional
public Order createOrder(Long customerId, Long productId, int quantity) {
// Todas las operaciones están en una transacción
// Si se lanza una excepción, todo se revierte (rollback)

Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));

product.setStock(product.getStock() - quantity);
productRepository.save(product);

Order order = new Order();
// ... creación de la orden
return orderRepository.save(order);
}
}

Ventajas:

  • ✅ Consistencia de los datos
  • ✅ Rollback automático en caso de error
  • ✅ ACID (Atomicidad, Consistencia, Aislamiento, Durabilidad)

Buenas Prácticas​

DO (A Hacer)​

✅ Inyección por constructor

public ProductService(ProductRepository repository) {
this.repository = repository;
}

✅ Métodos con nombres explícitos

public Product createProduct(Product product)
public void activateUser(Long userId)
public double calculateTotalPrice(Order order)

✅ Validación de negocio en el Service

if (product.getPrice() < 0) {
throw new IllegalArgumentException("Prix invalide");
}

✅ Gestión de excepciones apropiadas

return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));

DON'T (A Evitar)​

❌ Lógica de negocio en el Controller

// MAL: En el controller
@PostMapping
public Product create(@RequestBody Product product) {
if (product.getPrice() < 0) { // ¡Lógica de negocio!
throw new Exception();
}
return service.create(product);
}

❌ Acceso directo al Repository desde el Controller

// MAL
@RestController
public class ProductController {
@Autowired
private ProductRepository repository; // ¡Directo!
}

❌ Inyección por campo

// MAL
@Autowired
private ProductRepository repository;

Ejercicios Prácticos​

Ejercicio 1: Service Básico​

Objetivo: Crear una capa Service completa

Instrucciones:

  1. Cree una entidad Student:
    • Long id
    • String firstName
    • String lastName
    • String email
    • int grade
  2. Cree StudentRepository
  3. Cree StudentService con:
    • Todas las operaciones CRUD
    • Validación: grade entre 0 y 100
    • Validación: email válido
  4. Cree StudentController que utilice el service

Ejercicio 2: Orquestación​

Objetivo: Coordinar varios repositories

Instrucciones:

  1. Cree Library (id, name, address)
  2. Cree Book (id, title, author, libraryId)
  3. Cree LibraryService con un método:
    public Library createLibraryWithBooks(Library library, List<Book> books)
    • Crear la biblioteca
    • Crear todos los libros asociados
    • Todo dentro de una transacción

Resumen del Capítulo​

Puntos Clave​

  • La capa Service contiene la lógica de negocio
  • Separación de preocupaciones: Controller → Service → Repository
  • Inyección por constructor (recomendada)
  • @Transactional para la consistencia de los datos
  • Validación en el Service, no en el Controller

Lo que Usted Sabe Ahora​

✅ Rol y responsabilidades de la capa Service
✅ Separación de preocupaciones
✅ Inyección de dependencias
✅ Orquestación de varios repositories
✅ Buenas prácticas profesionales


Capítulo siguiente: Validación de Datos

Volver al Módulo 3