8. Buenas prácticas y patrones asociados

Las capas no son una garantía automática de orden. Requieren principios que las guíen y patrones que aseguren la evolución sostenible del código. En este tema reunimos prácticas aplicables al ejemplo del coworking para reforzar el diseño.

8.1 Principio de inversión de dependencias

El principio de inversión de dependencias (DIP) establece que los módulos de alto nivel no deben depender de detalles, sino de abstracciones. En una arquitectura en capas esto se traduce en que la capa de aplicación define interfaces y la capa de infraestructura provee implementaciones.

En el caso de uso ReservarSala la inversión evita que el dominio conozca el mecanismo de persistencia:

package com.example.coworking.domain.port;

import com.example.coworking.domain.model.Reserva;

public interface ReservaRepositorio {
    void guardar(Reserva reserva);
    boolean existeConflicto(String salaId, LocalDateTime desde, LocalDateTime hasta);
}

La capa de infraestructura queda acoplada a la interfaz, no al caso de uso:

package com.example.coworking.infrastructure.persistence;

public class ReservaRepositorioPostgres implements ReservaRepositorio {
    // Implementación concreta usando JDBC o JPA
}

El DIP facilita sustituir la base de datos por una versión en memoria durante las pruebas o migrar a un servicio administrado sin tocar el dominio.

8.2 Uso de interfaces para desacoplar

Las interfaces son el pegamento que mantiene separadas las capas. Todo intercambio entre capas debe pasar por contratos compartidos en el módulo de dominio. Esto también aplica a servicios externos como sistemas de notificación o calendarios.

public interface CalendarioSala {
    void verificarDisponibilidad(String salaId, LocalDateTime desde, LocalDateTime hasta);
}

public class CalendarioGoogleAdapter implements CalendarioSala {
    private final GoogleCalendarClient client;
    // ...
}

public class CalendarioInMemory implements CalendarioSala {
    // Implementación usada en pruebas
}

La interfaz garantiza que la capa de aplicación no dependa de una librería específica. Cambiar el proveedor solo implica registrar otro adaptador.

8.3 Aplicación de principios SOLID

Los principios SOLID ayudan a mantener las capas cohesionadas:

  • Responsabilidad única: cada clase del dominio debe enfocarse en una regla específica; los controladores solo orquestan peticiones.
  • Abierto/cerrado: permitir extensiones sin modificar clases existentes; por ejemplo, agregar un nuevo canal de notificación implementando la misma interfaz.
  • Sustitución de Liskov: asegurar que adaptadores alternativos cumplan los contratos definidos, evitando sorpresas en ejecución.
  • Segregación de interfaces: dividir contratos grandes en Interfaces pequeñas para no obligar a las capas inferiores a implementar métodos innecesarios.
  • Inversión de dependencias: reforzando el punto anterior, los casos de uso dependen de abstracciones.

Una aplicación del principio abierto/cerrado en la capa de notificación:

public class ServicioNotificacionComposite implements ServicioNotificacion {

    private final List<ServicioNotificacion> canales;

    public ServicioNotificacionComposite(List<ServicioNotificacion> canales) {
        this.canales = canales;
    }

    @Override
    public void enviarConfirmacion(Reserva reserva) {
        canales.forEach(canal -> canal.enviarConfirmacion(reserva));
    }
}

Agregar un nuevo canal (Slack, SMS) solo requiere proporcionar otra implementación y sumarla al composite.

8.4 Validaciones y control de errores entre capas

El control de errores debe mantenerse consistente en todas las capas. Las validaciones de formato pertenecen a la presentación, las invariantes al dominio y los errores técnicos se encapsulan en adaptadores. Un enfoque eficaz es utilizar excepciones personalizadas y objetos de resultado.

public class ReservaInvalidaException extends RuntimeException {
    public ReservaInvalidaException(String mensaje) {
        super(mensaje);
    }
}

public class ReservarSala {
    // ...
    public Reserva ejecutar(ReservarSalaCommand command) {
        if (command.desde().isAfter(command.hasta())) {
            throw new ReservaInvalidaException("El horario de inicio no puede ser posterior al horario de fin");
        }
        // resto de la lógica
    }
}

La capa de presentación transforma las excepciones del dominio en respuestas adecuadas:

@ControllerAdvice
public class ErrorHandler {

    @ExceptionHandler(ReservaInvalidaException.class)
    public ResponseEntity<ErrorResponse> manejarReservaInvalida(ReservaInvalidaException ex) {
        return ResponseEntity.badRequest()
            .body(new ErrorResponse("VALIDATION_ERROR", ex.getMessage()));
    }

    @ExceptionHandler(Throwable.class)
    public ResponseEntity<ErrorResponse> manejarErrorGenerico(Throwable ex) {
        return ResponseEntity.internalServerError()
            .body(new ErrorResponse("UNEXPECTED_ERROR", "Ha ocurrido un error inesperado"));
    }
}

El resultado es un flujo uniforme: la UI recibe mensajes claros, el dominio mantiene sus reglas y la infraestructura encapsula detalles de acceso a recursos externos.

8.5 Próximos pasos

Adoptar estas buenas prácticas fortalece la arquitectura en capas y prepara el terreno para evolucionar hacia modelos más complejos sin perder control. En el próximo tema compararemos la arquitectura N-Tier con otros estilos para elegir la estrategia adecuada según el contexto.