目录
创建型模式(5个)
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
(记忆:抽象想象的方法,建造一个原来单一的工厂)
结构型模式(7个)
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
行为模式(11个)
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
所有的设计模式都是为了程序能更好的满足这六大原则。设计模式一共有 23 种,今天我们先来学习构建型模式,一共五种,分别是:
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
一、适配器模式
适配器的定义:
适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作
。适配器模式分为类结构型模式
和对象结构型模式
两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些
。
适配器模式的结构
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者
类适配器模式的结构图
对象适配器模式的结构图
类适配器模式的结构图
目标接口
适配者接口/类
类适配器
客户端代码
对象适配器的基本代码模式
主要改变的就是利用构造传参,代替继承(解决了java单继承的缺点),其他代码不变
客户端测试代码
网上大部分的代码,都止步于此,但是我越看越觉得不对劲,比如一个接口是MP3 -- 方法 play(),一个接口是MP4 ---方法play, 最后我是适配了MP4 ,但是我原本的mp3 功能去哪里拉? 有没有啥办法,能保留我之前的mp3功能还能适配mp4功能嘛?
下面的场景,来帮你解决这个问题。
实际应用场景的案例展示
案例场景:我们有一个 MediaPlayer 接口和一个实现了 MediaPlayer 接口的实体类 AudioPlayer。默认情况下,AudioPlayer 可以播放 mp3 格式的音频文件。
我们还有另一个接口 AdvancedMediaPlayer 和实现了 AdvancedMediaPlayer 接口的实体类。该类可以播放 vlc 和 mp4 格式的文件。
(我们回到之前适配器模式得定义:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作
。 在这里也就是,我希望MP3的接口,能够调用MP4和VLC的所有功能)
MP3 播放器接口
- 在这里增加了audioType,可以根据后序传过来的type判断使用 谁的额外的功能
AdvancedMediaPlayer
- 高级播放器的接口,分别定义了能播放vlc 和 mp4的功能
vlc播放器的实现类
MP4播放器的实现类
创建实现了 MediaPlayer 接口的适配器类。
- 这里注意,再构造方法时候,主要根据type去判断使用那个高级播放器的对象,然后使用play去进行调用额外功能(那么我之前的mp3的功能呢?)
创建实现了 MediaPlayer 接口的实体类。
-
在这里起始就是MP3的播放实现类,可以看到,在他完善自己播放MP3的时候,同时再部署的自己的功能时候,利用适配器增加额外的功能。
-
在这里使用 AudioPlayer 来播放不同类型的音频格式。
适配器模式的优缺点
优点
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
- 在很多业务场景中符合开闭原则。
缺点
- 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
- 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱
二、装饰器模式
装饰器模式得定义
装饰器的核⼼就是再不改原有类的基础上给类新增功能
。不改变原有类,可能有的⼩伙伴会想到继承、AOP切⾯,当然这些⽅式都可以实现,但是使⽤装饰器模式会是另外⼀种思路更为灵活,可以避免继承导致的⼦类过多,也可以避免AOP带来的复杂性
常见的装饰器使用场景
new BufferedReader(new FileReader(""));
,这段代码你是否熟悉,相信学习java开发到字节流、字符流、⽂件流的内容时都⻅到了这样的代码,⼀层嵌套⼀层,⼀层嵌套⼀层
,字节流转字符流等等,⽽这样⽅式的使⽤就是装饰器模式的⼀种体现。
之所以这么设计的原通常情况下在于,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀
。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰器模式的目标
。下面来分析其基本结构和实现方法。
装饰器模式的基本代码接口
抽象构件角色
具体构件角色
抽象装饰角色
具体装饰角色
- 装饰器最后的亮点就在于调用后了super 方法,这里很像aop。
测试类
实战,重构前的项目现状
但随着业务的不断发展,团队⾥开始出现专⻔的运营⼈员、营销⼈员、数据⼈员,每个⼈员对于ERP的使⽤需求不同,有些需要创建活动,有些只是查看数据。同时为了保证数据的安全性,不会让每个⽤户都有最⾼的权限。
那么以往使⽤的 SSO 是⼀个组件化通⽤的服务,不能在⾥⾯添加需要的⽤户访问验证能。这个时候我们就可以使⽤装饰器模式,扩充原有的单点登录服务。但同时也保证原有功能不受破坏,可以继续使⽤。
1 模拟Spring的HandlerInterceptor
这个是我们自定义,仅仅只是为了模拟拦截器的拦截。
2. 模拟单点登录的拦截
- 这⾥的模拟实现⾮常简单只是截取字符串,实际使⽤需要从 HttpServletRequest request 对象中获取 cookie 信息,解析 ticket 值做校验。
- 在返回的⾥⾯也⾮常简单,只要获取到了 success 就认为是允许登录。
- 模拟员工登录的场景
- 编写测试类
总结:上面的LoginSsoDecorator 针对之前拦截器返回sucess扩展了用户具有那些访问权限。
在重构之前,有如下几点,需要特意说明下。
在装饰器模式中有四个⽐较重要点抽象出来的点;
- 抽象构件⻆⾊(Component) - 定义抽象接⼝
- 具体构件⻆⾊(ConcreteComponent) - 实现抽象接⼝,可以是⼀组
- 装饰⻆⾊(Decorator) - 定义抽象类并继承接⼝中的⽅法,保证⼀致性
- 具体装饰⻆⾊(ConcreteDecorator) - 扩展装饰具体的实现逻辑
抽象装饰类
- 在装饰类中有两个᯿点的地⽅是;1)继承了处理接⼝、2)提供了构造函数、3)覆盖了⽅法preHandle 。
- 以上三个点是装饰器模式的核⼼处理部分,这样可以踢掉对⼦类继承的⽅式实现逻辑功能扩展
装饰⻆⾊逻辑实现
- 在具体的装饰类实现中,继承了装饰类 SsoDecorator ,那么现在就可以扩展⽅法;
- preHandle在 preHandle 的实现中可以看到,这⾥只关⼼扩展部分的功能,同时不会影响原有类的核⼼服务,也不会因为使⽤继承⽅式⽽导致的多余⼦类,增加了整体的灵活性。
测试类
总结:从下面的的图中能发现最大的不同点
左边没有重构前的:之前SsoInterceptor 已经实现了相关的截取判断是否成功的代码,结果在OldLoginSsoDecorator有继续重写了一遍。
右边的重构后的:重复的代码,我直接super一笔带过即可。
这其中很大的重点在于
从中我们也知道相关的截取字符串的逻辑,判断为success在SsoInterceptor 实现的。在这里他也没有集成SsoInterceptor 为什么直接super就可以调用SsoInterceptor的之前的实现呢 ?
重点就在于:下面的构造参数,这样就可以灵活根据构造方法,去super 不同实现类的实现。
从而达到最后测试类的效果。
@Test
public void test_LoginSsoDecorator() {
LoginSsoDecorator ssoDecorator = new LoginSsoDecorator();
String request = "1successhuahua";
boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu",
"t");
System.out.println("登录校验:" + request + (success ? " 放⾏" : " 拦
截"));
};
}
装饰器模式的优缺点
优点
- 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
- 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果装饰器模式完全遵守开闭原则
缺点
- 装饰器模式会增加许多子类,过度使用会增加程序得复杂性。
三、代理模式
代理模式的定义: 由于某些原因需要给某对象提供一个代理以控制对该对象的访问
。这时,访问对象不适合或者不能直接引用目标对象
,代理对象作为访问对象和目标对象之间的中介。
代理的模式的主要结构
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
代理模式的基本代码模式
起始项目中,我们管理代理,用的最多的是jdk代理和cglib代理。他们都分别是代理模式的实现方式之一,下面的我分别讲述他们的使用和区别
JDK动态代理
- 我们定义一个接口,定义它的访问前缀和后缀
实现这个接口的实现类
代理接口以及实现类
- 此时如果他不实现这个接口,如果是jdk代理报错,cglib没事
代理类
测试类
Cglib动态代理
AOP的接口和实现类不变
这里没有实现接口
代理类
测试类
CGLIB与JDK动态代理区别
-
Jdk动态代理:利用拦截器(必须实现InvocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
-
Cglib动态代理:利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理
-
JDK动态代理只能对实现了接口的类生成代理,而不能针对类Cglib是针对类实现代理,
Cglib比JDK快
cglib底层是ASM字节码生成框架,但是字节码技术生成代理类,在JDL1.6之前比使用java反射的效率要高
在jdk6之后逐步对JDK动态代理进行了优化,在调用次数比较少时效率高于cglib代理效率
只有在大量调用的时候cglib的效率高,但是在1.8的时候JDK的效率已高于cglib
Spring如何选择是用JDK还是cglib
- 目标对象生成了接口 默认用JDK动态代理
- 如果目标对象没有实现接口,必须采用cglib库,
- 自己可以通过设置,即便对象不生成接口,也可以强制使用cglib
四、外观模式
什么是外观模式
在现实生活中,常常存在办事较复杂的例子,如办房产证或注册一家公司,有时要同多个部门联系,这时要是有一个综合部门能解决一切手续问题
就好了。
软件设计也是这样,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了“开闭原则”,也违背了“迪米特法则”,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度
,这就是外观模式的目标。
外观模式的结构
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
- 客户(Client)角色:通过一个外观角色访问各个子系统的功能
外观模式的java基本结构
子系统角色
外观角色
测试类
- 从上面的基本结构中,我们可以看出,外观模式,好想大致就类似我们开发常用的把方法封装一下
案例模拟
在本案例中我们模拟⼀个将所有服务接⼝添加⽩名单的场景
,在项⽬不断壮⼤发展的路上,每⼀次发版上线都需要进⾏测试,⽽这部分测试验证⼀般会进⾏⽩名单开
量或者切量的⽅式进⾏验证。那么如果在每⼀个接⼝中都添加这样的逻辑,就会⾮常麻烦且不易维护
。
另外这是⼀类具备通⽤逻辑的共性需求,⾮常适合开发成组件,以此来治理服务,让研发⼈员更多的关
⼼业务功能开发。
⼀般情况下对于外观模式的使⽤通常是⽤在复杂或多个接⼝进⾏包装统⼀对外提供服务上
,此种使⽤⽅式也相对简单在我们平常的业务开发中也是最常⽤的。你可能经常听到把这两个接⼝包装⼀下
,但在本例⼦中我们把这种设计思路放到中间件层,让服务变得可以统⼀控制
场景描述
一般的解决办法
- 在这⾥⽩名单的代码占据了⼀⼤块,但它⼜不是业务中的逻辑,⽽是因为我们上线过程中需要做的开量前测试验证。
- 如果你⽇常对待此类需求经常是这样开发,那么可以按照此设计模式进⾏优化你的处理⽅式,让后续的扩展和摘除更加容易
重构后
配置服务类
配置类注解定义
- ⽤于定义好后续在 application.yml 中添加 itstack.door 的配置信息。
⾃定义配置类信息获取
- 以上代码是对配置的获取操作,主要是对注解的定
义; @Configuration 、 @ConditionalOnClass 、 @EnableConfigurationProperties ,这
⼀部分主要是与SpringBoot的结合使⽤
切⾯注解定义
- 定义了外观模式⻔⾯注解,后续就是此注解添加到需要扩展⽩名单的⽅法上。
- 这⾥提供了两个⼊参,key:获取某个字段例如⽤户ID、returnJson:确定⽩名单拦截后返回的
具体内容
⽩名单切⾯逻辑
- 这⾥包括的内容较多,核⼼逻辑主要是; Object doRouter(ProceedingJoinPoint jp) ,接下
来我们分别介绍
@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
定义切⾯,这⾥采⽤的是注解路径,也就是所有的加⼊这个注解的⽅法都会被切⾯进⾏管理。
getFiledValue
获取指定key也就是获取⼊参中的某个属性,这⾥主要是获取⽤户ID,通过ID进⾏拦截校验。
returnObject
返回拦截后的转换对象,也就是说当⾮⽩名单⽤户访问时则返回⼀些提示信息。
doRouter
切⾯核⼼逻辑,这⼀部分主要是判断当前访问的⽤户ID是否⽩名单⽤户,如果是则放
⾏ jp.proceed(); ,否则返回⾃定义的拦截提示信息
测试验证
这⾥的测试我们会在⼯程: itstack-demo-design-10-00 中进⾏操作,通过引⼊jar包,配置注解的
⽅式进⾏验证
这⾥的测试我们会在⼯程: itstack-demo-design-10-00 中进⾏操作,通过引⼊jar包,配置注解的
⽅式进⾏验证
3.1 引⼊中间件POM配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>itstack-demo-design-10-02</artifactId>
</dependency>
打包中间件⼯程,给外部提供jar包服务
3.2 配置application.yml
这⾥主要是加⼊了⽩名单的开关和⽩名单的⽤户ID,逗号隔开。
在Controller中添加⾃定义注解
这⾥核⼼的内容主要是⾃定义的注解的添加 @DoDoor ,也就是我们的外观模式中间件化实现。
key:需要从⼊参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使⽤。
returnJson:预设拦截时返回值,是返回对象的Json。
启动SpringBoot
五、享元模式
享元模式的定义
在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例
的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。
例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源
,这就是享元模式的产生背景。
享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用
。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
享元模式的结构
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
享元模式的基本代码结构
非享元角色
抽象享元角色
具体享元角色
享元工厂角色
测试类
案例场景模拟
城市中随处可见的共享单车就是一个典型的“享元模式”案例,其中单车的品牌是固定的,使用的人是不确定的。下面就让我们用代码来实现一下。
用户类
共享交通工具抽象类
交通工具,自行车的实现类
自行车工厂类
客户端测试类
享元模式的优缺点
优点:
- 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
缺点:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
六、桥接模式
什么是桥接模式
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女、支持不同平台和不同文件格式的媒体播放器等。如果用桥接模式就能很好地解决这些问题。
桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化
。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
桥接模式的结构
- 桥接(Bridge)模式包含以下主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
桥接模式的基本代码实现
实现化角色
具体实现化角色
抽象化角色
扩展抽象化角色
测试类
场景模拟
在前面,我们了解,桥接模式,主要解决当有m种前提下,会有n种情况的问题。此时我们举个场景:面向不同品牌,不同类型的笔记本设计一个具有开关机的程序。比如笔记本,有华为笔记本,小米笔记本。 同时这些有些本,又分为商业笔记本和游戏笔记本。
电脑品牌接口 => 对应行为实现类接口:
不同品牌的笔记本 => 行为实现的具体实现类:
华为笔记本
小米笔记本
不同种类的笔记本 => 对应抽象类的子类:
通过类,将不同牌子和不同类型,这2个关系联系起来。
不同种类的笔记本 => 对应抽象类的子类:
测试类
桥接模式的优缺点
优点
- 抽象与实现分离,扩展能力强
- 符合开闭原则
- 符合合成复用原则
- 其实现细节对客户透明
缺点
- 由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
七、组合模式
在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣服与衣柜、以及厨房中的锅碗瓢盆等。在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。
组合(Composite Pattern)模式的定义:有时又叫作整体-部分(Part-Whole)模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性
组合模式的基本结构组成
- 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
- 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。
- 树枝构件(Composite)角色 / 中间构件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法
组合模式的基本代码结构
抽象构件
树叶构件
树枝构件
测试类
案例需求
已知一个公司有多个部门,多个部门下,又有多个员工,请别列出所有部门的,所有人。
组织构成 => 对应Component角色
公司 => 对应Composite角色
部门 => 对应Composite角色
员工 => 对应的是Leaf角色
测试代码
组合模式的优缺点
优点
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
缺点
- 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
- 不容易限制容器中的构件;
- 不容易用继承的方法来增加构件的新功能;