Strategy Pattern no Spring Boot Usando Enum

nenhumrafael

Rafael Lemes

Posted on October 21, 2022

Strategy Pattern no Spring Boot Usando Enum

Recentemente me passaram uma Feature que se encaixava muito bem com a utilização do Strategy Pattern, decidi colocar a mão na massa e logo me deparei com os seguintes desafios:

  • Implementar essa solução aproveitando os Recursos de Injeção de Dependência do Spring com Inversão de Controle mantendo-a o mais desacoplado possível
  • Obter a instância gerenciada do Spring da implementação da Estratégia em tempo de execução
  • Não a criar Instâncias da Estratégia que não será utilizada na memória desnecessariamente sobrecarregando a mesma.
  • Criar um factory simples e de fácil extensão que irá entregar a instância da Estratégia baseado em um identificador que será passado como parâmetro em uma requisição REST

Como em uma pesquisa rápida, não achei nenhum material em português, resolvi acabar com o medo de escrever e criei um projetinho de exemplo.
Para quem não tem paciência em ler artigos e já quer ver o código é só clicar em: Talk is Cheap. Show me the Code.

Antes de Seguirmos, gostaria de deixar claro que esse texto:

  • Assume que o leitor já tenha um conhecimento médio ou Familiaridade com Java e Spring Boot
  • Não tem a intenção de ensinar os conceitos básico do Design Pattern ou do Spring Boot
  • Tem como objetivo ser uma referência direta e rápida de uma maneira de implementar o Pattern
  • Explicar o motivo de algumas decisões de Design

Vamos lá.

Uma característica dessa Feature (Nesse caso é um requisito não funcional ligado ao design da aplicacão), é que essa 'Estratégia' só pode ser usado por seu 'Caso de Uso' correspondente, logo, para evitar o vazamento dessa abstração para outros casos de uso dentro da mesma aplicação, todas as classes e interfaces ficarão dentro do mesmo pacote, e com exceção da Interface 'FooUseCase' que será 'public', as demais classes terão o modificador de acesso como 'default' de modo a restringir o acesso por outras classes fora do Pacote.
Obs: Esta é só uma forma de implementação não é uma restrição, se não houver esse tipo de 'característica' em sua feature, pode-se deixar todas as classes como 'public').
Segue abaixo a estrutura de Pacote:

Image description

Dito isso, vamos por partes:

Primeiramente, cria-se uma Interface que irá representar o 'Contrato', em outras palavras, a estrutura/comportamento padrão da 'Estratégia' que será utilizado pelo 'Client'(Classe que irá efetivamente utilizá-la, no nosso caso a Classe que representa o Caso de uso em si)



package br.com.lemes.enumstrategy.usecase.foo;

interface FooStrategy {
    String execute();
}


Enter fullscreen mode Exit fullscreen mode

Em seguida, as classes que implementarão cada Estratégia:



package br.com.lemes.enumstrategy.usecase.foo;

import org.springframework.stereotype.Component;
@Component
final class FooStrategyImpl1 implements FooStrategy{

     @Override
     public String execute() {
         return "Foo Strategy Impl 1";
     }
 }


Enter fullscreen mode Exit fullscreen mode


package br.com.lemes.enumstrategy.usecase.foo;

import org.springframework.stereotype.Component;

@Component
final class FooStrategyImpl2 implements FooStrategy{

     @Override
     public String execute() {
         return "Foo Strategy Impl 2";
     }
 }


Enter fullscreen mode Exit fullscreen mode


package br.com.lemes.enumstrategy.usecase.foo;

import org.springframework.stereotype.Component;

@Component
final class FooStrategyImpl3 implements FooStrategy{

     @Override
     public String execute() {
         return "Foo Strategy Impl 3";
     }
 }



Enter fullscreen mode Exit fullscreen mode

Em seguida, vamos criar a Interface do UseCase e sua Classe Concreta que irá Implementa-la:



package br.com.lemes.enumstrategy.usecase.foo;

public interface FooUseCase {
    String call(String id);
}


Enter fullscreen mode Exit fullscreen mode


package br.com.lemes.enumstrategy.usecase.foo;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;

@Service
@RequiredArgsConstructor
final class FooUseCaseImpl implements FooUseCase {

    @NonNull
    private final ApplicationContext context;


    @Override
    public String call(String id){

        Objects.requireNonNull(id);
        FooEnumStrategy fooEnumStrategy = FooEnumStrategy
                .findById(id)
                .orElseThrow(IllegalArgumentException::new); //Could be a Business Exception

        FooStrategy impl = fooEnumStrategy.getImpl(context);

        return impl.execute();
    }



    private enum FooEnumStrategy{

        IMPL1("Foo1", FooStrategyImpl1.class),
        IMPL2("Foo2", FooStrategyImpl2.class),
        IMPL3("Foo3", FooStrategyImpl3.class);

        FooEnumStrategy(String id, Class<? extends FooStrategy> impl) {
            this.id = id;
            this.impl = impl;
        }

        final String id;
        final Class<? extends FooStrategy> impl;

        public static Optional<FooEnumStrategy> findById(String id){


            Objects.requireNonNull(id);
            Supplier<Stream<FooEnumStrategy>> fooEnumStrategyStream
                    = () -> Stream.of(values())
                    .filter(strategy -> strategy.id.equals(id));


            if(fooEnumStrategyStream.get().count() > 1){
                fooEnumStrategyStream
                        .get()
                        .findFirst()
                        .ifPresent(strategy -> {
                            throw new RuntimeException(
String.format("Same id For two or more different Strategy: %s ", strategy.id));
                        });
            }


            return fooEnumStrategyStream.get().findFirst();
        }

