设计模式

软件设计原则

开闭原则

对扩展开放,对修改关闭
适合用抽象类和接口实现

里氏代换原则

  • 任何基类出现的地方,子类一定可以出现
  • 子类可扩展但不能改变父类原有功能,就是尽量不重写父类方法

反例
假设有个长方形类,正方形类继承长方形类,同时自己内部重写了长和宽的 setter 方法,之后假设有个 resize 方法拓宽长方形,长方形实例没
问题,但是正方形实例 resize 方法就会同时拓宽长和宽,违反里氏代换原则
解决方法
定义一个四边形接口,正方形类和长方形类都实现这个接口,这样长方形和正方形就没有继承关系了,符合里氏代换原则

依赖倒置原则

  • 高层模块不应该依赖低层模块,两者都应该依赖抽象
  • 抽象不应该依赖细节,细节应该依赖抽象(细节就是具体实现类)

反例
定义一个电脑类,电脑有 CPU,内存,硬盘等,电脑直接依赖希捷硬盘,英特尔 CPU,金士顿内存条,之后如果用别的牌子的设备就要对电脑类的 setter 方法里面的类进行修改
解决方法
定义 CPU,内存,硬盘等接口,所有牌子的设备实现对应种类的接口,电脑类的 setter 方法传入接口,这样电脑类就只依赖抽象,不依赖具体实现类,符合依赖倒置原则

接口隔离原则

  • 客户端不应该被迫依赖他不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上
  • 就假设一个类实现了接口,接口有方法 1 和方法 2,但这个类并不需要方法 2,所以应该给两个方法分别都写成接口,类需要哪个就实现哪个接口

反例
假设有个安全门接口,有三个功能防盗、防火、防水,A 类安全门功能都需要直接实现就行,但是 B 类安全门只需要防盗防火功能,他不需要实现防水,被迫依赖了
解决方法
给三个功能全抽成接口,A 类安全门实现三个接口,B 类安全门只实现两个接口

迪米特法则(最少知识原则)

只与你的直接朋友交谈,不和陌生人说活
其含义是:如果两个软件实体无需直接通信,那么就不应当发生直接的相互调用,而是通过第三方转发

例子
假设有明星类、粉丝类、公司类
建立一个经纪人类。前三个类都是经纪人类的成员变量,分别有对这三个类的方法,就能降低耦合度

合成复用原则

指尽量使用组合、聚合的方式,其次才考虑继承
组合和聚合就是使用成员变量的方式
组合是强依赖,由整体对象作为部分创建出来,整体对象销毁时也随之销毁(独占)
聚合是弱依赖,由外部创建后引入进来(共享)

单一职责原则

一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分

创建者模式

单例模式

该类负责创建自己的对象,同时确保只有单个对象被创建,并提供一个访问其唯一对象的方式

单例设计模式分类两种:

  • 饿汉式:类加载的时候创建对象
  • 懒汉式:类加载的时候不创建对象,而是第一次调用的时候创建对象

饿汉式单例实现方式

  • 静态成员变量
    • private Singleton () {};// 私有构造方法,不写的话会用默认构造器,默认是 public 的
    • private static Singleton instance = new Singleton();
  • 静态代码块
    • private Singleton () {};
    • private static Singleton instance;
    • static {instance = new Singleton();}

懒汉式实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 私有构造方法,防止外部调用构造器创建对象
// 必须显示写出来,不然会用默认构造器,默认是 public 的,能被外部调用
private Singleton () {};

// 声明Singleton类型的变量instance
private static Singleton instance;// 只是声明没有赋值

// 对外提供访问方式,用synchronized修饰,保证多线程安全
// 用static修饰,使这个方法为静态的,属于这个类本身而不需要new 对象来调用
public static synchronized Singleton getInstance() {
// 判断instance是否为空,如果为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
// 私有构造方法
private Singleton () {};

// volatile修饰确保可见性和有序性
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 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被static修饰,保证只实施例换一次,并严格保证实例化顺序
在没加锁的情况下保证线程安全,并且没有任何性能影响和空间浪费

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 私有构造方法
private Singleton () {};

// 定义一个静态内部类
private static class SingletonHolder {
// 在内部类中声明并初始化外部类的对象
// 用final修饰,防止被修改,提高可读性
// final 修饰的字段必须在声明时或构造函数中初始化,所以之前的懒汉式不能用
// // 只会在类加载的时候new一次,后续直接返回
private static final Singleton INSTANCE = new Singleton();
}

// 提供公共的访问方法
public static class SingletonHolder {
// 静态内部类只有在被调用的时候才会被加载一次
// 静态内部类本身就是懒加载,所以静态内部类中的字段也是懒加载
return SingletonHolder.INSTANCE;
}

枚举方式(饿汉式)

枚举方式(最推荐)(属于饿汉式,在不考虑内存浪费的时候最推荐)
线程安全,而且只会加载一次,而且是所有单例实现里面唯一不会被破坏的

1
2
3
public enum Singleton {
INSTANCE;// 这就完事了
}

单例的破坏

除了枚举方式,其余的都可以被破坏单例模式,就是通过其他方式创建多个对象

  1. 反序列化,就比如给单例用输出流写入一个文件,然后用输入流读取就能创建一个新对象

  2. 反射

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 1. 获取Singleton的字节码对象
    Class clazz = Singleton.class;
    // 2. 获取无参构造器方法对象
    Constructor cons = clazz.getDeclaredConstructor();
    // 3. 取消访问检查,绕过private和protected等
    cons.setAccessible(true);
    // 4. 创建Singleton对象
    Singleton s1 = (Singleton) cons.newInstance();
    Singleton s2 = (Singleton) cons.newInstance();
    system.out.println(s1 == s2);

解决破坏单例的问题

  1. 反序列化破坏的解决方法
    在 Singleton 类中添加 readResolve()方法,在反序列化的时候被反射调用,如果定义了这个方法就返回这个方法的值没有就返回新 new 出来的对象

    1
    2
    3
    4
    5
    // 在静态内部类里面加这个方法
    // 当进行反序列化时,会自动调用这个方法,返回该方法的返回值
    public Object readResolve() {
    return SingletonHolder.INSTANCE;
    }
  2. 反射破坏的解决方法
    先在内部类里面设置布尔静态变量,然后在私有构造器方法里面添加同步代码块包裹的判断语句,判断这个变量,然后将其设置为 true。后续调用构造器时都会判断如果为 true 就抛出异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 属性,用于判断单例是否已存在
    private static boolean flag = false;
    // 私有构造方法
    private Singleton () {
    synchronized (Singleton.class) {
    // 如果为true说明不是第一次访问,直接抛出一个异常
    if (flag) {
    throw new RuntimeException("不能创建多个对象");
    }
    // 第一次创建之后设置flag为true
    flag = true;
    }
    }

工厂模式

在 Java 中,万物皆对象,这些对象都需要创建,如果创建的时候直接 new 该对象,就会对该对象耦合严重,假如我们要更换对象,所有 new 对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。

简单工厂模式

简单工厂不是一种设计模式,不在 23 种设计模式中,反而像一种编程习惯,包含以下角色

  • 抽象产品:定义了产品的规范,描述产品的主要特性和功能。
  • 具体产品:实现或继承抽象产品的子类
  • 具体工厂:提供了常见产品的方法,调用者通过该方法来获取产品

假设有个咖啡店类,他有个点咖啡方法会根据传进来的 type 生产对应咖啡的对象
改进:咖啡店类内部的点咖啡方法内使用工厂方法,这个工厂会根据传进 type 返回对应咖啡对象,以后如果有新的咖啡,只需要在工厂类中添加新的方法,返回新的咖啡对象,不需要修改咖啡店类,实现了咖啡店和咖啡的解耦

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
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
// 声明Coffee类型的变量,根据不同类型创建不同的coffee子类对象
// 本来这段是直接写在在Coffee类中的,改在这里就实现了解耦
Coffee coffee = null;
if ("american".equals(type)) {
coffee = new AmericanCoffee();
} else if ("latte".equals(type)) {
coffee = new LatteCoffee();
} else {
throw new RuntimeException("没有该类型咖啡");
}
return coffee;
}
}

// 咖啡店类
public class CoffeeStore {
public Coffee orderCoffee(String type) {
// 创建工厂对象
SimpleCoffeeFactory factory = new SimpleCoffeeFactory();
// 通过工厂对象获取产品对象
return factory.createCoffee(type);
}
}

后期如果要增加新的咖啡种类,势必要修改简单工厂的代码,这其实违反了开闭原则,但是一个工厂类可以在多个类里面使用,就比如美团外卖也点咖啡,就只需要把工厂接过去,拓展 更容易

静态工厂

就是将简单工厂内部的生产方法设置为静态的,这样外部类就可以直接调用静态方法来获取对象了,不用创建工厂对象

1
2
3
4
5
6
public class CoffeeStore {
public Coffee orderCoffee(String type) {
// 用类名调用静态方法
return SimpleCoffeeFactory.createCoffee(type);
}
}

工厂方法模式

简单工厂的缺点,使用工厂模式可以完美解决,完全遵循开闭原则

概念:定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。

结构:

  • 抽象工厂:定义了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
  • 具体工厂:实现抽象工厂接口,完成具体产品的创建。
  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品:实现了抽象产品接口,由具体工厂来创建,它与具体工厂之间一一对应。
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
// 抽象工厂
public interface CoffeeFactory {
// 创建咖啡对象的方法
Coffee createCoffee();
}

// 具体工厂
public class AmericanCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
}

// 咖啡店类
public class CoffeeStore {
// 成员变量,抽象工厂类型
private CoffeeFactory factory;

// 通过setter方法设置工厂对象
public void setFactory(CoffeeFactory factory) {
this.factory = factory;
}

// 点咖啡功能
public Coffee orderCoffee() {
return factory.createCoffee();
}
}

之后要添加新的咖啡种类只需要添加对应的工厂类,工厂类实现抽象工厂接口,返回对应的咖啡对象,咖啡店要生产什么咖啡也只需要引用对应工厂,不需要修改代码,符合开闭原则

抽象工厂模式

同一产品族(例如品牌,风味)的工厂可以合一,减少工厂类数量

