架构风格:数据流

本文探讨:

  • 什么是管道过滤器风格(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度后,传感器就通知控制器控制执行器停止工作

过程控制风格将系统拆分为一个个的子系统或模块:

  • 各个子系统或模块相互独立,提高了模块的复用性,以及系统的进化性可维护性
  • 子系统或模块间的交互可能会影响性能

参考资料

posted @ 2019-01-14 17:42  一瑜一琂  阅读(4266)  评论(0编辑  收藏  举报