使用场景:面向对象->功能模块(设计模式+算法)->框架(多种设计模式)->架构
基础常识
1. 设计模式的六(七)大原则
- 单一职责原则
- 接口隔离
- 依赖倒转
- 里氏替换
- 开闭原则(ocp)
- 迪米特法则
- 合成复用
2. 设计模式解决的问题
最终实现:高内聚、低耦合
- 代码复用(相同功能不用重复编写)
- 可读性(增加程序的规范性,方便阅读)
- 可扩展(方便增加新的功能)
- 可靠性(增加新的功能后对原来的功能没有影响)
一、设计模式的七大原则
设计模式的原则是设计模式为什么如此设计的依据。
1. 单一职责
简介:
一个类只能负责一项职责,如果类A负责两个职责,那么更改其中一个职责时就可能造成职责2执行错误。所以应该将A类分成A1、A2两个类。
注意事项:
- 当类中方法足够少的时候,也可以在方法级别保持单一原则,只有当一个类的方法足够少时,才在方法级别保持单一原则
- 降低类的复杂度,一个类只负责一项原则
- 提高类的可读性、可维护性
- 降低变更引起的风险
2. 接口隔离原则(ISP)
全称:
Interface Segregation Principle
简介:
通常一个类对于另外一个类的依赖是通过接口依赖的,因此客户端不应该依赖他不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
举例:
如果类A通过接口1依赖类B,类C通过接口1依赖类D,如果接口1对于类A和类C来说不是最小接口,那么就造成类B和类D不得不实现他们不需要的方法。
处理方式:
将接口1进行拆分,使得类A和类C分别与他们需要的接口建立依赖关系——即接口隔离原则
3. 依赖倒置原则
依赖倒置的核心思想是:面向接口编程。
相关概念
- 抽象:一般指接口或者抽象类
- 细节:指具体实现
简介:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象类
- 抽象不应该依赖细节,细节应该依赖抽象
- 抽象的东西相比于细节要稳定的多,要以抽象为基础搭建架构
- 使用接口和抽象类去制定好规范,而不涉及任何具体的操作,细节由实现类去完成。
依赖关系传递的三种方式(参考12)
- 接口传递
- 构造方法传递
- setter方式传递
注:@TODO 需要重新学一下
注意事项:
- 底层模块尽量都要有抽象类或者接口,或两者都有,程序的稳定性好
- 变量的声明类型尽量是抽象类或者接口,这样变量的引用和实际对象间存在一个缓冲层,利于扩展和优化。
- 继承时遵循里氏替换原则。
4. 里氏替换原则
解决的问题:
继承带来便利的同时也带来了弊端,增加了对象间的耦合,当一个父类拥有多个子类时,当需要修改父类时,必须要考虑到所有子类,所涉及的子类的功能都可能产生故障。
简介:
- 所引用基类的地方必须能够透明的使用其子类的对象,即子类尽量不要重写父类的方法,可以通过让子类和父类同时继承一个更基础的类。
- 继承实际上是让耦合增强了,在适当情况下可以通过 聚合、组合、依赖 来解决问题。
聚合、组合、依赖:
前:B继承A,A继承BASE
后:A、B均继承BASE,但B还要使用A中的方法,采用下述方式
- 聚合:
- 组合:在B类中声明一个A的实例(组合了一个A的对象),B的方法中通过该实例来使用A的方法。
- 依赖:
5. 开闭原则
简介:
- 开闭原则是编程中最基础、最重要的设计原则
- 一个软件的实体,应该对扩展开放(对提供方),对修改关闭(对使用方),用抽象构建框架(通过接口、或抽象方法实现),用实现扩展细节。
- 当软件需要变化时,通过扩展软件实体的方式来实现变化,儿不是通过修改已有代码来实现。
- 编程中的其他原则和设计模式的目的就是为了遵循开闭原则。
6. 迪米特法则(最少知道原则)
简介:
- 一个对象应对其他对象保持最少的了解
- 类与类的关系越密切,耦合度越大
- 对于被依赖的类,不管多么复杂,尽量将逻辑封装在类的内部,对外除了提供public方法以外,不泄露任何信息。
理解迪米特法则:
对象之间只与直接朋友进行通信。
- 直接朋友1——成员对象:A类中有B类的成员对象。
- 直接朋友2——方法参数:A类的方法的参数中,接收了B类的对象。
- 直接朋友3——方法返回值:A类的方法返回一个B类的对象。
- 非直接朋友——局部变量中的类:A的方法内部用到B,直接new了一个B的对象来使用。
注意事项:
- 迪米特法则的核心是降低类之间的依赖
- 迪米特法则只是要求降低类与类之间的依赖,并不是要求完全没有依赖关系。
7. 合成复用原则
基本介绍:
尽量使用合成、聚合方式,而不是使用继承。(依赖、合成、聚合、组合的具体意思在UML类图中介绍。)
设计原则的核心思想
- 找出应用中可能变化的部分,独立出来,不要和那些不需要变化的代码混在一起
- 针对接口编程而不是针对实现编程
- 为了交互对象之间的松耦合设计而努力
二、UML类图
简介:
UML——Unified modeling language(统一建模语言),用于软件系统分析和设计的语言工具。
UML由一套符号规定,用来描述模型中各个元素之间的关系,如类、接口、实现、泛化、依赖、组合、聚合等
- 一般编译器可以安装插件来画类图,并且把写好的类直接拖拽就可以自动生成类图。
关系:
具体就记住泛化、实现、聚合、组合就可以。
1. Dependency:依赖(使用)
概念:
在A类中用到了B类,依赖关系就成立,A依赖于B。
- 类中用到了对方
- 作为类的成员属性
- 作为A类中方法的返回类型
- 作为A类方法中接收的参数类型
- 方法中使用到
2. Generalization:泛化(继承)
概念:
泛化关系就是继承关系,是依赖关系的一种特例。
3. Association:关联
简介:
关联关系是类与类之间的一种联系,他是依赖关系的一种特例。具有导航性和多重性。
- 单项的一对一的关系:A类的成员变量中有B类,但B类中没有A类的成员变量
双向的一对一的关系:A类的成员变量中有B类,B类中也有A类的成员变量
导航性:谁聚合谁,如A聚合B
- 多重性:即A中有多少个B的对象实例,由此分为单聚合和多重聚合。
4. Realization:实现
简介:
实现关系就是A类实现了B类(通常B是接口),实现也是依赖关系的一种特例。具有导航的关联性和多重性。
5. Aggregation:聚合
简介:
表示整体和部分的关系,整体与部分可以分开,聚合关系是关联关系的一种特例。
举例:
如A类成员变量中有B类的实例,当B类可以从A类中分开时,称为聚合。即在创建A类的对象时并没有同时创建B类的对象(没有new),比如人和身份证的关系,人可以没有身份证,不需要new。
6. Composite:组合
简介:
表示整体和部分的关系,整体与部分不可以分开,组合关系是关联关系的一种特例。
举例:
如A类成员变量中有B类的实例,当B类不可以从A类中分开时,称为聚合。即在创建A类的对象时同时创建B类的对象(有new),比如人和脑袋的关系,人不可以没有脑袋,需要new。#####
注意事项:
对于级联删除,当我们删除一个对象时,连同另一个对象一起删除了,说此时两者之间发的关系就是组合。
- 举例:当班级中所有的人都被删除后,班级就没有意义,需要顺带删除,此时Person与Class的关系就是组合。
三、设计模式分类‘
1. 创建型模型
简介:
针对对象的创建
分类
单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式
2. 结构型模型
简介
针对如何让软件有伸缩性、有弹性
分类
适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
3. 行为型模式
简介
让我们的方法的设计更合理
分类
模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter)、状态模式、策略模式、职责链模式(职责链模式)。
四、23种设计模式
1. 单例模式
简介:
采取一种方法保证整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
JDK源码使用
java中的 Runtime类,使用的是饿汉式。
使用场景
- 单例保证了系统内存中只存在一个对象,对于一些需要频繁创建销毁的对象,使用单例模式可提高系统性能
- 创建对象时耗时过多或者耗费的资源过多(即重量级对象),但又经常用到的对象
- 工具类对象
- 频繁访问数据库或者文件的对象(如数据源、session工厂等)
实现方式:
① 饿汉式1(静态常量)
步骤:
- 构造器私有化(防止外部new)
- 类的内部创建对象
- 向外暴露一个静态的公共方法
代码实现:
参考笔记
优缺点:
- 在类装载的时候就完成了实例化,避免了线程同步问题
- 在类装载的时候就完成了实例化,没有达到懒加载的效果,如果自始至终未使用这个类会造成内存浪费
- 导致类装载的方式有很多种,不能确定有其他方式(或其他静态方法)导致类加载,这时候初始化类就没有达到懒加载效果。
总结:
这种方式可用,可能会造成一个内存的浪费。
② 饿汉式2(静态代码块)
步骤:
- 构造器私有化(防止外部new)
- 类的内部创建对象(对象的创建在静态代码块中)
- 向外暴露一个静态的公共方法
代码实现:
参考
优缺点和总结:
与饿汉式1的相同,仅仅是实现的方式不一样。
③ 懒汉式1(线程不安全)
步骤:
- 声明静态对象
- 构造器私有化(防止外部new)
- 向外暴露一个静态的公共方法,在方法中创建类的实例(实例非空时才创建)
代码实现:
参考
优缺点和总结:
- 起到了懒加载的效果(即用到时才创建)
- 如果在多个线程下,比如两个线程同时进入了对创建实例的判断,会导致产生多个实例,就不符合单例模式
结论:
实际开发中不使用
④ 懒汉式2(线程安全)
步骤:
- 声明静态对象
- 构造器私有化(防止外部new)
- 向外暴露一个静态的公共方法,在方法中创建类的实例(实例非空时才创建),对该方法加入同步处理的代码(synchronized)
代码实现:
参考
优缺点和总结:
- 通过synchronized 解决了线程不安全问题
- 效率太低了,每个线程在调用该公共的方法时都要进行同步,而实际上这个方法只要执行一次实例化的代码就够了,后面想获得直接return就OK。
总结:
在实际开发中不推荐使用
⑤ 懒汉式3(线程不安全、同步代码块)
简介:
相对于第二种写法,这种写法将线程同步放到了方法内部的代码块进行判断,但导致了线程不安全,实际开发中不使用。
代码实现:
参考33
⑥ 双重检查(推荐使用)
步骤:
- 声明静态对象(通过volatile进行修饰,使得当其改变后会立即更新到储存)
- 构造器私有化(防止外部new)
- 向外暴露一个静态的公共方法,在方法中使用双重检查判断。
代码实现:
1 | class Singleton { |
优缺点和总结:
- 通过两次if(对象 == null)检查和同步代码,保证了线程安全
- 实例化只执行一次,后面访问时直接返回实例化的对象,避免反复同步,效率高。
- 需要时才去创建,实现了懒加载
结论:
实际开发中推荐使用
⑦ 静态内部类(推荐使用)
静态内部类:
- 在外部类装载时并不会导致内部类的装载,从而保证了懒加载
- 当调用外部类方法时(因为返回的是 内部类的属性)会导致内部类的加载,而JVM在装载类的过程中是线程安全的。
步骤:
- 声明静态对象(通过volatile进行修饰,使得当其改变后会立即更新到储存)
- 构造器私有化(防止外部new)
- 写一个静态内部类,该类中有一个外部类的静态属性
- 向外暴露一个静态的公有方法,在方法中直接返回静态内部类的属性。
代码:
1 | class Singleton { |
优缺点和总结:
- 利用静态内部类的特点,避免了线程不安全,实现了懒加载,效率高
结论:
推荐使用
⑧ 枚举方式(推荐使用)
代码:
1 | enum Singleton { |
优缺点:
- 可以避免多线程的问题
- 可防止反序列化重新创建新的对象
- java作者所提倡的方式
结论:
- 推荐使用
2. 工厂模式
① 简单工厂模式
简介
定义了一个创建对象的类(工厂类),这个类用来封装实例化对象的行为。当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式。
类图
@TODO
JDK源码使用
在Calendar类中,使用了简单工厂模式
② 工厂方法模式
简介
定义一个创建对象的抽象工厂类,该工厂类中通过抽象方法来创建对象,由工厂子类决定要实例化的对象。工厂方法模式将对象实例化推迟到了子类。
类图
@TODO
③ 抽象工厂模式
简介
- 定义一个interface用于创建相关或者有依赖的对象簇,无需指明具体的类
- 抽象工厂模式是对简单工厂和工厂方法模式的整合
- 将工厂抽象成两层,抽象工厂和具体实现的工厂子类,这样就可根据创建的对象类型来使用对应的工厂子类。更便于维护。
类图
@TODO
工厂模式总结
- 将实例化的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖和解耦,提高扩展性和可维护性
- 创建对象不要直接new类,把这个new的动作放到一个工厂的方法中,并返回。——即变量不要直接持有具体类的引用。
- 不要让类集成具体类,而是继承抽象类或实现接口。
- 不要覆盖基类中已经实现的方法。
- 遵循设计模式的依赖抽象原则。
3. 原型模式
简介
- 原型模式是指用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
- 原型模式是创建型设计模式,将一个原型对象传给哪个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝他们自己来实施创建
注: java中的Object类是所有类的跟类,Object提供了一个clone()方法,该方法可以将java对象赋值一份,但是需要实现clone的java类必须要实现一个接口Cloneable,该接口表示该类该类具有复制的能力。
Spring源码使用
在bean的创建中使用了原型模式。
浅拷贝
简介:
- 浅拷贝是使用,默认的clone()方法来实现的
- 对数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性复制一份给新的对象
- 对数据类型是引用数据类型的成员变量(如数组、某个类的对象),浅拷贝会进行引用传递,实际上两个对象的该成员变量都指向同一个实例,这是在一个对象中修改成员变量会影响到另一个对象的该成员变量的值。
深拷贝
简介:
- 复制对象的所有基本数据类型的成员变量
- 为所有引用数据类型的成员变量申请内存空间,并复制每个引用数据类型成员变量所引用的对象。即深拷贝是对整个对象进行拷贝
实现方式:
重写clone方法来实现深拷贝,
- 在重写的方法中将基本数据类型的属性clone拷贝,其他引用数类型单独通过该数据类型的clone拷贝进来。
通过对象序列化实现深拷贝(推荐使用)
- 先通过将对象序列化,然后再反序列化,得到对象的拷贝。
- 这样的方式避免了第一个方式中一个一个处理需要拷贝的对象的问题。
- 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50public class DeepProtoType implements Serializable, Cloneable{
public String name; //String 属性
public DeepCloneableTarget deepCloneableTarget;// 引用类型
public DeepProtoType() {
super();
}
//深拷贝 - 方式2 通过对象的序列化实现 (推荐)
public Object deepClone() {
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this); //当前这个对象以对象流的方式输出
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
DeepProtoType copyObj = (DeepProtoType)ois.readObject();
return copyObj;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
return null;
} finally {
//关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
// TODO: handle exception
System.out.println(e2.getMessage());
}
}
}
}
总结
- 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
- 不用重新初始化对象,克隆时而可以动态的获取最新的对象的运行时状态
- 原始对象发生改变时,在克隆对象时也会发生变化,无需修改代码
- 在实现深克隆的时候可能需要比较复杂的代码
- 需要为每一个类配备一个克隆方法,对已有的类改造时会违反ocp原则。
4. 建造者模式
简介
建造者模式也叫生成器模式,是将产品(属性)和产品的创建过程解耦。
可以将复杂对象的建造过程抽象出来,抽象过程的不同实现方法可以构造出不同表现的对象。
- 一步步创建一个复杂的对象,允许用户只通过指定复杂对象的类型和内容就可以构建他们,而不需要知道具体的细节。
角色
- Product(产品角色):一个具体的产品对象
- Builder(抽象的建造者):创建一个product对象的各个部分指定的接口或抽象类。
- ConcreteBuilder(具体建造者):实现接口,创建和装配各个部件
- Director(指挥者):构建一个使用Builder接口的对象,用来创建一个复杂对象
- 隔离用户与对象的生产过程
- 控制产品对象的生产过程
JDK源码使用
建造者模式在StringBuilder中使用
总结
客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,相同的创建过程也可以创建不同的产品对象
用户使用不同的建造者可以得到不同的产品对象
- 可以更加精细的控制产品的创建过程,将产品的创建步骤分解在不同的方法,使得创建过程更加清晰
- 增加新的具体建造者无需修改原有的类库代码,复合开闭原则
- 要求创建的产品具有较多的共同点,其组成部分相似,不然不适合使用建造者模式。
- 当产品的内部变化复杂时,会导致有很多具体的建造者的类。
与抽象工厂对比
- 抽象工厂:实现对产品家族的创建,具有不同维度的产品组合,抽象工厂模式不关心构建过程,只关心由什么工厂生产。
- 建造者模式:要求按照指定的蓝图建造产品,主要是通过组装零配件而产生一个新的产品。
5. 适配器模式(Adapter)
简介
- 适配器模式又叫包装器
- 将某个类的接口转换成客户端期望的另一个接口表示,让原本因接口不匹配而不能一起工作的两个类可以协同工作。
- 分类:类适配器模式、对象适配器模式、接口适配器模式
① 类适配器模式
介绍
Adapter类通过继承src类,实现dst接口,完成适配。
类图
总结
- Java是单继承机制,所以类适配器需要继承src类,要求dst必须是接口,有一定的局限性。
- src类的方法在Adapter中都会暴露出来,增加了使用成本
- 由于Adapter继承了src类,可更具需求重写src方法,增加了灵活性。
② 对象适配器
介绍
- 基本思路与类适配器相同,只是让Adapter不再继承src类,而是持有src类的实例(聚合)来解决兼容的问题,即聚合src类来实现dst类的接口,完成src->dst的适配。
- 使用合成复用原则,用关联代替继承。
类图
总结
- 对象适配器和类适配器是同一种思想的不同实现方式,根据合成复用原则,使用组合代替继承,解决了类适配器必须继承src的局限性,不再要求dst必须是接口。
- 使用成本低,更灵活。
③ 接口适配器
- 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择的覆盖父类的某些方法来实现需求。
- 适用于不想使用一个接口所有方法的情况。
SpringMVC源码使用
spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类
适配器代替controller执行相应方法,扩展controller时只需要增加一个适配器就完成扩展。
总结
- 类适配器:src为类,Adapter继承src
- 对象适配器:src为类,Adapter聚合src对象
- 接口适配器:Adapter将src作为一个接口来实现
6. 桥接模式
简介
- 将实现与抽象放在不同的类层次中,使两个层次可以独立改变
- Bridge模式基于 类的最小设计原则(类尽可能少),通过使用封装、聚合、继承等行为让不同的类承担不同的职责
- 把抽象与行为实现分离开来,保证各部分的独立性和功能扩展
类图
原始方案:
桥接模式:
JDBC源码使用
JDBC的Driver接口
总结
使用场景:
- 不希望使用继承或使用继承导致类爆炸问题的系统
- 转账分类:网上 转帐、柜台转账、ATM转账
- 转账用户:普通用户、银卡用户、黑卡用户
特点:
- 将抽象与实现分层独立出来,有助于系统分层设计
- 对于高层部分,只需要知道抽象部分和实现部分的接口就可以了,其他的由具体业务来完成
- 桥接模式代替多层继承,可以减少子类的个数
- 桥接模式需要正确的识别出系统中两个独立变化的维度,有固定的使用场景。
如:手机-直板手机-华为品牌-mate系列 这种多层继承的关系。
7. 装饰者模式(decorator)
简介
动态的将功能附加到对象上,再对象功能扩展方面,他比继承更具有弹性,也体现了OCP原则。
问题模型
1. 用户去点不同的咖啡以及不同的多种配料组合的问题。
2. 打包东西:多种不同的组合打包
- 衣服、陶瓷:**被装饰者**
- 报纸、泡沫、纸板、箱子:**装饰者**
类图
以点咖啡为例。
JDK源码
Java的IO结构中,FilterInputStream就是装饰者,装饰了InputStream
8.组合模式(部分整体模式)
简介
- 创建了对象组的树形结构,将对象组合丞树状结构以表示 部分-整体 的层次关系
- 通过树形结构来组合对象,用来表示部分和整体的层次
- 使得用户可以对单个对象和组合对象的访问具有一致性
使用场景
- 要处理的对象可以生成一颗树形结构,再我们对树上的结点和叶子进行操作时,它能够提供一致的方式
- 例如学校、学院、专业这种为组成关系的场景,他们之间没有继承关系,而是一个树形结构(更好实现管理)。
类图
JDK源码使用
Java集合类HashMap的实现使用了组合模式,put 和 putAll 方法
- Map:抽象的构建 -> Component
- HashMap:中间的构建 -> composite (相当于College 或 University)
- Node:叶子结点 -> Department
总结
- 客户端只需要面对一致的对象,不用考虑整体部分或叶子结点的问题
- 扩展性较强,方便做出复杂的层次结构,容易添加结点或叶子,从而创建出复杂的树形结构
- 需要遍历组织机构,或者处理的对象具有树形结构时,适合使用组合模式
- 如果叶子和结点有很多差异的话,如很多方法属性都不同,那么久不适合使用组合模式。
9 外观模式(过程模式)
简介
- 外观模式为子系统中的一组接口提供一个一致的界面,即定义了一个高层接口,这个接口可以使得子系统更加容易使用
- 通过定义一个一致的接口可以屏蔽内部子系统的细节,调用端只需要调用这个接口,无需关心这个子系统的内部细节
应用场景
从用户角度出发,通过定义一个一致的接口,用以屏蔽内部子系统的细节,使得调用端只需要调用这个接口,而无需关心子系统的内部细节。
类图
MyBatis源码中得应用
Configuration 创建MetaObject对象使用到了外观模式
总结
- 屏蔽了子系统得细节,降低了客户端对子系统使用得复杂度
- 通过合理得使用外观模式,可以更好得划分访问层次
- 当系统需要进行分层设计时,考虑使用Facade模式
- 在维护一个遗留的大型系统时,可以考虑为新系统开发一个Facade,让新的系统与Facade交互
- 不要过多的或者不合理的使用外观模式,要让系统有层次,利于维护。 子系统的组合使用很复杂时才使用。
10. 享元模式
享元的意思就是共享对象
简介
- 享元模式(Flyweight Pattern)也叫蝇量模式,运用共享技术有效的支持大量细粒度的对象
- 常用于系统底层开发,解决性能问题 ,如数据库连接池,里面有创建好的连接对象,在这些连接对象中,我们需要就可以直接拿来用。
- 享元模式可以解决重复对象的内存浪费问题,当系统有大量相似对象时,不需要总是创建对象,可以直接从缓冲池里拿。
- 内部状态: 对象共享出来的信息,存储在享元对象的内部,不随环境的改变而改变
- 外部状态: 对象得以依赖的一个标记,随环境而改变,不可共享的状态
应用场景
经典的应用场景就是池技术,String 常量池,数据库连接池、缓冲池等。
类图
JDK源码使用
Integer使用到了享元模式,在用valueOf创建Integer对象时,相同的值实际上只创建了一个对象。-128~127之间的话使用的是享元模式创建的。否则就直接new一个对象。
总结
- 当系统有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,使用享元模式
- 使用唯一标识码判断,如果内存中有,则返回唯一标识码标识的对象,用HashMap或HashTable存储(加快了查询速度)
- 享元模式可大大降低程序的内存占用
- 需要分离外部状态和内部状态,外部状态具有固化性,不随内部状态的改变而改变。
11. 代理模式
简介
- 为对象提供一个替身,以控制对象的访问,通过代理对象访问目标对象,可在目标对象实现的基础上增加额外的功能
- 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
- 代理模式不同的形式
- 静态代理
- 动态代理:JDK代理、接口代理
- Cglib代理:可以在内存中动态创建对象,不需要实现接口
① 静态代理
需要定义接口或者父类,被代理对象和代理对象一起实现相同的接口或者继承相同的父类
类图
应用场景
比如想对所有的接口进行统一的jwt验证,这时候就需要使用到代理模式。
总结
- 先执行代理对象的方法 ,代理对象再调用目标对象,可实现在目标对象执行之前和之后加一些内容。
- 不修改目标对象的前提下,通过代理对象对目标对象进行扩展
- 代理对象和目标对象都要实现一样的接口,导致会有很多代理类,且更改时都需要维护
② 动态代理
简介
- 代理对象不需要实现接口,但目标对象要实现接口
- 代理对象的生成使用JDK的API,动态的在内存中构建代理对象
- 动态代理也叫JDK代理、接口代理
- 代理类所在的包:java,lang.reflect.Proxy,只需使用 newProxyInstance 方法
类图
总结
代理的对象可以动态获取,是通过JDK内置的方法来实现这个过程,最终为了实现的目的都相同。
③ Cglib代理
- Cglib代理又叫子类代理,他是在内存中构建一个子类对象,从而实现对目标对象功能的扩展,(代理的类不能为final)
- 当目标对象没有实现任何接口时,可使用目标对象的子类实现代理
- Cglib是一个强大的高性能的代码生成包,可以在运行期扩展java类与实现java接口,广泛被应用于AOP框架
- Cglib的底层通过使用字节码处理框架ASM来转换字节码并生成新的类
代理模式选择
- 目标对象需要实现接口:JDK代理
- 姆博埃对象不需要实现接口:Cglib代理
类图
总结
- 需要引入Cglib的包
- 相当于拦截器的作用,重写intercept方法,当该方法执行时会调用目标对象的方法。
12. 模板方法模式
简介
- 又叫模板模式,在一个抽象类直接定义了执行它的方法模板,子类可以按需重写方法,但是调用将以抽象类中定义的方式进行
- 模板方法定义了一个操作中的算法骨架,将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构就可以重定义该算法的某些特定步骤
- 客户端子类对象直接调用父类中的template方法,从而实现不同的调用
类图
应用场景
- 带有大部分相同流程,小部分流程不同的任务,如制作不同口味的豆浆
- 计算代码的执行时间
模板方法中的钩子方法
简介
- 模板方法中的父类中可以定义这样一个方法,它不做任何事情,子类可以视情况决定要不要覆盖,即钩子
- 比如要实现的是制作纯豆浆,不需要添加任何配料。这时就需要对是否执行制作中的某个流程加上判断语句,判断语句的结果为钩子方法的返回值,这样,纯豆浆可以通过重写这个钩子方法对某个流程是否执行进行控制。
- 上述过程,钩子方法可以是空方法,也可以让子类来重写方法,子类重写了就做,没有重写就什么也不做。
Spring源码使用
Spring IOC容器初始化时运用了模板方法模式,具体参考。
13. 命令模式
简介
- 使得请求的发送者和接受者之间解耦,让对象之间调用关系更加灵活。
- 在命令模式中,将一个请求封装成一个对象,使用不同的参数来表示不同的请求,命令模式支持撤销的操作,。
类图
- Invoker:调用者
- command:命令角色,需要执行的命令都在这里,可以是接口或者抽象类
- Receiver:接受者的角色,知道如何实施和执行一个请求的相关操作
- ConcreteCommand:将一个接收者对象与一个动作绑定,调用接收者相应的操作,实现execute。
- 实际需要看课程的代码去理解。
Spring JDBC源码使用
在JDBC的Template使用到了命令模式,jdbcTemplate就是Invoker
总结
- 使得请求的发送者和接受者之间解耦,发起请求的是调用者,只需要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的。命令对象起到了纽带桥梁的作用。
- 容易实现请求的撤销
- 会导致系统有很多具体的命令类,增加了系统的复杂度。
- 空命令也算一种设计模式,省去了对空的判断。
- 经典的应用场景:界面的一个按钮都是一条命令、模拟CMD、订单的撤销恢复、触发和反馈机制。
14. 访问者模式
简介
- 封装一些作用于某些数据结构的各元素的操作,可以在不改变数据结构的前提下定义作用于这些元素的新操作
- 将数据结构和数据操作分离,解决数据结构和操作耦合的问题。
- 在被访问的类中添加一个对外提供接待访问者的接口
应用场景
需要对一个对象结构中的对象进行很多不同的操作(操作之间没有关联),同时需要避免让这些操作“污染”这些对象的类
类图
- Visitor:抽象的访问者,为每一个ConcreteElement类声明一个visit操作。
- ConcreteVisitor:是一个具体的访问者,实现Visitor声明的操作
- ObjectStructure:能枚举他的元素,提供一个高层的接口,允许访问者访问元素
- Element:定义一个accept方法,接收一个访问者对象
- ConcreteElement:具体的元素,实现了accept方法
总结
- 访问者模式符合单一原则,扩展性很好
- 对功能进行统一,可以做报表、UI、拦截器、过滤器,适用于数据结构相对稳定的系统
- 访问者关注了其他类的内部细节,对具体的元素变更造成了困难,违反了迪米特法则
- 访问者依赖的是具体元素而不是抽象元素,违反了依赖倒置
- 适用于系统有稳定的数据结构,又有经常变化的功能需求。
15 迭代器模式(Iterrator)
简介
- 如果我们的集合元素是用不同方式存储的,如数组、集合等,当客户端要遍历这些集合元素时,就需要多种遍历方式,还会暴露元素的内部结构。
- 迭代器模式为遍历集合提供统一的接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示。
应用场景
当不同学院存储各个专业采取不同方式时(如数组、集合),为了对学院的专业进行遍历,需要采取迭代器模式。
类图
- Iterator:迭代器接口,系统提供的
- ConcreteIterator:具体的迭代器,管理迭代
- Aggregate:一个统一的聚合接口,将客户端和具体的聚合解耦
- ConcreteAggregate:具体的聚合持有对象的集合,并提供一个方法,返回一个迭代器,迭代器可以正确的遍历集合
- Client:客户端,通过Iterator和Aggregate依赖到子类。
JDK-List源码
在JDK-ArrayList中使用到了迭代器模式。因为List 里面会存储不同类型的集合,因此需要通过迭代器来实现。ArrayList、LinkedList、HashList等都实现了List。
总结
- 提供了统一的方法遍历对象,如我们使用List的时候,不需要考虑聚合的类型,用统一的方法就可以实现遍历。
- 隐藏了内部结构,用户只能取到迭代器
- 把管理对象集合和遍历对象集合的责任分开(单一职责原则)。
16 观察者模式(observer)
简介
用于设计开放式的API,便于第三方接入获取数据,并具有通知功能。
相当于消息的订阅,被订阅方为主题,订阅方为观察者。
观察者模式用集合方式管理用户(Observer),包括注册、移除和通知
- 我们增加观察者(即公告板),不需要修改核心类,满足OCP原则。
类图
JDK源码使用
JDK的Observable使用了观察者模式。
17 中介者模式(mediator)
简介
- 用一个中介对象来封装一系列的对象交互,中介者使得各个对象之间不需要显示的相互交互,从而实现松耦合,可以独立的改变他们之间的交互。
类图
总结
- 多个类相互耦合会形成网状结构,中介者模式使网状结构分离为星型结构
- 减少类间的依赖,降低了耦合,符合迪米特法则
- 中介者承担了主要责任,一旦中介者出问题,整个系统就会受到影响
- 设计不当会造成中介者过于复杂。
18 备忘录模式
简介
- 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态
- 用来记录一个对象的某种状态或者某些数据,当要做回退时,可以从备忘录对象中获取原来的数据进行恢复操作。
类图
- originator:需要保存状态的对象
- Memento:备忘录对象,负责保存好记录,即originator内部的状态
- Caretaker:守护者对象,负责保存多个备忘录对象,使用集合管理,提高效率
- 当需要保存多个originator对象的不同时间的状态时,是需要用HashMap
适用场景
游戏存档、浏览器后退、数据库事务。
总结
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便的回到某个历史版本
- 实现了信息的封装,是用户不需要关心状态的保存细节
- 类的成员变量过多时,会占用比较大的资源,每一次保存都会消耗一定的内存
- 可以和原型模式配合适用,节省资源
19 解释器模式(Interpreter)
慎用,比较复杂。
简介
- 指定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式),通常会有一组解释器。
应用场景
- 将需要解释执行的语言中的句子表示为一个抽象的语法树
- 一些重复出现的问题可以用一种简单的语言来描述
- 一个简单语法需要解释的场景
- 如:编译器、正则表达式、python解释器等
类图
- Context:环境角色,含有解释器之外的全部信息
- AbstractExpression:抽象表达式,声明一个抽象的解释操作,这个方法被语法树种所有方法共享
- TerminalExpression:终结符表达式,实现与文法中的终结符相关的解释操作
- NoneTerminalExpression:非终结符表达式,为文法中非终结符实现解释操作
- 说明:Context和TerminalExpression消息通过Client输入
Spring 源码应用
在SpelExpressionParser使用到了解释器模式
20 状态模式(state)重要
简介
- 主要解决对象在向外输出多种状态时,需要对外输出不同的行为的问题,状态和行为是 一 一 对应的,状态之间可以相互转换。
- 当一个对象的内在状态改变时,允许改变其行为,这个对象看起开像是改变了其类。
类图
- Context:环境角色(比如活动),用于维护ConcreteState实例,这个实例定义当前的状态。
- State:抽象的状态角色,定义一个接口,封装与Context相关的行为
- ConcreteState:具体的状态角色,每个子类实现与Context的一个状态相关的行为。
应用场景
当一个事件有多种状态时,状态之间会相互转换,对不同的状态要求有不同的行为的时候,考虑使用状态模式。
总结
- 使代码具有可读性,避免了使用了大量的fi else判断,方便维护
- 符合开闭原则
- 状态比较多时会产生很多的类,需要维护好
21 策略模式(strategy)
简介
- 策略模式用来定义算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的用户
- 把变换的代码从不变的代码中分离出来
- 针对接口编程而不是具体的类,将不同的行为接口聚合到父类中,用户可自行定义其行为。
- 多用聚合/组合的方式,少用继承(客户通过组合方式使用策略)
原理类图
用户context聚合或组合strategy或其他策略接口的成员变量,至于需要使用到哪个策略,可以在构造器中指定。
JDK源码
JDK的Array的Comparator使用了策略模式,用户可自行排序的策略。
总结
- 分析项目中变化的部分和不变的部分,多用组合聚合,少用继承,用行为类组合而不是行为的继承
- 满足OPC原则,增加行为不用修改原代码,只需要添加一种策略(行为)即可。
- 提供了可以替换继承的方法,将算法封装在独立的Strategy类,使得你可以独立其Context改变它。易于切换、理解、扩展。
- 每一个策略(行为)都是一个类,导致类数目过多。
22 职责链模式(Chain of Responsibility)
简介
- 为请求创建了一个接收者对象的链,这种模式对请求的发送者和接受者进行解耦
- 通常每个接收者都包含另一个接收者的引用,如果一个对象不能处理该请求,那么他会把这个请求传递给下一个接收者,以此类推。
原理类图
- Handler:抽象的处理者,定义了一个处理请求的接口,同时定义另外的Handler
- ConcreteHandler:具体的处理者,处理他自己负责的请求,可以访问他的后继者(即下一个处理者),如果可以处理当前请求则处理,如果不能则交给后继者处理,从而形成一条链。
- Request:含有很多属性,表示一个请求。
- 可以形成一个环状也可以是一条处理链
SpringMVC源码
SpringMVC的 HandlerExecutionChain用到了职责链模式
应用场景
有多个对象可以处理同一个请求的时候,如多级请求、请假等审批流程,拦截器中也会用到。
总结
- 把请求和处理分开,实现 解耦
- 简化了对象,不需要知道链的内部操作
- 当链比较长的时候,性能会受到影响,通常在Handler中设置一个最大的节点数量
五、总结
- 模式模式不是100% 的跟标准设计模式一样
- 一个功能模块可能用到多个模式
...
...
本文为作者原创文章,未经作者允许不得转载。