意大利风味甜品工厂和美式风味甜品工厂都实现甜品抽象工厂接口,而不是每个甜品和咖啡都新建一个工厂类,避免类爆炸

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
// 抽象工厂
public interface DessertFactory {
// 生产咖啡的功能
Coffee createCoffee();
// 生产甜品的功能
Dessert createDessert();
}

// 具体工厂:美式风味甜品工厂
public class AmericanDessertFactory implements DessertFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
public Dessert createDessert() {
return new MatchaMousse();
}
}

// 使用抽象工厂
public static void main(String[] args) {
// 创建意大利风味甜品工厂对象
// ItalianDessertFactory factory = new ItalianDessertFactory();
// 创建美式风味甜品工厂对象
AmericanDessertFactory factory = new AmericanDessertFactory();
// 获取美式风味的咖啡和甜品
Coffee coffee = factory.createCoffee();
Dessert dessert = factory.createDessert();
}

优点:当一个产品族中的多个对象被设计成一起工作时,他能保证客户端始终只使用同一个产品族中的对象。

缺点:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

模式扩展

简单工厂+配置文件解除耦合

在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。

第一步:创建配置文件
存储键值对,值是全类名

1
2
american=com.imooc.design.pattern.creational.factory.AmericanCoffee
latte=com.imooc.design.pattern.creational.factory.LatteCoffee

