3、设计模式之结构型模式
3.1 代理(Proxy)模式
3.1.1 引入案例:
前阵子疫情期间,为了打发时间竟然入了游戏的坑。一个多月的时间内我竟然打怪、升级、砍人、被砍,沉迷游 戏,不可自拔,陷入打怪、升级、打怪、升级......的死循环中无法逃脱。不过升级还是挺快的哈,小有成就感。这 段时间真真儿的体会了什么叫苦乐参半。参与工会攻城胜利之后超开心,觉得自己还真厉害(可能只是队友厉害 哈),但是 苦的就是为了升级就要不停的打怪、做任务(毕竟游戏外挂管的的也太紧了,怕被封号不敢用哈),升 级基本靠自己,梦中还在和大BOSS进行PK。
有了这样一段经历也比较可贵。咱们作为程序员,能不能把这段打游戏的过程系统化呢?
说来就来:分析、动手。
接口:IGamePlayer,所有网游的玩家(作者也是其中一个哈,包括你吗?)
实现类:实现游戏爱好者为了玩游戏要执行的功能
运行结果记录了我的网游生涯。打游戏结束后网游成瘾综合征体现的淋漓尽致,不想放弃游戏账号,又不想这样打 游戏精疲力尽,怎么办呢?
找代练啊!让他们帮我去打怪、去升级。
定义代练类:代练也不能作弊哦,也是手动打怪升级呢!
看到结果了吧:没有任何改变!你没干活,但是有人帮你干了,你的游戏已经不知不觉中升级啦,躺赢变大佬!
这就是代理模式!
3.1.2 代理模式的定义与结构
3.1.2.1 代理模式的定义:
为其他对象提供一种代理以控制这个对象的访问。这是一个使用频率非常高的模式。
3.1.2.2 代理模式的结构
代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访 问。
1、代理模式的主要角色如下
- 抽象主题(Subject)角色:抽象主题类可以是接口或抽象类,是一个普通的业务类型定义,声明真实主题和 代理对象实现的业务方法,无特殊要求。
- 真实主题(Real Subject)角色:真实主题角色类也叫作被委托角色、被代理角色,实现了抽象主题中的具体 业务,是代理对象所代表的真实对象,是最终要引用的对象,是业务逻辑的具体执行者。
- 代理(Proxy)角色:也叫做委托类、代理类。他负责对真实角色的应用,把所有抽象主题类定义的方法限制 委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后的工作。提供了与真实主题相 同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
2. 模式的实现
代理模式的实现代码如下:
一个代理类可以代理多个被委托或者被代理者,因此一个代理类具体代理哪个真实主题角色是由场景类决定的。最 简单的情况就是一个主题类和一个代理类,这是最简单的代理模式。所以上面的结构中通过构造方法传入被代理对 象就是最简单的方式。
3.1.3 代理模式的优缺点
代理模式的主要优点有:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
其主要缺点是:
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
- 增加了系统的复杂度;
3.1.4 代理模式的应用场景
在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就 是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如买房 子、找保姆、找工作等都可以通过找中介完成。
在软件设计中,使用代理模式的例子也很多,例如,spring框架中就使用了动态代理模式!
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申 请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空 间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因 某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器 慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数 的功能,这样当该对象没有被引用时,就可以自动释放它。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表 的延时加载。
3.1.5 代理模式的扩展
在前面介绍的代理模式中,代理类中包含了对真实主题的引用,这种方式存在两个缺点。
- 真实主题与代理主题一一对应,增加真实主题也要增加代理。
- 设计代理以前真实主题必须事先存在,不太灵活。采用动态代理模式可以解决以上问题。
什么是动态代理?动态代理实在实现阶段不关心代理谁,而是在运行阶段才指定哪一个代理对象。
我们继续通过打游戏的案例来看动态代理如何实现。
定义一个实现了InvocationHandler接口的MyInvocationHandler类。其中InvocationHandler接口是JDK提供好的 动态代理接口。
还是代练在帮我打游戏,可是我们既没有创建代理类,也没有实现IGamePlayer接口,这就是动态代理。
如果想让游戏登录后发个信息就更好了,怎么处理呢?
如果有人用我的账号,就发送一个信息,看看自己的账号是不是被盗了,其实这就是AOP编程。
动态代理的实现:
上面的DynamicProxy类是一个通用的类,不具有业务意义,我们可以来一个更具体的类:
写法更简洁了。该动态代理只是给出了一个通用的代理框架。大家可以根据自己的需求设计自己的AOP框架。
上面是基于JDK的动态代理,还有基于CGLIB的动态代理。大家可以自行查阅。
3.2 适配器(Adapter)模式
在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,讲中 文的人同讲英文的人对话时需要一个翻译,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。
在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的 接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题。
简单来说就是:让原来不兼容的两个接口协同工作。
3.2.1 适配器模式的定义与结构
3.2.1.1 适配器模式的定义如下:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。 适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件 库中的相关组件的内部结构,所以应用相对较少些。
画个图表示一下原始的适配器:
AB两个物体接口不一致,不能安装在一起,这个时候引入物体C,要求C既能适应A也能适应B,这样三者就可以完 美融合。
3.2.1.2 模式的结构
适配器模式包含以下主要角色:
- 目标(Target)角色:该角色定义把其他类转换为何种接口,也就是我们的期望接口。它可以是抽象类或接 口。
- 源(Adaptee)角色:你想把谁转换为目标角色,这个“谁”就是源角色,他是已经存在的、运行良好的类或者 对象,经过适配器角色的包装,他会成为一个新的角色。
- 适配器(Adapter)角色:是适配器模式的核心橘色,其他两个角色都是已经存在的角色,而适配器角色是需 要新建立的,他的职责很简单:把原角色转换为目标角色。如何转换?通过继承或者类关联的方式。
我们可以按照分析抽取一下通用源码:
3.2.2 适配器模式的优缺点
该模式的主要优点如下。
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
其缺点是:对类适配器来说,更换适配器的实现过程比较复杂。
3.2.3 适配器模式的应用实例
案例:话说前几年,买彩票中大奖啦!所以来了一次说走就走的旅行,去往梦想中的国度--希腊。这里有太多喜欢 的风景啦!于是眼睛欣赏的时候,手机拍照根本停不下来。等到回到酒店的时候手机和充电宝宝都没电了!明天还 要继续拍照呢,赶紧充电呗!拿出充电器发现一个问题,希腊的插座与中国不同,为欧式两圆孔插座,部分酒店为 内嵌式插座,中国电器插头不能直接使用。没错,就是下图中这样的!
怎么办呢?询问前台之后发现他们很贴心的准备了转换器,于是借来之后顺利充电成功。
我们用代码实现这个转换过程:
3.2.4 适配器模式的使用场景
(1)其中一个使用的场景是像上面所说的一样,有两个接口,你主动的想去连接着两个接口,写个适配器,感觉 这种情况也不是很多,因为很多时候都是些一个实体类对象调用另一个实体类对象。
(2)被动使用的情况,这种情况我可能见得比较多。举个栗子,比较极端的栗子,你和你同伴一起合作开发,你 同伴写一个部分,你写一个部分,现在两个部分要对接。结过到对接时,你们发现两个人都自定义了接口,而且两 个人都开发完了,都不想改,那怎么办,只能写一个适配器去适配两个接口。又或者说你开发新版本的时候重新定 义了接口,要和旧版本写适配的时候,为了方便也可以使用适配器模式。
适配器模式(Adapter)通常适用于以下场景。
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
适配器模式是一个补偿模式,或者说是一个“补救”模式。通常用来解决接口不相容的问题。一般项目开始的时候用的偏 少。大多数是在项目的需求不断变化的时候,技术为了业务服务的,因此业务在变化的时候,对技术也提出了要求,这些 时候可能就需要这样的补救模式诞生。
3.3 装饰(Decorator)模式
3.3.1 案例引入
大家上学的时候有没有遇到过这样的情况:
老师:考试成绩出来了,大家把成绩单拿回家给家长看并请家长签字明天带回来。
我:......(按惯例,一顿“竹笋炒肉”是少不了了)
我们用程序来描绘一下这个过程:
这样的成绩单肯定少不了一顿打,所以我就开始想办法,能不能把成绩单装饰一下,不让我的成绩看起来那么像挨 揍的成绩呢?于是我想了以下办法(注意:修改成绩造假那是不可以滴):
- 汇报一下最高成绩:其实是本次考试成绩都不好,最高分也都是七十多分,如果我报告了最高成绩,老爸一 看我成绩跟最高成绩对比,就会觉得我这个分数还能接受;
- 汇报一下班级排名:告诉老爸我再全班排名32,当然我不会告诉他我们班考试人数也就是38个人,因为有好 几个同学因为生病没参加考试。成绩单上没有这些信息,我得趁此机会免遭一顿打啊。
说做就做,于是修改一下原有的程序展示这个过程。最简单的办法就是添加一个子类,重写show方法:
我们通过继承的方式解决了这个问题。老爸看后没揍我就签字了。但是现实的情况可能有很多种:
- 看完最高成绩和我的成绩,直接签名了,不看后面的排名
- 老爸要先看排名,再看我的成绩,再看最高成绩
现实中遇到不同的情况怎么办呢?要继续扩展多少个子类呢?这个还是需要装饰的条件比较少的情况,如果条件多 了,不是2个,而是20个,那要有多少个子类啊?!
这就是继承解决该问题带来的问题,继承带来的类越多,后期维护的成本也会越高。那是不是应该优化一下咱们的 设计呢?所有,咱们就定义一批专门负责装饰的类,然后根据实际情况来决定是否需要进行装饰。
程序实现:
运行结果:
实现的结果一样!而如果我还需要其他的修饰条件,我们只需要实现Decotator类就可以啦!这就是装饰模式!
3.3.2 装饰模式的定义与结构
3.3.2.1 装饰模式的定义:
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构 型模式。
3.3.2.2 装饰模式的结构与实现
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增 多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的 类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。下面来分析其基本结构和实现方法。
1. 模式的结构
装饰模式主要包含以下角色。
- 抽象构件(Component)角色:是一个抽象类或者接口,定义最核心的对象,也就是最原始的对象,例如上 面的成绩单。
- 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:一般是一个抽象类,继承抽象构件,实现其抽象方法,里面不一定有抽象的方 法,在他的属性里一般都会有一个private变量指向Component抽象构件 。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
2. 模式的实现
装饰模式的实现代码如下:
3.3.3 装饰模式的优缺点
装饰(Decorator)模式的主要优点有:
- 采用装饰模式扩展对象的功能比采用继承方式更加灵活。
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
其主要缺点是:装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。
3.3.4 装饰模式的应用场景
装饰模式通常在以下几种情况使用。
- 当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类 是终极类或者采用继承方式会产生大量的子类。
- 当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装 饰模式却很好实现。
- 当对象的功能要求可以动态地添加,也可以再动态地撤销时。
- 需要为一批的兄弟类进行改装或者加装功能的时候,可以首选装饰模式。
3.3.5 装饰模式在java中的应用
装饰模式在 java语言中的最著名的就是 Java I/O 标准库的设计了。
例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它 们都是抽象装饰类。
下面代码是为 FileReader 增加缓冲区而采用的装饰类 BufferedReader 的例子:
BufferedReader in=new BufferedReader(new FileReader("filename.txtn));
String s=in.readLine();
3.4 亨元(Flyweight)模式
在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多 的系统资源,它是系统性能提高的一个瓶颈。
例如:String常量池、数据库连接池、缓冲池等等都是享元模式的应用,所以说享元模式是池技术的重要实现方 式。
比如我们每次创建字符串对象时,都需要创建一个新的字符串对象的话,内存开销会很大,所以如果第一次创 建了字符串对象“adam“,下次再创建相同的字符串”adam“时,只是把它的引用指向”adam“,这样就实现 了”adam“字符串再内存中的共享。
再举个例子:网络联机下棋的时候,一台服务器连接了多个玩家,如果我们每个棋子都要创建对象,那一盘棋可 能就有上百个对象产生,玩家多点的话,因为内存空间有限,一台服务器就难以支持了,所以这里要使用享元模 式,将棋子对象减少到几个实例。
3.4.1 享元模式的定义与结构
3.4.1.1 享元模式的定义:
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数 量、避免大量相似类的开销,从而提高系统资源的利用率。
3.4.1.2. 享元模式的结构
享元模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分;
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态, 并将外部状态外部化。下面来分析其基本结构和实现方法。
1、享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):简单理解就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口 或者实现。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法 中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工 厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享 元对象。
2. 享元模式的实现
享元模式的参考实现代码如下:
3.4.2 享元模式的优缺点:
优点:
大大减少应用程序创建的对象,相同对象只要保存一份,,降低程序内存的占用,这降低了系统中对象的数量,从 而降低了系统中细粒度对象给内存带来的压力。
缺点:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
3.4.3 享元模式的应用实例
实现一个生活中的场景:开部门会议
3.4.4 享元模式的应用场景
- 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
- 细粒度的对象都具备比较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定的身份。
- 需要缓冲池的场景