Article image
Giovanni Rozza
Giovanni Rozza21/06/2023 11:49
Compartilhe

Boas práticas de programação Java usando Generics e Reflection

  • #Spring Framework
  • #Java

Pessoal, gostaria de compartilhar com vocês a evolução em termos de boas práticas de programação de um endpoint que desenvolvi usando REST e Spring. O endpoint é simples, apenas um CRUD que opera sobre a tabela de formas de pagamento. No momento a tabela é bem simples e a classe que a modela é a seguinte:

@Entity
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class FormaPagamento {
  @EqualsAndHashCode.Include
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(nullable = false)
  private String descricao;


}

Vou focar na camada do controlador (CONTROLLER) onde faço a interface com os requests HTTP, nesta camada existem 2 classes que manipulo:

@Getter
@Setter
public class FormaPagamentoDto {


  private Long id;
  private String descricao;
}

Esta classe eu mapeio da entidade FormaPagamento para enviar as respostas em Json das requisições HTTP:

@Getter
@Setter
public class FormaPagamentoInput {
  @NotBlank
  private String descricao;


}

Esta classe eu mapeio para a entidade FormaPagamento o corpo em Json das requisições HTTP POST e PUT .

Finalmente a minha classe controller original para lidar com as requisições:

@RestController
@RequestMapping(value = "/formaspagamento")
public class FormaPagamentoController {


  private final FormaPagamentoService formaPagamentoService;
  private final ModelMapper modelMapper;


    public FormaPagamentoController(FormaPagamentoService formaPagamentoService,ModelMapper modelMapper) {
      this.formaPagamentoService = formaPagamentoService;
      this.modelMapper 	= modelMapper;


    }

  @GetMapping
  public List<FormaPagamentoDto> listar() {

      return formaPagamentoService.listar().stream()
      .map(formaPgto-> modelMapper.map(formaPgto, FormaPagamentoDto.class))
      .collect(Collectors.toList());

  }
  @GetMapping("/{fomaPagamentoId}")
  public FormaPagamentoDto buscar(@PathVariable Long fomaPagamentoId) {

      return modelMapper.map(formaPagamentoService.buscarOuFalhar(fomaPagamentoId),FormaPagamentoDto.class);

  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public FormaPagamentoDto adicionar(@RequestBody @Valid FormaPagamentoInput fomaPagamentoInput) {

      FormaPagamento formaPagamento  = formaPagamentoService.salvar(modelMapper.map(fomaPagamentoInput,FormaPagamento.class)); 
      return  modelMapper.map(formaPagamento,FormaPagamentoDto.class) ;


  }
  @PutMapping("/{fomaPagamentoId}")
  public FormaPagamentoDto atualizar(@PathVariable Long fomaPagamentoId, @RequestBody @Valid  FormaPagamentoInput fomaPagamentoInput)
  {        
  
      FormaPagamento formaPagamento  = formaPagamentoService.buscarOuFalhar(fomaPagamentoId);
      modelMapper.map(fomaPagamentoInput,formaPagamento);
      return  modelMapper.map(formaPagamentoService.salvar(formaPagamento),FormaPagamentoDto.class) ;
 
  }


  @DeleteMapping("/{fomaPagamentoId}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void remover(@PathVariable Long fomaPagamentoId) {


          formaPagamentoService.excluir(fomaPagamentoId);
  }


}

Na classe controller eu procuro não usar o annotation Autowired, pois a anotation dificulta a construção de testes via Junit, prefiro criar o construtor, além de ajudar a legibilidade do código. Percebam que eu também usei o ModelMapper para fazer o mapeamento entre as classes FormaPagamento, FormaPagamentoInput e FormaPagamentoDto. Apesar de estar legal, da pra melhorar né? Todas essas chamadas repetidas do bean do ModelMapper, não seria melhor encapsular em uma função?

Bom podemos fazer isso, criando as seguintes classes:

@Component
public class FormaPagamentoDtoAssembler {


  private final ModelMapper modelMapper;


      public FormaPagamentoDtoAssembler( ModelMapper modelMapper){
          this.modelMapper = modelMapper;
      }
  public FormaPagamentoDto toDto(FormaPagamento formaPagamento) {
      return modelMapper.map(formaPagamento, FormaPagamentoDto.class);
  }
  
  public List<FormaPagamentoDto> toCollectionDto(List<FormaPagamento> formasPagamentos) {
      return formasPagamentos.stream()
              .map(formaPgto-> toDto(formaPgto))
              .collect(Collectors.toList());
  }
  
}

A classe acima mapeia a entidade FormaPagamento para seu DTO FormaPagamentoDto.

@Component
public class FormaPagamentoInputDisassembler {


   
  private final ModelMapper modelMapper;


