Como implementar o padrão Strategy utilizando Java e Spring

O que é o Padrão Strategy?

O padrão Strategy é um padrão de design comportamental que permite definir uma família de algoritmos, encapsulá-los e torná-los intercambiáveis. Esse padrão permite que o algoritmo varie independentemente dos clientes que o utilizam. Em outras palavras, ele permite que você escolha o algoritmo a ser usado em tempo de execução.

Benefícios do Padrão Strategy

  1. Manutenção Facilitada: Com o Strategy, você pode adicionar novas estratégias (algoritmos) sem modificar o código existente, seguindo o princípio aberto/fechado (Open/Closed Principle).
  2. Redução da Duplicação de Código: Encapsulando algoritmos em classes separadas, evitamos duplicação de código e promovemos o reuso.
  3. Melhoria na Testabilidade: Testar cada estratégia isoladamente é mais fácil do que testar uma classe com múltiplos comportamentos embutidos.
  4. Flexibilidade: Permite que os algoritmos sejam escolhidos e alterados em tempo de execução, oferecendo maior flexibilidade na aplicação.

Quando usar o Padrão Strategy?

  • Quando você tem muitas classes relacionadas que diferem apenas no comportamento.
  • Quando você precisa de diferentes variações de um algoritmo.
  • Quando os algoritmos específicos das classes podem ser expostos para que outros objetos possam interagir com eles.
  • Substituição de if e switch-case.

Exemplo de Implementação em Java

Enum para auxiliar na escolha de estratégia:
package com.example.demo.strategy;

public enum DiscountType {
    CHRISTMAS,
    NEW_YEAR,
    EASTER
}

Interface das estratégias:
package com.example.demo.strategy;

import java.math.BigDecimal;

public interface DiscountStrategy {

    BigDecimal applyDiscount(BigDecimal price);

    boolean selector(DiscountType discountType);
}
Estratégias
package com.example.demo.strategy;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class ChristmasDiscountStrategy implements DiscountStrategy{
    @Override
    public BigDecimal applyDiscount(BigDecimal price) {
        return price.multiply(BigDecimal.valueOf(0.90)); // 10% de desconto
    }

    @Override
    public boolean selector(DiscountType discountType) {
        return DiscountType.CHRISTMAS.equals(discountType);
    }
}
package com.example.demo.strategy;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class EasterDiscountStrategy implements DiscountStrategy{
    @Override
    public BigDecimal applyDiscount(BigDecimal price) {
        return price.multiply(BigDecimal.valueOf(0.80)); // 20% de desconto
    }

    @Override
    public boolean selector(DiscountType discountType) {
        return DiscountType.EASTER.equals(discountType);
    }
}
package com.example.demo.strategy;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class NewYearDiscountStrategy implements DiscountStrategy{
    @Override
    public BigDecimal applyDiscount(BigDecimal price) {
        return price.multiply(BigDecimal.valueOf(0.85)); // 15% de desconto
    }

    @Override
    public boolean selector(DiscountType discountType) {
        return DiscountType.NEW_YEAR.equals(discountType);
    }
}
Como implementar a seleção da estratégia

É muito comum ver essa seleção de estratégia sendo implementada com a inserção manual dessas mesmas estratégias na lista. Mas podemos utilizar o próprio spring pra isso, tendo declarado todas estratégias como componentes do spring. Como fizemos acima. Então quando criamos uma lista de estratégias, e pedimos o lombok pra criar um construtor com argumentos necessários, as estratégias já são inseridas automaticamente, sem a necessidade de inserção manual.

package com.example.demo.service;

import com.example.demo.strategy.DiscountStrategy;
import com.example.demo.strategy.DiscountType;
import jakarta.el.MethodNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class DiscountService {

    private final List<DiscountStrategy> strategies;
    public DiscountStrategy getStrategy(DiscountType discountType){
        return strategies.stream().filter(strategy -> strategy.selector(discountType))
                .findFirst()
                .orElseThrow(MethodNotFoundException::new);
    }
}

Conclusão

O padrão Strategy é uma poderosa ferramenta para separar os algoritmos das classes que os utilizam, permitindo flexibilidade e facilidade de manutenção. É especialmente útil em situações onde você precisa de múltiplas variações de um algoritmo e deseja selecionar qual usar em tempo de execução.

