Este artigo discutirá o uso do padrão de tubulações e filtros.
Primeiro, analisaremos um exemplo de função, que reescreveremos mais tarde usando o padrão mencionado acima. As alterações no código ocorrerão gradualmente e cada vez que criarmos uma versão viável até que possamos nos concentrar na solução usando DI (neste exemplo da Primavera).
Assim, criaremos várias soluções, oferecendo a oportunidade de usar qualquer uma.
No final, comparamos as implementações inicial e final, analisamos exemplos de aplicação em projetos reais e resumimos.
Desafio
Suponha que tenhamos um monte de roupas que obtemos da secagem e que agora precisamos mudar para o armário. Acontece que os dados (roupas) vêm de um serviço separado e a tarefa é fornecer esses dados ao cliente da forma correta (em um armário no qual ele pode obter roupas).
Na maioria dos casos, você não pode usar os dados recebidos na forma em que eles chegam até nós. Esses dados precisam ser verificados, transformados, classificados etc.
Suponha que um cliente exija que as roupas sejam passadas a ferro se forem de hortelã.
Então, pela primeira vez, criamos um Modifier
, no qual prescrevemos as alterações:
public class Modifier { public List<> modify(List<> ){ (); return ; } private void (List<> ) { .stream() .filter(::) .forEach(o -> {
Nesta fase, tudo é simples e claro. Vamos escrever um teste que verifique se todas as roupas amassadas foram passadas.
Porém, com o tempo, novos requisitos aparecem e toda vez que a funcionalidade da classe Modifier
expande:
- Não coloque roupas sujas no armário.
- Camisas, jaquetas e calças devem pendurar nos ombros.
- As meias com vazamento precisam ser costuradas primeiro
- ...
A sequência de mudanças também é importante. Por exemplo, você não pode pendurar as roupas nos ombros e depois passar a ferro.
Assim, em algum momento, o Modifier
pode assumir a seguinte forma:
public class Modifier { private static final Predicate<> ___ = ((Predicate<>).class::isInstance) .or(.class::isInstance) .or(.class::isInstance) ; public List<> modify(List<> ){ (); (); (); ();
Da mesma forma, os testes se tornaram mais complicados, que agora devem pelo menos verificar cada etapa individualmente.
E quando um novo requisito chega, observando o código, decidimos que chegou a hora da refatoração.
Refatoração
A primeira coisa que chama sua atenção é o rebentamento frequente de todas as roupas. Portanto, o primeiro passo, movemos tudo em um ciclo e também transferimos a verificação de limpeza para o início do ciclo:
public class Modifier { private static final Predicate<> ___ = ((Predicate<>).class::isInstance) .or(.class::isInstance) .or(.class::isInstance) ; public List<> modify(List<> ){ List<> result = new ArrayList<>(); for(var o : ){ if(o.()){ continue; } result.add(o); (o); (o); (o);
Agora, o tempo de processamento de roupas é reduzido, mas o código ainda é muito longo para uma classe e para o corpo do ciclo. Vamos tentar encurtar o corpo do ciclo primeiro.
Após verificar a limpeza, você pode fazer todas as chamadas em um método de modify( )
separado modify( )
:
public List<> modify(List<> ){ List<> result = new ArrayList<>(); for(var o : ){ if(o.()){ continue; } result.add(o); modify(o); } return result; } private void modify( o) { (o); (o); (o);
Você pode combinar todas as chamadas em um Consumer
:
private Consumer<> modification = ((Consumer<>) this::) .andThen(this::) .andThen(this::);
Sem corte: peek
Eu costumava dar uma espiada. O Sonar dirá que esse código não deve ser feito, porque Javadoc informa que o método existe principalmente para depuração. Mas se você reescrevê-lo no mapa: .map (o -> {modification.accept (o); retornar o;}), a IDEA dirá que é melhor usar a espiada
Tropeço: Consumidor
Um exemplo com Consumidor (e subseqüente com Função) é dado para mostrar os recursos do idioma.
Agora, o corpo do ciclo ficou mais curto, mas até agora a própria classe ainda é muito grande e contém muita informação (conhecimento de todas as etapas).
Vamos tentar resolver esse problema usando padrões de programação já estabelecidos. Nesse caso, usaremos Pipes & Filters
.
Tubos e filtros
O modelo de canal e filtro descreve uma abordagem na qual os dados recebidos passam por várias etapas de processamento.
Vamos tentar aplicar essa abordagem ao nosso código.
Etapa 1
De fato, nosso código já está próximo desse padrão. Os dados obtidos passam por várias etapas independentes. Até agora, cada método é um filtro e a própria modify
descreve o canal, primeiro filtrando todas as roupas sujas.
Agora vamos transferir cada etapa para uma classe separada e ver o que obtemos:
public class Modifier { private final ; private final ; private final ;
Assim, colocamos o código em classes separadas, simplificando os testes para transformações individuais (e criando a possibilidade de reutilizar etapas). A ordem das chamadas determina a sequência de etapas.
Mas a própria classe ainda conhece todas as etapas individuais, controla a ordem e, portanto, possui uma enorme lista de dependências. Além de adicionar uma nova etapa, seremos forçados a não apenas escrever uma nova classe, mas também adicioná-la ao Modfier
.
Etapa 2
Simplifique o código usando o Spring.
Primeiro, crie uma interface para cada etapa individual:
interface Modification { void modify( ); }
Modifier
próprio Modifier
agora será muito menor:
public class Modifier { private final List<Modification> steps; @Autowired public Modifier(List<Modification> steps) { this.steps = steps; } public List<> modify(List<> ) { return .stream() .filter(o -> !o.()) .peek(o -> { steps.forEach(m -> m.modify(o)); }) .collect(Collectors.toList()); } }
Agora, para adicionar uma nova etapa, basta escrever uma nova classe que implemente a interface Modification
e colocar o @Component
acima dela. O Spring o encontrará e o adicionará à lista.
Modifer
próprio Modifer
não sabe nada sobre as etapas individuais, o que cria uma "conexão fraca" entre os componentes.
A única dificuldade é definir a sequência. Para fazer isso, o Spring possui uma anotação @Order
na qual você pode passar um valor int. A lista é classificada em ordem crescente.
Assim, pode acontecer que, adicionando uma nova etapa no meio da lista, você precise alterar os valores de classificação das etapas existentes.
O Spring poderia ter sido dispensado se todas as implementações conhecidas fossem passadas manualmente para o construtor Modifier. Isso ajudará a resolver o problema de classificação, mas novamente complicará a adição de novas etapas.
Etapa 3
Agora passamos no teste de limpeza em uma etapa separada. Para fazer isso, reescrevemos nossa interface para que ela sempre retorne um valor:
interface Modification { modify( ); }
Verifique a limpeza:
@Component @Order(Ordered.HIGHEST_PRECEDENCE) class CleanFilter implements Modification { modify( ) { if(.()){ return null; } return ; } }
Modifier.modify
:
public List<> modify(List<> ) { return .stream() .map(o -> { var modified = o; for(var step : steps){ modified = step.modify(o); if(modified == null){ return null; } } return modified; }) .filter(Objects::nonNull) .collect(Collectors.toList()); }
Nesta versão, o Modifier
não possui nenhuma informação de dados. Ele simplesmente os transmite a todos os passos conhecidos e coleta os resultados.
Se uma das etapas retornar nulo, o processamento dessa peça de roupa será interrompido.
Um princípio semelhante é usado no Spring for HandlerInterceptors. Antes e depois da chamada do controlador, todos os interceptores apropriados para este URL são chamados. Ao mesmo tempo, retorna true ou false no método preHandle para indicar se o processamento e a chamada de interceptores subseqüentes podem continuar
Etapa n
O próximo passo é adicionar o método de matches
à interface Modification
, na qual as etapas para um atributo separado da roupa são verificadas:
interface Modification { modify( ); default matches( ) {return true;} }
Devido a isso, você pode simplificar um pouco a lógica nos métodos de modify
movendo as verificações de classes e propriedades para um método separado.
Uma abordagem semelhante é usada no filtro Spring (Request), mas a principal diferença é que cada filtro é um wrapper no próximo e chama explicitamente FilterChain.doFilter para continuar o processamento.
Total
O resultado final é muito diferente da versão inicial. Comparando-os, podemos tirar as seguintes conclusões:
- A implementação baseada em tubulações e filtros simplifica a própria classe
Modifier
. - Responsabilidades melhor distribuídas e conexões "fracas" entre componentes.
- Mais fácil para testar etapas individuais.
- Mais fácil de adicionar e remover etapas.
- Um pouco mais difícil de testar toda uma cadeia de filtros. Já precisamos de IntegrationTests.
- Mais aulas
Por fim, uma opção mais conveniente e flexível que a original.
Além disso, você pode simplesmente paralelizar o processamento de dados usando o mesmo parallelStream.
O que este exemplo não resolve
- A descrição do padrão diz que filtros individuais podem ser reutilizados criando outra cadeia de filtros (canal).
- Por um lado, isso é fácil de usar usando o
@Qualifier
. - Por outro lado, definir uma ordem diferente com o
@Order
falhará.
- Para exemplos mais complexos, você precisará usar várias cadeias, usar cadeias aninhadas e ainda alterar a implementação existente.
- Por exemplo, a tarefa: "para cada meia, procure um par e coloque-o em uma instância de <? Extends Clothing>" não se encaixará bem na implementação descrita, porque Agora, para cada dedo do pé, você deve classificar todas as roupas e alterar a lista de dados inicial.
- Para resolver isso, você pode escrever uma nova interface que aceite e retorne uma Lista <Vestuário> e a transfira para uma nova cadeia. Mas você precisa ter cuidado com a sequência de chamadas das próprias cadeias, se as meias puderem ser costuradas apenas pelo hotel.
Obrigado pela atenção.