DEV Community

Davi Lopes
Davi Lopes

Posted on • Edited on

Utilizando Strategy Pattern no Spring Boot

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.

Image description

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.

Image description

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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() da UserService passando o id do User 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());
    }
}
Enter fullscreen mode Exit fullscreen mode
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());
    }
}
Enter fullscreen mode Exit fullscreen mode
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
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode
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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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)