Código completo: https://github.com/nathalia-amarals/strategy-java-spring-boot

Referências:

Como configurar circuit breaker com openfeign e resilience4j

Se você está desenvolvendo um microserviço com Spring Boot e precisa se comunicar com outros serviços via HTTP, o OpenFeign é uma excelente escolha. Ele simplifica a criação de clientes HTTP declarativos. Além disso, quando estamos lidando com comunicação entre serviços, é essencial implementar padrões de tolerância a falhas, como o Circuit Breaker. Para isso, o Resilience4J é uma biblioteca robusta que pode ser facilmente integrada ao Spring Boot.

Neste post, vamos configurar e utilizar o OpenFeign e o Circuit Breaker do Resilience4J em um projeto Spring Boot com Gradle, utilizando Java 21. Também veremos por que é vantajoso usar Gradle com Kotlin ao invés de Groovy.

O que é um circuit breaker?

Um circuit breaker é essencial em comunicações síncronas REST para garantir a resiliência e robustez do sistema. Ele monitora interações entre componentes e bloqueia temporariamente solicitações a serviços com falhas ou alta latência, prevenindo sobrecargas e falhas em cascata. Isso permite que o sistema continue operando, melhora a latência percebida pelo usuário e fornece visibilidade sobre a saúde dos serviços. Além disso, o circuit breaker testa periodicamente os serviços problemáticos, reativando-os quando voltam a funcionar corretamente, o que garante uma recuperação controlada e eficiente. Possui também um esquema de fallback, que permite tratar APIs problemáticas.

