Este escrito está basado en el trabajo de Gozde Saygili Yalcin.

Su trabajo original está genial y me gustaría añadir algunas cosas que creo son relevantes en los ejemplos, como la gestión personalizada de excepciones mediante aspectos, junto con la traducción al español.

Muchas gracias Gozde.

Voy a hablar sobre los principios SOLID poniendo énfasis en como el framework Spring ha sido diseñado alrededor de estos principios, garantizandonos que si seguimos estos consejos, podremos acercarnos al ideal de tener un software bien estructurado y fácil de refactorizar. En el fondo del corazón del Tito Bob, creo que esa era la idea. Refactoriza usando el sentido común pensando que las interfaces de usuario están en el lenguaje para permitir expresar el comportamiento de una clase y es mejor hilar fino que hilar de manera gruesa con interfaces que intentan hacer muchas cosas. Hizo un libro super ventas, el cual su versión en Español se puede encontrar en muchos sitios. Los principios son cinco:

  1. S – Principio de Responsabilidad Única (Single Responsibility Principle):
    • Una clase debe tener una sola razón para cambiar, es decir, debe tener una única responsabilidad en el sistema.
  2. O – Principio de Abierto/Cerrado (Open/Closed Principle):
    • Las entidades del software deben estar abiertas para su extensión pero cerradas para su modificación directa.
  3. L – Principio de Sustitución de Liskov (Liskov Substitution Principle):
    • Los objetos de una clase base deben poder ser reemplazados por objetos de sus clases derivadas sin afectar la funcionalidad del programa.
  4. I – Principio de Segregación de Interfaces (Interface Segregation Principle):
    • Es preferible tener muchas interfaces específicas que una interfaz general, evitando que las clases implementen métodos que no necesitan.
  5. D – Principio de Inversión de Dependencias (Dependency Inversion Principle):
    • Los módulos de alto nivel no deben depender de módulos de bajo nivel; ambos deben depender de abstracciones. Además, las abstracciones no deben depender de los detalles, sino los detalles deben depender de las abstracciones.

  1. Single Responsibility Principle (SRP)

El primer principio habla de que una clase debe tener un único motivo para cambiar y debe ser responsable de hacer una única tarea. Veámoslo en un ejemplo:

// Incorrect implementation of SRP
@RestController
@RequestMapping("/report")
public class ReportController {

    private final ReportService reportService;

    public ReportController(ReportService reportService) {
        this.reportService = reportService;
    }

