O Strategy Pattern é um dos padrões comportamentais (Behavioral Patterns) e tem como objetivo permitir a seleção dinâmica de algoritmos em tempo de execução. Ele define uma família de algoritmos encapsulados em classes distintas, todas com uma interface comum, e permite que o código cliente escolha qual deles utilizar sem conhecer os detalhes da implementação.
Esse padrão é útil quando você tem múltiplas variações de um comportamento e quer tornar o sistema mais flexível e desacoplado. A ideia é separar o "o que fazer" (o comportamento) de "como fazer" (a implementação concreta), e poder trocar essa implementação sem alterar o restante do código.
Vamos seguir para um exemplo prático para clarear as ideias. Digamos que em um processamento de um pedido de compra é aplicado um único desconto no valor total da compra.
O que fazer = Aplicar Desconto no valor total.
Como fazer = Aplicar desconto por cupom, por fidelidade ou primeira compra.
Desta forma, definimos três formas de aplicar um desconto, agora podemos desenhar como ficará a estrutura do Strategy Pattern, separando o nosso caso de uso nos 4 conceitos abordados por esse pattern que são: Context, Strategy Interface, Concrete Strategies e Client Code.
Vamos destrinchar cada conceito mergulhando no código, por ordem de criação dos itens.
1. Strategy Interface
Começar pela definição do contrato que todas as Estratégias Concretas devem implementar, ou seja a Strategy Interface.
Para nosso caso, a interface terá duas assinaturas de métodos, canApply(Order order)
que verificará a condição da estratégia ser executada e apply(Order order)
que será a aplicação do desconto.
public interface DiscountStrategy {
boolean canApply(Order order);
void apply(Order order);
}
Como parâmetro, é recebido a entidade Order com os seguintes atributos
@Data
@NoArgsConstructor
public class Order {
private Long id;
private String couponCode;
private String product;
private Integer quantity;
private BigDecimal price;
private BigDecimal totalPrice;
private BigDecimal totalPriceWithDiscount;
private User user;
public boolean hasCouponCode() {
return couponCode != null && !couponCode.isEmpty();
}
}
2. Concrete Strategies
Em seguida, criaremos as classes concretas que implementam a Strategy Interface.
2.1 FirstPurchaseDiscount
Essa é a classe que implementa as regras do desconto por primeira compra.
O método canApply
verifica se o usuário vinculado ao pedido possui a flag firstPurchase ativa.
O método apply
executa 2 ações:
- Seta o campo
totalPriceWithDiscount
da classe Order aplicando o desconto de 10% no valor total da compra. - Desabilita o desconto chamando o método
disableFirstPurchaseDiscount()
daUserService
passando o id doUser
como parâmetro.
@Service
@RequiredArgsConstructor
public class FirstPurchaseDiscount implements DiscountStrategy {
private final UserService userService;
@Override
public boolean canApply(Order order) {
return order.getUser().isFirstPurchase();
}
@Override
public void apply(Order order) {
order.setTotalPriceWithDiscount(order.getTotalPrice().multiply(new BigDecimal("0.1"))); // 10% discount
userService.disableFirstPurchaseDiscount(order.getUser().getId());
}
}
2.2 CouponDiscount
Essa é a classe que implementa as regras do desconto por cupom.
O método canApply
verifica se a Order
possui o campo couponCode preenchido a partir do método hasCouponCode()
O método apply
executa 3 ações:
- Consulta o valor de desconto oferecido pelo cupom a partir da chamada do método
getDiscountByCouponCode(order.getCouponCode())
, passando como parâmetro o código do cupom. - Seta o campo
totalPriceWithDiscount
da classe Order aplicando o desconto retornado pela consulta anterior. - Desabilita o cupom de desconto para a conta chamando o método
disableCouponByUser(order.getCouponCode(), order.getUser());
, passando como parâmetro o código do cupom e o user id.
@Service
@RequiredArgsConstructor
public class CouponDiscount implements DiscountStrategy {
private final CouponService couponService;
@Override
public boolean canApply(Order order) {
return order.hasCouponCode();
}
@Override
public void apply(Order order) {
BigDecimal discountPercentage = couponService.getDiscountByCouponCode(order.getCouponCode());
order.setTotalPriceWithDiscount(order.getTotalPrice().multiply(discountPercentage));
couponService.disableCouponByUser(order.getCouponCode(), order.getUser().getId());
}
}
2.3 LoyaltyDiscount
Essa é a classe que implementa as regras do desconto por fidelidade.
O método canApply
verifica se o usuário vinculado ao pedido possui a flag LoyalCustomer ativa.
O método apply
executa 1 ação:
- Seta o campo
totalPriceWithDiscount
da classe Order aplicando o desconto de 5% no valor total da compra.
@Service
public class LoyaltyDiscount implements DiscountStrategy {
@Override
public boolean canApply(Order order) {
return order.getUser().isLoyalCustomer();
}
@Override
public void apply(Order order) {
order.setTotalPriceWithDiscount(order.getTotalPrice().multiply(new BigDecimal("0.05"))); // 5% discount
}
}
3. Context
O Context é a classe que mantém referência para umas lista de estratégias, seguindo a ideia de que o Context conhece as possíveis estratégias, mas não sabe qual será aplicada até o momento da execução.
Então, agora criaremos essa classe, que em resumo, gerencia as estratégias da interface DiscountStrategy
.
@Service
@RequiredArgsConstructor
public class DiscountApplyContext {
private final List<DiscountStrategy> discountStrategyList;
public void execute(Order order){
Optional<DiscountStrategy> discountApply = discountStrategyList.stream()
.filter(discount -> discount.canApply(order))
.findFirst();
.ifPresent(discount -> discount.apply(order));
}
}
3.1 Como funciona:
Com @RequiredArgsConstructor
, o Spring injeta automaticamente todas as dependências final no construtor — neste caso, uma lista de DiscountStrategy
.
O Spring detecta e injeta todas as implementações da interface DiscountStrategy
disponíveis no contexto da aplicação, preenchendo a lista discountStrategyList
.
3.2 Lógica do método execute
:
Recebe um objeto
Order
como parâmetro.Percorre a lista de estratégias de desconto (
discountStrategyList
).Para cada estratégia, chama o método
canApply(order)
para verificar se ela é aplicável ao pedido.Ao encontrar a primeira estratégia aplicável, ela é capturada como um
Optional<DiscountStrategy>.
Se nenhuma estratégia aplicável for encontrada (Optional vazio), o método encerra sem fazer nada.
Se uma estratégia for encontrada, chama-se
apply(order)
para aplicar o desconto, nesse caso, apenas uma estratégia de desconto será aplicada.
4. Client Code
Por fim, temos o Client Code, que representa a parte do sistema responsável por utilizar o Context para executar alguma lógica baseada em estratégias. O client não conhece os detalhes internos das estratégias, apenas sabe que o Context possui a capacidade de executá-las de acordo com a situação.
No exemplo abaixo, a classe OrderPaymentService
é responsável por processar o pagamento de um pedido. Antes de realizar o pagamento, o método execute(order)
chama discountApplyContext.execute(order)
, permitindo que uma estratégia de desconto apropriada seja aplicada ao pedido, caso exista.
Desta forma, a classe OrderPaymentService
se enquandra como um Client, pois utiliza o Context(DiscountApplyContext
) para aplicação de uma estratégia no seu fluxo de código.
@Service
@RequiredArgsConstructor
public class OrderPaymentService {
private final DiscountApplyContext discountApplyContext;
public void execute(Order order){
discountApplyContext.execute(order);
processPayment(order);
}
private void processPayment(Order order) {
//Processing payment for order: " + order.getId());
}
}
Conclusão
O Strategy Pattern é uma técnica eficaz para evitar estruturas complexas de if-else ou switch, promovendo um código mais limpo, modular e de fácil manutenção. Ao delegar a lógica de decisão para classes específicas, como fizemos com as estratégias de desconto, o código cliente não precisa se preocupar com as variações de comportamento. Por exemplo, se surgir uma nova regra de desconto, basta criar uma nova classe que implemente a interface DiscountStrategy e deixá-la disponível para o Spring — sem tocar na lógica do serviço principal.
Essa abordagem facilita a escalabilidade da aplicação e torna o sistema mais preparado para mudanças futuras.
Top comments (0)