      public FormaPagamentoInputDisassembler(ModelMapper modelMapper){
          this.modelMapper = modelMapper;
      }
  
  public FormaPagamento toFormaPagamento(FormaPagamentoInput formaPagamentoInput) {
      return modelMapper.map(formaPagamentoInput, FormaPagamento.class);
  }
  
  public void copyToFormaPagamento(FormaPagamentoInput formaPagamentoInput, FormaPagamento formaPagamento) {
      modelMapper.map(formaPagamentoInput, formaPagamento);
  }   
}

A classe acima mapeia a FormaPagamentoInput para a entidade FormaPagamento. E finalmente a classe FormaPagamentoController ficaria:

@RestController
@RequestMapping(value = "/formaspagamento")
public class FormaPagamentoController {


  private final FormaPagamentoService formaPagamentoService;
  private final FormaPagamentoDtoAssembler formaPgtoDtoAssembler;
  private final FormaPagamentoInputDisassembler  formaPgtoInputDissasembler;


    public FormaPagamentoController(FormaPagamentoService formaPagamentoService,
                                  FormaPagamentoDtoAssembler formaPgtoDtoAssembler,
                                  FormaPagamentoInputDisassembler formaPgtoInputDissasembler ) {
          this.formaPagamentoService = formaPagamentoService;
          this.formaPgtoDtoAssembler = formaPgtoDtoAssembler;
          this.formaPgtoInputDissasembler = formaPgtoInputDissasembler;


    }

  @GetMapping
  public List<FormaPagamentoDto> listar() {

  return formaPgtoDtoAssembler.toCollectionDto(formaPagamentoService.listar());

  }

  @GetMapping("/{formaPagamentoId}")
  public FormaPagamentoDto buscar(@PathVariable Long formaPagamentoId) {

      return formaPgtoDtoAssembler.toDto(formaPagamentoService.buscarOuFalhar(formaPagamentoId));

  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public FormaPagamentoDto adicionar(@RequestBody @Valid FormaPagamentoInput formaPagamentoInput) {

    FormaPagamento formaPagamento  = formaPagamentoService.salvar(formaPgtoInputDissasembler.toFormaPagamento(formaPagamentoInput));
    return formaPgtoDtoAssembler.toDto(formaPagamento);		
 
  }
  @PutMapping("/{formaPagamentoId}")
  public FormaPagamentoDto atualizar(@PathVariable Long formaPagamentoId, @RequestBody @Valid  FormaPagamentoInput formaPagamentoInput)
  {        
        FormaPagamento formaPagamento  = formaPagamentoService.buscarOuFalhar(formaPagamentoId);
        formaPgtoInputDissasembler.copyToFormaPagamento(formaPagamentoInput,formaPagamento);
        return formaPgtoDtoAssembler.toDto(formaPagamento);		
 
  }

  @DeleteMapping("/{formaPagamentoId}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void remover(@PathVariable Long formaPagamentoId) {

          formaPagamentoService.excluir(formaPagamentoId);
  }


}

Ficou mais limpo e legivel o código não? Todas aquelas conversõe do ModelMapper estão agora encapsuladas nos métodos das duas classes FormaPagamentoInputDisassembler e FormaPagamentoDtoAssembler . Mas percebam que a medida que vou criando mais entidades, eu vou ter que criar mais classes do tipo EntidadeDtoAssembler e EntidadeInputDisassembler que fazem praticamente a mesma coisa, eu apenas desloquei a repetição de código pra outro lugar. Se eu pudesse ainda mais centralizar essa conversões e só precisasse gerar classes que herdam essas conversões? Bom ai que entra dois recursos poderosos do Java: Reflexão (Reflection) e Genéricos (Generics).

Antes uma pequena introdução teórica sobre esses recursos:

Reflexão em Java é um recurso poderoso que permite que um programa Java examine e modifique sua própria estrutura interna em tempo de execução. Com a reflexão, é possível obter informações sobre classes, métodos, campos e construtores em tempo de execução, bem como manipular esses elementos.

A reflexão em Java é realizada usando a classe java.lang.reflect, que fornece métodos para acessar e manipular informações sobre classes e objetos em tempo de execução. Alguns dos recursos que a reflexão em Java oferece incluem:

