架构风格:数据流
本文探讨:
- 什么是管道过滤器风格(Pipe-and-filter Style)
- 管道过滤器风格的约束
- 管道过滤器风格的适用场景
- 什么是批量顺序处理风格(Batch-sequential Style)
- 批量顺序处理风格的约束
- 批量顺序处理风格的适用场景
- 批量顺序处理与管道过滤器的差异是什么
- 什么是过程控制风格(Process Control Style)
- 过程控制处理风格的约束
- 过程控制处理风格的适用场景
不论是出国还是看电影,不知道你有没有发现,很多国家的自来水是可以直接喝的,而中国的就不行。你有没有想过,为什么我们国家的自来水不能直接喝呢?而且好像就只有中国人特别喜欢喝热水!感冒、咳嗽、不舒服,发烧、流感、大姨妈,只要身体有点什么不舒服了,不管三七二十一,先喝点热水!
这主要和以前的瘟疫、寄生虫、肠胃炎有关系。以前的水基本没有过滤,寄生虫病菌比较多,经常引起各种疾病,所以很多国家都对水进行了净化处理,能达到直接喝的标准。而以前我们国家还没有能力做这种净化处理,但是发现水烧开了也可以有效的杀死这些细菌,所以喝热水的传统就这么延续下来了。
对水的净化处理其实原理很简单,就是「过滤」!如果你家里安装过净化水系统,应该会发现净化器一般都会有几个过滤器,一般是三个。作用就是自来水经过一个个的过滤器,去除了水中的各种杂质,能达到直接饮用的程度。其中,每一个过滤器都是独立的,可以独立的替换。
数据流风格就是类似的处理方式,将数据看做水,一个个的处理单元看过一层层的过滤,数据经过一个个的处理单元,最终达到我们需要的结果。
说到过滤器,我们的第一印象应该就是Web开发中遇到的「过滤器/拦截器」了,继而想到的应该就是设计模式中的职责链模式。
所以我们就从我们熟悉的职责链模式开始,来聊聊「数据流风格」。
职责链模式
职责链的类图如下:
从结构上看,职责链模式有两个对象:Client和Handler(包括具体的Handler实现)
- Client发送消息给Handler
- Handler将消息委托给具体的Handler链条处理
- 处理完成后,返回给Client
Handler的具体实现就是一个个的过滤器,独立的处理数据,也可以独立的替换,这就解耦了请求发送者和接收者。请求沿着Handler链条依次传递,每个Handler判断是否需要处理该请求,直到所有Handler都处理完成为止。
职责链主要适用于如下场景:
- 有多个对象可以处理一个请求,但是哪个对象处理该请求需要运行时自动确定
- 需要在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
- 可处理一个请求的对象集合是动态指定的
下面是一个简单的clojure例子:
(defn handler-request1 [condition]
(if (= "ConcreteHandler1" condition)
(println "ConcreteHandler1 handled ")
(println "ConcreteHandler1 passed ")))
(defn handler-request2 [condition]
(if (= "ConcreteHandler2" condition)
(println "ConcreteHandler2 handled ")
(println "ConcreteHandler2 passed ")))
(defn handler-requestn [condition]
(println "ConcreteHandlern handled "))
(->> "ConcreteHandler2"
handler-request1
handler-request2
handler-requestn)
;handler-request2和handler-requestn会处理此字符串
管道过滤器风格
管道过滤器风格和职责链模式在结构上比较类似。管道过滤器风格包含了四个组件:
- 读端口:用于读取数据,如标准输入流、文本文件或传感器采集的数据等。类似职责链的Client发送消息。
- 写端口:用于写出数据,如文本文件、数据库、标准输出等。类似职责链的Client接收处理完的消息。
- 管道:用于连接各个过滤器,使得消息能在各个过滤器之间进行流转,也包含数据缓冲作用。类似职责链里的调用(消息发送)
- 过滤器:处理消息组件,过滤器可以通过pull(后续过滤器从当前过滤器中拉取数据),push(前面的过滤器向当前过滤器推送数据)或主动方式(不断从前面的过滤器中拉取数据,并向后面的过滤器推送数据)来获取消息。类似职责链模式里的Handler。
管道过滤器风格的完整流程为:「读端口」获取需要处理的信息,通过管道传递给过滤器链,每个过滤器自行判断是否需要对信息进行处理,一个过滤器处理完后通过管道将消息传递给下一个或多个过滤器,直到所有的过滤器全部处理完毕,通过写端口,将处理完成的信息写出到目标位置。
管道过滤器风格的典型应用有:
- Linux的Shell
- JavaEE Servlet Filter
- 传统的编译器:一个阶段(包括词法分析、语法分析、语义分析和代码生成)的输出是另一个阶段的输入。
以Linux的Shell为例:
cat "app.log" | grep "^error" | cut -f 2
- cat指令获取app.log的内容,将结果通过管道传递给grep
- grep指令过滤出所有以error开头的行,将结果通过管道传递给cut
- cut取出第二列的内容
在这里cat,grep,cut就是一个个的过滤器;而 | 就是管道;输入输出则是标准输入输出。
管道过滤器风格将处理逻辑封装到独立的过滤器中:
- 可以通过新增过滤器的方式增加对信息的处理逻辑,相反可以通过移除过滤器的方式减少对信息的处理逻辑。提高了系统的扩展性
- 过滤器之间没有逻辑上的关联关系,提高了复用性
- 同时易于增加过滤器和移除过滤器,提高了系统的可维护性
- 但是每个过滤器需要对信息进行解析,降低了系统性能,以及增加了过滤器自身的复杂度
- 不过过滤器可以并行执行,这可以提高系统性能
管道过滤器风格主要用于处理数据的系统,不适用于处理交互的应用。
批量顺序处理风格
同样的「批量顺序处理风格」也是主要用于处理数据的架构风格。一般它的处理组件称为阶段(stages)或者步骤(steps)。
它的处理流程如下:
- 读取一批数据,进行处理
- 信息不是通过所谓的「管道」在各个阶段之间进行流转,而是通过类似临时中间文件来进行阶段之间的流转,而文件可以被删除
- 下一阶段需要在前一阶段处理完后才能执行
- 下一阶段会从前一阶段处理后的文件里,选择自己需要的文件进行处理,处理后再写到文件中
批量顺序处理风格的每一步处理都是独立的,并且每一步是顺序执行的。只有当前一步处理完,后一步处理才能开始。数据传达在每一步处理之间作为一个整体。比较适用于需要顺序执行的某些固定操作的场景,并且这种流程不经常进行变化。Windows下的BAT程序就是这种应用的典型实例。
批量顺序处理风格对架构属性的影响与管道过滤器基本一致,这里不再赘述。
管道过滤器与批量顺序处理差异
两种风格都包含一系列的计算组件,通过将数据交给这一系列的计算组件来处理,来完成任务。
两种风格的不同之处在:
管道过滤器风格 | 批量顺序处理风格 |
---|---|
细粒度 | 粗粒度 |
结果驱动处理 | 高延迟 |
内部输入 | 外部访问输入 |
可以并发 | 不支持并发 |
交互不友好 | 不支持交互 |
内部输入:过滤器之间直接传递数据
外部访问输入:批量顺序处理一般都会先输出到一个临时文件,下一个阶段再从临时文件中获取
过程控制风格
过程控制风格和上面的两个风格处理方式差异较大,一般应用在嵌入式开发中,包含了四个组件:
- 传感器(Sensor):监听某些重要信息
- 控制器(Controller):逻辑控制
- 执行器(Actuator):操作过程的物理方法
- 过程(Process):你想要控制的内容
空调使用的就是过程控制风格,如下图:
- 我们打开空调,设置了21度
- 空调的温度传感器(Sensor)检测到室内温度(Process)为15度,温差为6度
- 控制器(Controller)控制执行器(Actuator)吹热风
- 当室内温度达到21度后,传感器就通知控制器控制执行器停止工作
过程控制风格将系统拆分为一个个的子系统或模块:
- 各个子系统或模块相互独立,提高了模块的复用性,以及系统的进化性和可维护性
- 子系统或模块间的交互可能会影响性能