Configuração do Projeto

  1. Inicializando o Projeto Spring Boot: Para começar, vamos criar um novo projeto Spring Boot. Você pode usar o Spring Initializr (https://start.spring.io/) para gerar um esqueleto de projeto com as dependências básicas. Selecione as seguintes dependências:
    • Spring Web
    • Spring Cloud OpenFeign
    • Spring Cloud Starter Circuit Breaker Resilience4J
  2. Como estamos utilizando Gradle, certifique-se de escolher esta opção no Initializr.
  3. Configuração do Gradle com Kotlin DSL: No arquivo build.gradle.kts (Gradle Kotlin DSL), adicione as seguintes dependências:
dependencies {

    implementation("org.springframework.boot:spring-boot-starter-web")
    // https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign
    implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1")
    // https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-circuitbreaker-resilience4j
    implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j:3.1.1")
}

Configurando OpenFeign

  1. Habilitando o OpenFeign no Spring Boot: No arquivo principal da aplicação (Application.java), habilite o Feign adicionando a anotação @EnableFeignClients.
  2. Criando um Cliente Feign: Crie uma interface Feign para definir o cliente HTTP. Por exemplo:
@FeignClient(name = "metMuseumClient", url = "https://collection.metmuseum.org/public/collection/v1")
public interface MetMuseumClient {

    @GetMapping("/objects/{object_id}")
    ObjectResponse getObject(@PathVariable("object_id")int objectId);
}

Configurando o Circuit Breaker com Resilience4J

  1. Habilitando o Circuit Breaker no Feign: Configure o Feign para utilizar o Circuit Breaker do Resilience4J. Adicione as configurações no arquivo application.yml.
  2. Configurando o Feign com Circuit Breaker: Adicione a configuração para habilitar o Resilience4J Circuit Breaker nas chamadas Feign. Crie um configurador Feign na sua aplicação:

Criando uma Classe Service

@Service
@RequiredArgsConstructor
public class ArtObjectService {

    private final MetMuseumClient metMuseumClient;

    public ObjectResponse getObject(int objectId) {
        return metMuseumClient.getObject(objectId);
    }
}

Criando um RestController para Acionar o Cliente Feign

Para expor um endpoint REST que utilize o serviço criado, crie um controlador REST. Este controlador chamará o serviço que, por sua vez, utilizará o cliente Feign para obter os dados dos objetos do met museum.

@RestController
@RequestMapping("/met")
@RequiredArgsConstructor
public class ObjectController {

    private final ArtObjectService artObjectService;

    @GetMapping("/object/{object_id}")
    public ResponseEntity getObject(@PathVariable("object_id") int objectId){
        return  ResponseEntity.ok(artObjectService.getObject(objectId));
    }
}

Criando as configurações do Cliente Feign

spring:
  cloud:
    circuitbreaker:
      resilience4j:
        enabled: true
    openfeign:
      circuitbreaker:
        enabled: true
        alphanumeric-ids:
          enabled: true
      client:
        config:
          default:
            loggerlevel: full
            connectTimeout: 1000
            readTimeout: 1000

resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        slidingWindowSize: 50 #como é baseado em tempo, ficará 50 segundos em monitoramento
        permittedNumberOfCallsInHalfOpenState: 3
        slidingWindowType: TIME_BASED #janela a ser monitorada
        minimumNumberOfCalls: 5
        waitDurationInOpenState: 500s
        failureRateThreshold: 3
        eventConsumerBufferSize: 10

Por que usar Gradle com Kotlin DSL ao invés de Groovy DSL

  1. Sintaxe Melhorada e Tipagem Estática: A sintaxe do Kotlin DSL é mais concisa e oferece tipagem estática, o que ajuda a detectar erros em tempo de compilação. Isso torna o processo de desenvolvimento mais seguro e reduz a probabilidade de erros em tempo de execução.
  2. Melhor Integração com IDEs: As IDEs, especialmente o IntelliJ IDEA, oferecem suporte superior para Kotlin DSL em comparação com Groovy DSL. A autocompletação, navegação de código e refatoração são mais eficientes e confiáveis, proporcionando uma melhor experiência de desenvolvimento.
  3. Consistência no Ecossistema Kotlin: Para equipes que já estão usando Kotlin em seus projetos, utilizar o Kotlin DSL para Gradle traz consistência no ecossistema de desenvolvimento. Isso facilita a curva de aprendizado e promove a reutilização de conhecimentos e práticas.
  4. Recursos Modernos do Kotlin: O Kotlin DSL aproveita recursos modernos do Kotlin, como lambdas, extensões de função e outras características avançadas, que permitem escrever scripts de build mais poderosos e expressivos.
  5. Manutenção a Longo Prazo: Gradle está investindo continuamente no Kotlin DSL, o que indica um forte suporte e melhorias futuras. Adotar o Kotlin DSL pode ser uma escolha estratégica para a manutenção e evolução a longo prazo dos scripts de build.

Conclusão

Configurar OpenFeign e o Circuit Breaker do Resilience4J em um projeto Spring Boot com Gradle e Java 21 é uma abordagem moderna e eficiente para criar microserviços resilientes. Usar Gradle com Kotlin DSL oferece várias vantagens sobre o Groovy, incluindo melhor suporte de IDE, tipagem estática e uma sintaxe mais clara e concisa. Experimente esta configuração em seu próximo projeto e veja como ela funciona.

Viu algum erro, tem alguma sugestão? Comenta aí.

Quer ver o código na íntegra? https://github.com/nathalia-amarals/openfeignresilienceconfig

Bibliografia:

Como configurar o swagger 3.0 (OpenAPI) no Spring Boot

Adicione a dependêcia no pom (caso utilize maven):

<!-- Swagger UI -->
<dependency>
   <groupId>org.springdoc</groupId>
   <artifactId>springdoc-openapi-ui</artifactId>
   <version>1.6.4</version>
</dependency>
<dependency>
   <groupId>org.springdoc</groupId>
   <artifactId>springdoc-openapi-data-rest</artifactId>
   <version>1.6.4</version>
</dependency>

Então é só acessar a página <endereço-da-sua-aplicação>/swagger-ui/index.html#/ exemplo http://localhost:8080/swagger-ui/index.html#/

É isso aí, simples assim.

[Dicas de Programação] Java 8 – Functional Interfaces

Uma das novas funcionalidades introduzidas por Java 8 são as Interfaces funcionais (functional interfaces).

Elas são interfaces que possuem um único método abstrato. Porém, métodos sobrescrevendo métodos da classe java.lang.Object não são considerados métodos da própria interface. E default method não é considerado método abstrato. Portanto ao utilizar esses métodos a interface não deixa de ser uma interface funcional.

Pode-se utilizar a annotation @FunctionalInterface para garantir que uma Interface não deixará de ser uma Interface funcional.

Exemplo de Interface Funcional:

@FunctionalInterface
public interface TestingFunctionalInterface {
    String fiMethod(String test);
}

O Java 8 já possui algumas interfaces funcionais prontas. As mais famosas são:

  1. Consumer
    Recebe um objeto/valor, faz algo com ele. Mas não retorna nada.
    Ex: foreach( )
  2. BiConsumer
    Funciona igual ao Consumer, porém ao invés de receber um único valor, recebe dois. Faz algo com eles, e não retorna nada.
  3. Supplier
    Não recebe nenhum argumento, porém devolve objetos/valores.
    Ex: Stream.generate( )
  4. Predicate
    Recebe 1 argumento e retorna true ou false.
    Ex: stream( ). filter( )
  5. BiPredicate
    Igualmente ao predicate retorna true ou false, porém recebe 2 argumentos.
  6. Function
    Recebe 1 argumento e retorna um objeto/valor.
    Ex: stream( ).map( )
  7. BiFunction
    Assim como a function retorna um objeto/valor, mas recebe 2 argumentos.
  8. UnaryOperator
    Igual a function, porém o argumento deverá ser do mesmo tipo do retorno.
  9. BinaryOperator
    Igual a BiFunction, porém os 2 argumentos de entrada e o retorno todos devem ter o mesmo tipo.
    Ex: stream( ).reduce( )
 public static void main(String[] args) {

        List<String> testList = new ArrayList<>();

        List<String> stringList = Arrays.asList("A","B","C","D","E");

        String test = Stream.generate(() -> "x").limit(110).collect(Collectors.joining());//Supplier
        System.out.println(test);

        testList = stringList.stream().filter(word -> word.equals("A")).collect(Collectors.toList()); //Predicate

        testList.forEach(word -> System.out.println(word)); //Consumer

        stringList.stream().map(word -> word.equals("A")); //Function

        stringList.stream().reduce((w1,w2) -> w2 + w1); //Reduce
    }

Algumas interfaces anteriores ao Java 8 também acabaram se enquadrando na categoria de Interfaces Funcionais, por possuírem a estrutura requisitada, são elas:

  • Runnable
  • Callable

Espero que esse conteúdo tenha te ajudado em algo.

Se tem alguma sugestão para melhorar esse artigo, ou encontrou algum erro nele, por favor deixe um comentário, que eu corrijo. Se gostou, por favor compartilhe nas suas redes e ajude o blog.

Te vejo na próxima!

Fontes:
https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html

https://www.youtube.com/watch?v=Ht0eto2mEpc

https://www.baeldung.com/java-8-lambda-expressions-tips

https://www.baeldung.com/java-8-functional-interfaces

[Dicas de Programação] Java 8 – Default Methods

Default Methods foram adicionados entre as funcionalidades que podem ser utilizadas nas Interfaces no Java 8. Antes disso toda classe que implementava uma Interface deveria implementar todos os métodos “assinados” por ela, para atender ao contrato. Isso várias vezes gerava muito código repetitivo por causa dos métodos.

Uma solução para isso era utilizar o design pattern Strategy. Criando uma outra classe somente para implementar esse método repetitivo.

Outra solução seria utilizar uma classe abstrata, ao invés de uma interface. Lembrando que ao se utilizar uma classe abstrata não temos garantia que as classes filhas implementarão todos os métodos da classe mãe, podendo levar a desencontros.

Os Default Methods surgem para dar uma solução mais simples para esse problema. Permitindo que possamos realizar a implementação de um método direto na Interface, sem a necessidade de que todas as suas filhas implementem esses métodos específicos, garantindo pelo menos uma implementação padrão (default) pra ele. Deixando para as classes filhas decidirem se querem ou não uma implementação diferente para esse método padrão (default method).

Exemplo:

public interface DefaultInterface {
    default String defaultMethod(){
        return "HelloWorld";
    }
    
    String method();
}

public class ImplementsDefaultInterface implements DefaultInterface{

    @Override
    public String method() {
        return "method";
    }
}

Espero que esse conteúdo tenha te ajudado em algo.

Se tem alguma sugestão para melhorar esse artigo, ou encontrou algum erro nele, por favor deixe um comentário, que eu corrijo. Se gostou, por favor compartilhe nas suas redes e ajude o blog.

Te vejo na próxima!

Update1: Como sugestão de um pessoal foda, uma outra vantagem que ficou de fora é: Poder adicionar novos métodos em Interfaces existentes que já foram implementadas por outras classes, sem quebrar nada.

Referência: https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html