第二步:改进工厂类
创建并存储存储配置类(coffee.properties)里面所有对应的咖啡子类对象

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
public class CoffeeFactory {
// 新建一个哈希表来存储键值对(类名和对象实例)
private static Map<String, Coffee> map = new HashMap<>();

// 静态代码块,在类加载的时候执行,只执行一次
static {
// 读取配置文件
Properties p = new Properties();
InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("coffee.properties");
try {
p.load(is);
// 从p集合中获取类名并创建对象
Set<Object> keys = p.keySet();
for (Object key : Keys) {
// 根据键从配置文件里获取值(全类名)
String className = p.getProperty((String) key);
// 获取字节码对象,通过反射创建对象
Class clazz = Class.forName(className);
Coffee obj = (Coffee) clazz.newInstance();
// 将名称和对象存储到map中
map.put((String)key, obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}

// 根据类名获取对象
public static Coffee createCoffee(String name) {
return map.get(name);
}
}

调用工厂类

1
2
3
4
public static void main(String[] args) {
// 传进去类名
Coffee coffee = CoffeeFactory.createCoffee("american");
}

工厂模式的案例(Java 迭代器)

1
2
3
4
5
6
7
// 获取迭代器对象
Iterator<String> it = list.iterator();
// 使用迭代器进行遍历
while (it.hasNext()) {
String element = it.next();
System.out.println(element);
}

上面的对应关系
抽象工厂类:Collection 接口;他里面有一个抽象方法,返回 Iterator 接口对象
具体工厂类:ArrayList 类;他实现了 List 接口(List 实现 Collection 接口),重写了返回 Iterator 接口对象的抽象方法
抽象产品类:Iterator 接口;定义了 Iterator 的规范
具体产品类:ArrayList 类中的迭代器 Iterator 接口的实现类

原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同的新对象

结构

  • 抽象原型类:规定了具体原型对象必须实现的 clone() 方法(JDK 里面定义好了 Cloneable 接口)
  • 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象
  • 访问类:使用具体原型类中的 clone() 方法来复制新的对象

实现

  • 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型(引用类型)属性,仍指向原有属性所指向的对象的内存地址。
  • 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不在指向原有对象地址

Java 中的 Object 类中提供了 clone()方法来实现浅克隆。Cloneable 接口是抽象原型类,而实现了 Cloneable 接口的类是具体原型类。具体原型重写 Object 类的 clone() 方法。

Cloneable 是一个标记接口,内部没有任何方法,它的唯一作用是作为一个“标记”:告诉 JVM 或 Object.clone() 方法:“这个类是允许被克隆的”,因为当调用 super.clone()时 JVM 会检查当前对象的类是否实现了 Cloneable 接口,如果没有实现会抛出 CloneNotSupportedException 异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 实现Cloneable接口
public class Realizetype implements Cloneable {
// 重写clone()方法,返回的是本类类型的对象
@Override
public Realizetype clone() throws CloneNotSupportedException {
// clone()方法返回的是Object类型,所以需要强转
return (Realizetype) super.clone();
}
}

public static void main(String[] args) {
// 创建一个原型对象
Realizetype r1 = new Realizetype();
// 克隆对象
Realizetype r2 = r1.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
public class Citation implements Cloneable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}

public static void main(String[] args) {
// 创建原型对象
Citation citation = new Citation();
// 克隆奖状对象
Citation citation1 = citation.clone();

citation.setName("张三");
citation1.setName("李四");
}

使用场景

  • 对象的创建非常复杂,可以用原型模式快捷地创建对象
  • 性能和安全要求比较高

扩展(深克隆)

通过序列化和反序列化就能实现深克隆,所有类都要实现 Serializable 接口
给奖状类内部的 name 属性改成学生类

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
// 定义学生类
public class Student implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

// 定义奖状类
public class Citation implements Cloneable, Serializable {
private Student stu;
public Student getStu() {
return stu;
}
public void setStu(Student stu) {
this.stu = stu;
}
}

// 深克隆
public static void main(String[] args) {
// 创建原型对象
Citation citation = new Citation();
// 创建学生对象
Student stu = new Student();
stu.setName("张三");
citation.setStu(stu);

// 创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/citation.obj"));
// 写对象
oos.writeObject(citation);
// 释放资源
oos.close();

// 创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/citation.obj"));
// 读取对象,创建深克隆对象
Citation citation1 = (Citation) ois.readObject();
// 释放资源
ois.close();
// 修改克隆对象的属性
citation1.getStu().setName("李四");
}

建造者模式

将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示
分离了部件的构造(由 Builder 负责)和装配(由 Director 负责)。从而可以构造出复杂的对象

建造者模式结构

建造者(Builder)模式包含如下角色:

  • 抽象建造者类 (Builder) :这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。
  • 具体建造者类 (concreteBuilder) :实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
  • 产品类 (product) :要创建的复杂对象。
  • 指挥者类 (Director) :调用具体建造者来创建复杂对象的各个部分在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

实例

创建共享单车:
它包含车架车座等组件的生产,而车架又有碳纤维,铝合金等材质,车座有橡胶,真皮等材质
这里 Bike 是产品,包含车架,车座等组件;Builder 是抽象建造者,MobikeBuilder 和 OfoBuilder 是具体的建造者

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
// 自行车类
public class Bike {
private String frame;
private String seat;

public String getFrame() {
return frame;
}
public String getSeat() {
return seat;
}
public void setFrame(String frame) {
this.frame = frame;
}
public void setSeat(String seat) {
this.seat = seat;
}
}

// 抽象建造者类
public abstract class Builder {
// 声明Bike类型的变量,并进行赋值
protected Bike bike = new Bike();

public abstract void buildFrame();

public abstract void buildSeat();

// 构建自行车的方法
public abstract Bike createBike();
}

// 具体的建造者类,Ofo内部就铝合金和橡胶
public class MobileBuilder extends Builder {

public void buildFrame() {
bike.setFrame("碳纤维车架");
}
public void buildSeat() {
bike.setSeat("真皮车座");
}

public Bike createBike() {
return bike;
}
}

// 指挥者类,调用建造者类的对象
public class Director {
// 声明builder类型的变量
private Builder builder;
// 有参构造函数
public Director(Builder builder) {
this.builder = builder;
}

// 组装自行车的功能
public Bike construct() {
builder.buildFrame();
builder.buildSeat();
return builder.createBike();
}
}

// 使用指挥者类创建对象
public static void main(String[] args) {
// 创建指挥者对象
Director director = new Director(new MobileBuilder());
// 让指挥者指挥组装自行车
Bike bike = director.construct();
}

简化

将指挥者类和抽象建造者结合
虽然简化了结构,但是也加重了抽象建造者类的职责,不太符合单一职责原则,如果 construct()方法过于复杂,还是建议封装到 Director 类中

1
2
3
4
5
6
7
8
9
10
11
12
13
// 抽象builder类
public abstract class Builder {
// 声明Bike类型的变量,并进行赋值
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
// 构建自行车
public Bike construct() {
this.buildFrame();
this.buildSeat();
return this.createBike();
}
}

建造者模式扩展

当传入参数很多时,可以用 builder 模式来提高可读性
用 lombok 的@Builder 注解可以快速生成 builder 类

通过类名 new 一个 Builder 对象(静态内部类),Builder 对象有和 Phone 类一样的属性,然后调用 Builder 对象的方法来填充属性,最后调用 build() 方法来使用 Phone 的构造方法,将 Builder 对象作为参数,把它的属性全都传给新的 Phone 对象,最后返回这个构造好的 Phone 对象

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
public class Phone {
// 手机的属性
private String cpu;
private String screen;
private String memory;
private String camera;

// 私有构造方法
private Phone(Builder builder) {
this.cpu = builder.cpu;
this.screen = builder.screen;
this.memory = builder.memory;
this.camera = builder.camera;
}

public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String camera;

public Builder cpu(String cpu) {
this.cpu = cpu;
return this;
}
public Builder screen(String screen) {
this.screen = screen;
return this;
}
public Builder memory(String memory) {
this.memory = memory;
return this;
}
public Builder camera(String camera) {
this.camera = camera;
return this;
}

public Phone build() {
// 内部类可以直接访问外部类方法
return new Phone(this);
}
}
}

// 使用Builder创建对象
public static void main(String[] args) {
// 创建手机对象 通过Builder对象获取手机对象
// 链式调用,每一个方法都会返回填充了属性的Builder对象给下一个方法
Phone phone = new Phone.Builder()
.cpu("Intel")
.screen("OLED")
.memory("16G")
.camera("12M")
.build();
}

结构型模式

描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式。前者用继承机制来组织接口和类,后者采用组合或聚合来组合对象。

组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式更灵活。

代理模式

由于某些原因,需要给某对象提供一个代理对象,以便控制对这个对象的访问。 这时访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介

Java 中的代理按照代理类生成的时机不同,分为静态代理和动态代理。静态代理代理类是编译期就生成,而动态代理代理类是 Java 运行时动态生成。动态代理又有 JDK 代理和 CGLIB 代理

代理(Proxy)模式结构:

  • 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类:实现了抽象主体中的具体业务,是代理对象所代表真实对象,是最终要引用的对象。
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

静态代理(编译期就生成)

卖票接口(抽象主题类):含有卖票方法
火车站(具体主题类):实现卖票接口
代收点(代理类):火车站的代理对象,实现卖票接口,内部组合一个火车站对象,同时加上收取服务费的逻辑

这个例子中测试类直接访问的是 ProxyPoint 类对象,也就是说 ProxyPoint 作为访问对象和目标对象的总结。同时对卖票方法进行增强(收取服务费)

JDK 动态代理(运行时动态生成)

Java 中提供了一个动态代理类 Proxy,Proxy 并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance 方法)来获取代理对象

基于上面的例子,创建一个 ProxyFactory 类,用于创建代理对象
ProxyFactory 类中组合一个火车站对象,

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
public class ProxyFactory {
// 声明目标对象
private TrainStation station = new TrainStation();

// 获取代理对象
public SellTickets getProxyObject() {

/*
* newProxyInstance()方法的三个参数
* ClassLoader loader : 类加载器,用于加载代理类。可以通过目标对象获取类加载器
* Class<?>[] interfaces : 代理类实现的接口的字节码对象
* InvocationHandler h : 代理对象的调用处理程序
*/
// 通过Proxy类中的newProxyInstance()方法创建代理对象
SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
/*
* invoke方法的三个参数,调用代理对象时自动传入
* Object proxy : 代理对象,和 proxyObject 对象是同一个对象,在invoke方法中基本不用
* Method method : 对接口中方法进行封装的method对象
* Object[] args : 调用方法的实际参数
* 返回值就是实际方法的返回值
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// System.out.println("invoke方法执行了");
System.out.println("增强方法,收取服务费20元(jdk动态代理)");
// 调用目标对象的方法
Object obj = method.invoke(station, args);
return obj;
}
}
);
return (SellTickets) proxyObject;
}
}


// 测试类内部
public static void main(String[] args) {
// 获取代理对象
// 1. 创建代理工厂对象
ProxyFactory factory = new ProxyFactory();
// 2. 使用代理工厂创建代理对象
SellTickets proxyObject = factory.getProxyObject();
// 3. 调用代理对象中的方法
proxyObject.sell();
}
  • ProxyFactory 不是代理模式中所说的代理类,他只创建了代理对象,代理类是程序在运行过程中在内存中生成的类。
  • 动态代理就是在运行时用反射获取已经有的类,进行修改后动态生成新的类,之后就操纵这个新类的对象
  • 代理类实现了和被代理类相同的接口
  • 根据多态特性,执行的是代理类($Proxy0)中的方法,返回的代理对象进行了接口类型的强转,所以只会执行重写的接口类的方法SellTickets proxyObject = factory.getProxyObject();
  • 代理类中的 sell()方法中又调用了我们自己 new 的 InvocationHandler() 的 invoke 方法
  • invoke 方法通过反射执行了真实对象所属类(TrainStation)中的 sell()方法

CGLIB 动态代理

如果没有定义 SellTickets 接口,只定义了 TrainStation 类,很显然 JDK 代理是无法使用了,因为 JDK 代理要求必须定义接口,对接口进行代理。

CGLIB 是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为 JDK 的动态代理提供了很好的补充。

改写上面的例

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
// 火车站类
public class TrainStation {
public void sell() {
System.out.println("火车站卖票");
}
}

// 代理对象工厂
public class ProxyFactory implements MethodInterceptor {

// 声明火车站对象
private TrainStation station = new TrainStation();

// CGLIB 生成的代理类是源类的子类,所以直接用源类接收
public TrainStation getProxyInstance() {
// 创建Enhancer对象(增强器),类似于JDK代理中的Proxy类
Enhancer enhancer = new Enhancer();
// 设置父类的字节码对象
enhancer.setSuperclass(TrainStation.class);
// 设置回调函数,代理工厂类要实现Callback接口(MethodInterceptor是Callback的子接口)
// 调用代理对象的sell方法时,是调用的CGLIB生成的子类里重写的sell方法,该方法内部会主动调用intercept方法
// sell方法确实会主动调用intercept方法,但是没有对象能让他调用,所以要拦截方法把this注入,回调
enhancer.setCallback(this);
// 创建代理对象
TrainStation proxyObject = (TrainStation) enhancer.create();
return proxyObject;
}

// 拦截方法,和JDK代理的 InvocationHandler 作用一致
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("代售点收取服务费(CGLIB代理)");
// 调用目标对象的方法
Object obj = method.invoke(station, args);
return obj;
}
}

// 测试类
public static void main(String[] args) {
// 创建代理工厂对象
ProxyFactory proxyFactory = new ProxyFactory();
// 获取代理对象
TrainStation proxyObject = proxyFactory.getProxyObject();
// 调用代理对象中的sell方法卖票
proxyObject.sell();
}

代理模式对比

JDK 和 CGLIB 代理的区别:

  • JDK 代理只能代理实现了接口的类,CGLIB 代理可以代理没有实现接口的类。
  • CGLIB 不能代理 final 修饰的类和方法。
  • JDK 代理效率比 CGLIB 高,所以有接口用 JDK 代理,没有接口用 CGLIB 代理。

动态代理和静态代理:

  • 动态代理是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke)中处理,当接口方法数量比较多的时候,我们可以灵活处理,而不需要像静态代理那样每一个方法都进行中转
  • 如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,动态代理就不用,他是动态的在内存中生成代理类

代理模式优缺点

优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
  • 代理对象可以扩展目标对象的功能
  • 代理模式能将客户端与目标对象分离开,在一定程度上降低了系统的耦合度

缺点:

  • 增加了系统复杂度

代理模式使用场景

  • 远程代理:为远程的对象创建一个代理,以便从本地访问该对象
    本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。就是 RPC 思想。
  • 防火墙代理
    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
  • 保护代理
    控制一个对象访问,如果需要,可以给不同的用户提供不用级别的权限

适配器模式

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的哪些类能一起工作
适配器模式分为类适配器模式和对象适配器模式,前者之间的耦合度比后者高

适配器模式结构

适配器模式(Adapter)包含以下主要角色

  • 目标(Target)接口:当前业务所期待的接口,它可以是抽象类或接口
  • 适配者(Adaptee):它是被访问和适配的现存组件库中的组件接口
  • 适配器(Adapter):它是一个转换器,通过继承或引用适配者的对象,把适配器接口转换成目标接口,让客户按目标接口的格式访问适配者

类适配器模式

实现方式:定义一个适配器类来实现当前系统的业务接口,同时继承现有组件库中已经存在的组件

【例子】读卡器
现有一台电脑只能读取 SD 卡,而要读取 TF 卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将 TF 卡中的内容读取出来

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
// TF卡接口
public interface TFCard {
// 从TF卡中读取数据
String readTF();
// 向TF卡中写数据
void writeTF(String msg);
}

// TF卡实现类
public class TFCardImpl implements TFCard {

public String readTF() {
String msg = "从TF卡中读取数据";
return msg;
}

public void writeTF(String msg) {
System.out.println("向TF卡中写入数据:" + msg);
}
}

// 目标接口
public interface SDCard {
// 从SD卡中读取数据
String readSD();
// 向SD卡中写数据
void writeSD(String msg);
}

// SD卡实现类
public class SDCardImpl implements SDCard {

public String readSD() {
String msg = "一串数据数据";
return msg;
}

public void writeSD(String msg) {
System.out.println("向SD卡中写入数据:" + msg);
}
}

// 计算机类,只有读取SD卡数据的功能
public class Computer {

// 从SD卡中读取数据
public String readSD(SDCard sdCard) {
// 判空
if (sdCard == null) {
throw new NullPointerException("sdCard is not null");
}
return sdCard.readSD();
}
}

// 适配器类,实现目标接口(SD接口),继承适配者(TF实现类)
public class SDAdapterTF extends TFCardImpl implements SDCard {

// 实现SD卡接口的读方法
public String readSD() {
String msg = "通过适配器,读取TF卡中的数据";
// 调用的TF的方法
return readTF();
}

// 实现SD卡接口的写方法
public void writeSD(String msg) {
System.out.println("通过适配器,向TF卡中写入数据:" + msg);
writeTF(msg);
}
}


// 测试类
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
String msg = computer.readSD(new SDCardImpl());

// 使用该电脑读取TF卡中的数据
// 定义适配器类,并作为参数传递给计算机对象
String msg1 = computer.readSD(new SDAdapterTF());// 适配器实现了目标接口所以能传进去
}

类适配器违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,房子不可用

对象适配器模式

实现方式:对象适配器模式可采用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口

改写上面的例子:就是适配器类不在继承适配者类,而是聚合适配者类的接口

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
// 适配者类,不在继承适配者类,而是聚合
public class SDAdapterTF implements SDCard {

// 声明适配者类
private TFCard tfCard;

// 声明有参构造函数
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}

public String readSD() {
String msg = "通过适配器,读取TF卡中的数据";
// 用成员变量调用TF的方法
return tfCard.readTF();
}

public void writeSD(String msg) {
System.out.println("通过适配器,向TF卡中写入数据:" + msg);
tfCard.writeTF(msg);
}
}

// 测试类
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();

// 使用该电脑读取TF卡中的数据
// 创建适配器对象,传入TF卡实现类
SDAdapterTF sdAdapterTF = new SDAdapterTF(new TFCardImpl());
String msg1 = computer.readSD(sdAdapterTF);
}

适配器模式应用场景

  • 以前开发的系统存在满足新系统功能需求的类,但接口和新系统的接口不一致
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同

JDK 源码中适配器模式的使用

Reader(字符流)、InputStream(字节流)的适配使用的是 InputStreamReader
InputSreamReader 继承自 java.io 包中的 Reader,对他中的抽象的未实现的方法给出实现
如:

1
2
3
4
5
6
7
public int read() throws IOException {
return sd.read();
}

public int read(char[] cbuf, int off, int len) throws IOException {
return sd.read(cbuf, off, len);
}

上述代码中的 sd(StreamDecoder 类对象),在 Sun 的 JDK 实现中,实际的方法是对 sun.nio.cs.StreamDecoder 类的同名方法的调用封装
我的理解就是 StreamDecoder 类是适配器类,它继承了 Read 抽象类(目标接口)重写了对应方法,然后聚合 InputStreamReader 类,然后在重写的方法中封装了 InputStreamReader 类的方法

装饰者模式

指在不改变现有对象结构的情况下,动态的给对象增加一些职责(即增加其额外功能)

快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。|

装饰者模式的结构

  • 抽象组件(Component):定义一个抽象接口以规范准备接受附加责任的对象
  • 具体组件(ConcreteComponent):实现抽象构件,通过角色为其添加一些职责
  • 抽象装饰(Decorator):继承或实现抽象构件,并包含具体构件的实例,可通过其子类扩展具体构建的功能
  • 具体装饰(ConcreteDecorator):实现抽象装饰角色,并给具体构件对象添加附加职责

【案例】快餐

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
// 快餐(抽象组件类)
public abstract class FastFood {

// 价格
private float price;
// 描述
private String desc;

// 上面属性的getter和setter方法
// 省略

// 有参构造函数
public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}
// 计算价格,抽象方法
public abstract float cost();
}

// 炒饭(具体组件类)
public class FriedRice extends FastFood {
public FriedRice() {
// 调用父类的有参构造器
super(10, "炒饭");
}

public float cost() {
return getPrice();
}
}

// 炒面(具体组件类)
public class FriedNoodles extends FastFood {
public FriedNoodles() {
super(8, "炒面");
}
public float cost() {
return getPrice();
}
}

// 配料(抽象装饰类)
public abstract class Garnish extends FastFood {
// 声明快餐类的变量
private FastFood fastFood;
// FastFood属性getter和setter方法
// 省略

// 构造方法
public Garnish(FastFood fastFood, float price, String desc) {
super(price, desc);
this.fastFood = fastFood;
}
}

// 鸡蛋(具体装饰类)
public class Egg extends Garnish {

public Egg(FastFood fastFood) {
super(fastFood, 1, "鸡蛋")
}

public float cost() {
// 计算价格
return getPrice() + fastFood.cost();
}

@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}

// 测试类
public static void main(String[] args) {
// 点一份炒饭
FriedRice food = new FriedRice();

// 在上面的炒饭中加一个鸡蛋
// 这是把当前food传给Egg的构造方法,创建一个food里加了鸡蛋的对象再赋回来
food = new Egg(food);
// 再加一个鸡蛋
food = new Egg(food);
}

好处:

  • 装饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则。继承是静态的附加责任,装饰者则是动态的附加责任
  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类功能

装饰者模式应用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时
    • 不能采用继承的情况主要有两类:
      • 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
      • 第二类是因为类定义不能继承(如 final 类)
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

JDK 源码中装饰者模式的使用

IO 流中的包装类使用到了装饰者模式。BufferedInputStream、BufferedOutStream、BufferedReader、BufferedWriter

BufferedWriter 继承自 Writer,BufferedWriter 又聚合了 FileWriter

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
public static void main(String[] args) {
// 创建BufferdWriter对象
// 创建FileWriter对象
FileWriter fw = new FileWriter("d:/a.txt");
BufferedWriter bw = new BufferedWriter(fw);

// 写数据
bw.write("hello world");

bw.close();
}
}

代理和装饰者的区别

  • 相同点:
    • 都要实现与目标相同的业务接口
    • 在两个类中都要声明目标对象
    • 都可以在不修改目标类的前提下增强目标方法
  • 不同点:
    • 目的不同:
      装饰者是为了增强目标对象
      静态代理是为了保护和隐藏目标对象
    • 获取目标对象构建的地方不同
      装饰者是由外界传进来的,可通过构造方法传递
      静态代理师在代理类内部创建,以此来隐藏目标对象

桥接模式

现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系。
假如我们在增加一个形状或颜色就要创建更多的类,可能会类爆炸,此时可以考虑使用桥接模式。

定义:将抽象和实现分离,是他们可以独立变化。它使用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度

桥接 (Bridge) 模式包含以下主要角色:(不好理解,还是看案例吧)

  • 抽象化 (Abstraction) 角色:定义抽象类,并包含一个对实现化对象的引用
  • 扩展抽象化 (Refined Abstraction) 角色是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法
  • 实现化 (lmplementor) 角色:定义实现化角色的接口,供扩展抽象化角色调用
  • 具体实现化 (concrete lmplementor) 角色:给出实现化角色接口的具体实现。

【案例】视频播放器
需要开发一个跨平台视频播放器,可以在不同操作系统平台(如 windows 、 Mac 、 Linux 等)上播族种格式的视频文件,常见的视频格式包括 MP4、AVI、WMV 等。该播放器包含了两个维度,适合使用桥接模式。

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
// 视频文件(实现化角色)
public interface VideoFile {
void decode(String fileName);
}

// AVI视频文件(具体实现化角色)
public class AVIFile implements VideoFile {
public void decode(String fileName){
System.out.println("avi文件:" + fileName);
}
}

// MP4视频文件(具体实现化角色)
public class MP4File implements VideoFile {
public void decode(String fileName){
System.out.println("mp4文件:" + fileName);
}
}

// 操作系统(抽象化角色)
public abstract class OpratingSystem {
// 声明videoFile变量,用组合来替代继承
// protected修饰,这样子类也能访问
protected VideoFile videoFile;

public OpratingSystem(VideoFile videoFile) {
this.videoFile = videoFile;
}

public abstract void play(String fileName);
}

// windows操作系统(扩展抽象化角色)
public class Windows extends OpratingSystem {

public Windows(VideoFile videoFile) {
super(videoFile);
}

public void play(String fileName) {
videoFile.decode(fileName);
}
}

// Mac操作系统(扩展抽象化角色)
public class Mac extends OpratingSystem {
public Mac(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}

// 测试类
public static void main(String[] args) {
// 创建windows操作系统对象
OpratingSystem windows = new Windows(new AVIFile());
// 使用操作系统播放avi文件
windows.play("战狼3.avi");
}

这个实现了操作系统维度视频格式维度的解耦,之后扩展当前维度也不影响另一个维度

好处:

  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
    如:如果现在还有一种视频文件类型 WMV,我们只需要再定义一个类实现 VideoFile 接口即可,其他类不需要发生变化
  • 实现细节对客户透明

桥接模式使用场景

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。

外观模式

有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票是很容易亏钱的,刚开始炒股肯定都会想,如果有个懂行的帮帮手就好,其实基金就是个“好帮手”,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外冫领域,而基全投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。

定义:

又名门面模式,是一种通过多个复杂的子系统提供一个一致的接口,从而使这些子系统更加容易被访问的模式。该模式对外有一个统一的接口,外部应用程序不用关心内部子系统的具体细节,大大降低了程序的复杂度。外观模式是迪米特法则的典型应用

外观模式结构

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口
  • 子系统(SubSystem)角色:实现子系统的功能,客户可以通过外观角色访问它

【例】智能家电控制
小明的爷爷已经 60 岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都较麻烦。所以小明给爷爷买了智能音可以通过语音直接控制这些智能家电的开启和关闭。

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
// 电灯类
public class Light {
// 开灯
public void on() {
System.out.println("打开电灯");
}
// 关灯
public void off() {
System.out.println("关闭电灯");
}
}
// 电视机类,同上
// 空调类,同上

// 外观类
public class Facade {
// 聚合电灯对象,电视机对象,空调对象
private Light light;
private TV tv;
private AirConditioner ac;

public Facade() {
light = new Light();
tv = new TV();
ac = new AirConditioner();
}

// 通过语音控制
public void say(String message) {
if (message.contains("打开")) {
on();
} else if (message.contains("关闭")) {
off();
} else {
System.out.println("我听不懂");
}
}

// 一键打开功能
private void on() {
light.on();
tv.on();
ac.on();
}
// 一键关闭功能
private void off() {
light.off();
tv.off();
ac.off();
}
}

// 测试类
public static void main(String[] args) {
// 创建智能音箱对象
Facade facade = new Facade();
// 控制家电
facade.say("打开家电");
facade.say("关闭家电");
}

外观模式的优缺点

优点:

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易

缺点:

  • 不符合开闭原则,修改很麻烦

外观模式使用场景

  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
  • 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

组合模式

文件夹的那种树形结构,有两种角色:容器(文件夹)、叶子结点(文件)。文件就只能读写,文件夹就只存储文件,但是客户端希望能一致的处理容器对象和叶子结点对象,所以引入组合模式

定义:
又名部分整体模式,是用于把一组相似的对象当作一个单一的对象·组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的讠十模式属于结构型模式,它创建了对象组的树形结构。

组合模式结构

  • 抽象根节点 (Component) 定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点 (Composite) 定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点 (Leaf) 叶子节点对象其下再无分支,是系统层次遍历的最小单位。

【例】软件菜单
如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。

无论是菜单,还是菜单项,都继承菜单组件(抽象根节点),菜单里面又聚合了菜单组件列表

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
// 菜单组件(抽象根节点)
public abstract class MenuComponent {
// 菜单组件的名称
protected String name;
// 菜单组件的层级
protected int level;

// 添加子菜单
public void add(MenuComponent menuComponent) {
// 子菜单会重写这些方法,子菜单项不会重写,如果子菜单项调用这些父类方法,就会抛出异常
throw new UnsupportedOperationException();
}
// 移除子菜单
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
// 获取指定的子菜单
public MenuComponent getChild(int index) {
throw new UnsupportedOperationException();
}
// 获取菜单或者菜单项的名称
public String getName() {
return name;
}

// 打印菜单名称的方法(包含子菜单和子菜单项)
public abstract void print();
}

// 菜单(树枝节点)
public class Menu extends MenuComponent {
// 菜单可以有多个子菜单或者子菜单项
private List<MenuComponent> menuComponents = new ArrayList<>();

// 构造方法
public Menu(String name, int level) {
this.name = name;
this.level = level;
}

// 重写添加、移除、获取子菜单的方法
@Override
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
@Override
public MenuComponent getChild(int index) {
return menuComponents.get(index);
}
@Override
public void print() {
// 打印菜单名称
// 根据菜单层级,打印缩进
for (int i = 0; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
// 打印子菜单名称(递归调用)
for (MenuComponent menuComponent : menuComponents) {
menuComponent.print();
}
}
}

// 菜单项(叶子结点)
public class MenuItem extends MenuComponent {
// 构造方法
public MenuItem(String name, int level) {
this.name = name;
this.level = level;
}
// 只重写这一个方法,调用其他方法会抛出父类默认声明的异常
@Override
public void print() {
// 打印菜单项名称
// 根据菜单层级,打印缩进
for (int i = 0; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
}
}

// 测试类
public static void main(String[] args) {
// 创建菜单树
MenuComponent menu1 = new Menu("菜单管理", 2);
menu1.add(new MenuItem("添加菜单", 3));
menu1.add(new MenuItem("删除菜单", 3));
menu1.add(new MenuItem("修改菜单", 3));
menu1.add(new MenuItem("展开菜单", 3));
MenuComponent menu2 = new Menu("权限管理", 2);
menu2.add(new MenuItem("添加权限", 3));
menu2.add(new MenuItem("删除权限", 3));
menu2.add(new MenuItem("修改权限", 3));

// 创建一级菜单
MenuComponent menuRoot = new Menu("系统管理", 1);
// 将二级菜单添加到一级菜单
menuRoot.add(menu1);
menuRoot.add(menu2);

// 打印菜单名称(如果有子菜单一块打印)
menuRoot.print();
}

组合模式的分类

  • 透明组合模式
    组合模式的标准形式,抽象根节点中声明的所有方法,确保所有构件都有相同的接口。但是叶子对象其实和容器有本质区别,叶子对象是没有下一级的,所以继承的抽象根节点很多方法都没有意义,如果直接调用,并且没有错误处理,就会报错
  • 安全组合模式
    在安全组合模式中,抽象根节点中没有声明任何用于管理成员对象的方法,而是在树枝结点 Menu 类中声明并实现这些方法。缺点就是不够透明,客户端不能完全针对抽象编程,必须有区别地对待容器和叶子对象

组合模式的优点

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  • 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合”开闭原则”。
  • 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

享元模式

定义:运用共享技有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。(享元,就是共享单元)

享元模式结构

享元(Flyweight)模式中存在以下两种状态:

  1. 内部状态,即不会随着环境的改变而改变的可共享部分
  2. 外部状态,指随环境改变而改变的的不可共享的部分。享元模式的实现要领就是区分应用中的这两种状态
  • 抽象享元角色 (Flyweight) :通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • 具体享元 (concrete Flyweight) 角色:它实现了抽象享元类,称为享元对象在具体享元类中为内部状态提供了诸空间。通常我们可以结合单例模式来十具体享元类,为每一个具体享元类提供唯一的享元对象。
  • 非享元 (Unsharab1e Flyweight) 角色:并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂 (Flyweight Factory) 角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

【例】俄罗斯方块

如果每个不同的方块都是一个实例对象,那这些对象就要占用很多内存空间,下面用享元模式来实现

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
// AbstractBox(抽象享元角色)
public abstract class AbstractBox {

// 后去图形的方法
public abstract String getShape();

// 显示图形及颜色
public void display(String color) {
System.out.println("方块形状:" + getShape() + ",方块颜色:" + color);
}
}

// I图形(具体享元角色)
public class IBox extends AbstractBox {
public String getShape() {
return "I";
}
}
// L图形(具体享元角色)
// O图形(具体享元角色)都省略了

// 工厂类,设计为单例
public class BoxFactory {

private HashMap<String, AbstractBox> map;

// 在私有构造方法中赋值
private BoxFactory() {
map = new HashMap<String, AbstractBox>();
map.put("I", new IBox());
map.put("L", new LBox());
map.put("O", new OBox());
}

// 静态变量,保证单例,所以构造方法也只会执行一次
private static BoxFactory factory = new BoxFactory();

// 提供一个方法获取该工厂类对象
public static BoxFactory getInstance() {
return factory;
}

// 根据名称获取图形对象
public AbstractBox getShape(String name) {
return map.get(name);
}
}

// 测试类
public static void main(String[] args) {
// 获取I图形对象
AbstractBox box1 = BoxFactory.getInstance().getShape("I");
box1.display("灰色");
// 获取L图形对象
AbstractBox box2 = BoxFactory.getInstance().getShape("L");
box2.display("蓝色");
// 获取O图形对象
AbstractBox box3 = BoxFactory.getInstance().getShape("O");
box3.display("绿色");

// 获取I图形对象,当前的I对象和之前是同一个对象
AbstractBox box4 = BoxFactory.getInstance().getShape("I");
box4.display("灰色");
}

享元模式的优缺点和使用场景

  1. 优点
    • 大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
    • 享元模式中的外部状态相对独立,且不影响内部状态
  2. 缺点
    • 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
  3. 使用场景
    • 一个系统有大量相同或者相似的对象,造成内存的大量耗费
    • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
    • 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

享元模式在 JDK 源码中的应用

Integer 类使用了享元模式

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;

System.out.println("i1和i2是否为同一个对象?" + i1 == i2);// true

Integer i3 = 128;
Integer i4 = 128;

System.out.println("i3和i4是否为同一个对象?" + i3 == i4);// false
}

Integer 默认先创建并缓存以 -128 ~ 127 之间数的 Integer 对象,当调用 valueof 时如果参数在 -128 ~ 127 之间,则计算下标并从缓存中返回,否则创建一个新的 Integer 对象
也就是说[-128,127]之间的 Integer 对象是复用的

行为型模式

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

模版方法模式

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。

例如,去银行办理业务一般要经过以下 4 个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

定义:
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

模版方法模式结构

  • 模板方法 (Temp1ate Method) 模式包含以下主要角色:
    • 抽象类 (Abstract Class) :负责给出一个算法的轮廓和骨架。它由一个模板方氵去和若干个基本方法构成。
      • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
      • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种
        • 具体方法 (Concrete Method):一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行盖也可以直接继承。
        • 抽象方法 (Abstract Method):一个抽象方法由抽象类声明、由其具体子类实现。(推迟到子类实现)
        • 钩子方法 (Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为 isXxx,返回值类型为 boolean 类型
  • 具体子类 (concrete C1ass) :实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

模版方法模式案例实现

【例】 炒菜

炒菜的步骤是固定的,分为倒油、热油、倒素材、倒调料品、翻炒。
其中倒油、热油都是固定的,但倒素材、倒调料品、翻炒是变化的,变化的部分由子类实现。

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
// 抽象类
public abstract class Cooking {
// 模版方法
public final void cookProcess() {
// 规定了执行顺序
pourOil();
heatOil();
pourVegetable();
pourSauce();
fry();
}

// 下面两步是固定的,所以直接实现
// 倒油
public void pourOil() {
System.out.println("倒油");
}
// 热油
public void heatOil() {
System.out.println("热油");
}
// 抽象方法,需要子类实现
}

// 爆炒包菜(具体子类)
public class BaoCai extends Cooking {

public void pourVegetable() {
System.out.println("倒包菜");
}
public void pourSauce() {
System.out.println("倒辣椒");
}
}

// 爆炒菜心(具体子类)
public class CaiXin extends Cooking {

public void pourVegetable() {
System.out.println("倒菜心");
}
public void pourSauce() {
System.out.println("倒蒜蓉");
}
}

// 测试类
public static void main(String[] args) {
// 炒包菜
// 创建对象
BaoCai baoCai = new BaoCai();
// 调用父类的炒菜功能(模版方法)
baoCai.cookProcess();
}

模版方法模式优缺点

优点:

  • 提高代码复用性将相同部分的代码放在抽象的父噩中,而将不同的代码放入不同的子类中。
  • 实现了反向控制通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制,并符合“开闭原则”

缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

模版方法模式适用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板去模式,将容易变的部分抽象出来,供子类实现。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

模版方法模式 JDK 源码解析

InputStream 类中就用了模版方法模式。在 InputStream 类中定义了多个 read()方法
一个抽象的无参 read()方法,和两个不同参数的 read()方法
有参的 read()方法,就是模版方法,里面调用了抽象无参 read()方法
调用的抽象方法,实际是子类实现的 read()方法,实现了反向控制

策略模式

有多种旅游出行方式可以选择,比如:骑自行车、坐汽车、坐火车、坐飞机
程序员开发软件也有多用 IDE 可以选择:比如:IDEA、Eclipse、Visual Studio

定义:
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式结构

策略模式包含以下主要角色:

  • 抽象策略 (strategy) 类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略 (concrete strategy) 类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
  • 环境 (context) 类:持有一个策略类的引用,最终给客户端调用。

策略模式案例实现

【例】促销活动

一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:

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
// 抽象策略类
public interface Strategy {
// 展示促销活动
void show();
}

// 具体策略类
// 为春节准备的促销活动A
public class StrategyA implements Strategy {
public void show() {
System.out.println("买一送一");
}
}
// 为中秋准备的促销活动B
public class StrategyB implements Strategy {
public void show() {
System.out.println("满300减100");
}
}
// 为圣诞准备的促销活动C
public class StrategyC implements Strategy {
public void show() {
System.out.println("满1000加一元换购任意200以下商品");
}
}

// 环境类(Context),用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
public class SalesMan {
// 聚合策略类
private Strategy strategy;
// 他的getter和setter方法,略

public SalesMan(Strategy strategy){
this.strategy = strategy;
}

// 由促销员展示促销活动给用户
public void salesManShow(){
strategy.show();
}
}

// 测试类
public static void main(String[] args) {
// 春节来了,使用春节促销活动
SalesMan salesMan = new SalesMan(new StrategyA());
// 展示促销活动
salesMan.salesManShow();

// 中秋节来了,使用中秋节促销活动
salesMan.setStrategy(new StrategyB());
salesMan.salesManShow();

// 圣诞节来了,使用圣诞节促销活动
salesMan.setStrategy(new StrategyC());
salesMan.salesManShow();
}

策略模式优缺点

优点:

  • 策略类之间可以自由切换由于策略类都实现同一个接口,所以使它们之间可以自由切换。
  • 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开闭原原则
  • 避免使用多重条件选择语句(if else) ,充分体现面向对象设计思想。

缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

策略模式使用场景

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。·系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

策略模式 JDK 源码解析

就比如 Arrays.sort()方法,里面可以传不同的比较器来实现不同的排序,这些不同的比较器就是不同的策略

命令模式

顾客把订单交给服务员,服务员拿了订单之后放在柜台喊一声订单来了,然后厨师根据订单准备餐点

定义:

将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开,这样两者之间通过命令对象进行沟通,这样方便将命对象进行存储、传递、调用、增加与管理

命令模式结构

  • 抽象命令类 (command) 角色:定义命令的接口,声明执行的方法。
  • 具体命令 (concrete command) 角色:具体的命令,实现命令接口;通常会持有接町者,并调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者 (Receiver) 角色:接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • 调用者/请求者 (lnvoker) 角色:要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。

命令模式案例实现

【例】上面的点餐案例

服务员:调用者角色,由他来发起命令
厨师:接收者角色,执行命令
订单:命令中包含订单

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
// 订单类
public class Order {
// 餐桌号码
private int diningTable;

// 所下餐品及份数
private Map<String, Integer> foodDir = new HashMap<String, Integer>();

// 省略getter和setter方法
}

// 厨师类
public class Chef {
public void makeFood(String name, int num) {
System.out.println("开始做:" + name + ",数量:" + num);
}
}

// 抽象命令类
public interface Command {
void execute();
}

// 具体命令类
public class OrderCommand implements Command {

// 持有接受者对象
private Chef chef;
private Order order;

// 构造函数
public OrderCommand(Chef chef, Order order) {
this.chef = chef;
this.order = order;
}

public void execute() {
System.out.println(order.getDiningTable() + "桌的订单:");
Map<String, Integer> foodDir = order.getFoodDir();
// 遍历Map集合
Set<String> keySet = foodDir.keySet();// 获取key的集合
for (String foodName : keySet) {
receiver.makeFood(foodName, foodDir.get(foodName));
}

System.out.println(order.getDiningTable() + "桌的订单处理完毕");
}
}

// 服务员类(请求者角色)
public class Waiter {
// 持有多个命令对象
private List<Command> commands = new ArrayList<Command>();

public void setCommand(Command command) {
// 将cmd对象存储到list集合中
commands.add(command);
}

// 发起命令功能 喊订单来了
public void orderUp() {
System.out.println("订单来了...");
// 遍历集合
for (Command command : commands) {
if (command != null) {
command.execute();
}
}
}
}

// 测试类
public static void main(String[] args) {
// 创建第一个订单对象
Order order1 = new Order();
order1.setDiningTable(1);
order1.getFoodDir().put("西红柿鸡蛋面", 1);
order1.getFoodDir().put("小杯可乐", 2);

// 创建第二个订单对象
Order order2 = new Order();
order2.setDiningTable(2);
order2.getFoodDir().put("尖椒肉丝盖饭", 1);
order2.getFoodDir().put("小杯雪碧", 2);

// 创建厨师对象
Chef chef = new Chef();
// 创建命令对象
OrderCommand cmd1 = new OrderCommand(chef, order1);
OederCommand cmd2 = new OrderCommand(chef, order2);

// 创建服务员对象
Waitor invoke = new Waiter();
invoke.setCommand(cmd1);
invoke.setCommand(cmd2);
// 发起命令
invoke.orderUp();
}

命令模式优缺点

优点:

  • 降低系统的合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
  • 增加或删除命非常方便。采用命令模式增加与删除命令不会影响其他类,它满足、“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类。
  • 系统结构更加复杂。

命令模式使用场景

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
  • 系统需要在不同的时间指定请求、将请求排队和执行请求
  • 系统需要支持命令的撤销 (Undo) 操作和恢复 (Redo) 操作

命令模式 JDK 源码解析

Runable 是一个典型命令模式,Runnable 担当了命令的角色,Thread 充当的是调用者,start 方法就是其执行方法

责任链模式

在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度,这样的例子还有很多,如找领导出差报销、生活中的”击鼓传花”游戏等。

定义:
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

责任链模式结构

  • 抽象处理者 (Handler) 角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者 (concrete Handler) 角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类 (Client) 角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

责任链模式案例实现

现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假 1 天到 3 天的假还需要部门经理同意;请求 3 天到 7 天还需要总经理同意才行

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
// 请假条
public class LeaveRequest {
// 姓名
private String name;
// 请假天数
private int num;
// 请假内容
private String content;

// 构造函数,略

// getter,setter 方法,略
}

// 抽象处理者类
public abstract class Handler {

protected final static int NUM_ONE = 1;
protected final static int NUM_THREE = 3;
protected final static int NUM_SEVEN = 7;

// 该领导的处理的请求天数区间
private int numStart;
private int numEnd;

// 声明后继者(声明上级领导)
private Handler nextHandler;

public Handler(int numStart, int numEnd) {
this.numStart = numStart;
this.numEnd = numEnd;
}

// 设置上级领导对象
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}

// 各级领导处理请假条的方法
protected abstract void handleLeave(LeaveRequest leave);

// 提交请假条的方法
public final void submit(LeaveRequest leave) {
// 该领导进行审批
this.handleLeave(leave);
// 如果他不可以处理该请求,则传递给它的后继者
if (this.nextHandler != null && leave.getNum() > this.numEnd) {
// 提交给上级领导
this.nextHandler.submit(leave);
} else {
System.out.println("流程结束!");
}
}
}

// 小组长类(具体处理者)
public class GroupLeader extends Handler {
// 构造函数
public GroupLeader() {
// 处理0到NUM_ONE天
super(0, Handler.NUM_ONE);
}

protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天,请假理由是:" + leave.getContent());
System.out.println("小组长审批:同意");
}
}

// 部门经理类(具体处理者)
public class Manager extends Handler {

public Manager() {
super(Handler.NUM_ONE, Handler.NUM_THREE);
}

protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天,请假理由是:" + leave.getContent());
System.out.println("部门经理审批:同意");
}
}

// 总经理类(具体处理者)
public class Manager extends Handler {

public Manager() {
super(Handler.NUM_THREE, Handler.NUM_SEVEN);
}

protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天,请假理由是:" + leave.getContent());
System.out.println("总经理审批:同意");
}
}

// 测试类
public static void main(String[] args) {
// 创建一个请假条对象
LeaveRequest leave = new LeaveRequest("小李", 1, "身体不健康");

// 创建各级领导对象
GroupLeader groupLeader = new GroupLeader();
Manager manager = new Manager();
GeneralManager generalManager = new GeneralManager();

// 设置处理责任链
groupLeader.setNextHandler(manager);
manager.setNextHandler(groupLeader);

// 小明提交请假条给小组长
groupLeader.submit(leave);
}

责任链模式优缺点

优点:

  • 降低了对象之间的耦合度
    该模式降低了请求发送者和接收者的耦合度
  • 增强了系统的可扩展性可以根据需要增加新的请理类,满足开闭原则
  • 增强了给对象指派职责的灵活性当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
  • 责任链简化了对象之间的连接一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if…else 语句。
  • 责任分担每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

缺点:

  • 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  • 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性譖受到一定影响。
  • 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

责任链模式源码解析

在 JavaWeb 应用开发中,FilterChain 类就是一种职责链(过滤器)模式的典型应用

  • 就是不同的 Filter 对象组成职责链,每一个进行自己的逻辑,然后传递给下一个 Filter 对象,这样,当请求进来的时候,就会按照顺序依次执行,最后返回给用户。
  • 每个过滤器都可以进行前置处理和后置处理

状态模式

【反例】通过按钮控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。

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
public interface ILift {

// 定义四个电梯状态常量
int OPENING_STATE = 1;
int CLOSING_STATE = 2;
int RUNNING_STATE = 3;
int STOPPING_STATE = 4;

// 设置电梯状态
void setState(int state);

// 电梯操作功能
void open();
void close();
void run();
void stop();
}

// 子实现类
public class Lift implements ILift {

// 声明一个记录当前电梯状态的变量
private int state;

public void setState(int state) {
this.state = state;
}

public void open() {
switch (state) {
case OPENING_STATE:
// 什么事都不做
break;
case CLOSING_STATE:
System.out.println("电梯门开启");
// 设置当前状态为开启
setState(OPENING_STATE);
break;
case STOPPING_STATE:
System.out.println("电梯门开启");
// 设置当前状态为开启
setState(OPENING_STATE);
break;
case RUNNING_STATE:
// 什么事都不做
break;
}
}

// 下面三个都要像上面这样写
public void close() { }

public void run() { }

public void stop() { }
}

// 测试类
public static void main(String[] args) {
// 创建电梯对象
Lift lift = new Lift();
// 设置电梯门开启状态
lift.setState(Lift.OPENING_STATE);
// 打开
lift.open();
lift.close();
lift.run();
lift.stop();
}

问题分析:

  • 使用了的大量的 switch…case,可读性差
  • 拓展性差,加个状态都要改

状态模式定义

对有状态的对象,把复杂的”判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

状态模式结构

  • 环境 (Context) 角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对
  • 抽象状态 (State) 角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  • 具体状态 (concrete State) 角色:实现抽象状态所对应的行为。

状态模式案例实现

对上面的电梯案例使用状态模式进行改进

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
// 抽象状态类
public abstract class LiftState {

// 声明环境角色类变量
protected Context context;

public void setContext(Context context) {
this.context = context;
}

// 电梯开启操作
public abstract void open();
// 电梯关闭操作
public abstract void close();
// 电梯运行操作
public abstract void run();
// 电梯停止操作
public abstract void stop();
}

// 具体状态类
public class OpeningState extends LiftState {
//当前状态要执行的方法
public void open() {
System.out.println("电梯门开启");
}

public void close() {
// 修改状态
super.context.setLiftState(Context.CLOSING_STATE);
// 调用当前状态中的context中的close方法
super.context.close();
}

public void run() {
// 什么都不做
}

public void stop() {
// 什么也不做
}
}
// 关闭状态,其他三个也这样
// 运行状态
// 停止状态

// 环境角色类
public class Context {

// 定义对象状态对象常量
public final static OpeningState OPENING_STATE = new OpeningState();
public final static ClosingState CLOSING_STATE = new ClosingState();
public final static RunningState RUNNING_STATE = new RunningState();
public final static StoppingState STOPPING_STATE = new StoppingState();

// 定义一个当前电梯状态变量
private LiftState liftState;

public LiftState getLiftState() {
return liftState;
}

// 设置当前状态对象
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
// 设置环境角色对象
this.liftState.setContext(this);
}

public void open() {
this.liftState.open();
}

public void close() {
this.liftState.close();
}

public void run() {
this.liftState.run();
}

public void stop() {
this.liftState.stop();
}
}

// 测试类
public static boid main(String[] args) {
// 创建环境角色对象
Context context = new Context();
// 设置当前电梯状态
context.setLiftState(Context.RUNNING_STATE);

context.open();
context.close();
context.run();
context.stop();
}

就是把对应状态的行为放在状态类里,把对象的行为放在环境角色(context)里,环境角色里会聚合状态对象

状态模式优缺点

优点:

  • 将所有与某个有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。

缺点:

  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对”开闭原则”的支持并不太好

状态模式使用场景

  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构并且这些分支决定于对象的状态时

观察者模式

定义:
又称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己

观察者模式结构

  • 抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
  • 具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • 抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。·
  • 具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

观察者模式案例实现

【例】微信公众号
在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。

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
// 抽象主题类
public interface Subject {

// 添加订阅者(观察者)
void attach(Observer observer);

// 删除订阅者
void detach(Observer observer);

// 通知订阅者更新消息
void notify(String message);
}

// 抽象观察者类
public interface Observer {

void update(String message);
}

// 具体主题类
public class SubscriptionSubject implements Subject {

// 定义一个集合,用来存储多个观察者对象
private List<Observer> weiXinUserList = new ArrayList<>();

public void attach(Observer observer) {
weiXinUserList.add(observer);
}

public void detach(Observer observer) {
weiXinUserList.remove(observer);
}

public void notify(String message) {
// 遍历集合
for (Observer observer : weiXinUserList) {
// 调用每一个观察者的更新方法
observer.update(message);
}
}
}

// 具体观察者类
public class WeiXinUser implements Observer {
private String name;

public WeiXinUser(String name) {
this.name = name;
}

public void update(String message) {
System.out.println(name + " 收到微信推送:" + message);
}
}

// 测试类
public static void main(String[] args) {
// 创建公众号对象
Subject subject = new SubscriptionSubject();

// 创建订阅者对象
subject.attach(new WeiXinUser("小王"));
subject.attach(new WeiXinUser("小张"));
subject.attach(new WeiXinUser("小李"));

// 公众号更新消息
subject.notify("程序猿,你关注的公众号有更新");
}

观察者模式优缺点

优点:

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系
  • 被观察者发送通知所有注册的观察者都会收到信息〔可以实现广播机制

缺点:

  • 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
  • 如果被观察者有循环依赖的话,那么被观察者发 i 关涌知会使观察者循环调用,会导致系统崩溃

观察者模式使用场景

  • 对象之间存在一对多的关系,一个对象的状态发生改变时,需要同时改变其他对象
  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时

观察者模式 JDK 中提供的实现

在 java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写 Java 观察者模式实例。

【例】警察抓小偷
警察抓小偷也可以使用观察者模式来实现,警察是观察者,小偷是被观察者。代码如下
小偷是一个被观察者,所以需要继承 Observable 类

小偷类,被观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Thief extends Observable {
private String name;

public Thief(String name) {
this.name = name;
}

public void setName(String name) {
this.name = name;
}

public void steal() {
System.out.println("我偷了东西,来个人抓我");
super.setChanged();
super.notifyObservers();
}
}

警察类,观察者,实现 Observer 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Policemen implements Observer {

private String name;

public Policemen(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public void getName() {
return name;
}

@Override
public void update(Observable o, Object arg) {
System.out.println("警察:" + ((Thief)o).getName() + "被抓住了");
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// 创建小偷对象
Thief t = new Thief("小偷");
// 创建警察对象
Policemen p = new Policemen("警察");
// 添加警察为小偷偷的观察者
t.addObserver(p);
// 小偷开始偷东西
t.steal();
}

中介者模式

假设有 123456 对象如果他们都是互相关联的就不利于维护,这时候可以每个对象都和中介者对象关联

定义:
又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,并且可以独立地改变他们之间的交互

中介者模式结构

  • 抽象中介者 (Mediator) 角色:它是中介者的接口,提供了同事对象注旺与转发同事对象信息的抽象方法。
  • 具体中介者 (ConcreteMediator) 角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类 (Colleague) 角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类 (Concrete Colleague) 角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续交互。

中介者模式案例实现

【例】租房
现在租房基本都甬过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。

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
// 抽象中介者
public abstract class Mediator {

public abstract void connect(String message,Person person);
}

// 抽象同事类
public abstract class Person {

protected String name;
protected Mediator mediator;

public Person(String name,Mediator mediator) {
this.name = name;
this.mediator = mediator;
}
}

// 租房者(具体同事类)
public class Tenant extends Person {
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}

// 和中介联系
public void constact(String message) {
mediator.connect(message,this);
}

// 获取信息
public void getMessage(String message) {
System.out.println("租房者" + name + "获取信息:" + message);
}
}

// 房东(具体同事类)
public class HouseOwner extends Person {

public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}

// 和中介联系
public void constact(String message) {
mediator.connect(message,this);
}

// 获取信息
public void getMessage(String message) {
System.out.println("房东" + name + "获取信息:" + message);
}
}

// 具体中介者
public class MediatorStructure extends Mediator {
// 聚合房主和租房者对象
private HouseOwner houseOwner;
private Tenant tenant;

// 上面两个的getter和setter方法,略

public void connect(String message,Person person) {
// 如果传递进来的person是房主
if (person == houseOwner) {
tenant.getMessage(message);
} else {
houseOwner.getMessage(message);
}
}
}

// 测试类
public static void main(String[] args) {
// 创建中介者对象
MediatorStructure mediator = new MediatorStructure();

// 创建租房者对象
Tenant tenant = new Tenant("张三",mediator);
// 创建房主对象
HouseOwner houseOwner = new HouseOwner("李四",mediator);

// 中介者要知道具体的房主和租户
mediator.setHouseOwner(houseOwner);
mediator.setTenant(tenant);

tenant.constact("我要租房");
houseOwner.constact("我有房,你租吗");
}

中介者模式优缺点

优点:

  • 松散耦合中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可独立地变化和复用,而不再像以前那样“牵一处而动全身“了。
  • 集中控制交互多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。
  • 一对多关联转变为一对一的关联没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。

缺点:

  • 当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

中介者模式使用场景

  • 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
  • 当想创建丨运行于多个类之间的对象,又不想生成新的子类时。

迭代器模式

定义:
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示

迭代器模式结构

  • 抽象聚合 (Aggregate) 角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器 (lterator) 角色:定义访问和遍历聚合元素的接口,通常包含 hasNext() 、 next() 等方法。
  • 具体迭代器 (Concretelterator) 角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

迭代器模式案例实现

【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现

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
// 学生类
public class Student {
private String name;
private int age;

// 省略getter和setter方法

// 打印学生信息
public String toString() {
return "学生姓名:" + name + ",学生年龄:" + age;
}

// 有参构造
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 无参构造
public Student() {}
}

// 迭代器接口(抽象迭代器)
public interface StudentIterator {

// 判断是否还有元素
boolean hasNext();

// 获取下一个元素
Student next();
}

// 具体迭代器
public class StudentIteratorImpl implements StudentIterator {

private List<Student> list;
private int position = 0; // 用来记录遍历时的位置

public StudentIteratorImpl(List<Student> list) {
this.list = list;
}

public boolean hasNext() {
return position < list.size();
}

public Student next() {
// 从集合中获取指定位置的元素
Student currentStudent = list.get(position);
position++;
return currentStudent;
}
}

// 抽象聚合角色接口
public interface StudentAggregate {

// 添加学生
void addStudent(Student student);

// 删除学生
void removeStudent(Student student);

// 获取迭代器
StudentIterator getStudentIterator();
}

// 具体聚合角色
public class StudentAggregateImpl implements StudentAggregate {

private List<Student> list = new ArrayList<Student>();

public void addStudent(Student student) {
list.add(student);
}

public void removeStudent(Student student) {
list.remove(student);
}

// 获取迭代器对象
public StudentIteratorImpl getStudentIterator() {
return new StudentIteratorImpl(list);
}
}

// 测试类
public static void main(String[] args) {
// 创建聚合对象
StudentAggregate studentAggregate = new StudentAggregateImpl();
// 添加对象
studentAggregate.addStudent(new Student("张三", 18));
studentAggregate.addStudent(new Student("李四", 19));
studentAggregate.addStudent(new Student("王五", 20));
studentAggregate.addStudent(new Student("赵六", 21));

// 获取迭代器对象
StudentIterator studentIterator = studentAggregate.getStudentIterator();
// 遍历
while (studentIterator.hasNext()) {
Student student = studentIterator.next();
System.out.println(student.toString());
}
}

聚合对象用来存储、添加、删除对象以及获取迭代器对象
迭代器用来遍历

迭代器模式优缺点

优点:

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器子类以支持新的遍历方式。
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足”开闭原则”的要求。

缺点:

  • 增加了类的个数,这在一定程度上增加了系统的复杂性。

迭代器模式使用场景

  • 当需要为聚合对象提供多种遍历方式时。
  • 当需要为遍历不同的聚合结构提供一个统一的接口时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

迭代器模式在 JDK 源码中的解析

迭代器模式在 JAVA 很多集合类中都被广泛应用

1
2
3
4
5
List<String> list = new ArrayList<>();
Iterrator<String> iterator = list.iterator(); // list.iterator() 返回的是Iterator接口的子实现类对象
while (iterator.hasNext()) {
System.out.println(iterator.next());
}

以 ArrayList 举例说明

  • List:抽象聚合类
  • ArrayList:具体聚合类
  • Iterator:抽象迭代器
  • List.iterator():返回的是 Iterator 接口的子实现类对象

访问者模式

定义:
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

访问者模式结构

  • 抽象访问者 (visitor) 角色:定义了对每一个元素 (Element) 访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element 的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者 (ConcreteVisitor) 角色:给出对每一个元素类访问时所产生的具体行为··抽象元素 (Element) 角色:定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素 (concreteE1ement) 角色:提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构 (Object structure) 角色:定义当中所提到的对象结构,对象结构是一个抽象表述具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素,并且可以迭代这些元素,供访问者访问。

访问者模式案例实现

【例】给宠物喂食

给宠物猫宠物狗喂食,主人可以喂,其他人也可以喂

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物猫、宠物狗
  • 结构对象角色:主人家
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
// 抽象访问者
public interface Person {

// 喂食宠物猫
void feed(Cat cat);
// 喂食宠物狗
void feed(Dog dog);
}

// 抽象元素角色
public interface Animal {

// 接受访问者访问的功能
void accept(Person person);
}

// 猫类(具体元素角色)
public class Cat implements Animal {

public void accept(Person person) {
person.feed(this);// 访问者给宠物猫喂食
System.out.println("猫吃饭");
}
}

// 狗类(具体元素角色)
public class Dog implements Animal {
public void accept(Person person) {
person.feed(this);// 访问者给宠物狗喂食
System.out.println("狗吃饭");
}
}

// 自己(具体访问者角色)
public class Owner implements Person {

public void feed(Cat cat) {
System.out.println("主人喂食宠物猫");
}

public void feed(Dog dog) {
System.out.println("主人喂食宠物狗");
}
}

// 其他人
public class Other implements Person {
public void feed(Cat cat) {
System.out.println("其他人喂食宠物猫");
}
public void feed(Dog dog) {
System.out.println("其他人喂食宠物狗");
}
}

// 家(对象结构类)
public class Home {

// 声明一个集合对象,用来存储元素对象
private List<Animal> animalList = new ArrayList<>();

// 添加元素功能
public void add(Animal animal) {
animalList.add(animal);
}

public void action(Person person) {
// 遍历集合,获取每一个元素,让访问者访问每一个元素
for (Animal animal : animalList) {
animal.accept(person);
}
}
}

// 测试类
public static void main(String[] args) {
// 创建Home对象
Home home = new Home();
// 添加元素到Home对象中
home.add(new Cat());
home.add(new Dog());

// 创建主人对象
Owner owner = new Owner();
// 让主人喂食所有宠物
home.action(owner);
}

访问者模式优缺点

优点:

  • 扩展性好,在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能
  • 复用性好,通过访问者来定义整个对象结构通用的功能,从而提高复用程度
  • 分离无关行为,通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一

缺点:

  • 对象结构变化很困难
  • 在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了”开闭原则“。
  • 违反了依赖倒置原则,访问者模式依赖了具体类,而没有依赖抽象类·

访问者模式使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构

备忘录模式

定义:
又叫快照模式,在不破坏封装性的前提下,获取一个对象的内部状态,并在该对象之外保存这个状态,以便以后当前需要时能将该对象恢复到原先保存的状态

备忘录模式结构

  • 发起人 (Originator) 角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录 (Memento) 角色:负责存储发起人的内部状态,在需要的时候提亻共这些内部状态给发起人。
  • 管理者 (Caretaker) 角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

备忘录有两个等效的接口:

  • 窄接口:管理者 (Caretaker) 对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口 (narror lnterface) ,这个窄接口只允许把把备忘录对象传给其他的对象。
  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide lnterface) ,这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

备忘录模式案例实现

【例】游戏打 BOSS
游戏中的某个场景,游戏角色有生命力、攻击力、防御力等数据,在打 BOSS 前和后一定会不一样的,我们允许玩家如果感觉与 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
// 游戏角色类(发起人角色)
public class GameRole {

private int vit; // 生命力
private int atk; // 攻击力
private int def; // 防御力

// 省略对应getter和setter方法

// 初始化内部状态
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 memento) {
// 将备忘录里面的属性赋给当前角色
this.vit = memento.getVit();
this.atk = memento.getAtk();
this.def = memento.getDef();
}

// 展示状态
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}
}

// 备忘录类(备忘录角色)
public class RoleStateMemento {

private int vit; // 生命力
private int atk; // 攻击力
private int def; // 防御力

// 省略对应getter和setter方法

public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}

// 无参构造,防止报错
public int RoleStateMemento() {}
}

// 备忘录管理对象管理角色
public class RoleStateCaretaker {

// 声明一个备忘录对象
private RoleStateMemento memento;

// getter 和 setter方法
public RoleStateMemento getMemento() {
return memento;
}
public void setMemento(RoleStateMemento memento) {
this.memento = memento;
}
}

// 测试类
public static void main(String[] args) {
// 大战 Boss 前
System.out.println("--------大战Boss前--------");
// 创建游戏角色对象
GameRole gameRole = new GameRole();
gameRole.initState();// 初始化状态
gameRole.stateDisplay();

// 备份游戏角色的状态
// 创建管理者对象
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());

// 大战 Boss 后
System.out.println("--------大战Boss后--------");
// 损耗严重
gameRole.fight();
gameRole.stateDisplay();

// 恢复之前的状态
System.out.println("--------恢复之前状态--------");
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}

“黑箱”备忘录模式

备忘录黑色对发起人对向提供一个宽接口,而为其他对象提供一个窄接口。在 Java 中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类

窄接口 Mememento,这是一个标识接口,因此没有定义出任何方法

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
// 备忘录接口,对外提供窄接口
public interface Memento {

}

// 游戏角色类(发起人角色)
public class GameRole {

private int vit; // 生命力
private int atk; // 攻击力
private int def; // 防御力

// 省略对应getter和setter方法

// 初始化内部状态
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 Memento saveState() {
return new RoleStateMemento(vit, atk, def);
}

// 恢复角色状态
public void recoverState(Memento memento) {
// 强制类型转换
RoleStateMemento memento = (RoleStateMemento) memento;
// 将备忘录里面的属性赋给当前角色
this.vit = memento.getVit();
this.atk = memento.getAtk();
this.def = memento.getDef();
}

// 展示状态
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}

private class RoleStateMemento implements Mememento {
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;
}

// 省略对应getter和setter方法
}
}

// 备忘录管理对象管理角色
public class RoleStateCaretaker {

// 声明一个备忘录对象
private Memento memento;

// getter 和 setter方法
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}

// 测试类
public static void main(String[] args) {
// 大战 Boss 前
System.out.println("--------大战Boss前--------");
// 创建游戏角色对象
GameRole gameRole = new GameRole();
gameRole.initState();// 初始化状态
gameRole.stateDisplay();

// 备份游戏角色的状态
// 创建管理者对象
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());

// 大战 Boss 后
System.out.println("--------大战Boss后--------");
// 损耗严重
gameRole.fight();
gameRole.stateDisplay();

// 恢复之前的状态
System.out.println("--------恢复之前状态--------");
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}

就是游戏角色类里有一个私有内部类实现了备忘录接口,然后管理者负责存储这个私有内部类的接口引用(窄接口)
游戏角色要恢复状态时,管理者将引用传递进来,因为是私有内部类,所以只有游戏角色类内部能将接口引用向下转型成实现类(也就是私有内部类)然后赋值

备忘录模式优缺点

优点:

  • 提供了一种可以恢复状态的机制,当用户需要时能够比较方便的将数据恢复到某个历史状态
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息
  • 简化了发起人。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则

缺点:

  • 资源消耗大,如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源

备忘录模式使用场景

  • 需要保存与恢复数据的场景,如玩游戏时中间结果的存档功能
  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop、idea 等软件在编辑时按 Ctrl+Z 时的撤销操作,还有数据库中事务操作

解释器模式

定义:
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

解释器模式结构

  • 抽象表达式 (Abstract Expression) 角色:定义解释器的接口,约定解释器的解释操作,主要含解释方法 interpret()。
  • 终结符表达式 (Terminal Expression) 角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应
  • 非终结符表达式 (Nonterminal Expression) 角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的条规则都对应于一个非终结符表达式。
  • 环境 (Context) 角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可从这里获取这些值。
  • 客户端 (Client) :主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

解释器模式案例实现

【例】设计实现加减法的软件

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
// 抽象表达式类
public abstract class AbstractExpression {

public abstract int interpret(Context context);
}

// 环境角色类
public class Context {

// 定义一个map集合,用来存储变量及对应的值
private Map<Variable, Integer> map = new HashMap<>();

// 添加变量的功能
public void assign(Variable var, int value) {
map.put(var, value);
}

// 根据变量获取对应的值
public int getValue(Variable var) {
return map.get(var);
}
}

// 封装变量的类
public class Variable extends AbstractExpression {

// 声明存储变量名的成员变量
private String name;

public Variable(String name) {
this.name = name;
}

public int interpret(Context context) {
// 直接返回变量的值
return context.getValue(this);
}

@Override
public String toString() {
return name;
}
}

// 加法表达式类
public class Plus extends AbstractExpression {

// + 左边的表达式
private AbstractExpression left;
// + 右边的表达式
private AbstractExpression right;

public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

public int interpret(Context context) {
// 将左边表达式的结果和右边表达式的结果进行相加
return left.interpret(context) + right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}

// 减法表达式类
public class Minus extends AbstractExpression {

// - 左边的表达式
private AbstractExpression left;
// - 右边的表达式
private AbstractExpression right;

public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

public int interpret(Context context) {
// 将左边表达式的结果和右边表达式的结果进行相加
return left.interpret(context) - right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}

// 测试类
public static void main(String[] args) {
// 创建环境对象
Context context = new Context();

// 创建多个变量对象
Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");

// 将变量存储到环境对象中
context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);

// 获取抽象语法树 a - ((b - c) + d)
AbstractExpression expression = new Minus(a, new Plus(new Minus(b, c), d));

// 解释(计算)
int result = expression.interpret(context);

System.out.println(expression + " = " + result);
}

表达式可以是值也可以是加减法表达式,只要实现了 AbstractExpression 接口就行
通过接口里面的 interpret 方法进行解释,值就是返回值,而加法表达式返回左右连个表达式翻译后的结果

解释器模式优缺点

优点:

  • 易于改变和扩展文法。由于在解释器式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
  • 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
  • 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合”开闭原则”。

缺点:

  • 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
  • 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。

解释器模式应用场景

  • 当语言的文法较为简单,且执行效率不是关键问题时
  • 当问题重复出现,且可以用一种简单的语言来表示时
  • 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候

设计模式
http://www.981928.xyz/2025/12/08/设计模式/
作者
981928
发布于
2025年12月8日
许可协议