À medida que os projetos de software crescem, torna-se cada vez mais importante manter o seu código organizado, mantível e escalável. É aqui que os padrões de design entram em jogo. Os padrões de design fornecem soluções comprovadas e reutilizáveis para desafios comuns de design de software, tornando o seu código mais eficiente e mais fácil de gerenciar.
Neste guia, vamos aprofundar em alguns dos padrões de design mais populares e mostrar como implementá-los no Spring Boot. No final, você não apenas entenderá esses padrões conceptualmente, mas também será capaz de aplicá-los em seus próprios projetos com confiança.
Sumário
Introdução aos Padrões de Design
Os padrões de design são soluções reutilizáveis para problemas comuns de design de software. Pense neles como as melhores práticas destiladas em modelos que podem ser aplicados para resolver desafios específicos em seu código. Eles não são específicos para nenhuma linguagem, mas podem ser particularmente poderosos em Java devido à sua natureza orientada a objetos.
Neste guia, abordaremos:
-
Padrão Singleton: Garantir que uma classe tenha apenas uma instância.
-
Padrão Factory: Criar objetos sem especificar a classe exata.
-
Padrão Strategy: Permitir a seleção de algoritmos em tempo de execução.
-
Padrão Observer: Estabelecer um relacionamento publicador-assinante.
Vamos não apenas abordar como esses padrões funcionam, mas também explorar como eles podem ser aplicados no Spring Boot para aplicações do mundo real.
Como Configurar Seu Projeto Spring Boot
Antes de nos aprofundarmos nos padrões, vamos configurar um projeto Spring Boot:
Pré-requisitos
Certifique-se de ter:
-
Java 11+
-
Maven
-
Spring Boot CLI (opcional)
-
Postman ou curl (para testes)
Inicialização do Projeto
Você pode criar rapidamente um projeto Spring Boot usando o 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
O que é o Padrão Singleton?
O padrão Singleton garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela. Esse padrão é comumente usado para serviços como logging, gerenciamento de configuração ou conexões de banco de dados.
Como Implementar o Padrão Singleton no Spring Boot
Os beans do Spring Boot são singletons por padrão, o que significa que o Spring gerencia automaticamente o ciclo de vida desses beans para garantir que exista apenas uma instância. No entanto, é importante entender como o padrão Singleton funciona internamente, especialmente quando você não está usando beans gerenciados pelo Spring ou precisa de mais controle sobre o gerenciamento de instâncias.
Vamos passar por uma implementação manual do padrão Singleton para demonstrar como você pode controlar a criação de uma única instância em sua aplicação.
Passo 1: Criar uma Classe LoggerService
Neste exemplo, vamos criar um serviço de logging simples usando o padrão Singleton. O objetivo é garantir que todas as partes da aplicação usem a mesma instância de logging.
public class LoggerService {
// A variável estática para manter a única instância
private static LoggerService instance;
// Construtor privado para evitar a instanciação de fora
private LoggerService() {
// Este construtor está intencionalmente vazio para evitar que outras classes criem instâncias
}
// Método público para fornecer acesso à única instância
public static synchronized LoggerService getInstance() {
if (instance == null) {
instance = new LoggerService();
}
return instance;
}
// Método de exemplo para registro
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
-
Variável Estática (
instance
): Esta mantém a única instância deLoggerService
. -
Construtor Privado: O construtor é marcado como privado para evitar que outras classes criem novas instâncias diretamente.
-
Síncrono
getInstance()
Método: O método é sincronizado para torná-lo seguro para threads, garantindo que apenas uma instância seja criada mesmo se várias threads tentarem acessá-lo simultaneamente. -
Inicialização Preguiçosa: A instância é criada apenas quando é solicitada pela primeira vez (
inicialização preguiçosa
), o que é eficiente em termos de uso de memória.
Uso do Mundo Real: Este padrão é comumente utilizado para recursos compartilhados, como logging, configurações de aplicativo ou gerenciamento de conexões de banco de dados, onde você deseja controlar o acesso e garantir que apenas uma instância seja usada em toda a sua aplicação.
Passo 2: Usar o Singleton em um Controlador Spring Boot
Agora, vamos ver como podemos usar nosso Singleton LoggerService
dentro de um controlador Spring Boot. Este controlador irá expor um endpoint que registra uma mensagem sempre que for acessado.
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() {
// Acessando a instância Singleton de LoggerService
LoggerService logger = LoggerService.getInstance();
logger.log("This is a log message!");
return ResponseEntity.ok("Message logged successfully");
}
}
-
Endpoint GET: Criamos um endpoint
/log
que, quando acessado, registra uma mensagem usando oLoggerService
. -
Uso do Singleton: Em vez de criar uma nova instância de
LoggerService
, chamamosgetInstance()
para garantir que estamos utilizando a mesma instância toda vez. -
Resposta: Após o registro, o endpoint retorna uma resposta indicando sucesso.
Passo 3: Testando o Padrão Singleton
Agora, vamos testar este endpoint usando o Postman ou seu navegador:
GET http://localhost:8080/log
Saída Esperada:
-
Log no console:
[LOG] Esta é uma mensagem de log!
-
Resposta HTTP:
Mensagem registrada com sucesso
Você pode chamar o endpoint várias vezes e verá que a mesma instância de LoggerService
é usada, conforme indicado pela saída de log consistente.
Casos de Uso do Padrão Singleton no Mundo Real
Aqui estão algumas situações em que você pode querer usar o padrão Singleton em aplicações do mundo real:
-
Gerenciamento de Configuração: Garantir que sua aplicação use um conjunto consistente de configurações, especialmente quando essas configurações são carregadas de arquivos ou bancos de dados.
-
Pools de Conexão de Banco de Dados: Controle o acesso a um número limitado de conexões de banco de dados, garantindo que o mesmo pool seja compartilhado em toda a aplicação.
-
Caching: Mantenha uma única instância de cache para evitar dados inconsistentes.
-
Serviços de Registro: Como mostrado neste exemplo, use um único serviço de registro para centralizar as saídas de log em diferentes módulos de sua aplicação.
Principais Pontos
-
O padrão Singleton é uma maneira fácil de garantir que apenas uma instância de uma classe seja criada.
-
A segurança de threads é crucial se várias threads estiverem acessando o Singleton, por isso usamos
synchronized
em nosso exemplo. -
Os beans do Spring Boot já são singletons por padrão, mas entender como implementá-los manualmente ajuda a obter mais controle quando necessário.
Isto cobre a implementação e uso do padrão Singleton. Em seguida, exploraremos o padrão Factory para ver como ele pode ajudar a otimizar a criação de objetos.
O que é o Padrão Factory?
O padrão Factory permite que você crie objetos sem especificar a classe exata. Este padrão é útil quando você tem diferentes tipos de objetos que precisam ser instanciados com base em alguma entrada.
Como Implementar uma Factory no Spring Boot
O padrão Factory é incrivelmente útil quando você precisa criar objetos com base em critérios específicos, mas deseja desacoplar o processo de criação de objetos da lógica principal de sua aplicação.
Nesta seção, vamos guiar a construção de uma NotificationFactory
para enviar notificações via E-mail ou SMS. Isso é especialmente útil se você planeja adicionar mais tipos de notificação no futuro, como notificações push ou alertas in-app, sem alterar seu código existente.
Passo 1: Criar a Interface Notification
O primeiro passo é definir uma interface comum que todos os tipos de notificação irão implementar. Isso garante que cada tipo de notificação (E-mail, SMS, etc.) terá um método send()
consistente.
public interface Notification {
void send(String message);
}
-
Propósito: A interface
Notification
define o contrato para enviar notificações. Qualquer classe que implemente esta interface deve fornecer uma implementação para o métodosend()
. -
Escalabilidade: Ao usar uma interface, você pode facilmente estender sua aplicação no futuro para incluir outros tipos de notificações sem modificar o código existente.
Passo 2: Implementar EmailNotification
e SMSNotification
Agora, vamos implementar duas classes concretas, uma para enviar e-mails e outra para enviar mensagens 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);
}
}
Passo 3: Criar um NotificationFactory
A classe NotificationFactory
é responsável por criar instâncias de Notification
com base no tipo especificado. Este design garante que o NotificationController
não precisa saber os detalhes da criação do objeto.
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 (createNotification()
):
-
O método de fábrica recebe uma string (
type
) como entrada e retorna uma instância da classe de notificação correspondente. -
Instrução Switch: A instrução switch seleciona o tipo de notificação apropriado com base na entrada.
-
Tratamento de Erros: Se o tipo fornecido não for reconhecido, ele lança uma
IllegalArgumentException
. Isso garante que tipos inválidos sejam identificados precocemente.
Porque Usar um Fábrica?
-
Desacoplamento: O padrão de fábrica desacopla a criação de objetos da lógica de negócios. Isso torna seu código mais modular e mais fácil de manter.
-
Extensibilidade: Se você deseja adicionar um novo tipo de notificação, só precisa atualizar a fábrica sem alterar a lógica do controlador.
Passo 4: Usar a Fábrica em um Controlador Spring Boot
Agora, vamos juntar tudo criando um controlador Spring Boot que usa a NotificationFactory
para enviar notificações com base na solicitação do usuário.
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 {
// Criar o objeto de Notificação apropriado usando a 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
):
-
O controlador expõe um endpoint
/notify
que aceita dois parâmetros de consulta:type
(ou “EMAIL” ou “SMS”) emessage
. -
Ele usa o
NotificationFactory
para criar o tipo de notificação apropriado e envia a mensagem. -
Tratamento de Erros: Se um tipo de notificação inválido for fornecido, o controlador captura a
IllegalArgumentException
e retorna uma resposta400 Bad Request
.
Passo 5: Testando o Padrão Factory
Vamos testar o endpoint usando Postman ou um navegador:
-
Enviar uma Notificação por Email:
GET http://localhost:8080/notify?type=email&message=Olá%20Email
Saída:
Enviando Email: Olá Email
-
Enviar uma Notificação por SMS:
GET http://localhost:8080/notify?type=sms&message=Hello%20SMS
Saída:
Enviando SMS: Hello SMS
-
Testar com um Tipo Inválido:
GET http://localhost:8080/notify?type=unknown&message=Test
Saída:
Requisição Inválida: Tipo de notificação desconhecido: unknown
Casos de Uso do Padrão Factory no Mundo Real
O padrão Factory é particularmente útil em cenários onde:
-
Criação Dinâmica de Objetos: Quando é necessário criar objetos com base na entrada do usuário, como enviar diferentes tipos de notificações, gerar relatórios em diversos formatos ou lidar com diferentes métodos de pagamento.
-
Desacoplamento da Criação de Objetos: Ao usar uma fábrica, você pode manter sua lógica de negócios principal separada da criação de objetos, tornando seu código mais fácil de manter.
-
Escalabilidade: Amplie facilmente sua aplicação para suportar novos tipos de notificações sem modificar o código existente. Basta adicionar uma nova classe que implementa a interface
Notificação
e atualizar a fábrica.
O que é o Padrão Estratégia?
O padrão Strategy é perfeito quando você precisa alternar entre múltiplos algoritmos ou comportamentos dinamicamente. Ele permite que você defina uma família de algoritmos, encapsule cada um em classes separadas e os torne facilmente intercambiáveis em tempo de execução. Isso é especialmente útil para selecionar um algoritmo com base em condições específicas, mantendo seu código limpo, modular e flexível.
Caso de Uso do Mundo Real: Imagine um sistema de comércio eletrônico que precisa suportar várias opções de pagamento, como cartões de crédito, PayPal ou transferências bancárias. Ao usar o padrão Strategy, você pode facilmente adicionar ou modificar métodos de pagamento sem alterar o código existente. Essa abordagem garante que sua aplicação permaneça escalável e mantida à medida que você introduz novos recursos ou atualiza os existentes.
Vamos demonstrar esse padrão com um exemplo do Spring Boot que lida com pagamentos usando a estratégia de cartão de crédito ou PayPal.
Passo 1: Definir uma Interface PaymentStrategy
Começamos criando uma interface comum que todas as estratégias de pagamento irão implementar:
public interface PaymentStrategy {
void pay(double amount);
}
A interface define um contrato para todos os métodos de pagamento, garantindo consistência entre as implementações.
Passo 2: Implementar Estratégias de Pagamento
Crie classes concretas para pagamentos com cartão de crédito e 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 classe implementa o método pay()
com seu comportamento específico.
Passo 3: Utilizar a Estratégia em um Controlador
Crie um controlador para selecionar dinamicamente uma estratégia de pagamento com base na entrada do usuário:
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;
}
}
}
O endpoint aceita método
e valor
como parâmetros de consulta e processa o pagamento usando a estratégia apropriada.
Passo 4: Testando o Endpoint
-
Pagamento com Cartão de Crédito:
GET http://localhost:8080/pay?method=credit&amount=100
Saída:
Pago $100.0 com Cartão de Crédito
-
Pagamento PayPal:
GET http://localhost:8080/pay?method=paypal&amount=50
Saída:
Pago $50.0 via PayPal
-
Método Inválido:
GET http://localhost:8080/pay?method=bitcoin&amount=25
Saída:
Método de pagamento inválido
Use Cases para o Padrão de Estratégia
-
Processamento de Pagamentos: Alternar dinamicamente entre diferentes gateways de pagamento.
-
Algoritmos de Ordenação: Escolher o melhor método de ordenação com base no tamanho dos dados.
-
Exportação de Arquivos: Exportar relatórios em vários formatos (PDF, Excel, CSV).
Principais Pontos
-
O padrão Strategy mantém seu código modular e segue o princípio Aberto/Fechado.
-
Adicionar novas estratégias é fácil – basta criar uma nova classe implementando a interface
PaymentStrategy
. -
É ideal para cenários em que você precisa de seleção flexível de algoritmos em tempo de execução.
Em seguida, exploraremos o padrão Observer, perfeito para lidar com arquiteturas orientadas a eventos.
O que é o Padrão Observer?
O padrão Observer é ideal quando você tem um objeto (o sujeito) que precisa notificar vários outros objetos (observadores) sobre mudanças em seu estado. É perfeito para sistemas orientados a eventos onde as atualizações precisam ser enviadas para vários componentes sem criar um acoplamento rígido entre eles. Este padrão permite manter uma arquitetura limpa, especialmente quando diferentes partes do seu sistema precisam reagir a mudanças de forma independente.
Caso de Uso do Mundo Real: Este padrão é comumente utilizado em sistemas que enviam notificações ou alertas, como aplicações de chat ou rastreadores de preços de ações, onde as atualizações precisam ser enviadas aos usuários em tempo real. Ao usar o padrão Observer, você pode adicionar ou remover tipos de notificação facilmente sem alterar a lógica central.
Vamos demonstrar como implementar este padrão no Spring Boot construindo um sistema simples de notificações onde tanto notificações por Email quanto SMS são enviadas sempre que um usuário se registra.
Passo 1: Criar uma Interface Observer
Começamos definindo uma interface comum que todos os observadores irão implementar:
public interface Observer {
void update(String event);
}
A interface estabelece um contrato onde todos os observadores devem implementar o método update()
, que será acionado sempre que o sujeito mudar.
Passo 2: Implementar EmailObserver
e SMSObserver
Em seguida, criamos duas implementações concretas da interface Observer
para lidar com notificações por email e SMS.
Classe EmailObserver
public class EmailObserver implements Observer {
@Override
public void update(String event) {
System.out.println("Email sent for event: " + event);
}
}
O EmailObserver
lida com o envio de notificações por email sempre que é notificado de um evento.
Classe SMSObserver
public class SMSObserver implements Observer {
@Override
public void update(String event) {
System.out.println("SMS sent for event: " + event);
}
}
O SMSObserver
lida com o envio de notificações por SMS sempre que é notificado.
Passo 3: Crie uma Classe UserService
(O Assunto)
Agora vamos criar uma classe UserService
que atua como o assunto, notificando seus observadores registrados sempre que um usuário 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 todos os observadores registrados de um evento
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
// Método para registrar um novo usuário e notificar observadores
public void registerUser(String username) {
System.out.println("User registered: " + username);
notifyObservers("User Registration");
}
}
-
Lista de Observadores: Mantém o controle de todos os observadores registrados.
-
registerObserver()
Método: Adiciona novos observadores à lista. -
notifyObservers()
Método: Notifica todos os observadores registrados quando um evento ocorre. -
registerUser()
Método: Registra um novo usuário e aciona notificações para todos os observadores.
Passo 4: Use o Padrão Observer em um Controlador
Finalmente, criaremos um controlador Spring Boot para expor um ponto de extremidade para o registro de usuário. Este controlador irá registrar tanto o EmailObserver
quanto o SMSObserver
com o 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!");
}
}
-
Endpoint (
/register
): Aceita um parâmetrousername
e registra o usuário, acionando notificações para todos os observadores. -
Observadores: Tanto o
EmailObserver
quanto oSMSObserver
são registrados com oUserService
, para que sejam notificados sempre que um usuário se registrar.
Testando o Padrão Observer
Agora, vamos testar nossa implementação usando o Postman ou um navegador:
POST http://localhost:8080/api/register?username=JohnDoe
Saída Esperada no Console:
User registered: JohnDoe
Email sent for event: User Registration
SMS sent for event: User Registration
O sistema registra o usuário e notifica tanto os observadores de Email quanto de SMS, demonstrando a flexibilidade do padrão Observer.
Aplicações do Mundo Real do Padrão Observer
-
Sistemas de Notificação: Enviando atualizações para usuários através de diferentes canais (e-mail, SMS, notificações push) quando eventos específicos ocorrem.
-
Arquiteturas Baseadas em Eventos: Notificando múltiplos subsistemas quando ações específicas ocorrem, como atividades de usuários ou alertas do sistema.
-
Streaming de Dados: Transmitindo mudanças de dados para vários consumidores em tempo real (por exemplo, preços de ações ao vivo ou feeds de mídia social).
Como Usar a Injeção de Dependência do Spring Boot
Até agora, estivemos criando objetos manualmente para demonstrar padrões de design. No entanto, em aplicações reais do Spring Boot, a Injeção de Dependência (DI) é a maneira preferida de gerenciar a criação de objetos. A DI permite que o Spring lide automaticamente com a instância e a ligação de suas classes, tornando seu código mais modular, testável e manutenível.
Vamos refatorar nosso exemplo de padrão de Estratégia para aproveitar as poderosas capacidades de DI do Spring Boot. Isso nos permitirá alternar dinamicamente entre estratégias de pagamento, utilizando as anotações do Spring para gerenciar dependências.
Padrão de Estratégia Atualizado Usando a DI do Spring Boot
No nosso exemplo refatorado, vamos aproveitar as anotações do Spring como @Component
, @Service
e @Autowired
para simplificar o processo de injeção de dependências.
Passo 1: Anotar as Estratégias de Pagamento com @Component
Primeiro, vamos marcar as implementações de estratégia com a anotação @Component
para que o Spring possa detectá-las e gerenciá-las automaticamente.
@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
Anotação: Ao adicionar@Component
, informamos ao Spring para tratar essas classes como beans gerenciados pelo Spring. O valor da string ("creditCardPayment"
e"payPalPayment"
) atua como identificador do bean. -
Flexibilidade: Essa configuração nos permite alternar entre estratégias usando o identificador do bean apropriado.
Passo 2: Refatorar o PaymentService
para Usar Injeção de Dependência
Em seguida, vamos modificar o PaymentService
para injetar uma estratégia de pagamento específica usando @Autowired
e @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);
}
}
-
@Service
Anotação: Marca oPaymentService
como um bean de serviço gerenciado pelo Spring. -
@Autowired
: O Spring injeta automaticamente a dependência necessária. -
@Qualifier
: Especifica qual implementação dePaymentStrategy
deve ser injetada. Neste exemplo, estamos usando"payPalPayment"
. -
Facilidade de Configuração: Ao simplesmente alterar o valor do
@Qualifier
, você pode trocar a estratégia de pagamento sem alterar nenhuma lógica de negócio.
Passo 3: Utilizando o Serviço Refatorado em um Controlador
Para ver os benefícios dessa refatoração, vamos atualizar o controlador para usar nosso 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
: O controlador recebe automaticamente oPaymentService
com a estratégia de pagamento injetada. -
Endpoint GET (
/pay
): Quando acessado, processa um pagamento usando a estratégia configurada atualmente (PayPal neste exemplo).
Testando o Padrão de Estratégia Refatorado com DI
Agora, vamos testar a nova implementação usando o Postman ou um navegador:
GET http://localhost:8080/api/pay?amount=100
Resultado Esperado:
Paid $100.0 using PayPal
Se você alterar o qualificador em PaymentService
para "pagamentoCartaoCredito"
, a saída mudará de acordo:
Paid $100.0 with Credit Card
Vantagens do Uso da Injeção de Dependência
-
Desacoplamento: O serviço e o controlador não precisam saber os detalhes de como um pagamento é processado. Eles simplesmente contam com o Spring para injetar a implementação correta.
-
Modularidade: Você pode facilmente adicionar novos métodos de pagamento (por exemplo,
PagamentoTransferenciaBancaria
,PagamentoCrypto
) criando novas classes anotadas com@Component
e ajustando o@Qualifier
. - Configurabilidade: Ao aproveitar os Perfis do Spring, você pode alternar estratégias com base no ambiente (por exemplo, desenvolvimento vs. produção).
Exemplo: Você pode usar @Profile
para injetar automaticamente diferentes estratégias com base no perfil ativo:
@Component
@Profile("dev")
public class DevPaymentStrategy implements PaymentStrategy { /* ... */ }
@Component
@Profile("prod")
public class ProdPaymentStrategy implements PaymentStrategy { /* ... */ }
Principais pontos
-
Ao usar DI do Spring Boot, você pode simplificar a criação de objetos e melhorar a flexibilidade do seu código.
-
O Padrão de Estratégia combinado com DI permite alternar facilmente entre diferentes estratégias sem alterar a lógica central do seu negócio.
-
O uso de
@Qualifier
e Perfis do Spring oferece a flexibilidade de configurar sua aplicação com base em diferentes ambientes ou requisitos.
Essa abordagem não apenas deixa seu código mais limpo, mas também o prepara para configurações mais avançadas e dimensionamento no futuro. Na próxima seção, exploraremos as Melhores Práticas e Dicas de Otimização para levar suas aplicações Spring Boot para o próximo nível.
Melhores Práticas e Dicas de Otimização
Práticas Gerais Recomendadas
-
Não abuse dos padrões: Use-os apenas quando necessário. A superengenharia pode tornar seu código mais difícil de manter.
-
Prefira composição sobre herança: Padrões como Estratégia e Observador são ótimos exemplos desse princípio.
-
Mantenha seus padrões flexíveis: Aproveite interfaces para manter seu código desacoplado.
Considerações de Desempenho
-
Padrão Singleton: Garanta a segurança de threads usando
synchronized
ou oDesign Singleton Bill Pugh
. -
Padrão de Fábrica: Faça cache de objetos se forem caros de criar.
-
Padrão Observador: Use processamento assíncrono se tiver muitos observadores para evitar bloqueios.
Tópicos Avançados
-
Usando Reflexão com o padrão Factory para carregamento dinâmico de classes.
-
Aproveitando Perfis do Spring para alternar estratégias com base no ambiente.
-
Adicionando Documentação Swagger para seus pontos finais de API.
Conclusão e Principais Conclusões
No tutorial de hoje, exploramos alguns dos padrões de design mais poderosos—Singleton, Factory, Strategy e Observer—e demonstramos como implementá-los no Spring Boot. Vamos resumir brevemente cada padrão e destacar para o que ele é mais adequado:
Padrão Singleton:
-
Resumo: Garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela.
-
Melhor Para: Gerenciar recursos compartilhados, como configurações, conexões de banco de dados ou serviços de log. É ideal quando você deseja controlar o acesso a uma instância compartilhada em toda a sua aplicação.
Padrão de Fábrica:
-
Resumo: Fornece uma maneira de criar objetos sem especificar a classe exata a ser instanciada. Esse padrão desacopla a criação de objetos da lógica de negócios.
-
Melhor Para: Cenários em que você precisa criar diferentes tipos de objetos com base em condições de entrada, como enviar notificações por e-mail, SMS ou notificações push. É ótimo para tornar seu código mais modular e extensível.
Padrão de Estratégia:
-
Resumo: Permite definir uma família de algoritmos, encapsular cada um e torná-los intercambiáveis. Esse padrão ajuda a escolher um algoritmo em tempo de execução.
-
Melhor Para: Quando você precisa alternar entre diferentes comportamentos ou algoritmos dinamicamente, como processar vários métodos de pagamento em um aplicativo de comércio eletrônico. Mantém seu código flexível e adere ao Princípio Aberto/Fechado.
Padrão Observador:
-
Resumo: Define uma dependência de um para muitos entre objetos, de modo que quando um objeto muda de estado, todos os seus dependentes são notificados automaticamente.
-
Melhor Para: Sistemas orientados a eventos como serviços de notificação, atualizações em tempo real em aplicativos de chat, ou sistemas que precisam reagir a mudanças em dados. É ideal para desacoplar componentes e tornar seu sistema mais escalável.
O que vem a seguir?
Agora que você aprendeu esses padrões de design essenciais, tente integrá-los em seus projetos existentes para ver como podem melhorar a estrutura e escalabilidade do seu código. Aqui estão algumas sugestões para exploração adicional:
-
Experimente: Tente implementar outros padrões de design como Decorador, Proxy e Builder para expandir seu conjunto de ferramentas.
-
Prática: Use esses padrões para refatorar projetos existentes e aprimorar sua manutenibilidade.
-
Compartilhe: Se tiver alguma dúvida ou quiser compartilhar sua experiência, sinta-se à vontade para entrar em contato!
Espero que este guia tenha ajudado você a entender como usar efetivamente padrões de design em Java. Continue experimentando e codificando feliz!
Source:
https://www.freecodecamp.org/news/how-to-use-design-patterns-in-java-with-spring-boot/