日期
Mar 10, 2022
Tags
设计模式
面向对象
导语
当你在编写代码时,需要扩展一个类的功能,或者是当前类的接口不能满足需求时,你会选择怎么做。重新编写子类,通过继承加入功能?修改原有类的接口使其符合现有环境?但你会发现这些改动是不完美的,它们并不符合面向对象的「开放-关闭原则」。
开放-关闭原则:
- 对扩展开放,对修改关闭
在软件设计模式中有一个更好的答案——包装。
今天介绍的四种设计模式都围绕着“包装”展开,那么首先先简单了解一下这些设计模式:
- 装饰者模式(Decorator Pattern):包装另一个对象,并提供额外的行为
- 适配器模式(Adapter Pattern):包装另一个对象,并提供不同的接口
- 外观模式(Facade Pattern):包装许多对象以简化它们的接口
- 代理模式(Proxy Pattern):包装另一个对象,并控制对它的访问
装饰模式(Decorator Pattern)
当你想要扩展一个类的功能时,最直接的方法就是编写一个子类,然后在子类中加入新的功能函数。但是这种更改往往会导致很多问题,例如:想要去掉父类中的一个方法时。这不是一种弹性设计,不符合「开放-关闭原则」。装饰模式提供了继承之外的一种新思路。
什么是装饰模式?
定义:动态地将责任附加到对象上,给对象添加额外的职责,对于扩展功能来说, 装饰者提供了比继承更有弹性的方法。
- 装饰者可以在所委托的被装饰者的行为之前或之后,加上自己的行为,达到特定的目的
- 使用装饰者模式的过程中,可以使用多个装饰类包装对象(数量没有限制),客户端可以在运行时有选择地使用装饰功能包装对象。
装饰者&被装饰对象
装饰者和被装饰对象需要具有相同的父类,装饰者模式用继承达到类型匹配的目的
- 每个装饰对象的实现与如何使用这个对象分离开,每个装饰对象只关心自己的功能,不需要关心如何被添加到对象链中。
使用方法:将需要实例化的对象传入装饰类中进行包装
Java.IO库就是以装饰者模式来编写的
装饰模式代码实例[3]:
在DecoratorClient中,经过装饰类的包装后,最终对象关系如下,被装饰者concreteComponent包装在装饰类中,同时具有了各个装饰类附加的方法和行为
装饰者模式和继承的区别:
- 继承设计子类,是在编译时静态决定的,通过组合的做法扩展对象,可以在运行时动态地进行扩展
- 装饰者模式通过组合和委托,可以在运行时动态地为对象加上新的行为
装饰模式的优缺点
优点:
- 把类中的装饰功能从类中搬移,简化原有类
- 有效地把类的核心职责和装饰功能区分开
- 去除相关类中重复的装饰逻辑
缺点:
- 装饰者模式常常造成设计中出现大量的小类,数据太多可能会使程序变得复杂,对使用API的程序员产生困扰
- 装饰者在实例化组件时除了实例化组件还要将组件包装进装饰者中,会增加代码复杂度
- 克服这一缺点,通过工厂模式和生成器模式对实例化部分代码进行封装
适配器模式(Adapter Pattern)
当系统的数据和行为都正确,需要使用一个现有类而其接口并不符合需求的时候,考虑用适配器,适配器模式主要应用于希望复用一些现存类,但是接口与复用环境不一致的情况。
什么是适配器模式?
定义:适配器模式将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以一起工作。
适配器通过使用对象组合,以修改的接口包装被适配者。
适配器把被适配者的接口转换为适配者的接口。
适用场景
- 需要使用一个现有类而其接口并不符合需求的时候
- 当两个类做的事情相同或相似,但具有不同接口时,可以使用适配器模式统一接口
适配器模式代码实例[3]:
适配器模式优缺点
优点:
- 不需要进行代码的大量改动,通过添加一个适配器类,将改变封装在类中,满足新的需求
- 让客户和实现的接口/适配器解耦
缺点:
- 导致更多的“包装”类,用以处理和其他组件的沟通,会导致复杂度和开发时间的增加,并降低运行时的性能
- 太多的类耦合在一起,会提高维护成本和降低代码的可理解性
适配器模式只有在碰到无法改变原有设计和代码的情况才考虑,在设计开发的过程中,应该预先预防接口不匹配的问题发生,当出现有小的接口不统一时,应及时重构,避免问题的扩大
适配器模式的类型
类适配器:继承被适配者类和目标类
- 类适配器通过多重继承对一个接口与另一个接口进行匹配
- 优点:
- 由于适配器类是适配者类的子类,因此可以再适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
- 缺点:
- 对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类
- 目标抽象类只能为接口,不能为类,其使用有一定的局限性,不能将一个适配者类和他的子类同时适配到目标接口。
对象适配器:利用组合的方式将请求传送给被适配者
- 可以适配某个类及其任何子类
- 优点:
- 把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和他的子类都适配到目标接口。
- 缺点:
- 把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和他的子类都适配到目标接口。
与装饰者模式的区别:
- 装饰者模式:需要添加新的功能时使用
- 加入新功能到类中,而无需改变现有代码
- 适配器模式:需要转换接口时使用
- 允许用户使用新的库和子集合,而无需改变原有代码
最少知识原则(Least Knowledge Principle)又名迪米特法则(Law of Demeter)
- 每个单元对其他单元只拥有有限的知识,只了解与当前单元紧密联系的单元;
- 即只和你的密友谈话
- 方针:只调用属于以下范围的方法
- 该对象本身
- 被当做方法的参数而传递进来的对象
- 此方法所创建或实例化的任何对象
- 对象的任何组件
外观模式(Facade Pattern)
当有很多复杂的接口需要使用时,通过一个类来将复杂的逻辑封装在内并提供简单的统一接口,可以很好的提高代码的可读性,降低程序复杂度。
外观模式就是这样的一个类,不过它并没有“封装”子系统。当你需要简化并统一一个很大的接口或一群复杂的接口时,可以使用外观模式。
什么是外观模式?
定义:外观模式提供了一个统一的接口,用来访问子系统中的一组接口。此模式定义了一个高层接口,使得子系统更容易使用。
- 可以创建多个外观
适用场景
通过使用外观模式,在数据访问层、业务逻辑层和表示层的层与层之间建立“外观”,降低耦合度
外观模式代码实例[3]:
维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,此时可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。
外观模式优缺点:
优点:
- 将客户端从子系统中解耦,客户端代码面向接口编写,若要修改子系统的组件时只需要改动外观类就可以
- 通过实现一个提供更合理的接口的外观类,隐藏复杂子系统背后的逻辑,提供一个方便使用的接口
- 没有“封装”子系统的类,只提供简化接口,子系统的类依然可以被调用。在提供简化接口的同时,将系统完整的功能暴露出来,以供需要的人使用。
- 遵循「最少知识原则」
缺点:
- 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了「开放-关闭原则」
与装饰模式、适配器模式的区别:
- 装饰者:不改变接口,但加入责任
- 适配器:将一个接口转成另一个接口,改变接口使其符合客户期望
- 外观:让接口更简单,提供子系统的一个简化接口
代理模式(Proxy Pattern)
代理从字面意思理解就是代理他人的职务。在计算机中,代理常用来控制和管理访问,如代理服务器,可以作为中转站,代理网络用户去获取网络信息。
什么是代理模式
定义:代理模式为另一个对象提供一个替身或占位符以便控制客户对对象的访问
- 使用代理模式创建代表,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象
代理控制访问方式:
- 远程代理:控制访问远程对象,为一个对象在不同的地理空间提供局部代表
- 虚拟代理:控制访问创建开销大的资源,通过代理替代实例化需要很长时间的真实对象(网页加载时的图片框)
- 保护代理:基于权限控制对资源的访问
代理模式代码实例[3]:
这里的代理只是简单的示例,实际上的代理往往在上面提到的几个场景(远程、虚拟、保护)中使用,用来控制和管理访问
代理模式的优缺点:
优点:
- 代理模式在访问对象时引入一定程度的间接性,这种间接性可以附加多种用途
- 代理模式在客户端与目标对象之间起中介作用,可以保护目标对象
- 使得客户端与目标对象分离,在一定程度上降低了耦合
缺点:
- 增加了系统的复杂度,客户端只能够看到代理类
- 会出现大量的重复代码。
与装饰模式、适配器模式的区别
- 装饰者为对象增加行为,而代理控制对象的访问
- 代理和适配器都挡在其他对象前面,负责将请求转发给它们
- 适配器会改变对象适配的接口,而代理则实现相同的接口
代理模式变体类型(简单了解)
- 防火墙代理(Firewal Proxy):控制网络资源的访问,保护主题免于“坏客户”的侵害
- 智能引用代理(Smart Reference Proxy):当主题被引用时,进行额外的动作,代理处理另一些事儿。例如计算一个对象被引用的次数
- 缓存代理(Caching Proxy):为开销大的运算结果提供暂时存储:它也允许多个客户共享结果,以减少计算或网络延迟
- 同步代理(Synchronization Proxy)在多线程的情况下为主题提供安全的访问
- 复杂隐藏代理(Complexity Hiding Proxy):用来隐藏一个类的复杂集合的复杂度,并进行访问控制,有时候也称为外观代理(Facade Proxy)
- 写入时复制代理(Copy-On-Write Proxy):用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止,这是虚拟代理的变体。
总结
对于装饰模式、适配器模式、外观模式和代理模式,他们彼此之间都有相似之处,例如四种都对对象进行了包装,适配器模式和外观模式都提供了接口,代理模式和装饰模式都可能会引入新功能......
但其实区分这几种设计模式重点不在于如何包装类,包装类的个数、是否添加新功能,重点在于不同设计模式的目的(意图)不同
四种设计模式的目的
- 装饰模式:将一个对象包装起来以增加新的行为和责任
- 不改变接口,但加入责任
- 适配器模式:将一个对象包装起来以改变其接口
- 将一个接口转成另一个接口,改变接口使其符合客户期望
- 外观模式:将一群对象包装起来以简化其接口
- 让接口更简单,提供子系统的一个简化接口
- 代理模式:将一个对象包装起来以控制对它的访问
- 控制和管理对对象的访问
把握了以上几点,也就记住了四种设计模式的本质区别,相信对于每个设计模式适用场景也能有更深的理解。
Reference
[1] 程杰. 大话设计模式[M/OL]. 清华大学出版社 2007-12-1, 2007.
[2] FREEMAN E, FREEMAN E, BATES B, 等. Head First 设计模式(中文版)[M/OL]. TAIWAN公司 O, 译. 中国电力出版社, 2007.
[3] JavaCodeAcc 模式相关代码 https://github.com/echoTheLiar/JavaCodeAcc/tree/master/src/designpattern/