管道模式(Pipeline Pattern) 是 责任链模式(Chain of Responsibility Pattern)的常用变体之一。在管道模式中,管道扮演着流水线的角色,将数据传递到一个加工处理序列中,数据在每个步骤中被加工处理后,传递到下一个步骤进行加工处理,直到全部步骤处理完毕。
PS:纯的责任链模式在链上只会有一个处理器用于处理数据,而管道模式上多个处理器都会处理数据。
何时使用管道模式
任务代码较为复杂,需要拆分为多个子步骤时,尤其是后续可能在任意位置添加新的子步骤、删除旧的子步骤、交换子步骤顺序,可以考虑使用管道模式。
愉快地使用管道模式
背景回放
最开始做模型平台的时候,创建模型实例的功能,包括: “输入数据校验 -> 根据输入创建模型实例 -> 保存模型实 例到相关 DB 表” 总共三个步骤,也不算复杂,所以当时的代码大概是这样的:
public class ModelServiceImpl implements ModelService { /** * 提交模型(构建模型实例) */ public CommonReponse buildModelInstance(InstanceBuildRequest request) { // 输入数据校验 validateInput(request); // 根据输入创建模型实例 ModelInstance instance = createModelInstance(request); // 保存实例到相关 DB 表 saveInstance(instance); }}然而没有过多久,我们发现表单输入数据的格式并不完全符合模型的输入要求,于是我们要加入 “表单数据的预处理”。这功能还没动手呢,又有业务方提出自己也存在需要对数据进行处理的情况(比如根据商家的表单输入,生成一些其他业务数据作为模型输入)。
所以在 “输入数据校验” 之后,还需要加入 “表单输入输出预处理” 和 “业务方自定义数据处理(可选)”。这个时候我就面临一个选择:是否继续通过在 buildModelInstance 中加入新的方法来实现这些新的处理步骤?好处就是可以当下偷懒,但是坏处呢:
ModelService 应该只用来接收 HSF 请求,而不应该承载业务逻辑,如果将 提交模型 的逻辑都写在这个类当中,违反了 单一职责,而且后面会导致 类代码爆炸将来每加入一个新的处理步骤或者删除某个步骤,我就要修改 buildModelInstance 这个本应该非常内聚的方法,违反了 开闭原则所以,为了不给以后的自己挖坑,我觉得要思考一个万全的方案。这个时候,我小脑袋花开始飞转,突然闪过了 Netty 中的 ChannelPipeline —— 对哦, 管道模式 ,不就正是我需要的嘛!
管道模式的实现方式也是多种多样,接下来基于前面的背景,我分享一下我目前基于 Spring 实现管道模式的 “最佳套路”(如果你有更好的套路,欢迎赐教,一起讨论哦)。
定义管道处理的上下文
/** * 传递到管道的上下文 */@Getter@Setterpublic class PipelineContext { /** * 处理开始时间 */ private LocalDateTime startTime; /** * 处理结束时间 */ private LocalDateTime endTime; /** * 获取数据名称 */ public String getName() { return this.getClass().getSimpleName(); }}定义上下文处理器
/** * 管道中的上下文处理器 */public interface ContextHandler { /** * 处理输入的上下文数据 * * @param context 处理时的上下文数据 * @return 返回 true 则表示由下一个 ContextHandler 继续处理,返回 false 则表示处理结束 */ boolean handle(T context);}为了方便说明,我们现在先定义出最早版 【 提交模型逻辑】 的上下文和相关处理器:
/** * 模型实例构建的上下文 */@Getter@Setterpublic class InstanceBuildContext extends Pipeline