A medida que los proyectos de software crecen, se vuelve cada vez más importante mantener tu código organizado, mantenible y escalable. Aquí es donde entran en juego los patrones de diseño. Los patrones de diseño proporcionan soluciones probadas y reutilizables a desafíos comunes de diseño de software, haciendo que tu código sea más eficiente y más fácil de gestionar.
En esta guía, profundizaremos en algunos de los patrones de diseño más populares y te mostraremos cómo implementarlos en Spring Boot. Al final, no solo comprenderás conceptualmente estos patrones, sino que también podrás aplicarlos en tus propios proyectos con confianza.
Tabla de contenidos
Introducción a los Patrones de Diseño
Los patrones de diseño son soluciones reutilizables a problemas comunes de diseño de software. Piensa en ellos como las mejores prácticas destiladas en plantillas que se pueden aplicar para resolver desafíos específicos en tu código. No son específicos de ningún lenguaje, pero pueden ser particularmente poderosos en Java debido a su naturaleza orientada a objetos.
En esta guía, cubriremos:
-
Patrón Singleton: Asegurando que una clase tenga solo una instancia.
-
Patrón Factory: Creando objetos sin especificar la clase exacta.
-
Patrón Estrategia: Permitiendo seleccionar algoritmos en tiempo de ejecución.
-
Patrón Observador: Estableciendo una relación de publicación-suscripción.
No solo cubriremos cómo funcionan estos patrones, sino que también exploraremos cómo se pueden aplicar en Spring Boot para aplicaciones del mundo real.
Cómo Configurar tu Proyecto Spring Boot
Antes de adentrarnos en los patrones, configuremos un proyecto Spring Boot:
Requisitos
Asegúrate de tener:
-
Java 11+
-
Maven
-
Spring Boot CLI (opcional)
-
Postman o curl (para pruebas)
Inicialización del Proyecto
Puedes crear rápidamente un proyecto Spring Boot usando Spring Initializr:
curl https://start.spring.io/starter.zip \
-d dependencies=web \
-d name=DesignPatternsDemo \
-d javaVersion=11 -o design-patterns-demo.zip
unzip design-patterns-demo.zip
cd design-patterns-demo
¿Qué es el Patrón Singleton?
El patrón Singleton asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a la misma. Este patrón se utiliza comúnmente para servicios como el registro, la gestión de configuraciones o las conexiones a bases de datos.
Cómo Implementar el Patrón Singleton en Spring Boot
Los beans de Spring Boot son singletons por defecto, lo que significa que Spring gestiona automáticamente el ciclo de vida de estos beans para asegurar que solo exista una instancia. Sin embargo, es importante comprender cómo funciona el patrón Singleton bajo la superficie, especialmente cuando no se están utilizando beans administrados por Spring o se necesita más control sobre la gestión de instancias.
Veamos una implementación manual del Patrón Singleton para demostrar cómo puedes controlar la creación de una única instancia dentro de tu aplicación.
Paso 1: Crear una Clase LoggerService
En este ejemplo, crearemos un servicio de registro simple utilizando el Patrón Singleton. El objetivo es asegurar que todas las partes de la aplicación utilicen la misma instancia de registro.
public class LoggerService {
// La variable estática para mantener la única instancia
private static LoggerService instance;
// Constructor privado para prevenir la instanciación desde fuera
private LoggerService() {
// Este constructor está intencionadamente vacío para evitar que otras clases creen instancias
}
// Método público para proporcionar acceso a la única instancia
public static synchronized LoggerService getInstance() {
if (instance == null) {
instance = new LoggerService();
}
return instance;
}
// Método de ejemplo para registro
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
-
Variable Estática (
instance
): Esto mantiene la única instancia deLoggerService
. -
Constructor Privado: El constructor está marcado como privado para evitar que otras clases creen nuevas instancias directamente.
-
Método
getInstance()
: El método está sincronizado para hacerlo seguro para hilos, asegurando que solo se cree una instancia incluso si varios hilos intentan acceder a él simultáneamente. -
Inicialización Perezosa: La instancia se crea solo cuando se solicita por primera vez (
inicialización perezosa
), lo que es eficiente en términos de uso de memoria.
Uso en el Mundo Real: Este patrón se utiliza comúnmente para recursos compartidos, como registros, configuraciones, o gestión de conexiones a bases de datos, donde se desea controlar el acceso y asegurar que solo se use una instancia a lo largo de la aplicación.
Paso 2: Usar el Singleton en un Controlador de Spring Boot
Ahora, veamos cómo podemos usar nuestro LoggerService
Singleton dentro de un controlador de Spring Boot. Este controlador expondrá un endpoint que registra un mensaje cada vez que se accede.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LogController {
@GetMapping("/log")
public ResponseEntity<String> logMessage() {
// Accediendo a la instancia Singleton de LoggerService
LoggerService logger = LoggerService.getInstance();
logger.log("This is a log message!");
return ResponseEntity.ok("Message logged successfully");
}
}
-
Endpoint GET: Hemos creado un
/log
endpoint que, al ser accedido, registra un mensaje usando elLoggerService
. -
Uso del Singleton: En lugar de crear una nueva instancia de
LoggerService
, llamamos agetInstance()
para asegurarnos de que estamos usando la misma instancia cada vez. -
Respuesta: Después de registrar, el endpoint devuelve una respuesta indicando éxito.
Paso 3: Probando el Patrón Singleton
Ahora, probemos este endpoint usando Postman o tu navegador:
GET http://localhost:8080/log
Salida Esperada:
-
Registro en consola:
[LOG] ¡Este es un mensaje de registro!
-
Respuesta HTTP:
Mensaje registrado con éxito
Puedes llamar al endpoint varias veces, y verás que se utiliza la misma instancia de LoggerService
, como lo indica la salida de registro consistente.
Casos de Uso en el Mundo Real para el Patrón Singleton
Aquí hay situaciones en las que podrías querer usar el patrón Singleton en aplicaciones del mundo real:
-
Gestión de Configuración: Asegúrate de que tu aplicación utilice un conjunto consistente de configuraciones, especialmente cuando esas configuraciones se cargan desde archivos o bases de datos.
-
Grupos de Conexiones a la Base de Datos: Controlar el acceso a un número limitado de conexiones a la base de datos, asegurando que el mismo grupo se comparta en toda la aplicación.
-
Caché: Mantener una única instancia de caché para evitar datos inconsistentes.
-
Servicios de Registro: Como se muestra en este ejemplo, utilizar un único servicio de registro para centralizar las salidas de registro en diferentes módulos de su aplicación.
Conclusiones Clave
-
El patrón Singleton es una manera fácil de asegurar que solo se cree una instancia de una clase.
-
La seguridad de hilos es crucial si múltiples hilos están accediendo al Singleton, por lo que utilizamos
synchronized
en nuestro ejemplo. -
Los beans de Spring Boot ya son singletons por defecto, pero comprender cómo implementarlo manualmente te ayuda a ganar más control cuando sea necesario.
Esto cubre la implementación y el uso del patrón Singleton. A continuación, exploraremos el patrón Factory para ver cómo puede ayudar a agilizar la creación de objetos.
¿Qué es el Patrón Factory?
El patrón Factory permite crear objetos sin especificar la clase exacta. Este patrón es útil cuando tienes diferentes tipos de objetos que necesitan ser instanciados según alguna entrada.
Cómo implementar una Factoría en Spring Boot
El patrón Factory es increíblemente útil cuando necesitas crear objetos basados en ciertos criterios, pero deseas desacoplar el proceso de creación de objetos de la lógica principal de tu aplicación.
En esta sección, recorreremos la construcción de una NotificationFactory
para enviar notificaciones por Email o SMS. Esto es especialmente útil si anticipas agregar más tipos de notificaciones en el futuro, como notificaciones push o alertas dentro de la aplicación, sin cambiar tu código existente.
Paso 1: Crear la Interfaz Notification
El primer paso es definir una interfaz común que todos los tipos de notificaciones implementarán. Esto asegura que cada tipo de notificación (Email, SMS, etc.) tendrá un método send()
consistente.
public interface Notification {
void send(String message);
}
-
Propósito: La interfaz
Notification
define el contrato para enviar notificaciones. Cualquier clase que implemente esta interfaz debe proporcionar una implementación para el métodosend()
. -
Escalabilidad: Al utilizar una interfaz, puedes extender fácilmente tu aplicación en el futuro para incluir otros tipos de notificaciones sin necesidad de modificar el código existente.
Paso 2: Implementar NotificaciónEmail
y NotificaciónSMS
Ahora, implementemos dos clases concretas, una para enviar correos electrónicos y otra para enviar mensajes SMS.
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
public class SMSNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
Paso 3: Crear una FábricaNotificaciones
La clase FábricaNotificaciones
es responsable de crear instancias de Notificación
basadas en el tipo especificado. Este diseño asegura que el ControladorNotificaciones
no necesita saber sobre los detalles de la creación de objetos.
public class NotificationFactory {
public static Notification createNotification(String type) {
switch (type.toUpperCase()) {
case "EMAIL":
return new EmailNotification();
case "SMS":
return new SMSNotification();
default:
throw new IllegalArgumentException("Unknown notification type: " + type);
}
}
}
Método de Fábrica (crearNotificación()
):
-
El método de fábrica recibe una cadena (
tipo
) como entrada y devuelve una instancia de la clase de notificación correspondiente. -
Declaración Switch: La declaración switch selecciona el tipo de notificación apropiado basado en la entrada.
-
Manejo de Errores: Si el tipo proporcionado no es reconocido, lanza una
IllegalArgumentException
. Esto asegura que los tipos inválidos sean capturados temprano.
¿Por qué usar una Fábrica?
-
Desacoplamiento: El patrón de fábrica desacopla la creación de objetos de la lógica de negocio. Esto hace que tu código sea más modular y fácil de mantener.
-
Extensibilidad: Si deseas agregar un nuevo tipo de notificación, solo necesitas actualizar la fábrica sin cambiar la lógica del controlador.
Paso 4: Usa la Fábrica en un Controlador de Spring Boot
Ahora, unamos todo creando un controlador de Spring Boot que utilice la NotificationFactory
para enviar notificaciones según la solicitud del usuario.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NotificationController {
@GetMapping("/notify")
public ResponseEntity<String> notify(@RequestParam String type, @RequestParam String message) {
try {
// Crea el objeto de Notificación apropiado usando la fábrica
Notification notification = NotificationFactory.createNotification(type);
notification.send(message);
return ResponseEntity.ok("Notification sent successfully!");
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
Endpoint GET (/notify
):
-
El controlador expone un endpoint
/notify
que acepta dos parámetros de consulta:type
(ya sea “EMAIL” o “SMS”) ymessage
. -
Utiliza el
NotificationFactory
para crear el tipo de notificación apropiado y envía el mensaje. -
Manejo de Errores: Si se proporciona un tipo de notificación inválido, el controlador captura la
IllegalArgumentException
y devuelve una respuesta400 Bad Request
.
Paso 5: Probar el Patrón de Fábrica
Probemos el endpoint usando Postman o un navegador:
-
Enviar una Notificación por Email:
GET http://localhost:8080/notify?type=email&message=Hola%20Email
Salida:
Enviando Email: Hola Email
-
Enviar una Notificación por SMS:
GET http://localhost:8080/notify?type=sms&message=Hola%20SMS
Salida:
Enviando SMS: Hola SMS
-
Prueba con un Tipo Inválido:
GET http://localhost:8080/notify?type=unknown&message=Prueba
Salida:
Solicitud Incorrecta: Tipo de notificación desconocido: unknown
Casos de Uso del Mundo Real para el Patrón de Fábrica
El patrón de Fábrica es particularmente útil en escenarios donde:
-
Creación Dinámica de Objetos: Cuando necesitas crear objetos basados en la entrada del usuario, como enviar diferentes tipos de notificaciones, generar informes en varios formatos o manejar diferentes métodos de pago.
-
Desacoplamiento de la Creación de Objetos: Al utilizar una fábrica, puedes mantener tu lógica de negocio principal separada de la creación de objetos, haciendo que tu código sea más mantenible.
-
Escalabilidad: Amplía fácilmente tu aplicación para soportar nuevos tipos de notificaciones sin modificar el código existente. Simplemente añade una nueva clase que implemente la interfaz
Notification
y actualiza la fábrica.
¿Qué es el Patrón Estrategia?
El patrón Estrategia es perfecto cuando necesitas cambiar entre múltiples algoritmos o comportamientos dinámicamente. Te permite definir una familia de algoritmos, encapsular cada uno dentro de clases separadas y hacer que sean fácilmente intercambiables en tiempo de ejecución. Esto es especialmente útil para seleccionar un algoritmo basado en condiciones específicas, manteniendo tu código limpio, modular y flexible.
Caso de uso del mundo real: Imagina un sistema de comercio electrónico que necesita admitir múltiples opciones de pago, como tarjetas de crédito, PayPal o transferencias bancarias. Al usar el patrón Strategy, puedes agregar o modificar métodos de pago fácilmente sin alterar el código existente. Este enfoque garantiza que tu aplicación siga siendo escalable y mantenible al introducir nuevas funciones o actualizar las existentes.
Mostraremos este patrón con un ejemplo de Spring Boot que maneja pagos usando una estrategia de tarjeta de crédito o PayPal.
Paso 1: Definir una Interfaz PaymentStrategy
Comenzamos creando una interfaz común que implementarán todas las estrategias de pago:
public interface PaymentStrategy {
void pay(double amount);
}
La interfaz define un contrato para todos los métodos de pago, asegurando consistencia en las implementaciones.
Paso 2: Implementar Estrategias de Pago
Crear clases concretas para pagos con tarjeta de crédito y PayPal.
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " with Credit Card");
}
}
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " via PayPal");
}
}
Cada clase implementa el método pay()
con su comportamiento específico.
Paso 3: Utilizar la Estrategia en un Controlador
Crear un controlador para seleccionar dinámicamente una estrategia de pago según la entrada del usuario:
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PaymentController {
@GetMapping("/pay")
public ResponseEntity<String> processPayment(@RequestParam String method, @RequestParam double amount) {
PaymentStrategy strategy = selectPaymentStrategy(method);
if (strategy == null) {
return ResponseEntity.badRequest().body("Invalid payment method");
}
strategy.pay(amount);
return ResponseEntity.ok("Payment processed successfully!");
}
private PaymentStrategy selectPaymentStrategy(String method) {
switch (method.toUpperCase()) {
case "CREDIT": return new CreditCardPayment();
case "PAYPAL": return new PayPalPayment();
default: return null;
}
}
}
El punto final acepta method
y amount
como parámetros de consulta y procesa el pago utilizando la estrategia adecuada.
Paso 4: Probar el Punto Final
-
Pago con Tarjeta de Crédito:
GET http://localhost:8080/pay?method=credit&amount=100
Salida:
Pagado $100.0 con Tarjeta de Crédito
-
Pago con PayPal:
GET http://localhost:8080/pay?method=paypal&amount=50
Salida:
Pagado $50.0 via PayPal
-
Método Inválido:
GET http://localhost:8080/pay?method=bitcoin&amount=25
Salida:
Método de pago inválido
Usos del Patrón de Estrategia
-
Procesamiento de Pagos: Cambiar dinámicamente entre diferentes pasarelas de pago.
-
Algoritmos de Ordenamiento: Elegir el mejor método de ordenamiento según el tamaño de los datos.
-
Exportación de Archivos: Exportar informes en varios formatos (PDF, Excel, CSV).
Aspectos Clave
-
El patrón Strategy mantiene su código modular y sigue el principio Abierto/Cerrado.
-
Agregar nuevas estrategias es fácil, simplemente crea una nueva clase que implemente la interfaz
PaymentStrategy
. -
Es ideal para escenarios donde se necesita una selección flexible de algoritmos en tiempo de ejecución.
A continuación, exploraremos el patrón Observer, perfecto para manejar arquitecturas dirigidas por eventos.
¿Qué es el Patrón Observer?
El patrón Observer es ideal cuando tienes un objeto (el sujeto) que necesita notificar a múltiples otros objetos (observadores) sobre cambios en su estado. Es perfecto para sistemas basados en eventos donde las actualizaciones deben ser enviadas a varios componentes sin crear un acoplamiento estrecho entre ellos. Este patrón te permite mantener una arquitectura limpia, especialmente cuando diferentes partes de tu sistema necesitan reaccionar a cambios de forma independiente.
Caso de uso del mundo real: Este patrón se utiliza comúnmente en sistemas que envían notificaciones o alertas, como aplicaciones de chat o rastreadores de precios de acciones, donde las actualizaciones deben ser enviadas a los usuarios en tiempo real. Al usar el patrón Observer, puedes agregar o quitar tipos de notificación fácilmente sin alterar la lógica central.
Mostraremos cómo implementar este patrón en Spring Boot construyendo un sistema simple de notificaciones donde se envían notificaciones tanto por correo electrónico como por SMS cada vez que un usuario se registra.
Paso 1: Crear una interfaz Observer
Comenzamos definiendo una interfaz común que implementarán todos los observadores:
public interface Observer {
void update(String event);
}
La interfaz establece un contrato donde todos los observadores deben implementar el método update()
, que se activará cada vez que el sujeto cambie de estado.
Paso 2: Implementar EmailObserver
y SMSObserver
Luego, creamos dos implementaciones concretas de la interfaz Observer
para manejar notificaciones por correo electrónico y SMS.
Clase EmailObserver
public class EmailObserver implements Observer {
@Override
public void update(String event) {
System.out.println("Email sent for event: " + event);
}
}
El EmailObserver
se encarga de enviar notificaciones por correo electrónico cada vez que es notificado de un evento.
Clase SMSObserver
public class SMSObserver implements Observer {
@Override
public void update(String event) {
System.out.println("SMS sent for event: " + event);
}
}
El SMSObserver
se encarga de enviar notificaciones por SMS cada vez que se le notifica.
Paso 3: Crear una clase UserService
(El Sujeto)
Ahora crearemos una clase UserService
que actúa como sujeto, notificando a sus observadores registrados cada vez que un usuario se registra.
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserService {
private List<Observer> observers = new ArrayList<>();
// Método para registrar observadores
public void registerObserver(Observer observer) {
observers.add(observer);
}
// Método para notificar a todos los observadores registrados de un evento
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
// Método para registrar un nuevo usuario y notificar a los observadores
public void registerUser(String username) {
System.out.println("User registered: " + username);
notifyObservers("User Registration");
}
}
-
Lista de Observadores: Lleva un registro de todos los observadores registrados.
-
registerObserver()
Método: Agrega nuevos observadores a la lista. -
notifyObservers()
Método: Notifica a todos los observadores registrados cuando ocurre un evento. -
registerUser()
Método: Registra un nuevo usuario y desencadena notificaciones a todos los observadores.
Paso 4: Utilizar el Patrón Observador en un Controlador
Finalmente, crearemos un controlador Spring Boot para exponer un punto final para el registro de usuarios. Este controlador registrará tanto EmailObserver
como SMSObserver
con el UserService
.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
public UserController() {
this.userService = new UserService();
// Registrar observadores
userService.registerObserver(new EmailObserver());
userService.registerObserver(new SMSObserver());
}
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestParam String username) {
userService.registerUser(username);
return ResponseEntity.ok("User registered and notifications sent!");
}
}
-
Punto final (
/registro
): Acepta un parámetronombre de usuario
y registra al usuario, activando notificaciones a todos los observadores. -
Observadores: Tanto
EmailObserver
comoSMSObserver
se registran conUserService
, por lo que son notificados cada vez que un usuario se registra.
Probando el Patrón Observer
Ahora, probemos nuestra implementación usando Postman o un navegador:
POST http://localhost:8080/api/register?username=JohnDoe
Salida esperada en la consola:
User registered: JohnDoe
Email sent for event: User Registration
SMS sent for event: User Registration
El sistema registra al usuario y notifica tanto a los observadores de Email como de SMS, demostrando la flexibilidad del patrón Observer.
Aplicaciones del Patrón Observer en el Mundo Real
-
Sistemas de Notificación: Enviar actualizaciones a los usuarios a través de diferentes canales (correo electrónico, SMS, notificaciones push) cuando ocurren ciertos eventos.
-
Arquitecturas Orientadas a Eventos: Notificar a múltiples subsistemas cuando se producen acciones específicas, como actividades de usuario o alertas del sistema.
-
Transmisión de Datos: Transmitir cambios de datos a varios consumidores en tiempo real (por ejemplo, precios de acciones en vivo o feeds de redes sociales).
Cómo Usar la Inyección de Dependencias de Spring Boot
Hasta ahora, hemos estado creando manualmente objetos para demostrar patrones de diseño. Sin embargo, en aplicaciones del mundo real de Spring Boot, la Inyección de Dependencias (DI) es la forma preferida de gestionar la creación de objetos. DI permite que Spring maneje automáticamente la instanciación y conexión de sus clases, haciendo que su código sea más modular, testeable y mantenible.
Refactoricemos nuestro ejemplo de patrón Strategy para aprovechar las potentes capacidades de DI de Spring Boot. Esto nos permitirá cambiar dinámicamente entre estrategias de pago, utilizando las anotaciones de Spring para gestionar las dependencias.
Patrón Strategy Actualizado Utilizando la DI de Spring Boot
En nuestro ejemplo refactorizado, aprovecharemos las anotaciones de Spring como @Component
, @Service
y @Autowired
para agilizar el proceso de inyección de dependencias.
Paso 1: Anotar las Estrategias de Pago con @Component
Primero, marcaremos nuestras implementaciones de estrategias con la anotación @Component
para que Spring pueda detectarlas y gestionarlas automáticamente.
@Component("creditCardPayment")
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " with Credit Card");
}
}
@Component("payPalPayment")
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid $" + amount + " using PayPal");
}
}
-
@Component
Anotación: Al agregar@Component
, indicamos a Spring que trate estas clases como beans gestionados por Spring. El valor de cadena ("creditCardPayment"
y"payPalPayment"
) actúa como identificador del bean. -
Flexibilidad: Esta configuración nos permite cambiar entre estrategias utilizando el identificador de bean apropiado.
Paso 2: Refactorizar el PaymentService
para Utilizar Inyección de Dependencias
Luego, modifiquemos el PaymentService
para inyectar una estrategia de pago específica usando @Autowired
y @Qualifier
.
@Service
public class PaymentService {
private final PaymentStrategy paymentStrategy;
@Autowired
public PaymentService(@Qualifier("payPalPayment") PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processPayment(double amount) {
paymentStrategy.pay(amount);
}
}
-
Anotación
@Service
: MarcaPaymentService
como un bean de servicio gestionado por Spring. -
@Autowired
: Spring inyecta la dependencia requerida automáticamente. -
@Qualifier
: Especifica qué implementación dePaymentStrategy
inyectar. En este ejemplo, estamos usando"payPalPayment"
. -
Fácil Configuración: Simplemente cambiando el valor de
@Qualifier
, puedes cambiar la estrategia de pago sin alterar la lógica del negocio.
Paso 3: Utilizando el Servicio Refactorizado en un Controlador
Para ver los beneficios de esta refactorización, actualicemos el controlador para usar nuestro PaymentService
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class PaymentController {
private final PaymentService paymentService;
@Autowired
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@GetMapping("/pay")
public String makePayment(@RequestParam double amount) {
paymentService.processPayment(amount);
return "Payment processed using the current strategy!";
}
}
-
@Autowired
: El controlador recibe automáticamente elPaymentService
con la estrategia de pago inyectada. -
Endpoint GET (
/pay
): Cuando se accede, procesa un pago utilizando la estrategia configurada actualmente (PayPal en este ejemplo).
Pruebas del Patrón de Estrategia Refactorizado con DI
Ahora, probemos la nueva implementación usando Postman o un navegador:
GET http://localhost:8080/api/pay?amount=100
Resultado Esperado:
Paid $100.0 using PayPal
Si cambias el calificador en ServicioDePago
a "pagoConTarjetaDeCredito"
, el resultado cambiará en consecuencia:
Paid $100.0 with Credit Card
Beneficios de Usar Inyección de Dependencias
-
Desacoplamiento: El servicio y el controlador no necesitan saber los detalles de cómo se procesa un pago. Simplemente confían en Spring para inyectar la implementación correcta.
-
Modularidad: Puedes añadir fácilmente nuevos métodos de pago (por ejemplo,
PagoTransferenciaBancaria
,PagoCriptográfico
) creando nuevas clases anotadas con@Component
y ajustando el@Qualifier
. - Configurabilidad: Al aprovechar los Perfiles de Spring, puedes cambiar estrategias basadas en el entorno (por ejemplo, desarrollo vs. producción).
Ejemplo: Puedes usar @Profile
para inyectar automáticamente diferentes estrategias basadas en el perfil activo:
@Component
@Profile("dev")
public class DevPaymentStrategy implements PaymentStrategy { /* ... */ }
@Component
@Profile("prod")
public class ProdPaymentStrategy implements PaymentStrategy { /* ... */ }
Aspectos clave
-
Al usar la Inyección de Dependencias de Spring Boot, puedes simplificar la creación de objetos y mejorar la flexibilidad de tu código.
-
El Patrón de Estrategia combinado con la Inyección de Dependencias te permite cambiar fácilmente entre diferentes estrategias sin modificar la lógica empresarial principal.
-
Usar
@Qualifier
y Perfiles de Spring te da la flexibilidad de configurar tu aplicación basándote en diferentes entornos o requisitos.
Este enfoque no solo hace que tu código sea más limpio, sino que también lo prepara para configuraciones más avanzadas y escalabilidad en el futuro. En la próxima sección, exploraremos las Mejores Prácticas y Consejos de Optimización para llevar tus aplicaciones de Spring Boot al siguiente nivel.
Mejores Prácticas y Consejos de Optimización
Mejores Prácticas Generales
-
No abusar de los patrones: Utilízalos solo cuando sea necesario. Sobrediseñar puede hacer que tu código sea más difícil de mantener.
-
Preferir la composición sobre la herencia: Patrones como Strategy y Observer son excelentes ejemplos de este principio.
-
Mantener tus patrones flexibles: Utiliza interfaces para mantener tu código desacoplado.
Consideraciones de Rendimiento
-
Patrón Singleton: Asegura la seguridad en hilos utilizando
synchronized
o elDiseño Singleton de Bill Pugh
. -
Patrón Factory: Cachéa objetos si son costosos de crear.
-
Patrón Observer: Utiliza el procesamiento asíncrono si tienes muchos observadores para evitar bloqueos.
Temas Avanzados
-
Usando Reflection con el patrón Factory para la carga dinámica de clases.
-
Aprovechando los Perfiles de Spring para cambiar estrategias basadas en el entorno.
-
Agregando Documentación Swagger para tus puntos finales de API.
Conclusión y puntos clave
En este tutorial, exploramos algunos de los patrones de diseño más poderosos: Singleton, Factory, Strategy y Observer, y demostramos cómo implementarlos en Spring Boot. Vamos a resumir brevemente cada patrón y resaltar para qué es mejor adecuado:
Patrón Singleton:
-
Resumen: Asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a ella.
-
Mejor para: Gestionar recursos compartidos como configuraciones, conexiones de base de datos o servicios de registro. Es ideal cuando deseas controlar el acceso a una instancia compartida en toda tu aplicación.
Patrón de Fábrica:
-
Resumen: Proporciona una forma de crear objetos sin especificar la clase exacta a instanciar. Este patrón desacopla la creación de objetos de la lógica empresarial.
-
Mejor Para: Escenarios donde necesitas crear diferentes tipos de objetos basados en condiciones de entrada, como enviar notificaciones por correo electrónico, SMS o notificaciones push. Es ideal para hacer que tu código sea más modular y extensible.
Patrón de Estrategia:
-
Resumen: Te permite definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. Este patrón te ayuda a elegir un algoritmo en tiempo de ejecución.
-
Mejor Para: Cuando necesitas cambiar entre diferentes comportamientos o algoritmos dinámicamente, como procesar diversos métodos de pago en una aplicación de comercio electrónico. Mantiene tu código flexible y se adhiere al Principio de Abierto/Cerrado.
Patrón Observador:
-
Resumen: Define una dependencia de uno a muchos entre objetos para que cuando un objeto cambie de estado, todos sus dependientes sean notificados automáticamente.
-
Mejor para: Sistemas basados en eventos como servicios de notificación, actualizaciones en tiempo real en aplicaciones de chat, o sistemas que necesitan reaccionar a cambios en los datos. Es ideal para desacoplar componentes y hacer que tu sistema sea más escalable.
¿Qué sigue?
Ahora que has aprendido estos patrones de diseño esenciales, intégralos en tus proyectos existentes para ver cómo pueden mejorar la estructura y escalabilidad de tu código. Aquí tienes algunas sugerencias para explorar más:
-
Experimenta: Intenta implementar otros patrones de diseño como Decorador, Proxy y Constructor para ampliar tu conjunto de herramientas.
-
Práctica: Utiliza estos patrones para refactorizar proyectos existentes y mejorar su mantenibilidad.
-
Compartir: ¡Si tienes alguna pregunta o quieres compartir tu experiencia, no dudes en comunicarte!
¡Espero que esta guía te haya ayudado a entender cómo utilizar eficazmente los patrones de diseño en Java. Sigue experimentando y feliz codificación!
Source:
https://www.freecodecamp.org/news/how-to-use-design-patterns-in-java-with-spring-boot/