  1. Obter informações sobre uma classe: É possível obter informações sobre uma classe, como seu nome, superclasse, interfaces implementadas, métodos, campos e construtores.
  2. Criar instâncias de classes dinamicamente: Com a reflexão, é possível criar instâncias de classes em tempo de execução, mesmo que o nome da classe seja desconhecido em tempo de compilação.
  3. Acessar e modificar campos: A reflexão permite acessar e modificar os valores dos campos de uma classe, mesmo que eles sejam privados.
  4. Invocar métodos: É possível invocar métodos de uma classe em tempo de execução, mesmo que o nome do método seja desconhecido em tempo de compilação.
  5. Manipular arrays: A reflexão permite criar e manipular arrays em tempo de execução.

Generics, ou "genéricos" em português, é um recurso poderoso da linguagem Java que permite criar classes, interfaces e métodos que podem ser parametrizados para trabalhar com diferentes tipos de dados. Com os genéricos, é possível escrever código flexível e reutilizável, que pode ser adaptado para lidar com diferentes tipos de objetos.

Os genéricos em Java fornecem os seguintes benefícios:

  1. Reutilização de código: Com os genéricos, é possível escrever classes e métodos que podem ser usados com diferentes tipos de dados, evitando a necessidade de duplicar código para cada tipo específico.
  2. Segurança de tipo: Os genéricos fornecem verificação de tipo em tempo de compilação, garantindo que apenas os tipos corretos sejam usados com as classes e métodos genéricos. Isso ajuda a evitar erros de tipo em tempo de execução.
  3. Maior legibilidade: O uso de genéricos torna o código mais legível, pois os tipos de dados são especificados explicitamente, tornando mais fácil entender o propósito e a funcionalidade do código.
  4. Desempenho: Os genéricos em Java não têm impacto no desempenho em tempo de execução, pois o compilador realiza a verificação de tipo em tempo de compilação e gera código otimizado.

Os genéricos são amplamente utilizados em coleções, como Listas, Conjuntos e Mapas, permitindo que essas estruturas de dados sejam parametrizadas para trabalhar com diferentes tipos de objetos.

Por exemplo, a classe ArrayList<T> é uma implementação genérica da interface List<T>, onde o tipo T representa o tipo de objeto que a lista irá armazenar. Isso permite que a mesma classe ArrayList seja usada para armazenar diferentes tipos de objetos, como Integer, String, etc.

Os genéricos em Java são uma ferramenta poderosa para criar código flexível, seguro e reutilizável. Eles são amplamente utilizados em bibliotecas e estruturas de dados da linguagem Java.

Vamos primeiro criar as classe abstratas a partir do qual instanciamos as classes de mapeamento das classes. A classe que mapeia a classe [Entidade]Input para [Entidade] está abaixo:

@Getter
public abstract class EntityInputDisassembler<I, D> {
 


  private final ModelMapper mapper;
  private final Class<D> entityObject;

  @SuppressWarnings("unchecked")
  public EntityInputDisassembler(ModelMapper mapper) {
      ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
      this.mapper = mapper;	
      this.entityObject= (Class<D>) type.getActualTypeArguments()[1];
  }

  public D toEntity(I inputObject) {
      return this.mapper.map(inputObject, this.entityObject);
  }

  public void copyToEntity(I inputObject, D entityObject) {
      mapper.map(inputObject, entityObject);
  }
}

A linha

ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();

é necessária para obter o tipo genérico da classe EntityInputDisassembler. O tipo genérico de uma classe é o tipo do objeto que está sendo usado para instanciar a classe. Neste caso, o tipo genérico da classe EntityInputDisassembler é <I, D>, onde I é o tipo do objeto de entrada e D é o tipo do objeto de domínio.

A linha

this.entityObject = (Class<D>) type.getActualTypeArguments()[1];

atribui o tipo do objeto de domínio ao campo entityObject. O método getActualTypeArguments() da classe ParameterizedType retorna um array dos argumentos de tipo reais do tipo parametrizado. Neste caso, o array terá dois elementos, sendo o primeiro elemento o tipo do objeto de entrada e o segundo elemento o tipo do objeto de domínio.

A anotação **@SuppressWarnings("unchecked")** é usada no código Java fornecido para suprimir o aviso de unchecked (não verificado) que ocorre ao fazer o cast da expressão type.getActualTypeArguments()[1] para Class<D>.

Neste código, o método type.getActualTypeArguments() retorna um array dos argumentos de tipo reais do tipo parametrizado. O código então tenta fazer o cast do segundo elemento deste array para Class<D>. No entanto, como as informações de tipo são apagadas em tempo de execução devido ao type erasure (apagamento de tipo) em generics do Java, o compilador não pode garantir a segurança de tipo desse cast. Ao usar @SuppressWarnings("unchecked"), o código instrui o compilador a suprimir o aviso de unchecked que normalmente seria gerado nessa situação. Isso indica que o desenvolvedor está ciente do possível problema de segurança de tipo e assumiu a responsabilidade de garantir a correção do código.

A classe que mapeia [Entidade] para [Entidade]Dto está abaixo, é semelhante a classe anterior:

@Getter
public abstract class EntitytDtoAssembler<M, D> {
   