    @PostMapping("/send")
    public ResponseEntity<Report> generateAndSendReport(
                                                        @RequestParam String reportContent,
                                                        @RequestParam String to,
                                                        @RequestParam String subject) {
        String report = reportService.generateReport(reportContent);
        reportService.sendReportByEmail(report, to, subject);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

// Incorrect implementation of SRP
// The above class is responsible for generating a report and sending email. BAD.
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {

    private final ReportRepository reportRepository;

    public ReportServiceImpl(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @Override
    public String generateReport(String reportContent) {
        Report report = new Report();
        report.setReportContent(reportContent);
        return reportRepository.save(report).toString();
    }

    @Override
    public void sendReportByEmail(Long reportId, String to, String subject) {
        Report report = findReportById(reportId);
        sendEmail(report.getReportContent(), to, subject);
    }

    private Report findReportById(Long reportId) {
        return reportRepository.findById(reportId)
                .orElseThrow(() -> new RuntimeException("Report not found"));
    }

    private void sendEmail(String content, String to, String subject) {
       log.info(content, to, subject);
    }
}

Por qué está mal esta implementación? por varias razones, la primera es que la clase de tipo Controller quiere hacer más de una tarea. Quiere generar reportes y poder enviarlos por email, y quiere hacerlo mediante un servicio ReportServiceImpl que haga las dos cosas a la vez. En si que una clase Controller quiera hacer esas dos cosas no es incorrecto de por sí, lo que si es incorrecto, porque tiende a generar bugs es que sea un único método de esa clase controller quien quiera hacer esas dos tareas y a su vez estés delegando en la clase ReportServiceImpl que sea la encargada tanto de generar el informe como de enviarlo por email. Lo correcto es que haya dos servicios, uno para gestionar los informes y otro para enviarlos por email. Teniendo dos servicios diferenciados puedes probar de manera individual cada servicio para luego tener una clase test para comprobar el funcionamiento del servicio.

Hay que evitar que tanto las clases como los métodos hagan más de una tarea.

Uncle Bob

Debería ser así:

@RestController
@RequestMapping("/report")
public class ReportController {

    private final ReportService reportService;
    private final EmailService emailService;

    public ReportController(ReportService reportService, EmailService emailService) {
        this.reportService = reportService;
        this.emailService = emailService;
    }

    @PostMapping("/send")
    public ResponseEntity<Report> generateAndSendReport(
                                                        @RequestParam String reportContent,
                                                        @RequestParam String to,
                                                        @RequestParam String subject) {
        // correct impl reportService is responsible for report generation
        Long reportId = Long.valueOf(reportService.generateReport(reportContent));
        // correct impl emailService is responsible for sending
        emailService.sendReportByEmail(reportId, to, subject);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

Ahora la clase RestController tiene inyectado dos servicios responsables de hacer su tarea, y nada más. Ahora fíjate que tanto ReportService como EmailService no lanza excepciones salvo la excepción genérica RuntimeException.

En mi opinión es algo a evitar,ya que suelen ocultar el verdadero problema subyacente detrás del problema, por lo que hay que procurar que cada método lance hacia arriba la excepción específica que ha provocado el fallo.

// algo mejor, aún no trato de controlar las excepciones, simplemente aplico Single Responsibility.
@Service
public class ReportServiceImpl implements ReportService {

    private final ReportRepository reportRepository;

    public ReportServiceImpl(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @Override
    public String generateReport(String reportContent) {
        Report report = new Report();
        report.setReportContent(reportContent);
//este método save probablemente lanzará excepciones específicas.
        return reportRepository.save(report).toString();
    }

}

@Service
public class EmailServiceImpl implements EmailService {

    private final ReportRepository reportRepository;

    public EmailServiceImpl(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @Override
    public void sendReportByEmail(Long reportId, String to, String subject) {
        Report report = findReportById(reportId);
        if (ObjectUtils.isEmpty(report) || !StringUtils.hasLength(report.getReportContent())) {
            throw new RuntimeException("Report or report content is empty");
        }
       // Enviar el reporte por correo electrónico...
    }

    private Report findReportById(Long reportId) {
        return reportRepository.findById(reportId)
                .orElseThrow(() -> new RuntimeException("Report not found"));
    }

}

Así está mejor, pero se puede mejorar, cómo? con la gestión personalizada de excepciones que puede lanzar este código. Tanto EmailServiceImpl como ReportServiceImpl pueden fallar a la hora de interactuar con la base de datos o con el hardware anfitrión que haya por debajo, por lo que, hay que crear excepciones personalizadas, un gestor de aspectos para tratar las excepciones para cada uno de los servicios y puede que habilitar el SpringBootExceptionHandler.

Centrémonos en EmailServiceImpl, en vez de hacer que findReportById lance RunTimeException en caso de algún error, vamos a lanzar las excepciones específicas que puede lanzar nuestro código y vamos a dejar sea un gestor de excepciones específico y creado por nosotros quien las trate. El gestor de excepciones estará a disposición de la clase Controller mediante un aspecto. Podemos ver los aspectos como aquellas funcionalidades comúnes que tiene nuestra aplicación que queremos tratar de manera común en una única clase, y no desperdigado por todo el código.

Otros aspectos comunes que no voy a tratar en este escrito son la seguridad, la gestión de transacciones, el logging, el cacheo de la información, gestión de la seguridad antes, durante y después de acceder a un recurso, la validación de la información,etc,…

El gestor de excepciones capturará las excepciones lanzadas por el servicio y delegará su tratamiento a la clase de gestión específica.

Una propuesta:

La clase EmailServiceImpl ahora hace una única cosa y lanza excepciones hacia el invocador. En este caso lanzará excepciones del JDK, como IOException, otra del framework Spring, como DataAccessException y otra creada por mí, BusinessException. Ya no lanzo una excepción genérica como RuntimeException hacía arriba que significa simplemente que algo ha ido mal, pero no indica el qué ha ido mal.

@Service
public class EmailServiceImpl implements EmailService {

    private final ReportRepository reportRepository;

    public EmailServiceImpl(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @Override
    public void sendReportByEmail(Long reportId, String to, String subject) throws IOException, DataAccessException, BusinessException {
        Report report = findReportById(reportId);
        if (ObjectUtils.isEmpty(report) || !StringUtils.hasLength(report.getReportContent())) {
            throw new BusinessException("El informe está vacío o no tiene contenido");
        }
        // Enviar el correo electrónico...
    }

    private Report findReportById(Long reportId) throws DataAccessException, ResourceNotFoundException {
        return reportRepository.findById(reportId)
                .orElseThrow(() -> new ResourceNotFoundException("Informe no encontrado"));
    }

}

public class ResourceNotFoundException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}

public class BusinessException extends RuntimeException {
   private static final long serialVersionUID = 1L;
}

Ahora la clase gestora de excepciones. Al marcarla con @ControllerAdvice, Spring instanciará un singleton per JVM de manera que se cargará al principio de la instanciación del contenedor Spring, estando a disposición del objeto que la necesite.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class ResourceExceptionController {
   @ExceptionHandler(value = ResourceNotFoundException.class)
   public ResponseEntity<Object> exception(ResourceNotFoundException exception) {
      return new ResponseEntity<>("Resource not found", HttpStatus.NOT_FOUND);
   }
}

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class BusinessExceptionController {
   @ExceptionHandler(value = BusinessException.class)
   public ResponseEntity<Object> exception(BusinessException exception) {
      return new ResponseEntity<>("Business not found", HttpStatus.NOT_FOUND);
   }
}

Nos podemos preguntar, realmente es necesario tener un @ControllerAdvice para gestionar una excepción?. En mi opinión, si tanto BusinessException como ResourceException aparecen en muchos lugares del código, es buena idea tener una gestor de excepciones. Ahora, si son excepciones que solo aparecen en un sitio, pues puedes gestionar ahí mismo el problema. Sentido común.

Ahora lo mismo pero con el otro servicio:

@Service
public class ReportServiceImpl implements ReportService {

    private final ReportRepository reportRepository;

    public ReportServiceImpl(ReportRepository reportRepository, EmailService emailService) {
        this.reportRepository = reportRepository;
    }

    @Override
    public String generateReport(String reportContent) {
        Report report = new Report();
        report.setReportContent(reportContent);
        return reportRepository.save(report).toString();
    }
}

De esta clase, el único método que puede lanzar alguna excepción es generateReport, en concreto la línea reportRepository.save(report) al interactuar con la base de datos, por lo que voy a hacer que lance DataAccessException

@Service
public class ReportServiceImpl implements ReportService {

    private final ReportRepository reportRepository;


    public ReportServiceImpl(ReportRepository reportRepository, EmailService emailService) {
        this.reportRepository = reportRepository;
    }

    @Override
    public String generateReport(String reportContent) throws DataAccessException{
        Report report = new Report();
        report.setReportContent(reportContent);
        return reportRepository.save(report).toString()
                               .orElseThrow(() -> new DataAccessException("Report NOT Saved!"));
    }

}

DataAccessException es una excepción que está en el framework, pero no existe un gestor AOP específico para tratar estas excepciones, por lo que te voy a mostrar un ejemplo de como crear el tuyo. Es super básico y por lo tanto tendrás que modificarlo para que haga lo que necesites.

Estoy simplemente lanzando HttpStatus.FORBIDDEN porque estoy suponiendo que está prohibido por alguna razón guardar en base de datos.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class DataAccessExceptionController {
   @ExceptionHandler(value = DataAccessException.class)
   public ResponseEntity<Object> exception(DataAccessException exception) {
      return new ResponseEntity<>("DataAccessException", HttpStatus.FORBIDDEN);
   }
}

Los estados HttpStatus están definidos aquí:

Spring Boot proporciona la clase SpringBootExceptionHandler como un mecanismo global para el manejo de excepciones no deseadas en la aplicación.

La diferencia es que este mecanismo es más genérico mientras que el @ControllerAdvice anterior está pensado para hilar fino, es decir, gestionar tus propias excepciones.

Aquí te explico cómo se usa:

1.1 Habilitación:

Para habilitar el uso de SpringBootExceptionHandler, debes crear una clase que extienda esta clase. Ten en cuenta que Spring Boot solo busca una única implementación de SpringBootExceptionHandler en el contexto de la aplicación.
Podemos verlo como un gestor de hilo grueso.

1.2 Manejo de excepciones:

Dentro de la clase que extiende SpringBootExceptionHandler, puedes definir métodos para manejar excepciones específicas utilizando la anotación @ExceptionHandler.

Por ejemplo, puedes definir un método para manejar todas las RuntimeException:

public class MyCustomExceptionHandler extends SpringBootExceptionHandler {

    // ... métodos para manejar excepciones específicas

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Object> handleRuntimeException(RuntimeException ex) {
        // Loggear o registrar la excepción
        logger.error("Ocurrió un error inesperado", ex);

        // Devolver una respuesta de error personalizada
        return new ResponseEntity<>("Ocurrió un error interno en la aplicación.", HttpStatus.INTERNAL_SERVER_ERROR);
}

}

1.3 Prioridad:

Si utilizas @ExceptionHandler en un controlador de excepciones personalizado junto con @ControllerAdvice, los métodos del controlador de excepciones (@ControllerAdvice) personalizado tienen mayor prioridad para manejar las excepciones, es decir, el gestor de hilo fino tiene prioridad sobre el gestor de hilo grueso.

Puntos a tener en cuenta:

SpringBootExceptionHandler se encarga de manejar excepciones no deseadas que no son capturadas por ningún otro mecanismo dentro de la aplicación.
Puedes definir métodos para manejar diferentes tipos de excepciones según tu necesidad.

Ten cuidado al manejar excepciones genéricas como Exception o RuntimeException, ya que puede ocultar errores más específicos. Evítalas en lo posible, sobre todo Exception. Captura todas las excepciones que puedan lanzar los métodos subyacentes y trátalas adecuadamente. La idea es tratar de remontar de manera automática un problema, o al menos indicar con la suficiente información el problema para que alguien pueda analizar lo ocurrido.


En comparación con los gestores de excepciones personalizables (@ControllerAdvice):

SpringBootExceptionHandler ofrece una configuración global para el manejo de excepciones, mientras que los gestores de excepciones personalizados permiten un manejo más granular a nivel de controlador.

Si necesitas un manejo específico para diferentes excepciones y controladores, considera utilizar gestores de excepcion específicos (@ControllerAdvice). No obstante, si deseas establecer un comportamiento predeterminado para excepciones no manejadas, SpringBootExceptionHandler es una opción viable.

Puedes tener una clase que extienda de SpringBootExceptionHandler y poner ahí la lógica para gestionar todas tus excepciones, o puedes crear tus propias clases @ControllerAdvice para gestionar las excepciones customizadas que pueden soltar tus propios servicios.

Tenemos también disponible un mecanismo específico para gestionar las excepciones en los controladores de manera que cuando ocurra un problema en esa capa podamos redireccionar a una página web personalizada. En concreto el uso de los HandlerExceptionResolver.

@Component
class RestResponseStatusExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(
           HttpServletRequest request, 
           HttpServletResponse response, 
           Object object, 
           Exception exception) {
        // gestión específica
        if( exception instanceof ResourceNotFoundException ){
            ModelAndView model = new ModelAndView();
            model.setView(new MappingJackson2JsonView());
            model.addObject("exception", exception.getMessage());
            return model;
        }
        // otra gestión específica, aquí redirecciono a un html
        if( exception instanceof BusinessException ){
            ModelAndView model = new ModelAndView();
            model.setView("errors/business-problem.html");
            model.addObject("exception", exception.getMessage());
            return model;
        }
        // gestión genérica.
        ModelAndView model = new ModelAndView();
        model.setView(new MappingJackson2JsonView());
        model.addObject("exception", exception.getMessage());
        return model;
    }

}

Si ResourceNotFoundException saltara, tendría un mensaje JSON tal que así:

{
      "resourceNotFoundException": {
      "cause": null,
      "stackTrace": [],
      "localizedMessage": "resource not found, 1-2 AM is service downtime!",
      "message": "1-2 AM is service downtime!",
      "suppressed": []
      }
}

Ese mensaje me lo he inventado, hay que setear localizedMessage y message con dichos mensajes.

  1. Open/Closed Principle (OCP)

Este principio dice que una clase debe estar abierta para extenderla pero cerrada para modificarla, es decir, queremos que no introduzcas bugs al código existente modificando el código anterior y queremos que si necesitas añadir nueva funcionalidad, queremos que lo hagáis extendiendo esta clase creando una nueva y ahí pongais vuestros nuevos métodos.

Veamos un ejemplo de como NO hacer las cosas:

// Incorrect implementation violating OCP
public class ReportGeneratorService {
    public String generateReport(Report report) {
        if ("PDF".equals(report.getReportType())) {
            // Incorrect: Direct implementation for generating PDF report
            return "PDF report generated";
        } else if ("Excel".equals(report.getReportType())) {
            // Incorrect: Direct implementation for generating Excel report
            return "Excel report generated";
        } else {
            return "Unsupported report type";
        }
    }
}

Tal y como está montada la clase anterior, si quiero añadir funcionalidad para generar ficheros csv, tendría que modificar esa clase y añadir un nuevo else if para tratar el manejo de ficheros csv. Podéis pensar que por dos o tres tipos de generadores no pasa nada, pero pensad que eso puede crecer mucho más, con muchas sentencias else if.

Mejor hacer algo así:

public interface ReportGenerator {
    String generateReport(Report report);
}

// Concrete implementation for generating PDF reports
@Component
public class PdfReportGenerator implements ReportGenerator {
    @Override
    public String generateReport(Report report) {
        // Impl of pdf report
        return String.format("PDF report generated for %s", report.getReportType());
    }
}

// Concrete implementation for generating Excel reports
@Component
public class ExcelReportGenerator implements ReportGenerator {
    @Override
    public String generateReport(Report report) {
        // Impl of excel report
        return String.format("Excel report generated for %s", report.getReportType());
    }
}

// Service that follows OCP
@Service
public class ReportGeneratorService {

    private final Map<String, ReportGenerator> reportGenerators;

    @Autowired
    public ReportGeneratorService(List<ReportGenerator> generators) {
        // Initialize the map of report generators
        this.reportGenerators = generators.stream()
                .collect(Collectors.toMap(generator -> generator.getClass().getSimpleName(), Function.identity()));
    }

    public String generateReport(Report report, String reportType) {
        return reportGenerators.getOrDefault(reportType, unsupportedReportGenerator())
                .generateReport(report);
    }

    private ReportGenerator unsupportedReportGenerator() {
        return report -> "Unsupported report type";
    }
}

Es decir, interfaces para definir comportamiento, componentes específicos que implementan dicho comportamiento y luego clases @Service que usan los componentes.

Spring va a escanear todas las clases marcadas con @Component y las que hagan match con la interfaz ReportGenerator van a pasar a ser gestionadas por Spring para ser inyectadas a la clase marcada por @Service.

Ahora, un poco más formal. Spring Boot escaneará y procesará las clases marcadas con @Component durante su fase de escaneo de componentes. Sin embargo, el procesamiento de estas clases por parte del constructor de la clase anotada @Service ReportGeneratorService requiere más explicación.

Escaneo de componentes @Component:

Spring Boot busca clases anotadas con @Component (incluidas sus especializaciones como @Service).
En su ejemplo, tanto PdfReportGenerator como ExcelReportGenerator se descubren debido a la anotación @Component.

Autowiring:

El constructor de ReportGeneratorService está marcado con @Autowired. Esto le dice a Spring que inyecte dependencias automáticamente.

Spring busca beans (objetos administrados) de tipo List en el contexto de la aplicación.

Inyección:

Dado que Spring Boot descubrió dos clases que implementan ReportGenerator (a saber, PdfReportGenerator y ExcelReportGenerator), crea dos instancias de estas clases y las agrega a una lista. Mira de nuevo el constructor de ReportGeneratorService.

Luego, esta lista se inyecta en el campo reportGenerators de la instancia ReportGeneratorService.
Todo esto es para que Spring gestione el ciclo de vida de estas clases de componentes y las inyecta en función de la configuración de cableado automático. Por definición, va a ser un singleton por instancia del objeto en la JVM.

En definitiva:

Spring no hace coincidir directamente las clases con la interfaz ReportGenerator. Lo que hace es buscar todas las clases anotadas @Component y luego verifica si implementan la interfaz ReportGenerator.

La anotación @Autowired en el argumento del constructor (List) le indica a Spring que inyecte una lista de todos los beans administrados que implementan la interfaz ReportGenerator.

Esto demuestra el principio abierto/cerrado (OCP), donde puede ampliar la funcionalidad agregando nuevas implementaciones de ReportGenerator sin modificar la clase ReportGeneratorService.

En resumen, los mecanismos de cableado automático y escaneo de componentes de Spring Boot funcionan juntos para administrar el ciclo de vida de los beans e inyectarlos en otros beans en función de sus dependencias.

Este mecanismo es la demostración de como Spring cumple con este principio Abierto/Cerrado.

En el fondo este principio te pide que primero pienses en las interfaces que definen un comportamiento, luego crees los componentes específicosque implementen dicho comportamiento general, para finalmente usar dichos componentes en una clase de servicio.

  1. Liskov’s Substitution Principle (LSP)

Este principio dice que si tienes una clase genérica, deberías ser capaz de cambiarla por una clase más específica sin romper el funcionamiento del programa.

Este principio habla de herencia sobre todo y comportamiento específico de las clases que heredan un tipo general. Por ejemplo, pensemos en Pájaros, Águilas, Pinguinos y Avestruces.

Todas las águilas, pinguinos y avestruces son pájaros, pero no todas ellas pueden volar. Los pinguinos no pueden. Ves por donde voy?

// Incorrect implementation violating LSP
public class Bird {
    public void fly() {
        // I can fly
    }

    public void swim() {
        // I can swim
    }
}

public class Penguin extends Bird {

    // Penguins cannot fly, but we override the fly method and throws Exception
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}

// Correct implementation for LSP
public class Bird {

    // methods
}

public interface Flyable {
    void fly();
}

public interface Swimmable {
    void swim();
}


public class Penguin extends Bird implements Swimmable {
    // Penguins cannot fly, therefore we only implement swim interface
    @Override
    public void swim() {
        System.out.println("I can swim");
    }
}

public class Eagle extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("I can fly");
    }
}

Bird es la clase genérica que alberga comportamiento común a todos los tipos de pájaro, como no soy ornitólogo, no pongo ningún comportamiento.
Las interfaces Flyable y Swimmable describen el comportamiento específico que podran hacer las diferentes tipos de aves. Por ejemplo, Penguin podrá nadar, pero no volar, de la misma manera se explica el comportamiento de Eagle, que podrá volar, pero no nadar. En definitiva, se trata una vez más de pensar en comportamiento específico de cada clase, definir lo específico en interfaces, definir lo general en clases e implementar las clases específicas implementando el comportamiento específico de las interfaces y heredando el comportamiento general de la clase más genérica.

  1. Interface Segregation Principle (ISP)

Este principio establece que es mejor tener interfaces pequeñas que grandes interfaces, así, las clases que implementen dichas interfaces solo tendrán que ocuparse de ese comportamiento.

Un ejemplo sobre como no hacer las cosas, imagina que John es un nadador, no hace salto de longitud ni carrera, por lo que esta interfaz y clase que le representa no le sirve:

public interface Athlete {

    void compete();

    void swim();

    void highJump();

    void longJump();
}

// Incorrect implementation violating Interface Segregation
public class JohnDoe implements Athlete {
    @Override
    public void compete() {
        System.out.println("John Doe started competing");
    }

    @Override
    public void swim() {
        System.out.println("John Doe started swimming");
    }

    @Override
    public void highJump() {
    // Not neccessary for John Doe
    }

    @Override
    public void longJump() {
    // Not neccessary for John Doe
    }
}

Ahora, corrijo este comportamiento:

public interface Athlete {

    void compete();
}

public interface JumpingAthlete {

    void highJump();

    void longJump();
}

public interface SwimmingAthlete {

    void swim();
}

// Correct implementation for Interface Segregation
public class JohnDoe implements Athlete, SwimmingAthlete {
    @Override
    public void compete() {
        System.out.println("John Doe started competing");
    }

    @Override
    public void swim() {
        System.out.println("John Doe started swimming");
    }
}

Ahora estoy diciendo que John es un athleta que puede nadar. Se trata de definir con precisión el comportamiento de las clases, no fuerzo a una clase a implementar un comportamiento que no necesita expresar.

  1. Dependency Inversion Principle (DIP)

Este principio establece el principio de inyeccion de dependencias, o también conocido por principio Hollywood, o «no me llames tú, ya te llamo yo». Supongo que en Hollywood estarán muy estresados y agobiados si nadie les llama.

Quiere decir que Spring va a tratar de crear los objetos y gestionar su ciclo de vida, mantener sus dependencias, primero creándolas y luego inyectándolas en los objetos que declaran usar esas dependencias.

// Incorrect impl of Dependency Inversion Principle
@Service
public class PayPalPaymentService {

    public void processPayment(Order order) {
        // payment processing logic
    }
}

@RestController
public class PaymentController {

    // Direct dependency on a specific implementation
    private final PayPalPaymentService paymentService;

    // Constructor directly initializes a specific implementation
    public PaymentController() {
        this.paymentService = new PayPalPaymentService();
    }

    @PostMapping("/pay")
    public void pay(@RequestBody Order order) {
        paymentService.processPayment(order);
    }
}

Como ves, en la clase @RestController estoy obligando a que solo pueda existir un único comportamiento, solo puedes pagar con paypal, si quieres por ejemplo poder pagar con Visa, si sigues el ejemplo erróneo de antes, tendrías que modificar la dependencia, volver a compilar, etc. En cambio siguiendo el siguiente ejemplo, puedes tener tantas implementaciones de pago como quieras tener, sin tener que cambiar nada del @RestController, si quieres, simplemente poniendo el @Primary
en la implementación que consideres primaria.

En el siguiente ejemplo tengo una interfaz que establece un comportamiento genérico como es establecer un mecanismo de pago en línea, luego dos objetos de tipo @Service que implementan el comportamiento específico y finalmente un objeto de tipo @RestController que va a hacer uso de uno de dichos servicios. Si tenemos varios servicios que implementan una interfaz, cuál será el inyectado?. Para ello en los servicios se usan las anotaciones @Primary y @Qualifier. Si una está marcada como @Primary y en el objeto que la usa no aparece @Qualifier, ésta será la que se inyectará, pero si aparece el qualifier, esa será la que se inyectará. En el ejemplo siguiente, el servicio que se inyectará en el @RestController será el de visa, aunque paypal esté marcado como @Primary. @Qualifier tiene preferencia.

// Introduced interface
public interface PaymentService {
    void processPayment(Order order);
}

// Implemented interface in a service class
@Service
@Primary
@Qualifier("paypal")
public class PayPalPaymentService implements PaymentService {
    @Override
    public void processPayment(Order order) {
        // payment processing logic
    }
}

// Implemented interface in a service class
@Service
@Qualifier("visa")
public class VisaPaymentService implements PaymentService {
    @Override
    public void processPayment(Order order) {
        // payment processing logic
    }
}

@RestController
public class PaymentController {

    private final PaymentService paymentService;

    // Constructor injection
    public PaymentController(@Qualifier("visa") PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @PostMapping("/pay")
    public void pay(@RequestBody Order order) {
        paymentService.processPayment(order);
    }
}

Hasta otra!

Deja un comentario