概述:**结构型模式(Structural Pattern)**关注如何将现有类或对象组织在一起形成更加强大的结构.
不同的结构型模式从不同的角度组合类或对象,它们在尽可能满足各种面向对象设计原则的同时为类或对象的组合提供一系列巧妙的解决方案。
- 类结构型模式:关心类的组合,由多个类组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。
- 对象结构型模式:关心类与对象的组合,通过关联关系,在一个类中定义另一个类的实例对象,然后通过该对象调用相应的方法。
1 适配器模式 Adapter Pattern
如何将一个不兼容的结构引入现有的代码中。
1.1 概述
定义:适配器模式是将一个类的接口转换成客户希望的另一个接口,适配器模式让那些接口不兼容的类可以一起共工作
对象结构型模式/类结构型模式
- 别名为包装器(Wrapper)模式
- 定义中所提及的接口是指广义的接口,它可以表示一个方法或方法的集合。
1.2 结构与实现
1.2.1 结构
- 类适配器:由于Java不支持多继承,所以在适配对象和目标对象之间需要有一个是接口。
- 对象适配器:对象适配器其实就是将
Adapter
继承Adaptee
变成组合的关系。
适配器模式包含以下3个角色:
- Target(目标抽象类)
- Adapter(适配器类)
- Adaptee(适配者类)
1.2.2 实现
- 典型的类适配器代码
public class Adapter extends Adaptee implements Target { |
- 典型的对象适配器代码
public class Adapter extends Target { |
1.3 缺省适配器模式
Default Adapter Pattern
定义:当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可以选择性地覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中的所有方法的情况,又称为单接口适配器模式。
Java8中为接口提供了default方法,其实有点类似缺省的适配器模式,在接口中声明
default
的方法,实现该接口的类可以不一定要实现该方法。如果一个类同时实现接口A和B,接口A和B中有相同的default方法,这时,该类必须重写接口中的default方法。
1.3.1 结构
可以用Java8中提供的接口的
default
方法实现
public abstract class AbstractServiceClass implements ServiceInterface { |
1.4 双向适配器
1.4.1 结构
对目标对象和适配者对象都采用组合的关系。
1.4.2 实现
public class Adapter implements Target, Adaptee { |
1.5 优缺点与适用环境
1.5.1 优点
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构
- 增加了类的透明性和复用性,提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用
- 灵活性和扩展性非常好
- 类适配器模式:置换一些适配者的方法很方便
- 对象适配器模式:可以把多个不同的适配者适配到同一个目标,还可以适配一个适配者的子类
1.5.2 缺点
- 类适配器模式:
- 一次最多只能适配一个适配者类,不能同时适配多个适配者
- 适配者类不能为最终类;
- 目标抽象类只能为接口,不能为类
- 对象适配器模式:
- 在适配器中置换适配者类的某些方法比较麻烦
1.5.3 适用环境
- 系统需要使用一些现有的类,而这些类的接口不符合系统的需要,甚至没有这些类的源代码
- 创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类,包括一些可能在将来引进的类一起工作
2 桥接模式 Bridge Pattern
2.1 概述
定义:将抽象部分与它的实现部分解耦,使得两者都能够独立变化(适用于有两个独立维度的变化的类中)
对象结构型模式
- 又被称为柄体(Handle and Body)模式或接口(Interface)模式
- 用抽象关联取代了传统的多层继承
- 将类之间的静态继承关系转换为动态的对象组合关系
2.2 结构与实现
2.2.1 结构
桥接模式包含以下4个角色:
- Abstraction(抽象类)
- RefinedAbstraction(扩充抽象类)
- Implementor(实现类接口)
- ConcreteImplementor(具体实现类)
2.2.2 实现
典型的实现类接口代码
public interface Implementor { |
典型的具体实现类代码
public class ConcreteImplementor implements Implementor { |
典型的抽象类代码
public abstract class Abstraction { |
典型的**扩充抽象类(细化抽象类)**代码
public class RefinedAbstraction extends Abstraction { |
2.3 桥接模式与适配器模式的联用
- 桥接模式:用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化
- 适配器模式:当发现系统与已有类无法协同工作时
2.4 优缺点与适用环境
2.4.1 优点
- 分离抽象接口及其实现部分
- 可以取代多层继承方案,极大地减少了子类的个数
- 提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,不需要修改原有系统,符合开闭原则
2.4.2 缺点
- 会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程
- 正确识别出系统中两个独立变化的维度并不是一件容易的事情
2.4.3 适用环境
- 需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系
- 抽象部分和实现部分可以以继承的方式独立扩展而互不影响
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立地进行扩展
- 不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统
3 组合模式 Composite Pattern
3.1 概述
定义:组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。
对象结构型模式
- 又称为“部分-整体”(Part-Whole)模式
- 将对象组织到树形结构中,可以用来描述整体与部分的关系。
3.2 结构与实现
3.2.1 结构
组合模式包含以下3个角色:
- Component(抽象构件)
- Leaf(叶子构件)
- Composite(容器构件)
3.2.2 实现
抽象构件角色典型代码
public abstract class Component { |
叶子构件角色典型代码
public class Leaf extends Component { |
容器构件角色典型代码
public class Composite extends Component { |
3.3 透明组合模式与安全组合模式
3.3.1 透明组合模式
- 抽象构件Component中声明了所有用于管理成员对象的方法,包括
add()
、remove()
,以及getChild()
等方法 - 在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以一致地对待所有的对象
- 缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的
3.3.2 安全组合模式
- 抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法
- 对于叶子对象,客户端不可能调用到这些方法
- 缺点是不够透明,客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件
3.4 优缺点和适用环境
3.4.1 优点
- 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,让客户端忽略了层次的差异,方便对整个层次结构进行控制
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
- 增加新的容器构件和叶子构件都很方便,符合开闭原则
- 为树形结构的面向对象实现提供了一种灵活的解决方案
3.4.2 缺点
- 在增加新构件时很难对容器中的构件类型进行限制
3.4.3 适用环境
- 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们
- 在一个使用面向对象语言开发的系统中需要处理一个树形结构
- 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型
4 装饰模式 Decorator Pattern
4.1 概述
- 装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为
- 是一种用于替代继承的技术,它通过一种无须定义子类的方式给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系
- 引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩展原有类的功能。
定义:动态地给一个对象增加一些额外的职责。就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案。
对象结构型模式
- 以对客户透明的方式动态地给一个对象附加上更多的责任
- 可以在不需要创建更多子类的情况下,让对象的功能得以扩展。
4.2 结构与实现
4.2.1 结构
装饰模式包含以下4个角色
- Component(抽象构件)
- ConcreteComponent(具体构件)
- Decorator(抽象装饰类)
- ConcreteDecorator(具体装饰类)
4.2.2 实现
- 抽象构件类的典型代码
public abstract class Component { |
- 具体构件类典型代码
public class ConcreteComponent extends Component { |
- 抽象装饰类典型代码:
public class Decorator extends Component { |
- 具体装饰类典型代码
public class ConcreteDecorator extends Decorator { |
4.3 透明装饰模式与半透膜装饰模式
- 透明(Transparent)装饰模式:
- 要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型
- 对于客户端而言,具体构件对象和具体装饰对象没有任何区别。
- 半透明(Semi-transparent)装饰模式:
- 用具体装饰类型类定义装饰之后的对象,而具体构件使用抽象构建类型来定义
- 对于客户端而言,具体构件类型无需关心,是透明的;但是具体装饰类型必须指定,这是不透明的。
4.4 优缺点与适用环境
4.4.1 优点
- 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为
- 可以对一个对象进行多次装饰
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,且原有类库代码无须改变,符合开闭原则
4.4.2 缺点
- 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,在一定程度上影响程序的性能
- 比继承更加易于出错,排错也更困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐
4.4.3 适用环境
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
- 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式
5 外观模式 Facade Pattern
5.1 概述
- 一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出现。
- 引入一个新的**外观类(Facade)**来负责和多个业务类(子系统Subsystem)进行交互,而客户类只需与外观类交互。
- 为多个业务类的调用提供了一个统一的入口,简化了类与类之间的交互。
没有外观类:每个客户类需要和多个子系统之间进行复杂的交互,系统耦合度将很大
引入外观类:客户类只需要直接与外观类交互,客户类与子系统之间原有的复杂引用关系由外观类来实现,从而降低了系统的耦合度。
**定义:**为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
对象结构型模式
- 又称为门面模式
- 是迪米特法则的一种具体实现
- 通过引入一个新的外观角色来降低原有系统的复杂度,同时降低客户类与子系统的耦合度。
- 所指的子系统是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统。
5.2 结构与实现
5.2.1 结构
外观模式包含以下2个角色:
- Facade(外观角色)
- SubSystem(子系统角色)
5.2.2 实现
- 子系统类典型代码
public class SubSystemA { |
- 外观类典型代码
public class Facade { |
- 客户类典型代码
public class Client { |
5.3 抽象外观类
**动机:**在标准的外观模式的结构图中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码,这将违背开闭原则,因此可以通过引入抽象外观类对系统进行改进,在一定程度上解决该问题
5.4 外观模式与单例模式联用
可以将外观类用单例模式来实现
5.5 优缺点与适用环境
5.5.1 优点
- 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易
- 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可
- 一个子系统的修改对其他子系统没有任何影响,而且子系统的内部变化也不会影响到外观对象
5.5.2 缺点
-
不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性
因为访问子系统都依赖于外观类是如何实现的,所以客户类其实灵活性很低,且只能够只用外观类有提供的关于子系统的工能,而无法根据自己需要来自由使用子系统,如果要自由使用子系统则一定程度上需要摒弃外观类。
-
如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则
5.5.3 适用环境
- 要为访问一系列复杂的子系统提供一个简单入口
- 客户端程序与多个子系统之间存在很大的依赖性
- 在层次化结构中,可以使用外观模式的定义系统中每一层的入口,层与层之间不直接产生联系,而是通过外观类建立联系,降低层之间的耦合度
6 享元模式 Flyweight Pattern
6.1 概述
动机:
-
如果一个软件系统在运行时所创建的相同或相似对象数量太多,将导致运行代价过高,带来系统资源浪费、性能下降等问题。
-
如何避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作呢?
享元模式
分析:
-
享元模式:通过共享技术实现相同或相似对象的重用
-
享元池(Flyweight Pool):存储共享实例对象的地方
-
内部状态(Intrinsic State):存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享(例如:字符的内容)
-
外部状态(Extrinsic State):随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的(例如:字符的颜色和大小)
原理:
- 将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的。
- 需要的时候将对象从享元池中取出,即可实现对象的复用。
- 通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。
享元模式定义:运用共享技术有效地支持大量细粒度对象的复用。
对象行为型模式
- 又称为轻量级模式
- 要求能够被共享的对象必须是细粒度对象
6.2 结构与实现
6.2.1 结构
享元模式包含以下4个角色:
- Flyweight(抽象享元类)
- ConcreteFlyweight(具体享元类)
- UnsharedConcreteFlyweight(非共享具体享元类)
- FlyweightFactory(享元工厂类)
6.2.2 实现
- 典型的抽象享元类代码
public abstract class Flyweight { |
- 典型的具体享元类代码
public class ConcreteFlyweight extends Flyweight { |
- 典型的非共享具体享元类代码
public class UnsharedConcreteFlyweight extends Flyweight { |
- 典型的享元工厂类代码
public class FlyweightFactory { |
6.3 实例
6.4 单纯享元模式与复合享元模式
6.4.1 单纯享元模式
所有的具体享元类都是可以共享的,不存在非共享具体享元类
6.4.2 复合享元模式
- 将一些单纯享元对象使用组合模式加以组合
- 如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式
6.5 优缺点与适用环境
6.5.1 优点
- 可以减少内存中对象的数量,使得相同或者相似的对象在内存中只保存一份,从而可以节约系统资源,提高系统性能
- 外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
6.5.2 缺点
- 使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化
- 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长
6.5.3 适用环境
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,在需要多次重复使用享元对象时才值得使用享元模式
7 代理模式 Proxy Pattern
7.1 概述
代理模式定义:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
对象结构型模式
- 引入一个新的代理对象
- 代理对象在客户端对象和目标对象之间起到中介的作用
- 去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务。
7.2 结构与实现
7.2.1 结构
代理模式包含以下3个角色
- Subject(抽象主体角色)
- Proxy(代理主体角色)
- RealSubject(真实主体角色)
7.2.1 实现
- 抽象主体类典型代码
public abstract class Subject { |
- 真实主体类典型代码
public class RealSubject extends Subject { |
- 代理主体类典型代码
public class Proxy extends Subject { |
7.2.3 几种常见的代理模式
- 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以在同一台主机中,也可以在另一台主机中,远程代理又称为大使(Ambassador)
- 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建
- 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限
- 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
- 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等
7.3 实例
7.4 远程代理
动机:
- 客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速地响应并处理客户端的请求
- 可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在
- 客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用
7.5 虚拟代理
动机:
- 对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理
- 在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象
- 使用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能
应用:
- 由于对象本身的复杂性或者网络等原因导致一个对象需要较长的加载时间,此时可以用一个加载时间相对较短的代理对象来代表真实对象(结合多线程技术)
- 一个对象的加载十分耗费系统资源,让那些占用大量内存或处理起来非常复杂的对象推迟到使用它们的时候才创建,而在此之前用一个相对来说占用资源较少的代理对象来代表真实对象,再通过代理对象来引用真实对象(用时间换取空间)
7.6 Java动态代理
- 动态代理(Dynamic Proxy)可以让系统在运行时根据实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实主体类而且可以代理不同的方法
- Java语言提供了对动态代理的支持,Java语言实现动态代理时需要用到位于
java.lang.reflect
包中的一些类
7.6.1 Proxy类
-
public static Class<?> getProxyClass(ClassLoaderloader, Class<?>... interfaces) <!--code25--> 该方法用于返回一个动态创建的代理类的实例,方法中第一个参数loader表示代理类的类加载器,第二个参数interfaces表示代理类所实现的接口列表(与真实主题类的接口列表一致),第三个参数h表示所指派的调用处理程序类。
7.6.2 InvocationHandler接口
-
InvocationHandler
接口是代理处理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(InvocationHandler
接口的子类) -
public Object invoke(Object proxy, Method method, Object[] args)
该方法用于处理对代理类实例的方法调用并返回相应的结果,当一个代理实例中的业务方法被调用时将自动调用该方法。invoke()方法包含三个参数,其中第一个参数proxy表示代理类的实例,第二个参数method表示需要代理的方法,第三个参数args表示代理方法的参数数组
7.7 优缺点与适用环境
7.7.1 优点
- 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
- 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性
- 模式优点:
- 远程代理:可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高了系统的整体运行效率
- 虚拟代理:通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
- 缓冲代理:为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间
- 保护代理:可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限
7.7.2 缺点
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢(例如保护代理)
- 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂(例如远程代理)
7.7.3 适用环境
- 当客户端对象需要访问远程主机中的对象时可以使用远程代理
- 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理
- 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理
- 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理
- 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理
Reference
- Java设计模式 – 刘伟,清华大学出版社
- 面向对象设计方法 – 南京大学计算机科学与技术系2022春季课程
- Design Pattern – GoF 23种设计模式