21. Buenas Prácticas y Arquitectura
🏛️ 21.1 Arquitectura en Capas
Section titled “🏛️ 21.1 Arquitectura en Capas”Estructura de capas tradicional
Section titled “Estructura de capas tradicional”src/main/java/com/miapp/├── controller/ # Capa de presentación (REST)│ └── ProductoController.java├── service/ # Capa de lógica de negocio│ ├── ProductoService.java│ └── impl/│ └── ProductoServiceImpl.java├── repository/ # Capa de acceso a datos│ └── ProductoRepository.java├── entity/ # Entidades JPA│ └── Producto.java├── dto/ # Objetos de transferencia│ ├── ProductoDTO.java│ └── ProductoCreateDTO.java├── mapper/ # Conversión entity <-> DTO│ └── ProductoMapper.java├── exception/ # Excepciones personalizadas│ └── ResourceNotFoundException.java└── config/ # Configuraciones └── SecurityConfig.javaFlujo de datos
Section titled “Flujo de datos”┌─────────────────────────────────────────────────────────────┐│ Cliente │└─────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Controller (REST) ││ - Recibe requests HTTP ││ - Valida entrada (@Valid) ││ - Retorna ResponseEntity │└─────────────────────────────────────────────────────────────┘ │ DTO ▼┌─────────────────────────────────────────────────────────────┐│ Service (Negocio) ││ - Lógica de negocio ││ - Transacciones (@Transactional) ││ - Orquestación │└─────────────────────────────────────────────────────────────┘ │ Entity ▼┌─────────────────────────────────────────────────────────────┐│ Repository (Datos) ││ - Acceso a base de datos ││ - Queries JPA/JPQL │└─────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Base de Datos │└─────────────────────────────────────────────────────────────┘🧱 21.2 Principios SOLID
Section titled “🧱 21.2 Principios SOLID”Single Responsibility (SRP)
Section titled “Single Responsibility (SRP)”// ❌ MAL: Clase con múltiples responsabilidades@Servicepublic class ProductoService { public Producto crear(ProductoDTO dto) { /* ... */ } public void enviarEmail(String email) { /* ... */ } // No pertenece aquí public void generarPDF(Producto p) { /* ... */ } // No pertenece aquí}
// ✅ BIEN: Una responsabilidad por clase@Servicepublic class ProductoService { public Producto crear(ProductoDTO dto) { /* ... */ }}
@Servicepublic class EmailService { public void enviar(String email, String mensaje) { /* ... */ }}
@Servicepublic class ReporteService { public byte[] generarPDF(Producto p) { /* ... */ }}Open/Closed (OCP)
Section titled “Open/Closed (OCP)”// ✅ Abierto para extensión, cerrado para modificaciónpublic interface DescuentoStrategy { BigDecimal calcular(Pedido pedido);}
@Componentpublic class DescuentoPorcentaje implements DescuentoStrategy { @Override public BigDecimal calcular(Pedido pedido) { return pedido.getTotal().multiply(new BigDecimal("0.10")); }}
@Componentpublic class DescuentoFijo implements DescuentoStrategy { @Override public BigDecimal calcular(Pedido pedido) { return new BigDecimal("50.00"); }}
// Agregar nuevo descuento sin modificar código existente@Componentpublic class DescuentoVIP implements DescuentoStrategy { @Override public BigDecimal calcular(Pedido pedido) { return pedido.getTotal().multiply(new BigDecimal("0.20")); }}Dependency Inversion (DIP)
Section titled “Dependency Inversion (DIP)”// ❌ MAL: Dependencia de implementación concreta@Servicepublic class PedidoService { private final MySQLPedidoRepository repository; // Acoplado a MySQL}
// ✅ BIEN: Dependencia de abstracción@Servicepublic class PedidoService { private final PedidoRepository repository; // Interfaz
public PedidoService(PedidoRepository repository) { this.repository = repository; }}
public interface PedidoRepository extends JpaRepository<Pedido, Long> { // Spring inyecta la implementación automáticamente}🧹 21.3 Clean Architecture
Section titled “🧹 21.3 Clean Architecture”Estructura hexagonal
Section titled “Estructura hexagonal”src/main/java/com/miapp/├── domain/ # Núcleo (sin dependencias externas)│ ├── model/│ │ └── Producto.java # Entidad de dominio│ ├── port/│ │ ├── in/ # Puertos de entrada│ │ │ └── CrearProductoUseCase.java│ │ └── out/ # Puertos de salida│ │ └── ProductoRepository.java│ └── service/│ └── ProductoService.java # Implementa casos de uso│├── application/ # Casos de uso│ └── usecase/│ └── CrearProductoUseCaseImpl.java│├── infrastructure/ # Adaptadores externos│ ├── adapter/│ │ ├── in/│ │ │ └── rest/│ │ │ └── ProductoController.java│ │ └── out/│ │ └── persistence/│ │ ├── ProductoJpaRepository.java│ │ └── ProductoEntity.java│ └── config/│ └── BeanConfig.javaPuerto de entrada (Use Case)
Section titled “Puerto de entrada (Use Case)”// Puerto (interfaz)public interface CrearProductoUseCase { ProductoResponse ejecutar(CrearProductoCommand command);}
// Comandopublic record CrearProductoCommand( String nombre, BigDecimal precio, String categoria) {}
// Respuestapublic record ProductoResponse( Long id, String nombre, BigDecimal precio) {}
// Implementación@Service@RequiredArgsConstructorpublic class CrearProductoUseCaseImpl implements CrearProductoUseCase {
private final ProductoRepository repository;
@Override @Transactional public ProductoResponse ejecutar(CrearProductoCommand command) { Producto producto = Producto.crear( command.nombre(), command.precio(), command.categoria() );
Producto guardado = repository.guardar(producto);
return new ProductoResponse( guardado.getId(), guardado.getNombre(), guardado.getPrecio() ); }}📦 21.4 DTOs y Mappers
Section titled “📦 21.4 DTOs y Mappers”DTOs por operación
Section titled “DTOs por operación”// DTO para crearpublic record ProductoCreateDTO( @NotBlank String nombre, @NotNull @Positive BigDecimal precio, @NotNull Categoria categoria) {}
// DTO para actualizarpublic record ProductoUpdateDTO( @NotBlank String nombre, @NotNull @Positive BigDecimal precio) {}
// DTO para respuestapublic record ProductoDTO( Long id, String nombre, BigDecimal precio, String categoria, LocalDateTime createdAt) {}
// DTO para lista (menos campos)public record ProductoResumenDTO( Long id, String nombre, BigDecimal precio) {}MapStruct
Section titled “MapStruct”<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.5.Final</version></dependency><dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.5.Final</version> <scope>provided</scope></dependency>@Mapper(componentModel = "spring")public interface ProductoMapper {
ProductoDTO toDTO(Producto entity);
List<ProductoDTO> toDTOList(List<Producto> entities);
@Mapping(target = "id", ignore = true) @Mapping(target = "createdAt", ignore = true) Producto toEntity(ProductoCreateDTO dto);
@Mapping(target = "id", ignore = true) @Mapping(target = "categoria", ignore = true) void updateEntity(ProductoUpdateDTO dto, @MappingTarget Producto entity);
// Mapeo personalizado @Mapping(target = "categoriaName", source = "categoria.nombre") @Mapping(target = "precioFormateado", expression = "java(formatearPrecio(entity.getPrecio()))") ProductoDetalleDTO toDetalleDTO(Producto entity);
default String formatearPrecio(BigDecimal precio) { return "$" + precio.setScale(2, RoundingMode.HALF_UP); }}🔧 21.5 Patrones de Diseño
Section titled “🔧 21.5 Patrones de Diseño”Repository Pattern
Section titled “Repository Pattern”// Interfaz de dominiopublic interface ProductoRepository { Producto guardar(Producto producto); Optional<Producto> buscarPorId(Long id); List<Producto> buscarPorCategoria(Categoria categoria); void eliminar(Long id);}
// Implementación con JPA@Repository@RequiredArgsConstructorpublic class ProductoRepositoryImpl implements ProductoRepository {
private final ProductoJpaRepository jpaRepository; private final ProductoEntityMapper mapper;
@Override public Producto guardar(Producto producto) { ProductoEntity entity = mapper.toEntity(producto); ProductoEntity saved = jpaRepository.save(entity); return mapper.toDomain(saved); }
@Override public Optional<Producto> buscarPorId(Long id) { return jpaRepository.findById(id) .map(mapper::toDomain); }}Factory Pattern
Section titled “Factory Pattern”public interface NotificacionFactory { Notificacion crear(TipoNotificacion tipo, String mensaje);}
@Componentpublic class NotificacionFactoryImpl implements NotificacionFactory {
private final Map<TipoNotificacion, NotificacionStrategy> strategies;
public NotificacionFactoryImpl(List<NotificacionStrategy> strategies) { this.strategies = strategies.stream() .collect(Collectors.toMap( NotificacionStrategy::getTipo, Function.identity() )); }
@Override public Notificacion crear(TipoNotificacion tipo, String mensaje) { NotificacionStrategy strategy = strategies.get(tipo); if (strategy == null) { throw new IllegalArgumentException("Tipo no soportado: " + tipo); } return strategy.crear(mensaje); }}Builder Pattern
Section titled “Builder Pattern”@Builder@Getterpublic class Pedido { private final Long id; private final Usuario usuario; private final List<LineaPedido> lineas; private final BigDecimal total; private final Estado estado; private final LocalDateTime fechaCreacion;
public static PedidoBuilder nuevo(Usuario usuario) { return Pedido.builder() .usuario(usuario) .lineas(new ArrayList<>()) .estado(Estado.PENDIENTE) .fechaCreacion(LocalDateTime.now()); }}
// UsoPedido pedido = Pedido.nuevo(usuario) .lineas(lineas) .total(calcularTotal(lineas)) .build();📋 21.6 Convenciones de Código
Section titled “📋 21.6 Convenciones de Código”Nombrado
Section titled “Nombrado”// Clases: PascalCasepublic class ProductoService { }public class UsuarioController { }
// Métodos: camelCase, verbospublic Producto crear(ProductoDTO dto) { }public List<Producto> listarPorCategoria(String cat) { }public void eliminar(Long id) { }public boolean existePorEmail(String email) { }
// Variables: camelCaseprivate final ProductoRepository productoRepository;private String nombreCompleto;
// Constantes: UPPER_SNAKE_CASEpublic static final int MAX_INTENTOS = 3;public static final String ROLE_ADMIN = "ROLE_ADMIN";
// Paquetes: lowercasepackage com.miempresa.miapp.producto.service;Estructura de métodos
Section titled “Estructura de métodos”@Service@RequiredArgsConstructor@Slf4jpublic class ProductoService {
// 1. Dependencias (final) private final ProductoRepository repository; private final ProductoMapper mapper; private final EventPublisher eventPublisher;
// 2. Métodos públicos (API del servicio) @Transactional public ProductoDTO crear(ProductoCreateDTO dto) { log.info("Creando producto: {}", dto.nombre());
validarNombreUnico(dto.nombre());
Producto producto = mapper.toEntity(dto); Producto guardado = repository.save(producto);
eventPublisher.publish(new ProductoCreadoEvent(guardado.getId()));
return mapper.toDTO(guardado); }
@Transactional(readOnly = true) public ProductoDTO obtenerPorId(Long id) { return repository.findById(id) .map(mapper::toDTO) .orElseThrow(() -> new ResourceNotFoundException("Producto", id)); }
// 3. Métodos privados (helpers) private void validarNombreUnico(String nombre) { if (repository.existsByNombre(nombre)) { throw new DuplicateResourceException("Producto con nombre ya existe"); } }}✅ 21.7 Checklist de Calidad
Section titled “✅ 21.7 Checklist de Calidad”✅ ARQUITECTURA□ Separación clara de capas□ Dependencias unidireccionales (Controller → Service → Repository)□ Sin lógica de negocio en controllers□ Sin acceso directo a repositorios desde controllers
✅ CÓDIGO□ Principios SOLID aplicados□ Métodos pequeños (< 20 líneas)□ Nombres descriptivos□ Sin código duplicado (DRY)□ Manejo de errores consistente
✅ DTOs□ DTOs separados para entrada/salida□ Validaciones en DTOs de entrada□ Mappers para conversión□ Sin exponer entidades directamente
✅ TESTING□ Tests unitarios para servicios□ Tests de integración para repositorios□ Tests de API para controllers□ Cobertura > 80%
✅ SEGURIDAD□ Validación de entrada□ Autenticación/Autorización□ Sin datos sensibles en logs□ Manejo seguro de contraseñas
🐝