50 | 装饰器模式:通过剖析Java IO类库源码学习装饰器模式
下载APP
关闭
渠道合作
推荐作者
50 | 装饰器模式:通过剖析Java IO类库源码学习装饰器模式
2020-02-26 王争 来自北京
《设计模式之美》
课程介绍
讲述:冯永吉
时长08:18大小6.66M
上一节课我们学习了桥接模式,桥接模式有两种理解方式。第一种理解方式是“将抽象和实现解耦,让它们能独立开发”。这种理解方式比较特别,应用场景也不多。另一种理解方式更加简单,类似“组合优于继承”设计原则,这种理解方式更加通用,应用场景比较多。不管是哪种理解方式,它们的代码结构都是相同的,都是一种类之间的组合关系。
今天,我们通过剖析 Java IO 类的设计思想,再学习一种新的结构型模式,装饰器模式。它的代码结构跟桥接模式非常相似,不过,要解决的问题却大不相同。
话不多说,让我们正式开始今天的学习吧!
Java IO 类的“奇怪”用法
Java IO 类库非常庞大和复杂,有几十个类,负责 IO 数据的读取和写入。如果对 Java IO 类做一下分类,我们可以从下面两个维度将它划分为四类。具体如下所示:
针对不同的读取和写入场景,Java IO 又在这四个父类基础之上,扩展出了很多子类。具体如下所示:
在我初学 Java 的时候,曾经对 Java IO 的一些用法产生过很大疑惑,比如下面这样一段代码。我们打开文件 test.txt,从中读取数据。其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率。
初看上面的代码,我们会觉得 Java IO 的用法比较麻烦,需要先创建一个 FileInputStream 对象,然后再传递给 BufferedInputStream 对象来使用。我在想,Java IO 为什么不设计一个继承 FileInputStream 并且支持缓存的 BufferedFileInputStream 类呢?这样我们就可以像下面的代码中这样,直接创建一个 BufferedFileInputStream 类对象,打开文件读取数据,用起来岂不是更加简单?
基于继承的设计方案
如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream,也算是可以接受的,毕竟继承结构还算简单。但实际上,继承 InputStream 的子类有很多。我们需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类。
除了支持缓存读取之外,如果我们还需要对功能进行其他方面的增强,比如下面的 DataInputStream 类,支持按照基本数据类型(int、boolean、long 等)来读取数据。
在这种情况下,如果我们继续按照继承的方式来实现的话,就需要再继续派生出 DataFileInputStream、DataPipedInputStream 等类。如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream、BufferedDataPipedInputStream 等 n 多类。这还只是附加了两个增强功能,如果我们需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。这也是我们在第 10 节中讲的不推荐使用继承的原因。
基于装饰器模式的设计方案
在第 10 节中,我们还讲到“组合优于继承”,可以“使用组合来替代继承”。针对刚刚的继承结构过于复杂的问题,我们可以通过将继承关系改为组合关系来解决。下面的代码展示了 Java IO 的这种设计思路。不过,我对代码做了简化,只抽象出了必要的代码结构,如果你感兴趣的话,可以直接去查看 JDK 源码。
看了上面的代码,你可能会问,那装饰器模式就是简单的“用组合替代继承”吗?当然不是。从 Java IO 的设计来看,装饰器模式相对于简单的组合关系,还有两个比较特殊的地方。
第一个比较特殊的地方是:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。比如,下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。
第二个比较特殊的地方是:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。实际上,符合“组合关系”这种代码结构的设计模式有很多,比如之前讲过的代理模式、桥接模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
实际上,如果去查看 JDK 的源码,你会发现,BufferedInputStream、DataInputStream 并非继承自 InputStream,而是另外一个叫 FilterInputStream 的类。那这又是出于什么样的设计意图,才引入这样一个类呢?
我们再重新来看一下 BufferedInputStream 类的代码。InputStream 是一个抽象类而非接口,而且它的大部分函数(比如 read()、available())都有默认实现,按理来说,我们只需要在 BufferedInputStream 类中重新实现那些需要增加缓存功能的函数就可以了,其他函数继承 InputStream 的默认实现。但实际上,这样做是行不通的。
对于即便是不需要增加缓存功能的函数来说,BufferedInputStream 还是必须把它重新实现一遍,简单包裹对 InputStream 对象的函数调用。具体的代码示例如下所示。如果不重新实现,那 BufferedInputStream 类就无法将最终读取数据的任务,委托给传递进来的 InputStream 对象来完成。这一部分稍微有点不好理解,你自己多思考一下。
实际上,DataInputStream 也存在跟 BufferedInputStream 同样的问题。为了避免代码重复,Java IO 抽象出了一个装饰器父类 FilterInputStream,代码实现如下所示。InputStream 的所有的装饰器类(BufferedInputStream、DataInputStream)都继承自这个装饰器父类。这样,装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现。
重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
课堂讨论
在上节课中,我们讲到,可以通过代理模式给接口添加缓存功能。在这节课中,我们又通过装饰者模式给 InputStream 添加缓存读取数据功能。那对于“添加缓存”这个应用场景来说,我们到底是该用代理模式还是装饰器模式呢?你怎么看待这个问题?
欢迎留言和我分享你的思考,如果有收获,也欢迎你把这篇文章分享给你的朋友。
分享给需要的人,Ta购买本课程,你将得29元
生成海报并分享
赞 80
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
49 | 桥接模式:如何实现支持不同类型和渠道的消息推送系统?
下一篇
51 | 适配器模式:代理、适配器、桥接、装饰,这四个模式有何区别?
精选留言(125)
- 下雨天2020-02-26你是一个优秀的歌手,只会唱歌这一件事,不擅长找演唱机会,谈价钱,搭台,这些事情你可以找一个经纪人帮你搞定,经纪人帮你做好这些事情你就可以安稳的唱歌了,让经纪人做你不关心的事情这叫代理模式。 你老爱记错歌词,歌迷和媒体经常吐槽你没有认真对待演唱会,于是你想了一个办法,买个高端耳机,边唱边提醒你歌词,让你摆脱了忘歌词的诟病,高端耳机让你唱歌能力增强,提高了基础能力这叫装饰者模式。展开共 33 条评论539
- 小晏子2020-02-26对于添加缓存这个应用场景使用哪种模式,要看设计者的意图,如果设计者不需要用户关注是否使用缓存功能,要隐藏实现细节,也就是说用户只能看到和使用代理类,那么就使用proxy模式;反之,如果设计者需要用户自己决定是否使用缓存的功能,需要用户自己新建原始对象并动态添加缓存功能,那么就使用decorator模式。共 10 条评论317
- Jxin2020-02-26今天的课后题: 1.有意思,关于代理模式和装饰者模式,各自应用场景和区别刚好也想过。 1.代理模式和装饰者模式都是 代码增强这一件事的落地方案。前者个人认为偏重业务无关,高度抽象,和稳定性较高的场景(性能其实可以抛开不谈)。后者偏重业务相关,定制化诉求高,改动较频繁的场景。 2.缓存这件事一般都是高度抽象,全业务通用,基本不会改动的东西,所以一般也是采用代理模式,让业务开发从缓存代码的重复劳动中解放出来。但如果当前业务的缓存实现需要特殊化定制,需要揉入业务属性,那么就该采用装饰者模式。因为其定制性强,其他业务也用不着,而且业务是频繁变动的,所以改动的可能也大,相对于动代,装饰者在调整(修改和重组)代码这件事上显得更灵活。展开共 12 条评论149
- 守拙2020-02-26补充关于Proxy Pattern 和Decorator Pattern的一点区别: Decorator关注为对象动态的添加功能, Proxy关注对象的信息隐藏及访问控制. Decorator体现多态性, Proxy体现封装性. reference: https://stackoverflow.com/questions/18618779/differences-between-proxy-and-decorator-pattern展开共 3 条评论94
- andi轩2020-04-15对于为什么必须继承装饰器父类 FilterInputStream的思考: 装饰器如BufferedInputStream等,本身并不真正处理read()等方法,而是由构造函数传入的被装饰对象:InputStream(实际上是FileInputStream或者ByteArrayInputStream等对象)来完成的。 如果不重写默认的read()等方法,则无法完成如FileInputStream或者ByteArrayInputStream等对象所真正实现的read功能。 所以必须重写对应的方法,代理给这些被装饰对象进行处理(这也是类似于代理模式的地方)。 如果像DataInputStream和BufferedInputStream等每个装饰器都重写的这些方法话,会存在大量重复的代码。 所以让它们都继承FilterInputStream提供的默认实现,可以减少代码重复,让装饰器只聚焦在它自己的装饰功能上即可。展开共 9 条评论56
- rammelzzz2020-02-26对于无需Override的方法也要重写的理解: 虽然本身BufferedInputStream也是一个InputStream,但是实际上它本身不作为任何io通道的输入流,而传递进来的委托对象InputStream才能真正从某个“文件”(广义的文件,磁盘、网络等)读取数据的输入流。因此必须默认进行委托。共 2 条评论35
- 万历十五年2020-11-25代理模式体现封装性,非业务功能与业务功能分开,而且使用是透明的,使用者只需要关注于自身的业务,在业务场景上适用于对某一类功能进行加强,比如日志,事务,权限。 装饰器模式体现多态性,优点在于避免了继承爆炸,适用于扩展多个平行功能。在场景上,这些扩展的功能可以像火车厢一样串起来,使原有的业务功能不断增强。 具体到“缓存”这个单个问题,两种模式都可以,主要还是看设计者的目的,如果是为了新增功能的隐藏性,就使用代理模式;如果设计者不仅要增加“缓存”功能,还要增加“过滤”等功能,就更适于装饰器模式。展开
作者回复: 嗯嗯 ������
共 4 条评论32 - Yo nací para que...2020-02-26对于为什么中间要多继承一个FilterInputStream类,我的理解是这样的: 假如说BufferedInputStream类直接继承自InputStream类且没有进行重写,只进行了装饰 创建一个InputStream is = new BufferedInputStream(new FileInputStream(FilePath)); 此时调用is的没有重写方法(如read方法)时调用的是InputStream类中的read方法,而不是FileInputStream中的read方法,这样的结果不是我们想要的。所以要将方法再包装一次,从而有FilterInputStream类,也是避免代码的重复,多个装饰器只用写一遍包装代码即可。展开共 4 条评论31
- 李小四2020-03-07设计模式_50: # 作业 正如文中所说,装饰器是对原有功能的扩展,代理是增加并不相关的功能。 所以问题就变成使用者认为“缓存”是否扩展了原功能 - 比如说需要把想把所有的网络信息都加上缓存,提高一些查询效率,这时候应该使用代理模式; - 如果我在设计网络通信框架,需要把提供“缓存”作为一种扩展能力,这时应该用装饰器模式; 现实中,大部分的网络缓存都以代理模式被实现。 另外,缓存(Cache)与缓冲(Buffer)是不同的概念,这里也可以区分一下。 # 感受 到了具体模式的课程,有一个明显的特点:一句话感觉看懂了,反复读才能发现有更多的信息在里面,坦白讲,很多模式编程中没有用过,与单纯地读原理和特征相比,我想真正用的时候才能理解更深入的东西。展开17
- 岁月神偷2020-02-27我觉得应该用代理模式,当然这个是要看场景的。代理模式是在原有功能之外增加了其他的能力,而装饰器模式则在原功能的基础上增加额外的能力。一个是增加,一个是增强,就好比一个是在手机上增加了一个摄像头用于拍照,而另一个则是在拍照这个功能的基础上把像素从800W提升到1600W。我觉得通过这样的方式区分的话,大家互相沟通起来理解会统一一些。共 1 条评论14
- iLeGeND2020-02-26// 代理模式的代码结构(下面的接口也可以替换成抽象类) public interface IA { void f(); } public class A impelements IA { public void f() { //... } } public class AProxy impements IA { private IA a; public AProxy(IA a) { this.a = a; } public void f() { // 新添加的代理逻辑 a.f(); // 新添加的代理逻辑 } } // 装饰器模式的代码结构(下面的接口也可以替换成抽象类) public interface IA { void f(); } public class A impelements IA { public void f() { //... } } public class ADecorator impements IA { private IA a; public ADecorator(IA a) { this.a = a; } public void f() { // 功能增强代码 a.f(); // 功能增强代码 } } 老师 上面代码结构完全一样啊 不能因为 f() 中写的 逻辑不同 就说是两种模式吧展开共 5 条评论10
- 唐朝农民2020-02-26订单的优惠有很多种,比如满减,领券这样的是不是可以使用decorator 模式来实现共 3 条评论8
- 楚小奕2021-01-13我觉得不要纠结代理和装饰吧, 自己清楚设计意图就好5
- 木头2020-02-26看业务场景,如果只是针对某个类的某一个对象,添加缓存,那么就使用装饰模式。如果是针对这个类的所有对象,添加缓存。那么就使用代理模式5
- 年轻的我们2020-03-08个人理解:装饰者模式就是代理模式中的静态代理模式,装饰类对于需要新增附加功能的方法,新增附加功能,对应不需要实现定制化功能的方法,继承和组合方式调用原始类功能即可共 2 条评论4
- 松花皮蛋me2020-02-26通过将原始类以组合的方式注入到装饰器类中,以增强原始类的功能,而不是使用继承,避免维护复杂的继承关系。另外,装饰器类通常和原始类实现相同的接口,如果方法不需要增强,重新调用原始类的方法即可。4
- JoeyforJoy2021-04-25代理模式和装饰器模式的区别要从类的调用者的角度来看。 对于代理模式,调用者不需要知道代理类中有哪些额外的功能模块,直接调用代理类即可。正如我们找别人“代理”办事一样,我不需要知道代理人为我们多做了哪些事,我们只关心我们交代的事情有没有办好。 而对于装饰器模式,调用者则需要知道装饰类提供的额外的功能,来满足定制化的服务。就好比买手机时的套餐服务,我们不仅关心手机的好坏,我们还关心提不提供保修、送不送充电器等额外的服务。 回过头来回答课后问题,对于类的设计者来说,如果设计者认为“缓存”功能对于调用者来说在任何时刻都是必要的,那么则选择代理模式;如果设计者认为“缓存”功能对调用者来说是可有可无的,希望调用者根据自己的需要来定制的,那么则选择装饰器模式展开共 1 条评论3
- 永旭2020-07-15读了第三遍了 , JAVA IO 用例才理解透 ~~共 2 条评论3
- Frank2020-02-27打卡 设计模式-装饰器模式 装饰器模式是一种类似于代理模式的结构型模式。主要意图是增强原始类的功能,可以实现多个功能的增强(即不同的功能单独一个类维护,使用该模式将其功能组合起来)。该模式主要是为了解决为了实现某些功能导致子类膨胀的问题。个人觉得主要体现了单一职责、组合优先于继承原则。主要应用场景有Java IO 流设计。但是有个疑惑,在Reader和Writer体系结构的设计中,并没有像InputStream和OutputStream那样设计一个过滤流类,而BufferedReader等直接继承了Reader。按照作者本文的分析,字符输入流直接跳过了使用中间类来继承的步骤,这样的设计又该如何理解? 对于课堂讨论,我觉得应该使用装饰器模式,因为“添加缓存”这个功能跟原始功能是由直接关系的。而代理模式所面向主要是将框架代码与业务代码解耦合。展开3
- 查理2020-09-10还是不太懂代理模式和装饰器模式的区别,感觉他们太像了,只不过人为规定,代理模式新增的是与原始类不相关的功能,装饰器模式是对原始功能的增强,但是他们在代码类结构上是相同的。共 5 条评论2