强大的wrapper
最近在看大数据和容器相关的东西,发现有一个模式被反复使用到,关键是被用的很恰当且优雅,并能在这些关键技术中都发挥着至关重要的核心作用。我想你已经猜到了,他就是Eminem——强大的rapp...
最近在看大数据和容器相关的东西,发现有一个模式被反复使用到,关键是被用的很恰当且优雅,并能在这些关键技术中都发挥着至关重要的核心作用。我想你已经猜到了,他就是Eminem——强大的rapper——哦,不对,是wrapper。接下来,这篇文章我就带大家看看,wrapper这玩意有什么特别之处,为啥这么有用?
1. 什么是wrapper
Wrapper就是包装的意思,广义上来说,Decorator和Adapter设计模式都属于Wrapper,只是二者的意图(intent)不一样。借用stackflow上一哥们的回答:
Decorator:
Allows objects to be composed/add capabilities by wrapping them with a class with the same interface.
Adapter:
Allows you to wrap an object without a known interface implementation so it adheres to an interface. The point is to "translate" one interface into another.
Wrapper:
Never heard of this as a design pattern, but I suppose it's just a common name for the above
大意是说Decorator是用组合的方式增强功能,而Adapter是为了做接口转换,二者都是采用wrapper的方式来做到这一点,形式是一样的,但意图不一样。
2. 流式计算中的wrapper
谈到wrapper(在本文中,你可以认为wrapper就是decorator),我认为使用最到位的,非流式计算莫属。从Java的IO流,到Java Stream,到Spark,Flink到处都能看到wrapper的身影。
2.1 Java IO流
我们先从Java IO流看起,假如现在我们需要从一个文件中读取信息打印到console,我们可以使用以下的方式:
Reader in = new BufferedReader( // 3. 缓冲字符,提升效率
new InputStreamReader( // 2. 将字节流转换成字符流
new FileInputStream("path"), "GBK") // 1. 读取文件的字节流
)
)
其执行过程如下图所示,首先从文件中读取字节流,然后放入字节缓冲区,例如“中”字,他的GBK编码是两个字节——D6D0,那么在InputStreamReader中等到这个字符形成以后,再输送到BufferedReader的字符缓冲区,当缓冲区满了以后,再溢写到console。
IO流的这种做法就是一种典型的wrapper,也就是decorator装饰者设计模式的实现。通过层层包装,我们获得了更强功能的流处理能力。
2.2 Spark中的RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。
Spark支持两个类型(算子)操作:Transformation和Action,主要做的是就是将一个已有的RDD生成另外一个RDD。Transformation具有lazy特性(延迟加载)。Transformation算子的代码不会真正被执行。只有当我们的程序里面遇到一个action算子的时候,代码才会真正的被执行。这种设计让Spark更加有效率地运行。
其中Transformation等价于Java Stream中的中间操作(Intermediate operations),而Action和Java Stream的结束操作(Terminal operations)表达的是一个意思,它们只是实现方式不一样,Java Stream是使用pipeline,而spark则采用了wrapper的方式来达到lazy的特性。以wordCount为例,它在spark中的scala实现如下:
//1. new HadoopRDD()
val lines: RDD[String] = sparkContext.textFile("path")
//2. new MapPartitionsRDD(hadoopRdd) - flatMap
val words: RDD[String] = lines.flatMap(_.split(regex=" "))
//3. new MapPartitionsRDD(mapPartitionsRdd) - map
val wordToOne = words.map(word=>(word, 1))
//4. new ShuffledRDD(mapPartitionsRdd)
val wordToSum:RDD[(String, Int)] = wordToOne.reduceByKey(_+_)
//5. collect
val array: Array[(String, Int)] = wordToSum.collect()
其在runtime被执行时,实际上,构建的是一个如下图所示的wrapper,也就是说,当flatmap被调用时,并不会真正执行flatmap这个动作,而是构建了一个MapPartitionsRDD包装了HadoopRDD,以此类推,直到collect方法被调用时,前序的转换指令(transformation)才会被真正执行。
同样的道理也适用于Flink,Flink中的各种transform算子也是通过这种wrapper的方式构建出来的。
3. wrapper的背后思想
通过上面的案例,可以看到wrapper这个模式的确很有用,再往下深挖一下,我们可以发现其背后隐藏着更深刻的设计道理——即分层和OCP。
3.1 分层
表面上是包装,其背后暗含的是一种分层结构。“复杂性常常以层次结构的形式存在。复杂的系统有一些相关的子系统组成,这些子系统又有自己的系统,如此下去,直到达到某种最低层次的基本组件。”
因此,wrapper这种构建类似洋葱圈层次结构的能力,正是构建复杂系统所需要的。也正因为有这样的层次结构,我们才得以能够理解、描述、实现这些复杂系统。话说回来,我们似乎也只能理解那些有层次结构的系统。这也是为什么分层架构是软件设计中最基础,也是最重要的架构之一。
3.2 OCP
"Open to extend,Close to change"可以说是我们工程师的梦想,没有人愿意修改原来已经work的东西,因为修改就意味着要重新测试,就以为着可能引入新的bug,就以为着头发变少...... 大部分的设计模式都是为了OCP这个目标在努力。
我们平时提倡的immutable(不可变性)也是这个道理,immutable的String让我们可以放心使用,而不用考虑参数传递问题;immutable的函数式编程不用担心线程安全问题;immutable的docker image使得分层构建和复用成为可能;immutable infrastructure(不可变基础实施)提升了运维效率,是云原生的必要条件。
wrapper通过包装的形式,在保存原有功能的同时,通过扩展实现新增能力,从而保证了原有能力的immutable,完美的实现了OCP。此外,wrapper还可以通过基本组件的组合,可以实现非常复杂的业务逻辑。比如Spark通过RDD之间的组合来完成从简单到非常复杂的数据处理业务逻辑。
更多推荐
所有评论(0)