Genéricos + primavera: que a força esteja com você

Era uma vez em um banco distante e distante ...


Bom dia, Habr. Hoje, finalmente, minhas mãos chegaram aqui novamente para escrever. Mas, diferentemente dos tutoriais anteriores - artigos de hoje, gostaria de compartilhar minha experiência e mostrar o poder de um mecanismo como os genéricos, que, juntamente com a magia da primavera, se tornam ainda mais fortes. Quero avisá-lo imediatamente que, para entender o artigo, você precisa conhecer os fundamentos da primavera e ter idéias sobre genéricos que são mais do que apenas "Genéricos são, bem, o que indicamos entre aspas em ArrayList".

Episódio 1:


Para começar, a tarefa que eu tinha no trabalho era algo assim: havia um grande número de transferências de dinheiro com um certo número de campos comuns. Além disso, cada uma das traduções correspondia a classes - solicitações de transferência de um estado para outro e redirecionamento para outra API. Consequentemente, havia construtores envolvidos na conversão.

Resolvi o problema com campos comuns simplesmente por herança. Então eu tenho aulas:

public class Transfer { private TransferType transferType; ... } public enum TransferType { INTERNAL, SWIFT, ...; } public class InternalTransfer extends Transfer { ... } public class BaseRequest { ... } public class InternalRequest extends BaseRequest { ... } ... 

Episódio 2:


Havia o problema com os controladores - todos tinham que ter os mesmos métodos - checkTransfer, approveTransfer, etc. Aqui, pela primeira vez, mas não pela última vez, os genéricos foram úteis para mim: criei um controlador geral com os métodos necessários e herdei o restante:

  @AllArgsConstructor public class TransferController<T extends Transfer> { private final TransferService<T> service; public CheckResponse checkTransfer(@RequestBody @Valid T payment) { return service.checkTransfer(payment); } ... } public class InternalTransferController extends TransferController<InternalTransfer> { public InternalTransferController(TransferService<InternalTransfer> service) { super(service); } } 

Bem, na verdade, o serviço:

 public interface TransferService<T extends Transfer> { CheckResponse checkTransfer(T payment); ApproveResponse approveTransfer(T payment); ... } 

Assim, o problema de copiar e colar foi reduzido apenas à chamada de superconstrutor e, no serviço, geralmente o perdemos.

Mas!

Episódio 3:


Ainda havia um problema dentro do serviço:
Dependendo do tipo de transferência, vários construtores precisavam ser chamados:

 RequestBuilder builder; switch (type) { case INTERNAL: { builder = beanFactory.getBean(InternalRequestBuilder.class); break; } case SWIFT: { builder = beanFactory.getBean(SwiftRequestBuilder.class); break; } default: { log.info("Unknown payment type"); throw new UnknownPaymentTypeException(); } } 

interface do construtor generalizado:

 public interface RequestBuilder<T extends BaseRequest, U extends Transfer> { T createRequest(U transfer); } 

O método de fábrica surgiu para otimização aqui, como resultado, os comutadores / caixas estão em uma classe separada. Parecia ser melhor, mas o problema permaneceu o mesmo - quando você adiciona uma nova tradução, é necessário modificar o código, e o volumoso interruptor / caixa não combina comigo.

Episódio 4:


Qual foi a saída? Inicialmente, ocorreu-me determinar o tipo de tradução pelo nome da classe e chamar o construtor desejado usando reflexão, o que forçaria os desenvolvedores que trabalharão com o projeto a atender a certos requisitos para os nomes de suas classes. Mas havia uma solução melhor. Tendo se perguntado, você pode chegar à conclusão de que o principal aspecto da lógica de negócios do aplicativo são as próprias traduções. Ou seja, se não houver, não haverá mais nada. Então, por que não amarrar tudo? Basta modificar um pouco nossas classes. E novamente os genéricos vêm em socorro.

Solicitar Classes:

 public class BaseRequest<T extends Transfer> { ... } public class InternalRequest extends BaseRequest<InternalTransfer> { ... } 

E a interface do construtor:

 public interface RequestBuilder<T extends Transfer> { BaseRequest<T> createRequest(T transfer); } 

E aqui está se tornando mais interessante. Somos confrontados com um recurso de genéricos que quase nunca é mencionado em lugar nenhum e é usado principalmente em estruturas e bibliotecas. De fato, como BaseRequest, podemos substituir seu herdeiro, que corresponde ao tipo T, ou seja:

 public class InternalRequestBuilder implements RequestBuilder<InternalTransfer> { @Override public InternalRequest createRequest(InternalTransfer transfer) { return InternalRequest.builder() ... .build(); } } 

No momento, conseguimos uma boa melhoria em nossa arquitetura de aplicativos. Mas o problema de troca / caixa ainda não foi resolvido. Ou ...?

Episódio 5:


É aqui que a magia da primavera entra em jogo.

O fato é que temos a oportunidade de obter uma matriz de nomes de posições correspondentes ao tipo desejado usando o método getBeanNamesForType (tipo ResolvableType) . E na classe ResolvableType, existe um método estático paraClassWithGenerics (Classe <?> Clazz, Classe <?> ... genéricos) , em que você precisa passar a classe (interface) como o primeiro parâmetro, que usa o segundo parâmetro como genérico e retorna o tipo correspondente. T e:

 ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass()); 

Retorna o seguinte:

 RequestBuilder<InternalTransfer> 

E agora um pouco mais de mágica - o fato é que, se você autowire uma planilha com a interface como genérica, todas as suas implementações estarão nela:

 private final List<RequestBuilder<T>> builders; 


Nós apenas temos que passar por isso e encontrar o apropriado usando a verificação de instância:

 builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get(); 


Da mesma forma que esta opção, ainda há a oportunidade de conectar automaticamente ApplicationContext ou BeanFactory e chamar o método getBeanNamesForType () sobre onde passar nosso tipo como parâmetro. Mas isso é considerado um sinal de mau gosto e essa arquitetura não é necessária (agradecimentos especiais a zolt85 pelo comentário).
Como resultado, nosso método de fábrica assume a seguinte forma:

  @Component @AllArgsConstructor public class RequestBuildersFactory<T extends Transfer> { private final List<RequestBuilder<T>> builders; public BaseRequest<T> transferToRequest(T transfer) { ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass()); RequestBuilder<T> builder = builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get(); return builder.createRequest(transfer, stage); } } 

Episódio 6: Conclusão


Portanto, temos uma mini-estrutura com uma arquitetura bem pensada, que obriga todos os desenvolvedores a aderirem a ela. E o que é importante: nos livramos da opção / troca complicada e a adição de novas traduções não afetará as classes existentes de nenhuma maneira, o que é uma boa notícia.

PS:
Este artigo não incentiva o uso de genéricos sempre que possível e impossível, mas com sua ajuda, quero compartilhar quais mecanismos e arquiteturas poderosos eles permitem que você crie.

Agradecimentos:
Agradecimentos especiais ao Sultansoy , sem o qual essa arquitetura não seria lembrada e, muito provavelmente, este artigo não seria.

Referências:
Código fonte do Github

Source: https://habr.com/ru/post/pt423655/


All Articles