        public FooStrategy getImpl(ApplicationContext context) {

            return context.getBean(impl);
        }
    }

}


Enter fullscreen mode Exit fullscreen mode

Considerações Importantes:

  • Foi uma decisão de design criar a Enum _que servirá como Factory como uma 'Inner Class' com o modificador de acesso 'private', uma vez que, como fora supracitado, essas 'Estratégias' só devem ser usadas dentro do Caso de Uso. Também seria possível criar essa _Enum _em um arquivo separado mantendo o modificador dela como _'Default' mantendo-a inacessível para outras classes fora do pacote de forma a atender a restrição.

Image description

  • Todas as classes concretas estão marcadas como 'final' que é para garantir que não haja extensibilidade das mesmas. Cada 'Estratégia' deve ter sua própria regra, e se por ventura uma estratégia depender de outra, a ideia é que essa dependência seja explicitada em forma de composição, embora seja bem improvável a necessidade disso, mas dessa maneira ainda é possível aplicar o Proxy Pattern. Exemplo:


@Component
final class FooStrategyImpl1 implements FooStrategy{

     @Override
     public String execute() {
         return "Foo Strategy Impl 1";
     }
 }


Enter fullscreen mode Exit fullscreen mode
  • A anotação @RequiredArgsConstructor do Lombok cria um construtor em tempo de compilação que recebe como parâmetro as variáveis declaradas como final, obrigando o Injetor de dependência a construir a Classe de implementação com todas as suas dependências. Esta é uma maneira de implementar a Inversão de controle garantindo o Estado da Classe. Vale ressaltar que: Declarar a Variável utilizando o @Autowired apenas indica para o Spring injetar a Dependência, isso não é inversão de controle e provavelmente deixará seus testes um pouco mais complicado

Image description

  • Foi decidido usar Optional no método findById da FooEnumStrategy pois o cliente poderia passar por parâmetro para o Enum um id inexistente, dessa forma fica a cargo da classe que implementa o 'Caso de uso' decidir a melhor maneira de lidar caso não haja uma 'Estratégia'. No caso do exemplo em questão, está lançando um IllegalArgumentException como pode-se ver abaixo, mas poderia ser um erro de negócio, ou retornar um 404 para o Client Rest, enfim, o que a feature pedir =).


@Override
    public String call(String id){

        Objects.requireNonNull(id);
        FooEnumStrategy fooEnumStrategy = FooEnumStrategy
                .findById(id)
                .orElseThrow(IllegalArgumentException::new); //Could be a Business Exception

        FooStrategy impl = fooEnumStrategy.getImpl(context);

        return impl.execute();
    }


Enter fullscreen mode Exit fullscreen mode
  • Ao decidir não utilizar a Ordenação natural do Enum, conforme o projeto for evoluindo e novas 'Estratégias' forem sendo adicionada, corre o risco do desenvolvedor erroneamente criar dois itens do Enum com o mesmo Identificador, gerando o que chamamos de 'Erro Silencioso' pois o Enum pode entregar a 'Estratégia' errada para o Caso de Uso, em outras palavras, um bug que compilará normalmente e que pode ser percebido tarde demais pois não dará uma Exceção explicita. Portanto, para evitar isso o método findById da Enum faz uma validação da quantidade elementos com o id que foi passado por parâmetro e se houver mais que um lançará uma 'RuntimeException' descrevendo o erro e apontando qual identificador está repetido. Pode haver o caso de haver dois identificadores diferentes utilizando a mesma 'Estratégia' e não necessariamente ser um erro, nesse caso não consegui pensar em muito no que fazer =/ Exemplo:


public static Optional<FooEnumStrategy> findById(String id){


            Objects.requireNonNull(id);
            Supplier<Stream<FooEnumStrategy>> fooEnumStrategyStream
                    = () -> Stream.of(values())
                    .filter(strategy -> strategy.id.equals(id));


            if(fooEnumStrategyStream.get().count() > 1){
                fooEnumStrategyStream
                        .get()
                        .findFirst()
                        .ifPresent(strategy -> {
                            throw new RuntimeException(
String.format("Same id For two or more different Strategy: %s ", strategy.id));
                        });
            }


            return fooEnumStrategyStream.get().findFirst();
        }


Enter fullscreen mode Exit fullscreen mode
  • O método getBean da ApplicationContext que tu encontra no trecho abaixo:


 public FooStrategy getImpl(ApplicationContext context) {

            return context.getBean(impl);
        }


Enter fullscreen mode Exit fullscreen mode

também aceita como parâmetro uma String que representa o nome do Bean, Este também é um jeito de implementar esse 'Factory'. Ficaria mais fácil de aplicar um Proxy Pattern na 'Estratégia' utilizando os recursos da anotação @Qualifier sem precisar mexer na Enum, entretanto deixaria de ser 'Type Safe', permitindo passar qualquer bean ou até mesmo um nome errado, resultando em um erro de ClassCastException ou BeansException em tempo de Execução. Em todo caso, ainda é possível aplicar o Proxy Pattern mas precisaria alterar a Enum passando como parâmetro do Elemento a Classe do Proxy (não é o melhor dos mundos mas é o que tem pra hoje kkk).
por exemplo:



IMPL4("Foo2", ProxyFooStrategyImpl2.class),


Enter fullscreen mode Exit fullscreen mode

Bom, é isso. Disse que iria ser rápido mas percebi que escrevi demais. Paciência. --\O/--

Segue Links para Referência:

💖 💪 🙅 🚩
nenhumrafael
Rafael Lemes

Posted on October 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related