  private final ModelMapper mapper;
  private final Class<M> dtoObject;
  
  @SuppressWarnings("unchecked")
  public EntitytDtoAssembler(ModelMapper mapper) {
      ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
      this.mapper = mapper;
      this.dtoObject = (Class<M>) type.getActualTypeArguments()[0];
  }

  public M toDto(D entityObject) {
      return  this.mapper.map(entityObject, this.dtoObject);
  }
 
  public List<M> toCollectionDto(List<D> listOfEntityObjects) {
      return listOfEntityObjects.stream().map(o -> this.toDto(o)).collect(Collectors.toList());
  }
}

Pronto! A partir das classes abstratas criamos as classes para cada entidade, no caso da entidade FormaPagamento . Note que definimos os tipos <M,D> ao estendermos FormaPagamentoDtoAssembler de EntitytDtoAssembler neste caso M=FormaPagamentoDto e D=FormaPagamento

@ Component
public class FormaPagamentoDtoAssembler extends EntitytDtoAssembler<FormaPagamentoDto, FormaPagamento>{


  public FormaPagamentoDtoAssembler(ModelMapper mapper) {
      super(mapper);
  }
}

@Component
public class FormaPagamentoInputDisassembler extends EntityInputDisassembler<FormaPagamentoInput, FormaPagamento>{


  public FormaPagamentoInputDisassembler(ModelMapper mapper) {
      super(mapper);
  }
}

E finalmente nossa classe da camada do controlador para a entidade FormaPagamento fica assim:

@RestController
@RequestMapping(value = "/formaspagamento")
public class FormaPagamentoController {


  private final FormaPagamentoService formaPagamentoService;
  private final FormaPagamentoDtoAssembler formaPagtoDtoAssembler;
  private final FormaPagamentoInputDisassembler formaPagtoInputDisassembler;


    public FormaPagamentoController(FormaPagamentoService formaPagamentoService,
            						  ModelMapper modelMapper,
            						  FormaPagamentoDtoAssembler formaPagamentoDtoAssembler,
            						  FormaPagamentoInputDisassembler formaPagtoInputDisassembler) {
      this.formaPagamentoService = formaPagamentoService;
      this.formaPagtoDtoAssembler = formaPagamentoDtoAssembler;
      this.formaPagtoInputDisassembler = formaPagtoInputDisassembler;


    }

  @GetMapping
  public List<FormaPagamentoDto> listar() {

      return formaPagtoDtoAssembler.toCollectionDto(formaPagamentoService.listar());
  }


  @GetMapping("/{fomaPagamentoId}")
  public FormaPagamentoDto buscar(@PathVariable Long fomaPagamentoId) {

      return formaPagtoDtoAssembler.toDto(formaPagamentoService.buscarOuFalhar(fomaPagamentoId));
  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public FormaPagamentoDto adicionar(@RequestBody @Valid FormaPagamentoInput formaPagamentoInput) {
      FormaPagamento formaPagamento  =    
      formaPagamentoService.salvar(formaPagtoInputDisassembler.toEntity(formaPagamentoInput)); 
      return formaPagtoDtoAssembler.toDto(formaPagamento);


  }

  @PutMapping("/{formaPagamentoId}")
  public FormaPagamentoDto atualizar(@PathVariable Long formaPagamentoId, @RequestBody @Valid  FormaPagamentoInput formaPagamentoInput)
  {        
      FormaPagamento formaPagamento  = formaPagamentoService.buscarOuFalhar(formaPagamentoId);
      formaPagtoInputDisassembler.copyToEntity(formaPagamentoInput, formaPagamento);	
      return formaPagtoDtoAssembler.toDto(formaPagamentoService.salvar(formaPagamento));
 
  }

  @DeleteMapping("/{fomaPagamentoId}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void remover(@PathVariable Long fomaPagamentoId) {

      formaPagamentoService.excluir(fomaPagamentoId);
  }


}

Bem mais clean né? E para as outras entidades basta gerarmos as classes com os tipos correspondentes (Dto, Input) estendendo da classes abstrata, não havendo repetição de código. E se precisarmos de alguma customização? Basta fazer overriding do método em questão e pronto.

Compartilhe
Comentários (1)

JS

Jefferson Silva - 21/06/2023 13:40

Muito bom!!!