本文将讨论Pipes&Filters模式的用法。
首先,我们将分析一个函数示例,稍后将使用上述模式将其重写。 代码中的更改将逐渐发生,每次我们创建一个可行的版本,直到我们使用DI停留在解决方案上(在此Spring示例中)。
因此,我们将创建几种解决方案,提供使用任何解决方案的机会。
最后,我们比较了最初和最终的实现,看一下实际项目中的应用示例并进行总结。
挑战赛
假设我们有一堆晾干的衣服,现在需要搬到壁橱里。 事实证明,数据(衣服)来自单独的服务,任务是以正确的形式(在衣橱里供他获取衣服的方式)向客户提供此数据。
在大多数情况下,您不能以收到的数据的形式使用收到的数据。 此数据需要检查,转换,排序等。
假设客户要求如果衣服是薄荷的,则应熨烫。
然后,第一次创建一个Modifier
,其中规定了更改:
public class Modifier { public List<> modify(List<> ){ (); return ; } private void (List<> ) { .stream() .filter(::) .forEach(o -> {
在此阶段,一切都简单明了。 让我们编写一个测试,检查所有起皱的衣服是否已熨烫。
但是随着时间的流逝,出现了新的要求,并且每次Modifier
类的功能扩展时:
- 不要将脏衣服放在壁橱里。
- 衬衫,夹克和裤子应垂在肩膀上。
- 漏水的袜子需要先缝起来
- ...
更改顺序也很重要。 例如,您不能先将衣服挂在他们的肩膀上,然后再熨烫。
因此,在某些时候, Modifier
可以采用以下形式:
public class Modifier { private static final Predicate<> ___ = ((Predicate<>).class::isInstance) .or(.class::isInstance) .or(.class::isInstance) ; public List<> modify(List<> ){ (); (); (); ();
相应地,测试变得更加复杂,现在至少必须单独检查每个步骤。
当一个新的需求到达时,查看代码,我们认为重构的时机已到。
重构
引起您注意的第一件事是经常将所有衣服弄破。 因此,第一步,我们将所有内容移入一个循环,并将清洁度检查转移到循环的开始:
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);
现在,减少了衣服的处理时间,但是对于一类和整个循环的主体,代码仍然太长。 让我们先尝试缩短周期的主体。
检查清洁度之后,您可以使用单独的modify( )
方法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);
您可以将所有呼叫合并到一个Consumer
:
private Consumer<> modification = ((Consumer<>) this::) .andThen(this::) .andThen(this::);
钝器:偷看
我简短地看了一下。 声纳会说这样的代码不应该做,因为 Javadoc告诉peek,该方法主要用于调试。 但是,如果您在地图上重写它:.map(o-> {modification.accept(o); return o;}),那么IDEA会说最好使用peek
绊脚石:消费者
给出了一个使用Consumer(然后使用Function)的示例,以显示该语言的功能。
现在,循环的主体变得更短了,但是到目前为止,类本身仍然太大,并且包含了太多的信息(所有步骤的知识)。
让我们尝试使用已经建立的编程模式来解决此问题。 在这种情况下,我们将使用Pipes & Filters
。
管道和过滤器
通道和过滤器模板 描述了一种方法,在该方法中,传入数据经过几个处理步骤。
让我们尝试将这种方法应用于我们的代码。
第一步
实际上,我们的代码已经接近这种模式。 获得的数据经过几个独立的步骤。 到目前为止,每种方法都是一个过滤器,并modify
其自身描述的通道,首先过滤掉所有脏衣服。
现在,我们将把每个步骤转移到一个单独的类上,看看我们得到了什么:
public class Modifier { private final ; private final ; private final ;
因此,我们将代码放在单独的类中,从而简化了单个转换的测试(并创造了重用步骤的可能性)。 调用的顺序决定了步骤的顺序。
但是类本身仍然知道所有单个步骤,控制顺序,因此具有大量的依赖项列表。 除了添加新步骤之外,我们不仅被迫编写一个新类,而且还将其添加到Modfier
。
第二步
使用Spring简化代码。
首先,为每个步骤创建一个界面:
interface Modification { void modify( ); }
Modifier
本身现在将更短:
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()); } }
现在,要添加一个新步骤,您只需要编写一个实现Modification
接口的新类,并将@Component
放在其上即可。 Spring将找到它并将其添加到列表中。
Modifer
本身对各个步骤一无所知,这会在组件之间创建“弱连接”。
唯一的困难是设置顺序。 为此,Spring有一个@Order
批注,您可以在其中传递一个int值。 该列表以升序排序。
因此,可能会发生这种情况,通过在列表中间添加新步骤,您将不得不更改现有步骤的排序值。
如果将所有已知的实现手动传递给Modifier构造函数,则可以取消Spring。 这将有助于解决排序问题,但又会使新步骤复杂化。
第三步
现在,我们在单独的步骤中通过了清洁度测试。 为此,我们重写接口,使其始终返回一个值:
interface Modification { modify( ); }
检查清洁度:
@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()); }
在此版本中, Modifier
没有任何数据信息。 他只是将它们传递到每个已知步骤并收集结果。
如果其中一个步骤返回null,则该服装的处理将中断。
Spring在HandlerInterceptor中使用了类似的原理。 在控制器调用之前和之后,将调用此URL的所有适当的拦截器。 同时,它在preHandle方法中返回true或false,以指示是否可以继续处理和调用后续Interceptor
步骤n
下一步是将matches
方法添加到Modification
接口,其中将检查到衣服的单独属性的步骤:
interface Modification { modify( ); default matches( ) {return true;} }
因此,可以通过将对类和属性的检查移到一个单独的方法中来稍微简化modify
方法中的逻辑。
Spring(请求)过滤器中使用了类似的方法,但是主要区别在于每个过滤器都是下一个过滤器的包装,并显式调用FilterChain.doFilter继续处理。
合计
最终结果与初始版本有很大不同。 比较它们,我们可以得出以下结论:
- 基于管道和过滤器的实现简化了
Modifier
类本身。 - 更好的分布式职责和组件之间的“弱”连接。
- 更容易测试各个步骤。
- 易于添加和删除步骤。
- 测试整个过滤器链要困难一些。 我们已经需要IntegrationTests。
- 更多课程
最终,将提供比原始版本更加便捷和灵活的选择。
此外,您可以使用相同的parallelStream简单地并行化数据处理。
这个例子没有解决什么
- 模式说明说,可以通过创建另一个过滤器链(通道)来重复使用各个过滤器。
- 一方面,使用
@Qualifier
很容易做到这一点。 - 另一方面,使用
@Order
设置其他订单将失败。
- 对于更复杂的示例,您将必须使用多个链,使用嵌套链,并且仍然更改现有实现。
- 因此,例如,任务:“对于每只袜子,寻找一双袜子,并将它们放入<?Extended Clothing>的一个实例中”,因为该实现不太适合所描述的实现,因为 现在,对于每个脚趾,您都必须对所有亚麻布进行分类并更改初始数据列表。
- 为了解决这个问题,您可以编写一个新接口,该接口接受并返回List <Clothing>,并将其传输到新链中。 但是,如果袜子只能由酒店缝制,则您需要注意链条本身的呼叫顺序。
谢谢您的关注。