设计模式学习笔记
[TOC]
1. 设计模式分类
创建型模式
单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
特点是“将对象的创建与使用分离”。
结构型模式
代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
用于描述如何将类或对象按某种布局组成更大的结构,类似与造房子,各种组件
行为型模式
模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。
2. UML图
统一建模语言
特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。
2.1 类图
作用:
- 类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解;
- 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
2.2类图表示方法
类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name,age和address这3个属性,以及work()方法。
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]
注意:
1,中括号中的内容表示是可选的
2,也有将类型放在变量名前面,返回值类型放在方法名前面
2.2.2 类与类之间关系的表示方式
2.3.2.1 关联关系
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,
分为一般关联关系、聚合关系和组合关系。
关联又可以分为单向关联,双向关联,自关联。
在UML类图中单向关联用一个带箭头的实线表示。
就是Customer类里面有Address的一个成员变量(一个类有持有另一个类的属性)。
- 双向关联
双向关联就是双方各自持有对方类型的成员变量。
双向关联用一个不带箭头的直线表示
上图中 Customer持有Product的属性(拥有多个产品),而Product也有Customer的属性(产品被谁购买)
- 自关联
自己包含自己。
2.3.2.2 聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系可以用带空心菱形的实线来表示,菱形指向整体。
比如这个图,大学里面包含了老师,但大学停办了老师依然存在。
成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。
2.3.2.3 组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
组合关系用带实心菱形的实线来表示,菱形指向整体。
比如人和器官的关系就算组合关系,人包含着器官,但器官离不开人。
整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。
2.3.2.4 依赖关系
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。
依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。
比如司机开车就是依赖关系,司机要用到车的一个功能(驾驶汽车)
2.3.2.5 继承关系
继承关系是对象之间耦合度最大的一种关系,父类与子类之间的关系,是一种继承关系。
继承关系用带空心三角箭头的实线来表示,箭头从子类指向父类。
就是父亲与儿子的关系,儿子可以有父亲所有功能,还可以加一改造
2.3.2.6 实现关系
实现关系是接口与实现类之间的关系。
实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。
类实现一个接口就需要实现接口里面所有的抽象方法。
3 软件设计原则
3.1 开闭原则
对扩展开放,对修改关闭。
意思就是,程序要扩展的可以开放,这样软件好增加功能,但对具体实现不能开放,因为这样会改变代码会造成紊乱。
如何实现:抽象类和接口实现
接口里面声明一些方法,然后让其有不同的实现,
比如把猫的颜色做一个抽象类,该类有一个颜色这个抽象方法
一个猫实现这个接口,变成白猫,另外一只猫也实现这个接口,变成黑猫。
我们扩展就只需要定义新的类实现这个颜色接口就可以了,而不需要修改原代码。
3.2 里氏代换原则
子类可以扩展父类的功能,但不能改变父类原有的功能。
就比如:正方形不是长方形,这个例子
正方形继承了长方形,并且重写了设置长和宽的方法
我们的测试方法resize是针对长方形的可以通过,但正方形由于继承了长方形因此也可以调用这个方法,这样就出错了。
我们修改后:
我们让长方形和正方形都实现一个设置长和宽的接口,但长方形和正方形没有关系,这样resize这个方法正方形就无法调用。从而保证了里氏代换原则。
儿子可以在家里面加东西,但不能改东西。
3.3 依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
简单来说,就是一台电脑有很多组件,比如cpu+硬盘等等,我们可以用不同品牌的cpu和不同品牌的硬盘。但不能一台电脑只能用专门的cpu而不能换吧!
比如这个图,咱们的电脑只能组装专门的零件。
这就是高层模块(电脑)依赖与低层模块(具体的cpu),导致模块间的耦合。
改进一下:
我们的电脑依赖与一个(cpu框架)抽象,而不依赖与一个具体实现,这样咱们电脑组装的时候就可以选择不同品牌的cpu了。
3.4 接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
通俗点就是咱们的微信有聊天,支付,点外卖这些功能,但咱们只需要聊天,支付这个功能。我们就需要把聊天,支付,点外卖这些功能抽成接口,我们微信只需要聊天和支付,咱们就实现聊天金额支付的接口,美团需要支付和点外卖,他就实现支付和点外卖这个接口。
改进前:
改进后:
3.5 迪米特法则
又称为最少知识原则。
只和你的直接朋友交谈,不跟“陌生人”说话。
你和她本无交集。但她和你朋友有交集,你就只能通过朋友来找她聊天。
“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
例子:明星与经纪人的关系实例
明星关注与唱歌这些,日常事务交给经纪人负责和公司交流和粉丝安排见面,而明星和粉丝和公司都不认识
3.6 合成复用原则
尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
对象间的耦合度低。可以在类的成员位置声明抽象。
复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
原本是这样,假如汽车多加一个颜色,那么又会产生很多子类导致复用性不高
改进:
把颜色抽陈一个接口,这样加一个颜色只需要实现Color这个接口。
4 创建者模式
创建型模式分为:
- 单例模式
- 工厂方法模式
- 抽象工程模式
- 原型模式
- 建造者模式
4.1 单例设计模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
4.1.2 单例模式的实现
饿汉模式(类加载就创建实例)
静态变量方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}缺点:instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
静态代码块方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}缺点:跟上面一种类似,会造成内存浪费
枚举方式(最推荐)
因为枚举类型是线程安全的,并且只会装载一次而且非常简单
1
2
3
4
5
6/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}
懒汉式(首次使用对象时候才创建实例)
线程不安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 懒汉式
* 线程不安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}缺点:多线程情况下出现线程安全问题。
线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 懒汉式
* 线程安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}缺点:锁的范围太大了,会导致执行效率过低
- 双重检查锁(比较完美但是较复杂)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}缺点:在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
改进:+volatile关键字可以保证可见性和有序性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}静态内部类方式(较常用)由JVM保证的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被
static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
4.1.3 存在的问题
除了用枚举类的方式,其它方式在遇到序列化和反射的时候都会破坏单例模式
4.1.3.2 问题的解决
序列化、反序列方式破坏单例模式的解决方法
在Singleton类中添加
readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}源码解析
ObjectInputStream类
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
38public final Object readObject() throws IOException, ClassNotFoundException{
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);//重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}
反射方式破解单例的解决方法
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
30public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
4.1.4 JDK源码解析-Runtime类
Runtime类就是使用的单例设计模式。
Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。
4.2 工厂模式
4.2.1 概述
原本的实现:
比如设计一个咖啡点餐系统,咱们用传统的方法,每一个东西都需要自己创建对象,比如自己要弄各种咖啡,然后拿来卖,但咱们使用工厂方法就是我们只需要点哪种咖啡,咖啡怎么弄是他的事。
如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。
简单工厂模式
结构:
- 抽象产品: 就是这个产品的规范,比如:咖啡抽象类,里面就定义了咖啡的属性和功能。
- 具体产品:具体实现或继承抽象产品的子类。就比如各种各样不同的咖啡,拿铁咖啡啊,美式咖啡等等。
- 具体工厂:提供创建产品的方法。
实现:
用简单工厂改进
改进之后咱们客户端只需要创建工厂类,然后选那种咖啡传进去就可以了,咱们不需要自己手动创建咖啡
实现了咖啡店和咖啡的耦合,但又产生了新的耦合。
优点:
装了创建对象的过程,可以通过参数直接获取对象。更加容易扩展。
缺点:
增加产品的时候还是需要修改工厂类代码,违背“开闭原则”。
在开发中一般把工厂定义为静态的。
工厂方法模式
概念:
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。
结构:
- 抽象工厂:提供创建产品的接口,咱们用户通过这个接口调用创建
- 具体工厂:创建产品的具体实现。
- 抽象产品:产品自身的规范
- 具体产品:具体实现的产品
实现:
改进后定义了一个咖啡工厂接口,然后不同的工厂实现不同种类咖啡工厂的创建,美式咖啡工厂创建美式咖啡。
假如增加一种拿铁咖啡,只需要添加拿铁咖啡工厂和拿铁咖啡类就可以了无需对之前的代码做修改。
优点:
- 只需知道工厂名称就可以创建产品,无需知道创建细节。
- 增加产品时候,只需添加代码,无需修改之前代码。
缺点:
- 每增加一个产品就需要加一个具体产品类和产品工厂类,会让系统的复杂度增加。
抽象工厂模式
抽象工厂可以实现同一品牌的产品的生产,比如:这个图电脑有不同品牌,
手机也有不同品牌,但电脑和手机是不同种类的。
而咱们抽象工厂就可以实现,同一种品牌的产品的创建。
概念:
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
结构:
- 抽象工厂:提供了产品的创建,里面包含着多个创建产品的方法,可以创建不同种的产品
- 具体工厂:实现抽象工厂种不同产品的创建
- 抽象产品:定义产品规范,描述产品主要特性和功能。
- 具体产品:实现抽查产品接口,由具体工厂创建。它和具体工厂是多对一的关系。
实现:
比如这个类图:
我们想要生产咖啡还要生产甜点,我们就把甜点和咖啡都定义为抽象产品,
它们各自有不同的实现,比如美式咖啡和提拉米苏甜点。
然后我们定义一个抽象工厂,里面有创建咖啡和甜点的方法。
美式甜点工厂实现这个抽象工厂创建,美式咖啡和提拉米苏。
然后其它风味的选择不同组合。
抽象工厂:
1 | public interface DessertFactory { |
具体工厂:
1 | //美式甜点工厂 |
如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。
优缺点:
- 优点:同种品牌中多个产品一起被创建时,保证这个工厂只会给你生产这个品牌的产品。
- 缺点:新增一个产品时,所有工厂都需要进行修改。
使用场景:
- 创建的产品需要互相依赖关联的。比如,格力工厂里面生产空调和配套的洗衣机电冰箱。
- 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
- 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
输入法换皮肤,一整套一起换。生成不同操作系统的程序。
模式扩展:
简单工厂+配置文件解除耦合
可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。
第一步:定义配置文件
为了演示方便,我们使用properties文件作为配置文件,名称为bean.properties
1 | american=com.itheima.pattern.factory.config_factory.AmericanCoffee |
第二步:改进工厂类
1 | public class CoffeeFactory { |
静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及创建对象写在静态代码块中,目的就是只需要执行一次。
JDK源码解析-Collection.iterator方法
Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类的对象。
4.3 原型模式
我的理解这个模式就是克隆,把一个类当作原型,复制创建信对象。
结构:
- 抽象原型类: 规定了clone()方法
- 具体原型类:作为可以被复制的对象,实现了clone()方法
- 访问类:使用原型类进行克隆对象
类图:
用代码理解:
Java中的Object类中提供了 clone()
方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。
1 | //奖状类 |
浅克隆:创建新对象,新对象的属性和原来对象完全相同,对于非基本类型属性(比如聚合了其它的类),仍指向原有属性所指向的对象的内存地址。
深克隆:创建一个新对象,被聚合的对象也会被克隆。
使用场景:
- 对象的创建非常复杂,可以使用原型模式快捷的创建对象。
- 性能和安全要求比较高。
4.4 建造者模式
4.4.1 概述
将复杂对象的创建和组合分离,比如有一个电脑对象,很复杂有CPU,内存条,硬盘等,我们专门拿个类做创建CPU这些部件,另外一个类(人)来组装这些部件。
- 分离了部件的构造和装配。
- 由于实现了构建和装配的解耦。->不同的构建器,相同的装配,也可以做出不同的对象。 相同的构建器,不同的装配顺序也可以做出不同的对象。
- 用户只需要指定复杂对象类型就可以得到对象,无需知道内部细节
4.4.2 结构
- 抽象建造者类(Build):规定要实现复杂对象哪些部分的创建。
- 具体建造者类:实现Build,完成创建各个部件的创建。
- 产品类:需要创建的复杂对象
- 指挥者类:调用建造者来创建产品,就是组装的人。
类图:
4.4.3 实例
创建共享单车
Bike是产品,Build是抽象建造者,有摩拜单车的建造者,还有of单车的建造者,最后还有一个组装者
具体代码:
1 | //自行车类 |
优点:
- 建造者模式的封装性很好。
- 在建造者模式中,客户端不必知道产品内部组成的细节就能创建产品,更好的解耦,使得相同的创建过程创建不同的产品。
- 可以更加精细地控制产品的创建过程 。有指挥者可以将创建过程分解。
- 建造者模式很容易进行扩展。如果有新需求,只需要增加新的建造者,不需要修改之前代码。
缺点:
- 建造者的产品一般具有共同点。如果差异较大,则不适合。
4.4.5 使用场景
- 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
- 创建复杂过程的算法比较独立。
4.4.6 模式扩展
我们平常使用build的链式调用就是这么实现的,如果用构造方法,代码可读性很差。
重构后代码:
使用单例模式+建造者模式可实现链式调用
1 | public class Phone { |
重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。
4.5 创建者模式对比
4.5.1 工厂方法模式VS建造者模式
- 工厂方法:注重的是整体对象的创建方式
- 建造者模式:注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。
工厂就是直接创建出来一个完整的,而建造者就是一步一步构建的。
4.5.2 抽象工厂模式VS建造者模式
抽象工厂模式:对产品家族的创建,一个产品家族是这样的一系列产品。
建造者模式:要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
5 结构型模式
就像建房子的工程一样,建房子有着各自各样的建造结构。
分为:
- 类结构型模式:采用继承机制组织,耦合度较高。
- 对象结构型模式:采用组合或者聚合来组织,耦合度较低,满足合成复用原则。
结构模式分为:
- 代理模式
- 适配者模式
- 装饰着模式
- 桥接模式
- 外观模式
- 组合模式
- 享元模式
5.1 代理模式
就像一个中间人,你通过中间人来做一些事情。
访问对象(你)和目标对象(房东)没有直接关系(你不认识他),这个时候就需要代理(中介)来操作,代理可以做一些增强操作。
代理分为
- 静态代理:编译时生成
- 动态代理:Java运行时动态生成
- JDK代理
- CGLib代理
结构:
- 抽象主题:通过接口或者抽象类声明代理对象和真实业务实现的方法。
- 真实主题:实现抽象主题的具体方法,是最终要引用的类。
- 代理:提供与真实主题一样的接口,内部含有真实主题的引用,并且可以做扩展和增强
静态代理:
这是一个火车购票系统,我们到火车站买票太麻烦,就需要线上购买,这个线上系统就是一个代理。火车站就是目标对象。
代码:
1 | //卖票接口 |
JDK动态代理
使用动态代理实现代码
1 | //卖票接口 |
我们的代理工厂类通过反射获取目标对象的属性,然后通过
InvocationHandler中的Invoke方法在运行时实现Proxy接口和反射获取的目标接口,然后动态创建代理类。
代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
运行时的代码:
1 | //程序运行过程中动态生成的代理类 |
执行流程如下:
1. 在测试类中通过代理对象调用sell()方法
2. 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
3. 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
4. invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法
CGLIB动态代理
跟JDK不同的是CGLIB动态代理不需要定义抽象接口类进行代理。
代码:
1 | //火车站 |
三种代理的对比
- jdk代理和CGLIB代理
- 用CGLIB代理在JDK1,6之前比jdk代理效率高,但不同对final的类进行代理。
- 在之后JDK优化之后是JDK代理效率高一些。
- 有接口时使用JDK代理,没有接口时使用CGLIB代理。
- 动态代理VS静态代理
- 最大优势:将接口声明的所有方法都被转移到一个集中方法处理。不需要像静态代理一样每一个方法进行中转。
- 接口增加方法,所有子类都需要实现这个方法,增加了代码的耦合度。
优缺点
优点:
- 两个不相干的对象交流,中介起到了一个保护作用
- 代理可以扩展原本的功能,AOP就是使用代理模式实现的。
- 将客户端和目标对象解耦。
缺点:
增加系统复杂度
使用场景
- 远程代理
- 防火墙代理
- 保护代理
5.2 适配器模式
就像充电器一样,我们的手机接口和插板不兼容,这就需要用充电器转换。
定义:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
可以分为:
- 类适配器:耦合度高,需要了解内部
- 对象适配器:耦合度低。
结构:
- 目标接口:就是类似与插板,可以是抽象类或者接口
- 适配者类:就是类似与你的手机接口,
- 适配者类:转换器,过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
类适配器模式
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
例如:电脑只能读取SD卡,但现在要求你读取TF卡的数据。
定义SD卡,TF卡接口,让并将其实现,然后定义一个适配器类继承SD卡接口实现转换。
但类适配器模式违背了合成复用原则。
1 | //SD卡的接口 |
对象适配器模式
实现方式:适配器类要聚合转换的接口,其余跟类适配器模式相同
区别就是TFCardImpl没有继承转换器,而是转换器聚合了TFCard接口。
1 | /创建适配器对象(SD兼容TF) |
应用场景
- 旧系统存在可以执行的类,但新系统与其不兼容。
- 第三方提供的组件,但组件接口的定义和自己的要求不相同。
JDK源码解析
Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。
这里采用了适配器模式
5.3 装饰者模式
想象一下,你走进一家咖啡店,点了一杯基础的美式咖啡。但是,你可能还想加点糖、牛奶或者奶油来让咖啡更加美味。装饰者设计模式就是让你能够自由地给咖啡添加这些额外的“装饰”。
就是在原有的基础上加东西让它变的好看。
结构:
- 抽象构建角色:定义了一个对象接口,可以给这些对象动态地添加职责。比如一个咖啡类,它定义了咖啡的基本属性和方法。
- 具体构建角色:实现抽象构件。
- 抽象装饰角色:持有一个组件对象的引用,并定义装饰物品的方法。
- 具体装饰角色:实现抽象装饰角色。
比如这个快餐店案例,FastFood就是抽象构建角色,它有两个子实现。
Garish就是抽象装饰角色,它聚合了快餐类,并定义了子实现类的规范。
1 | //快餐接口 |
好处:
- 提供更比继承加灵活的扩展。
实现了装饰类和被装饰类的解耦。可动态扩展
使用场景:
不使用继承进行系统维护时
不能采用继承的情况
- 系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长(类爆炸)
- 类不能定义为继承。
不影响其它对象,并为其动态,透明的给单个对象添加职责。
JDK源码解析:
IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。
代理和装饰者的区别
静态代理和装饰者模式的区别:
- 相同点
- 都要实现与目标类相同的业务接口
- 都要声明目标对象
- 都可以增强目标方法
- 不同点:
- 静态代理是为了保护和隐藏目标对象
- 装饰着是为了增强目标对象
- 装饰者是由外界传递进来,可以通过构造方法传递
静态代理是在代理类内部创建,以此来隐藏目标对象
5.4 桥接模式
想象一下,你正在设计一个软件,这个软件可以打印不同的文档。这些文档有不同的格式,比如PDF、Word、PPT,同时,打印的方式也可以不同,比如黑白打印、彩色打印、双面打印等。如果使用传统的继承方式,你可能会得到很多组合,这会导致类的数量急剧增加,而且难以管理。
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
结构:
- 抽象化角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化角色:给出实现化角色接口的具体实现。
需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。
类图如下:
OperatingSystem就是抽象化角色,里面包含了实现化角色(视频文件)的引用,Windows和Linux就是扩展抽象化角色,两个实现类AVI格式就是具体实现化角色
1 | //视频文件 |
好处:
- 提高了系统的可扩充性
- 实现细节对客户透明
使用场景
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
桥接模式和装饰者模式有什么区别
桥接模式
桥接模式就像是你有一个多功能的遥控器,它可以控制不同的电子设备,比如电视、音响、灯光等。每种设备都有自己的功能和特性,遥控器并不关心这些设备的具体实现,它只关心如何发送指令。
- 目的:桥接模式的目的是将一个类的抽象部分和它的实现部分分离,使它们可以独立地变化。
- 场景:当你有多个类层次结构,并且想要避免它们之间的强耦合时,使用桥接模式。
装饰者模式
装饰者模式就像是你走进一家咖啡店,点了一杯基础的咖啡,然后可以选择添加不同的调料,比如糖、牛奶、奶油等,来定制你的咖啡。每添加一种调料,都会给咖啡增加一些新的属性或行为。
- 目的:装饰者模式的目的是动态地给一个对象添加额外的职责或行为。
- 场景:当你想要在不修改原有对象的情况下,给对象添加新功能时,使用装饰者模式。
区别
- 目的不同:
- 桥接模式关注的是将抽象和实现分离,让它们可以独立地扩展。
- 装饰者模式关注的是动态地给对象添加额外的职责。
- 使用场景不同:
- 桥接模式适用于处理多个类层次结构,避免它们之间的耦合。
- 装饰者模式适用于在运行时动态地扩展对象的功能。
- 实现方式不同:
- 桥接模式通过定义两个独立的类层次结构来实现,一个是抽象部分,另一个是实现部分。
- 装饰者模式通过定义一个抽象装饰者和多个具体装饰者来实现,它们持有并装饰一个组件对象。
- 灵活性:
- 桥接模式提供了在两个维度上的灵活性,你可以独立地扩展抽象部分和实现部分。
- 装饰者模式提供了在单个维度上的灵活性,你可以逐层地添加装饰者来扩展功能。
用一个简单的比喻来总结:桥接模式就像是你有一个可以更换镜头的相机,你可以根据不同的拍摄需求更换镜头;而装饰者模式就像是你给相机添加不同的滤镜,每次添加都会改变照片的效果。
5.5 外观模式
外观模式就像是你使用一个遥控器来控制家庭影院系统。遥控器上有简单的按钮,比如“播放”、“暂停”、“音量+”、“音量-”等。你不需要知道家庭影院的内部是如何工作的,也不需要分别操作DVD播放器、音响系统和电视。你只需要按遥控器上的按钮,就可以享受电影。
外观(Facade)模式是“迪米特法则”的典型应用
结构:
- 外观角色:为多个子系统对外提供一个共同的接口。
- 子系统角色:实现系统的部分功能,客户可以通过外观角色访问它。
案例:
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。
1 | //灯类 |
好处:
- 降低子系统和客户端的耦合
- 对客户端屏蔽子系统组件
缺点:
- 修改很麻烦,违背了开闭原则。
使用场景:
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
5.6 组合模式
想象一下,你有一个由多个文件夹和文件组成的文件系统。在这个系统中,每个文件夹可以包含文件和其他文件夹,形成一个层次结构。当你需要对文件系统中的某个部分进行操作时,不管是单个文件还是包含多个文件和子文件夹的文件夹,操作的方式都是相同的。
定义:
把一组相似的对象当作一个单一的对象,组合模式依据树形结构来组合对象。
结构
- 抽象根节点:定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
- 树形节点:定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
- 叶子节点:叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
实现
软件菜单
实现这种菜单中:
MenuCompent:是抽象根节点定义了节点的基本实现
Menu:树形节点,实现抽象根节点又聚合抽象根节点
MenuItem:叶子节点
1 | //菜单组件 不管是菜单还是菜单项,都应该继承该类 |
组合模式的分类
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
缺点就是不够安全,叶子节点不实现某些方法,但继承又要实现这些方法,如果没写好就可能会有错误
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点
Menu
类中声明并实现这些方法。缺点就是不够透明。
优点
灵活性:组合模式使得你可以在运行时动态地添加或删除对象,而不需要修改现有代码。
一致性:用户可以以相同的方式处理单个对象和组合对象,这简化了客户端代码。
- 可扩展性:通过增加新的叶节点或容器节点,可以轻松扩展系统。
- 简化设计:它简化了对象的组织和管理,使得设计更加清晰。
使用场景
- 出现树形结构的地方
5.7 享元模式
享元模式想象成一个资源节约的策略,它通过共享不变的部分来减少资源消耗,同时允许每个对象保持其独特的状态或行为。
结构:
- 内部状态:不会随着环境而改变的状态,可共享部分
- 外部状态:随环境改变而改变的状态,不可共享部分
角色:
- 抽象享元角色:声明具体享元类公共方法,通常是接口或抽象类
- 具体享元角色:实现抽象享元类,在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元角色:享元对象的某些状态不能共享,这些状态被称为外部状态。非享元角色通常用来存储这些外部状态。
- 享元工厂:负责创建和管理享元角色。
案例
俄罗斯方块:
每种方块出现都需要创建,太占用内存,这时候我们把不同方块都当成一个实例对象。
1 | //抽取共同的属性和行为 |
优缺点和使用场景
优点:
- 极大节省相似对象的内存
- 外部状态相对独立,且不影响内部状态
缺点:
- 让程序复杂
使用场景:
- 场景中有大量相似或者相同的对象,造成内存大量消耗。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
JDK源码解析
Integer类使用了享元模式。
Integer
默认先创建并缓存 -128 ~ 127
之间数的 Integer
对象,当调用 valueOf
时如果参数在 -128 ~ 127
之间则计算下标并从缓存中返回,否则创建一个新的 Integer
对象。
6 行为型模式
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。
- 模板方法模式
- 策略模式
- 命令模式
- 职责链模式
- 状态模式
- 观察者模式
- 中介者模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 解释器模式
除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。
6.1 模板方法模式
概述
想象一下你是一家餐厅的厨师长,你负责制定一道菜的烹饪流程。这个流程包括几个基本步骤:准备食材、烹饪、装盘。但是,不同的菜可能在这些步骤中有不同的做法。比如,做鱼和做鸡的步骤虽然大体相似,但具体细节却大不相同。
模板方法模式就像是你制定的这个烹饪流程,它规定了基本的步骤顺序,但允许不同的菜肴(子类)在这些步骤中有自己的特色做法。
结构
抽象类:负责给出一个算法的轮廓和骨架。由一个模板方法和诺干其余方法组成
模板方法:定义算法骨架
基本方法:实现算法个步骤的方法
可分为:
- 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。
- 具体方法:个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承
- 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
具体子类:实现抽象类。
案例
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
1 | public abstract class AbstractClass { |
一般模板方法都需要加上final,防止子类重写修改算法。
优缺点
优点:
- 提高代码复用性
- 实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
适用场景
- 算法的整体步骤很固定,但其中个别部分易变时。
- 需要通过子类来决定父类算法中某个步骤是否执行
JDK源码解析
InputStream类就使用了模板方法模式。在InputStream类中定义了多个 read()
方法
1 | public abstract class InputStream implements Closeable { |
在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。
6.2 策略模式
作为程序员,可以选择多种编译器开发软件。
想象一下,你是一个旅行者,计划去不同的城市旅行。每个城市都有不同的交通方式:有些地方适合开车,有些地方适合骑自行车,还有些地方适合步行。
策略模式就像是你旅行时的交通方式选择方案。你可以根据目的地和个人喜好,灵活地选择最合适的交通方式。
结构:
- 抽象策略:这就像是你的旅行计划,定义了基本的交通方式(开车、骑自行车、步行)。每种交通方式都有一些基本的操作,比如“前进”、“停止”。
- 具体策略:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境类:持有一个策略类的引用,最终给客户端调用。
实现:
商店买东西提供了不同从促销活动
1 | //定义策略的共同方法 |
优缺点
优点:
- 策略类之间可以自由切换
- 易于扩展
- 避免使用多重条件选择语句(if else)
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类。
使用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
JDK源码解析
Comparator
中的策略模式。
1 | public class Arrays{ |
Arrays就是一个环境角色类,这个sort方法可以传一个新策略让Arrays根据这个策略来进行排序。
6.3 命令模式
想象一下,你是一个餐厅的顾客,你想要点菜。在命令模式中,你的点菜请求就像是发出一个命令。
定义
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
结构
- 抽象命令类:这就像是菜单上的菜品列表,它定义了所有可能的请求的格式。每个菜品都对应一个命令。
- 具体命令类:实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
- 实现者/接收者:接收者,真正执行命令的对象。
- 调用者/请求者:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。
实现
代码:
1 | //抽象命令类 |
优缺点
优点:
- 降低系统耦合度,将调用操作的对象与实现该操作的对象解耦。
- 增加和删除命令十分方便
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点:
- 是系统变复杂
- 会有太多具体命令类
使用场景
- 系统需要将请求调用者和请求接收者解耦,且双方不直接交互。
- 系统需要在不同的时间指定请求、
- 将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
JDK源码解析
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法
1 | //命令接口(抽象命令角色) |
6.4 责任链模式
想象一下,你在一个公司工作,当你遇到一个问题时,你首先会向你的直接上司求助。如果上司解决不了,他可能会把问题交给他的上司,也就是你的大老板。这个过程就像是一条链,每个环节都有机会处理这个问题,直到问题被解决为止。
定义:
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
结构
- 抽象处理者:定义处理请求接口,包含抽象处理方法和后继连接。
- 具体处理者:实现抽象处理者的处理方法,如果不能处理请求,转发给后继者
- 客户类角色:创建处理类,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
实现:
上面的案例的实现
1 | //请假条 |
优缺点
优点
- 降低对象之间的耦合度-降低请求发送者和接收者的耦合度
- 增强系统的可扩展性
- 责任链简化了对象之间的连接
- 责任分担
缺点
不能保证每个请求一定被处理。
对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性
源码解析
在javaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用
6.5 状态模式
想象一下,你有一个智能助手,它可以根据不同的情况(状态)来调整它的行为。比如,当你在工作时,智能助手会保持安静,只在你提问时才回答;当你在休息时,它可能会播放轻松的音乐;当你在运动时,它可能会播放动感的音乐并提醒你注意呼吸。
状态模式的核心思想是将行为和状态封装在不同的类中,这样当状态改变时,只需要改变对象的状态类即可,而不需要修改对象本身的代码。
结构
- 环境角色:这是包含状态的对象,它将不同的状态作为其行为的一部分。
- 抽象状态角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态角色:实现抽象状态所对应的行为
实现
通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。
1 | //抽象状态类 |
优缺点
优点:
- 封装性:状态模式将与特定状态相关的行为封装在状态对象中,使得状态转换不会影响到其他状态。
- 灵活性:新增状态时,只需增加相应的状态类,无需修改现有代码,符合开闭原则。
- 可维护性:状态模式使得状态转换逻辑集中管理,易于追踪和维护状态之间的转换。
- 可读性:状态模式通过类和接口的实现,使得代码结构清晰,易于理解。
缺点:
- 资源消耗:如果状态非常多,可能会导致系统中存在大量的状态类,增加系统的复杂性。
- 状态管理:状态模式可能会使得状态转换的管理变得复杂,特别是当状态转换依赖于多个条件时。
- 状态依赖:状态模式可能导致状态类之间存在依赖关系,这可能会使得状态转换逻辑难以理解和维护。
- 状态一致性:在某些情况下,需要确保所有状态对象都能保持一致的行为,这可能需要额外的工作来实现。
使用场景
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
6.6 观察者模式
概述
类似与发布订阅模型,定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。
想象一下,你订阅了一个天气预报服务。每当天气发生变化时,比如从晴天变成雨天,这个服务就会给你发送通知,告诉你天气变了。在这个例子中,天气预报服务就是”被观察者”,而你就是”观察者”。
结构
- 主题:被观察者,维护一组观察者,并提供增加和删除观察者对象。
- 观察者:定义更新接口,使得在得到主题更改通知时更新自己。
- 具体主题:实现主题接口,维护当前状态,当状态改变时,通知所有观察者。
- 具体观察者:实现观察者接口,以便在得到主题更改通知时更新自身的状态。
实现
微信公众号
我们关注了一个公众号以后,这个公众号有什么事就需要像所有用户发通知。
1 | //观察者 |
优缺点
优点
- 降低了目标与观察者之间的耦合关系
- 可以实现广播机制
缺点
- 观察者特别多的时候,有的观察者收到被观察者发送的通知会耗时
- 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃
使用场景
- 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。
6.7 中介者模式
想象一下,你在一个大型聚会上,有很多人在交流。但是,如果每个人都要直接找到其他人进行交流,那么这个聚会就会变得非常混乱,人们需要花费很多时间来寻找交流对象。为了避免这种情况,聚会上有一个主持人(中介者),他会协调所有人的交流。如果你想和某个人交流,你只需要告诉主持人,主持人会帮你找到那个人并安排你们交流。
结构
- 中介者:中介者接口,提供同时对象注册与转发同时对象信息的抽象方法。
- 具体中介者:实现中介者接口,定义List集合管理同事对象,协调各个同事。协调与各个同事之间的交互关系,它依赖于同事对象。
- 抽象同事类:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类:抽象同事类的实现类。
案例实现
租房
租房子要有一个中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。
1 | //抽象中介者 |
6.8 迭代器模式
就是遍历集合对象数据的一种方式。
结构
- 迭代器:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
- 具体迭代器:实现迭代器。
- 集合:定义了创建迭代器的方法
- 具体集合:实现了集合接口,提供一个方法来创建具体迭代器实例。
案例:
1 | //StudentIterator:迭代器接口 |
优缺点
优点:
- 迭代器模式提供了一种抽象的遍历方式,允许用户以统一的方式访问集合中的元素,而不需要了解集合的具体实现细节。
- 迭代器简化了集合类。在原有的集合对象中不需要再自行提供数据遍历等方法,这样可以简化集合类的设计。
- 易于扩展
缺点
- 增加了类的个数,提升了系统的复杂性。
6.9 访问者模式
想象一下,你有一个图书馆,里面有各种类型的书籍。现在你想要对这些书籍进行不同的操作,比如计算价格、检查是否需要维修、或者更新书籍的分类信息。每种操作都可能需要访问书籍的不同属性。访问者模式就是用来解决这种问题的。
它允许你将算法与其所作用的对象结构分离,从而可以在不修改对象结构的情况下,增加新的操作。
结构
- 访问者:定义了对每一个元素
(Element)
访问的行为。 - 具体访问者:给出对每一个元素类访问时所产生的具体行为
- 元素:定义了一个接受访问者的方法,通常称为
accept
,它接收一个访问者对象作为参数。 - 具体元素:实现了元素接口,具体元素类将为访问者提供访问其内部元素的方法。
- 对象结构:定义当中所提到的对象结构
案例实现
给宠物喂食
- 访问者:给宠物喂食的人
- 具体访问者:主人、其他人
- 元素:动物抽象类
- 具体元素:宠物狗、宠物猫
- 结构对象:主人家
1 | //访问者 |
优缺点
优点:
扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
2,缺点:
对象结构变化很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
6.10 备忘录模式
想象一下,你在玩一个视频游戏,游戏中有保存进度的功能。当你达到某个关键点或者想要尝试不同的策略时,你可以保存当前的游戏状态。如果后来你想要回到之前的状态,只需要加载之前保存的状态即可。备忘录模式就是实现这种功能的模式。
又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
结构
- 发起人:负责创建一个备忘录,用以记录当前时刻的内部状态,并可以恢复状态。
- 备忘录:存储发起人的当前状态,供以后恢复使用。
- 管理者:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
备忘录有两个等效的接口:
- 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
- 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
案例实现
游戏挑战BOSS
白箱”备忘录模式
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137//游戏角色类-发起人
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力
//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
//保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(vit, atk, def);
}
//回复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
//游戏状态存储类(备忘录类)
public class RoleStateMemento {
private int vit;
private int atk;
private int def;
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
//角色状态管理者类
public class RoleStateCaretaker {
private RoleStateMemento roleStateMemento;
public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}
public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
}
}
//测试类
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();
//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());
System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.stateDisplay();
}
}白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。
“黑箱”备忘录模式
备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。
1 | //窄接口`Memento`,这是一个标识接口,因此没有定义出任何的方法 |