设计模式

设计模式是什么?

设计模式是我们对问题所提出的解决方案,就像一个个蓝图,通过对问题的一些综合考虑,采用最合适的设计方案来解决问题。就像一个工具箱,我们要看具体的情况,来决定使用哪把工具。那么设计模式是如何诞生的呢,设计模式最开始也是一个解决方案,只不过这个方案在各种项目中得到了验证。最终得到认可,是前辈们一个个试验,一步一个坑踩过来,最终被后人们整理,收纳,所归类的出的一种新领域

设计模式的优点

  • 提高我们的思维能力和设计能力
  • 使程序的设计变得标准化、流程化,增强开发效率
  • 对代码来说,提高了可读性和复用性以及可扩展性

设计模式的六大原则

  1. 单一职责: 一个类应该只有一个会引起它变化的原因,也就是一个类只负责一个职责
  2. 开闭原则: 对扩展开放,对修改关闭
  3. 里氏代换原则: 子类应该可以替换父类对象,并保持逻辑不变
  4. 依赖倒转原则: 抽象不依赖细节,细节依赖于抽象。也就是对接口编程,不要直接使用实现类
  5. 接口隔离原则: 不应该强迫一个类实现它不需要的方法,而是使用多个精细化的接口
  6. 迪米特法则: 一个实体类尽量少与其他实体类有相互作用

设计模式的分类

创建型模式:通过提供创建对象的机制,增加已有代码的灵活性和可复用性

创建型有种模式:工厂方法、抽象工厂、建造者、原型、单例

结构型模式:如何将对象和类组装成较大的结构,同时保持结构的灵活和高效

结构型有种模式:适配器、桥接、组合、装饰、外观、享元、代理

行为型模式:负责对象间的高效沟通和职责委派

行为型有十一种模式:责任链、命令、迭代器、解释器、中介者、备忘录、观察者、状态、策略、模板方法、访问者

现在我们对设计模式有了初步认识,下面我们对每一种设计模式进行详细了解,并逐一举例

PS:本文缓慢更新(当你看到这句话的时候表明没有更新完),更新顺序会参考我们平时的使用频率以及重要程度随缘更新 项目源代码:sora33-设计模式

创建型

单例模式(Singleton)

单例模式是设计模式中最简单也是最好理解的一个模式,它确保一个类只有一个实例,并且这个实例对外开放,可以被其他类调用使用。单例的核心在于如何创建单例,创建单例的方式常用的有4种,这里因为篇幅原因仅使用静态内部类的方式来实现单例,因为它保证了懒加载并且避免了同步开销。想知道其他3种创建方式和进一步了解单例的可以查看这篇文章:手写单例模式以及保证安全性

首先我们先来看一下使用静态内部类实现单例的代码:

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 class Music {

// 私有化构造方法,防止外部实例化
private Music() {}

private static class Handler {
private static final Music INSTANCE = new Music();
}

public static Music getInstance() {
return Handler.INSTANCE;
}
}

// 客户端代码
public class Main {
public static void main(String[] args) {
Music music1 = Music.getInstance();
Music music2 = Music.getInstance();
System.out.println(music1);
System.out.println(music2);
}
}

// 控制台输出
-- com.sora.Music@72ea2f77
-- com.sora.Music@72ea2f77

我们以音乐类举例,需要私有化构造方法,防止外部创建对象。继续在内部创建一个静态内部类,我们都知道静态成员(如静态变量和静态方法)随着类的加载而加载,而静态内部类的加载是延迟的,只有在被显式调用时才会加载。因此,静态内部类不仅实现了懒加载,而且避免了同步开销,同时又保证了线程安全。

这里继续说点跟单例相关的几个问题,感兴趣的可以看看:

在Spring框架中,Bean默认是单例的,这是基于Spring容器级别的单例,它的生命周期和Spring容器生命周期绑定,而我们通过Java创建的单例对象则是JVM级别,会在整个应用程序的JVM生命周期内存在。

Spring中,我们可以通过**@Scope(“Prototype”)**将默认的单例创建改为原型创建。还有request、session等,这些都用的不多

使用单例对象时,我们的重点肯定是线程安全性。假设我们有一个类负责累加一个数并且返回,如果此时多个线程都调用了这个类,而这个类又是由Spring管理且是单例的,那么Spring如何保证线程安全呢?答案是Spring并不会保证多线程下的线程安全。因此多线程环境下,如果该单例对象存在可变状态,就需要我们手动处理线程安全问题,常见的几种方法如下:

1. 无状态设计:无状态表示该对象不包含任何可变的实例对象,每次调用方法时,所有操作都依赖参数进行处理
2. 使用局部变量:如果我们要保存一个临时结果,不要使用全局变量!使用**局部变量**!因为局部变量是线程私有的,线程之间不会共享。
3. ThreadLocal:ThreadLocal为每一个线程都提供了一个副本,实现了线程间的隔离
4. 如果这个类必须要使用全局变量保存一些数据,那么必须使用线程安全的类,例如**ConcurrentHashMap**

适用场景:缓存类的实现、线程池管理、全局ID生成器、数据库连接池等等

工厂方法(Factory Method)

在讲解工厂方法之前,我们先来了解一下简单工厂,简单工厂是由一个接口、多个接口实现类以及一个工厂类组成,我们以游戏平台举例,平台就是一个接口,而Steam、Epic等就是具体的实现类,工厂类则是负责创建实现类的地方。我们具体来看代码:

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 Platform {
void print();
}

// 第一个实现类
public class Epic implements Platform {
@Override
public void print() {
System.out.println("我是Epic平台");
}
}

// 第二个实现类
public class Origin implements Platform {
@Override
public void print() {
System.out.println("我是橘子平台");
}
}

// 第三个实现类
public class Steam implements Platform {
@Override
public void print() {
System.out.println("我是Steam平台");
}
}

// 工厂类
public class GamePlatform {
public Platform getPlatform(String platform) {
if ("Epic".equalsIgnoreCase(platform)) {
return new Epic();
} else if ("Steam".equalsIgnoreCase(platform)) {
return new Steam();
} else if ("Origin".equalsIgnoreCase(platform)) {
return new Origin();
}
return null;
}
}

// 使用简单工厂
public class Main {
public static void main(String[] args) {
String param = "steam";
// 创建工厂类实例
GamePlatform gamePlatform = new GamePlatform();

Platform steam = gamePlatform.getPlatform(param);
steam.print();
param = "epic";
Platform epic = gamePlatform.getPlatform(param);
epic.print();
}
}

// 控制台输出
-- 我是Steam平台
-- 我是Epic平台

如果我们不使用简单工厂来完成这个案例,我们会直接new Steam(), new Epic()来完成对象的创建,那我们为什么要把创建对象的任务交给工厂类呢?因为简单工厂提供了解耦、维护性和灵活性。如果我们不使用简单工厂,那么当一个对象的构造逻辑发生改变,例如需要传入一些固定参数,我们需要在每一个用到的地方去修改并测试,但如果是简单工厂,我们直接在工厂类内进行修改即可,这便是解耦和维护。灵活性则表现在扩展,如果后续我们需要新加平台,没有使用简单工厂的情况下,我们需要去每一个地方去手动新增一个平台,而简单工厂只需要新增一个实现类,随后在工厂类内新加一个条件即可,通过参数来控制工厂类获取的具体实现对象。但这样破坏了开闭原则(对扩展开放,对修改关闭),所以工厂方法就这样出来了。

工厂方法在简单工厂的基础上做了一些修改,将原本一个工厂拆开了,每个对象都有属于自己的工厂。工厂方法共有四个角色:

  1. 产品接口:定义所有产品的行为
  2. 产品实现类:实现产品接口,负责具体的产品实现
  3. 抽象工厂:所有工厂实现类的父类,声明返回产品对象的工厂方法,返回值必须是产品接口
  4. 工厂实现类:继承抽象工厂类,重写返回产品对象工厂的方法,使其返回不同类型的产品

下面我们以创建不同的支付对象举例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 支付接口
public interface Pay {
void pay();
}

// 支付宝支付类
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("用户使用AliPay支付,开始执行具体逻辑……");
}
}

// 微信支付类
public class WechatPay implements Pay {
@Override
public void pay() {
System.out.println("用户使用微信支付,开始执行具体逻辑……");
}
}

到目前为止都是跟简单工厂一样,工厂部分可以看到阿里云和微信都有对应的工厂且继承了一个抽象类,这个抽象类就是负责声明一个创建工厂的方法。具体工厂代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 支付工厂 所以支付对象工厂都要继承 相当于父类
public abstract class PayFactory {
public abstract Pay getPayType();
}

// 阿里云支付工厂
public class AliPayFactory extends PayFactory {
@Override
public Pay getPayType() {
return new AliPay();
}
}

// 微信支付工厂
public class WechatPayFactory extends PayFactory {
@Override
public Pay getPayType() {
return new WechatPay();
}
}

之后通过参数来控制工厂的创建并调用支付方法,以后要新增支付方法,只需要实现一个新的支付类,创建一个新的支付工厂即可,解决了简单工厂的弊端(新增内容需要修改工厂类),符合开闭原则。下面是具体的客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
// 支付参数
String param = "ali";

PayFactory factory = null;
if ("wechat".equals(param)) {
factory = new WechatPayFactory();
} else if ("ali".equals(param)) {
factory = new AliPayFactory();
}

Pay payType = factory.getPayType();
payType.pay();
}
}

// 控制台输出
-- 用户使用AliPay支付,开始执行具体逻辑……

适用场景:开发与线上环境的快速切换,数据库连接类型,多类型文件生成与读取等等

建造者(Builder)

建造者模式更注重构建对象的这个过程,通过分步创建一个复杂的对象,将产品的创建与产品的本身进行分离,构建的过程就可以获得不同的对象。在代码中使用链式调用,方便的同时增加了可读性。我们一般通过建造者模式来创建那些有非传参数的对象或者参数过多的对象。

相信大家肯定见过类似于下面这样的代码,一个类的构造函数有多个可选参数,为了保证正常调用会写多个方法来进行重载,在这种情况下,我们就可以使用建造者的设计模式来完成。

1
2
3
4
public class Computer {
Computer(String cpu) { …… }
Computer(String cpu, String gpu) { …… }
Computer(String cpu, String gpu, String board) { …… }

这里我们假设cpu、gpu、board和arm是必传参数。使用建造者模式:

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
public class Computer {
private String cpu;
private String gpu;
private String board;
private String arm;
private String ssd;
private String power;

public Computer(ComputerBuilder computerBuilder) {
this.cpu = computerBuilder.cpu;
this.gpu = computerBuilder.gpu;
this.board = computerBuilder.board;
this.arm = computerBuilder.arm;
this.ssd = computerBuilder.ssd;
this.power = computerBuilder.power;
}

public static class ComputerBuilder{
private final String cpu;
private final String gpu;
private final String board;
private final String arm;
private String ssd;
private String power;

public ComputerBuilder(String cpu, String gpu, String board, String arm) {
this.cpu = cpu;
this.gpu = gpu;
this.board = board;
this.arm = arm;
}

public ComputerBuilder setSsd(String ssd) {
this.ssd = ssd;
return this;
}

public ComputerBuilder setPower(String power) {
this.power = power;
return this;
}

public Computer build() {
return new Computer(this);
}
}

@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", gpu='" + gpu + '\'' +
", board='" + board + '\'' +
", arm='" + arm + '\'' +
", ssd='" + ssd + '\'' +
", power='" + power + '\'' +
'}';
}
}

// 客户端代码
public class Main {
public static void main(String[] args) {
Computer computerBuilder = new Computer
.ComputerBuilder("7800x", "7900xtx", "B660M", "32G")
.build();
Computer computerBuilder2 = new Computer
.ComputerBuilder("7800x", "7900xtx", "B660M", "32G")
.setSsd("2T")
.setPower("1000w")
.build();
System.out.println(computerBuilder);
System.out.println(computerBuilder2);
}
}

// 控制台输出
-- Computer{cpu='7800x', gpu='7900xtx', board='B660M', arm='32G', ssd='null', power='null'}
-- Computer{cpu='7800x', gpu='7900xtx', board='B660M', arm='32G', ssd='2T', power='1000w'}

电脑类的所有属性都要经过ComputerBuilder来完成,并且在内部配置了必选参数和可选参数,必选参数保证了对象创建的完整性同时避免了构造函数爆炸的情况,可选参数带来了扩展性与灵活性,可以随时新增字段而不影响现有代码,链式调用又直观体现了该对象的整体结构

适用场景:复杂对象的创建,数据库连接参数配置,http连接参数的配置等等

抽象工厂(Abstract Factory)

在讲抽象工厂前强烈建议先把工厂方法搞明白,会好理解很多。下面我们开始学习抽象工厂。

学习抽象工厂前我们要先了解2个概念,产品族产品等级结构

产品族:是指一组相关联的产品,它们一起协作或具有某种共同属性。以麦当劳举例,麦当劳生产的汉堡、薯条、可乐可以被看作一个产品族,所有这些产品都属于麦当劳的风格。

产品等级结构:指产品的不同种类或类型的层次关系。比如,汉堡、薯条、饮料这些都是“快餐产品”的不同类型,它们代表了产品的不同等级结构。

下面继续说说抽象工厂的角色,总体上,抽象工厂的角色与工厂方法类似,但具体的职责会有点不同:

  • 工厂方法侧重为单个产品提供接口,每个工厂只会生产一个产品
  • 抽象工厂则为产品族提供接口,每个工厂可以生产同一产品族下的所有产品类型

抽象工厂中主要有以下角色:

  1. 产品接口:为每种产品声明接口
  2. 产品实现类:实现产品接口,负责具体的产品实现
  3. 抽象工厂:所有工厂实现类的父类,声明创建了一系列相关产品的接口
  4. 工厂实现类:继承抽象工厂类,负责创建具体的产品

抽象工厂模式的核心思想是为产品族提供一个创建接口。一个工厂不但负责创建一类产品(比如汉堡),还要为同一个产品族中的其他相关产品提供创建方式(如薯条、饮料)。通过抽象工厂模式,我们可以根据具体的需求生成同一产品族中的相关产品,而不必关心每个产品的具体实现细节,同时也隔离了具体类,客户端只通过接口来与产品交互,不直接依赖具体产品类,做到了解耦的同时增强了扩展性。但缺点同样是因为高扩展性,我们新增一个产品等级结构时,需要修改抽象工厂类以及各个工厂的具体实现。

下面以游戏开发商举例,每个游戏开发商都会开发不同类型的游戏,比如 RPG(角色扮演)、ACT(动作)等,这些不同的游戏类型属于产品等级结构。而多个游戏开发商,如卡普空、索尼等,都能够开发这些类型的游戏,这些开发商则可以看作是产品族的不同实现。我们以卡普空公司为例,创建这两种类型的游戏:

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
// 动作类游戏接口 (字段:游戏名、游戏详情、上手难度)
public interface ACT {
void name();
void details();
void level();
}

// 角色扮演类游戏接口 (字段:游戏名、游戏详情)
public interface RPG {
void name();
void details();
}

// 卡普空的动作类游戏具体实现
public class MonsterHunter implements ACT {
@Override
public void name() {
System.out.println("游戏:怪物猎人");
}

@Override
public void details() {
System.out.println("游戏介绍:在怪物猎人的世界中,你将扮演一名猎人在新大陆上……");
}

@Override
public void level() {
System.out.println("上手难度:★★★★☆");
}
}

// 卡普空的角色扮演类游戏具体实现
public class ResidentEvil implements RPG {
@Override
public void name() {
System.out.println("游戏:生化危机");
}

@Override
public void details() {
System.out.println("游戏介绍:里昂接到总统特令,负责去孤岛上寻找阿什莉并将其救出,在岛上……");
}
}

// 抽象工厂类 声明所有的产品等级结构
public abstract class GameFactory {
public abstract ACT createACTGame();
public abstract RPG createRPGGame();
}

// 创建卡普空工厂,返回所有产品等级结构
public class CapComFactory extends GameFactory {
@Override
public ACT createACTGame() {
return new MonsterHunter();
}

@Override
public RPG createRPGGame() {
return new ResidentEvil();
}
}

// 客户端
public class Main {
public static void main(String[] args) {
CapComFactory capComFactory = new CapComFactory();
System.out.println("创建卡普空下的RPG游戏:");
RPG capComRPGGame = capComFactory.createRPGGame();
capComRPGGame.name();
capComRPGGame.details();

System.out.println("\n创建卡普空下的ACT游戏:");
ACT capComACTGame = capComFactory.createACTGame();
capComACTGame.name();
capComACTGame.details();
capComACTGame.level();
}
}

// 控制台输出
-- 创建卡普空下的RPG游戏:
-- 游戏:生化危机
-- 游戏介绍:里昂接到总统特令,负责去孤岛上寻找阿什莉并将其救出,在岛上……

-- 创建卡普空下的ACT游戏:
-- 游戏:怪物猎人
-- 游戏介绍:在怪物猎人的世界中,你将扮演一名猎人在新大陆上……
-- 上手难度:★★★★☆

在这个例子中,卡普空这个产品族为我们提供了两个产品等级结构的游戏:一个动作类(ACT)和一个角色扮演类(RPG)。通过抽象工厂,我们实现了不同类型游戏的创建。同时我们可以很容易地扩展另一个游戏开发商产品族:

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
// 索尼的动作类游戏具体实现
public class GodOfWar implements ACT {
@Override
public void name() {
System.out.println("游戏:战神");
}

@Override
public void details() {
System.out.println("游戏介绍:奎托斯与儿子成功将母亲的遗骨撒落在高山,但门外的男人究竟是……");
}

@Override
public void level() {
System.out.println("上手难度:★★☆☆☆");
}
}

// 索尼的角色扮演类游戏具体实现
public class TheLastOfUs implements RPG {
@Override
public void name() {
System.out.println("游戏:最后生还者");
}

@Override
public void details() {
System.out.println("开发商:人类因现代传染病而面临绝种危机,当环境从废墟的都市再度自然化时……");
}
}

// 创建索尼工厂,返回所有产品等级结构
public class SonyFactory extends GameFactory {
@Override
public ACT createACTGame() {
return new GodOfWar();
}

@Override
public RPG createRPGGame() {
return new TheLastOfUs();
}
}

// 客户端
public class Main {
public static void main(String[] args) {
SonyFactory sonyFactory = new SonyFactory();
System.out.println("\n创建索尼下的RPG游戏:");
RPG sonyRPGGame = sonyFactory.createRPGGame();
sonyRPGGame.name();
sonyRPGGame.details();

System.out.println("\n创建索尼下的ACT游戏:");
ACT sonyACTGame = sonyFactory.createACTGame();
sonyACTGame.name();
sonyACTGame.details();
sonyACTGame.level();
}
}

// 控制台输出
-- 创建索尼下的RPG游戏:
-- 游戏:最后生还者
-- 开发商:人类因现代传染病而面临绝种危机,当环境从废墟的都市再度自然化时……

-- 创建索尼下的ACT游戏:
-- 游戏:战神
-- 游戏介绍:奎托斯与儿子成功将母亲的遗骨撒落在高山,但门外的男人究竟是……
-- 上手难度:★★☆☆☆

可以看到,我们只需要在产品接口的基础上,完成对索尼产品的具体实现,并新增一个索尼工厂,即可获得索尼产品族下的所有产品。也就是对于扩展性而言,新增一个产品族非常简单,没有破坏开闭原则,而当新增产品等级结构时,需要修改现有的抽象工厂类以及工厂类的实现会破坏开闭原则。

适用场景:系统需要与多个产品族进行交互(例如windows与mac、跨平台应用)

原型模式(Prototype)

原型模式的核心是使用现有对象作为模板,通过克隆的方式创建新的对象。一般当创建对象的开销比较大或者对象结构比较复杂时,会使用原型模式,通过clone方法来实现对象的复制,同样我们也可以自己选择实现方式是浅拷贝还是深拷贝,下面是原型模式的角色:

  1. 原型接口:定义克隆方法,所有具体原型类都要实现这个接口
  2. 具体原型类:实现原型接口,具体定义了如何复制自身

这里再提一嘴深拷贝和浅拷贝:

  • 深拷贝:复制对象以及引用类型的数据,克隆出的对象与原对象完全独立,互不影响

  • 浅拷贝:只复制对象的基本数据类型,引用数据类型的引用,克隆出的对象和原对象共享同一个引用地址

如果我们使用浅拷贝,修改任一对象里的对象或集合,另一个也会发生改变,因为这个对象都指向同一个地址。

PS:String数据类型也是引用类型的数据,但因为底层是不可变的数组,所以我们修改其中一个对象另一个并不会发生改变。所以对于String类型,我们可以放心使用,因为任何修改本质上都是创建了新的对象

下面我们来看原型模式的示例代码:

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
// 原型接口
public interface Prototype {
Prototype clone();
}

// 原型类
public class Album implements Prototype {
public String name;
public String sings;
public ArrayList<String> list = new ArrayList<>();

public Album() {
}

public List getList() {
return list;
}

@Override
public String toString() {
return "Album{" +
"name='" + name + '\'' +
", sings='" + sings + '\'' +
", list=" + list +
'}';
}

public Album(String name, String sings, ArrayList<String> list) {
this.name = name;
this.sings = sings;
this.list = list;
}

// 克隆实现(浅拷贝)
@Override
public Prototype clone() {
return new Album(this.name, this.sings, this.list);
}
}

原型模式的使用很简单,只需要一个原型接口即可,内部包含一个clone方法,具体实现深拷贝还是浅拷贝则由原型类决定。这里我克隆Album类,实现原型接口后,在clone方法内创建一个新对象并填充数据返回,这样的方式是浅拷贝。我们来看一下浅拷贝的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 客户端示例
public class Main {
public static void main(String[] args) {
Album album = new Album("透明な君を掬う", "nayuta", new ArrayList(){{
add("透明な君");
add("誰そ彼");
add("追慕");
add("僕が見つけた綻び");
add("灰燼");
add("彼は誰");
add("君を掬う");
}});
System.out.println("原对象::" + album);
Album clone = (Album) album.clone();
System.out.println("克隆对象:" + clone);
System.out.println("修改克隆对象,删掉最后一首歌曲……");
clone.getList().removeLast();
System.out.println("原对象:" + album);
System.out.println("克隆对象:" + clone);
}
}

// 控制台输出
-- 原对象::Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 修改克隆对象,删掉最后一首歌曲……
-- 原对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰]}

我们删除克隆对象集合的一个数据后,原对象也发生了相应的变更,因为他们的使用的list实际上是一个。如果要实现深拷贝,需要将原型类中的clone方法需要改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这里我通过序列化和反序列化的方式实现了深拷贝,但注意对象必须实现 Serializable 接口
// 克隆实现(深拷贝)
public Prototype clone() {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Prototype) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}

改为深拷贝后,只有克隆对象的集合数据发生了改变,原对象的集合并没有收到影响,这就是深拷贝和浅拷贝的区别

1
2
3
4
5
-- 原对象::Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 修改克隆对象,删掉最后一首歌曲……
-- 原对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰, 君を掬う]}
-- 克隆对象:Album{name='透明な君を掬う', sings='nayuta', list=[透明な君, 誰そ彼, 追慕, 僕が見つけた綻び, 灰燼, 彼は誰]}

适用场景:需要大量创建的对象、缓存对象的创建、复杂商品/报表模板的生成等等

结构型

适配器(Adapter)

适配器模式用于将一个类的接口转为期望的一个接口,来使原本不兼容的接口可以正常工作,适配器的核心是为现有类提供一个兼容的接口,解决接口不兼容的问题,而不需要修改原有代码。

适配器主要有三个角色,分别是目标接口、被适配类、适配器类

  1. 目标接口:客户端所期望的接口,也就是客户想要的数据类型
  2. 被适配类:需要被适配的类,且这个类目前无法直接转为目标接口
  3. 适配器类:实现目标接口,将被适配类转为目标接口,从而让客户端和被适配类完美工作

下面我以货币间转换来举例,我们现在有2个货币类,日元类和人民币类,日元类将输入的金额转为美元、人民币类则直接返回输入的金额。如果需要将日元转为人民币,那么目标接口就是人民币类(因为我们的目标是返回人民币)、被适配类是日元类(目前日元功能是转为美元,不符合我们需求,所以需要被适配)适配器类(进行具体转换的类)就是我们新建的类。具体代码如下:

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 class Cny {

private Double rmb;

public Cny() {
}

public Cny(Double rmb) {
this.rmb = rmb;
}

public Double getRmb() {
return rmb;
}
}

// 日元类
public class Jpy {

private Double jpy;

public Jpy() {
}

public Jpy(Double jpy) {
this.jpy = jpy;
}

public Double getJpy() {
return jpy / 144.33;
}
}

// 适配器类
public class JpyToCnyAdapter extends Cny{

private Jpy jpy;

public JpyToCnyAdapter(Jpy jpy) {
this.jpy = jpy;
}

@Override
public Double getRmb() {
Double result = jpy.getJpy() * 7.05;
return result;
}
}

// 客户端
public class Main {
public static void main(String[] args) {
Double price = 100.00;
Cny cny = new Cny(price);
System.out.println("人民币类输入金额:" + price + " ,返回:" + cny.getRmb());

Jpy jpy = new Jpy(100.00);
System.out.println("日元类输入金额:" + price + " ,返回:" + jpy.getJpy());

JpyToCnyAdapter cnyAdapter = new JpyToCnyAdapter(jpy);
System.out.println("适配器类输入金额:" + price + " ,返回:" + cnyAdapter.getRmb());
}
}

// 控制台输出
-- 人民币类输入金额:100.0 ,返回:100.0
-- 日元类输入金额:100.0 ,返回:0.6928566479595372
-- 适配器类输入金额:100.0 ,返回:4.884639368114737

对于适配器类,我们一般记住,实现/继承的是目标接口、将被适配类引入并获取结果对其转换。

下面再来看一个例子,new Thread()接受的参数是Runnable类型,那如何支持Callable类型的任务呢,同样新建一个适配器接口即可。

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
// Callable接口的任务 不可以作为new Thread的参数
public class SimpleCallable implements Callable {
@Override
public Object call() throws Exception {
System.out.println("callable执行成功");
return true;
}
}

// 适配器类,实现Runnable接口,并将callable的任务引入,在run方法内执行
public class RunnableAdapter implements Runnable{

private Callable callable;

public RunnableAdapter(Callable myCallable) {
this.callable = myCallable;
}

@Override
public void run() {
try {
callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

// 客户端代码
public class Main {
public static void main(String[] args) {
Callable callable = new SimpleCallable();
// callable不可以作为Thread的参数
// Thread thread = new Thread(callable);

// 通过适配器将其转为Runnable
RunnableAdapter adapter = new RunnableAdapter(callable);
Thread thread = new Thread(adapter);
thread.start();
}
}

我们的目标是将Callable转为Runnable,所以目标接口是Runnable,将被适配的类引入,进行逻辑重写,这里我们仅仅调用call方法即可。随后Callable类型的接口,经过适配器转换就可以变为Runnable类型的接口。

这就是适配器模式,适配器在我们的代码中有很多表现形式,这里也仅仅是冰山一角,但其核心思想是将不兼容的对象变为兼容的对象。

适用场景:第三方库的兼容、多格式文件转为统一格式、多日志集成等等

外观(Facade)

外观模式也叫门面模式,通过提供一个简单的接口,将复杂的子系统包装起来,简化了客户端与复杂系统之间的交互,隐藏了内部细节。外观模式有2个角色,分别是外观类和子系统类,外观类负责提供统一的外部接口调用,调用各个子系统的功能。子系统类则是负责实现系统的具体功能,被外观类调用。下面以用户查看订单为例完成一个外观模式:

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 Order {
void logic();
}

// 获取订单编号类
public class OrderNumber implements Order {
@Override
public void logic() {
System.out.println("订单编号:1097361294729");
}
}

// 获取订单价格类
public class OrderPrice implements Order {
@Override
public void logic() {
System.out.println("订单金额:1020元");
}
}

// 获取订单名称类
public class OrderShop implements Order {
@Override
public void logic() {
System.out.println("订单商品名:TouHou Remilia card");
}
}

// 外观类
public class FacadeOrder {

private OrderNumber orderNumber;
private OrderPrice orderPrice;
private OrderShop orderShop;

public FacadeOrder() {
orderNumber = new OrderNumber();
orderPrice = new OrderPrice();
orderShop = new OrderShop();
}

public void getNumber() {
orderNumber.logic();
}

public void getShop() {
orderShop.logic();
}

public void getPrice() {
orderPrice.logic();
}
}

// 客户端代码
public class Main {
public static void main(String[] args) {
FacadeOrder facadeOrder = new FacadeOrder();
facadeOrder.getShop();
facadeOrder.getNumber();
facadeOrder.getPrice();
}
}

// 控制台输出
-- 订单商品名:TouHou Remilia card
-- 订单编号:1097361294729
-- 订单金额:1020

其实很简单,外观模式就是对多个类的操作进行了封装,将原本需要调用3个或更多接口的操作统一封装到了一个类里,我们只需要对外暴露出外观类即可,这样调用方只需要调用一个接口就可以完成之前需要调用多个不同的接口才能完成的功能。减少了客户端与子系统之间的耦合,不过要注意不要过多的使用外观模式,否则系统反而失去了模块化的优势。

适用场景:用户下单、多支付场景集成、需要对外提供一个统一接口且内部实现比较复杂等等

行为型

责任链(Chain of Command)

责任链可以使多个对象都有机会处理请求,我们只需要将这些对象串成一条链,那么请求就会在这条链上传递,直到被成功处理为止。责任链的一大优点就是灵活,我们可以为每个请求分配属于自己的工作链,并且可以轻松扩展。例如我们现在有吃饭、睡觉、学习三个行动,对于婴儿来说,我们只需要为其分配吃饭和睡觉,而儿童我们则可以在吃饭后追加一个学习的行为。

责任链主要由抽象处理者具体处理者客户端三个角色组成:

  1. 抽象处理者:负责定义处理请求的接口,并且持有对下一处理者的引用

  2. 具体处理者:抽象处理者的子类,负责实现抽象处理者的方法并完成对应逻辑

  3. 客户端:调用的一方,在这里我们需要完成责任链的创建并设置每个责任链的上级

这里我们以公司内申请预算为例,如果金额在100及以下,那么交给小组审核,如果在1000及以下则交给经理,在1000以上则交给CEO处理,那么我们的责任链就可以串为小组-经理-CEO。具体代码如下:

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
// 抽象处理者
public abstract class Handler {

protected Handler handler;

public void setNextHandler(Handler handler) {
this.handler = handler;
}

public abstract boolean request(int price);
}

// 小组具体处理者
public class GroupHandler extends Handler{

@Override
public boolean request(int price) {
if (price >= 10 && price <= 100) {
System.out.println("组审核成功!");
return true;
}
System.out.println("组审核失败,发送至下一级");
return handler.request(price);
}
}

// 经理具体处理者
public class ManagerHandler extends Handler{

@Override
public boolean request(int price) {
if (price > 100 && price <= 1000) {
System.out.println("管理审核成功");
return true;
}
System.out.println("管理审核失败,发送至下一级");
return handler.request(price);
}
}

// CEO具体处理者
public class CEOHandler extends Handler{

@Override
public boolean request(int price) {
if (price > 1000 && price < 10000) {
System.out.println("CEO审核成功");
return true;
}
System.out.println("金额过大,通过失败!");
return false;
}
}

// 客户端
public class Main {
public static void main(String[] args) {
// 创建所有责任链
GroupHandler groupHandler = new GroupHandler();
ManagerHandler managerHandler = new ManagerHandler();
CEOHandler CeoHandler = new CEOHandler();
// 设置每个责任链的上级
groupHandler.setNextHandler(managerHandler);
managerHandler.setNextHandler(CeoHandler);
System.out.println(groupHandler.request(1001));
}
}

// 控制台输出
-- 组审核失败,发送至下一级
-- 管理审核失败,发送至下一级
-- CEO审核成功
-- true

适用场景:一个请求需要被多个对象按照顺序处理时、规则校验

观察者(Observer)

观察者模式可以在一个对象发生改变时,通知所有依赖于它的对象。就跟订阅机制一样,当有更新时,所有订阅了该频道的人都会收到消息通知,这些人可以去看新消息,也可以不看,这都取决于订阅者的操作。

观察者模式由被观察者接口、被观察者实现类、观察者接口和观察者实现类组成:

  1. 被观察者接口:主题是被观察者的对象,提供添加、删除、通知观察者方法

  2. 被观察者实现类:被观察的对象,实现被观察者接口,可以通过通知观察者方法告诉所有观察者

  3. 观察者接口:通常只会有一个方法。所有观察者类必须实现该接口来接收通知

  4. 观察者实现类:实现了观察者接口的类,当观察的对象改变时,被观察者会调用观察者接口内的方法,并执行对应观察者类的逻辑

下面的代码以CEO为被观察的对象,员工和经理为观察者,首先将员工和经理添加到了CEO的观察者集合内,CEO调用接口发起通知,此时二者都会接收到消息。继续将员工踢出观察者集合,CEO再次调用接口,只有经理收到消息。具体代码如下:

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
// 被观察者接口
public interface Observer {
void addSubject(Subject subject);

void removeSubject(Subject subject);

void notifyObservers(String message);
}

// 被观察者实现类
public class CEOObServer implements Observer{

private List<Subject> observers = new ArrayList<>();

@Override
public void addSubject(Subject subject) {
observers.add(subject);
}

@Override
public void removeSubject(Subject subject) {
observers.remove(subject);
}

@Override
public void notifyObservers(String message) {
observers.forEach(data -> data.update(message));
}
}

// 观察者接口
public interface Subject {
void update(String message);
}

// 员工观察者实现类
public class EmployeeSubject implements Subject{
@Override
public void update(String message) {
System.out.println("EmployeeSubject : " + message);
}
}

// 经理观察者实现类
public class ManagerSubject implements Subject{
@Override
public void update(String message) {
System.out.println("ManagerSubject : " + message);
}
}

// 客户端
public class Main {
public static void main(String[] args) {
// 创建一个CEO发布者和2个订阅者
CEOObServer ceoObServer = new CEOObServer();
EmployeeSubject employeeSubject = new EmployeeSubject();
ManagerSubject managerSubject = new ManagerSubject();

ceoObServer.addSubject(employeeSubject);
ceoObServer.addSubject(managerSubject);

// 发布者发布消息
ceoObServer.notifyObservers("消息通知");
System.out.println("---删除employee订阅者---");
ceoObServer.removeSubject(employeeSubject);
ceoObServer.notifyObservers("新消息通知");
}
}

// 控制台输出
-- EmployeeSubject : 消息通知
-- ManagerSubject : 消息通知
-- ---删除employee订阅者---
-- ManagerSubject : 新消息通知

适用场景:消息系统、多人协作实时编辑、对特定数据值进行监听等等

策略模式(Strategy)

策略模式可以将我们定义的一系列算法,封装到一个个独立的类中,在运行时选择不同的算法。策略模式的角色由策略接口、策略接口实现类和上下文类组成:

策略接口:是所有实现类的共同接口

接口实现类:负责实现具体的策略,封装算法详情

上下文类:提供策略对象的引用并通过接口调用返回具体策略的算法

这里以用户观看动画来做示例,现在我们有一个动画对象,其中名字、上线年份、集数字段有值,字幕组字段没值,因为有的人喜欢A字幕组、有的人喜欢B字幕组,所以我们要让用户自己选择看哪个字幕组,用户选A我们就加载A字幕,选B我们就加载B字幕,我们通过策略模式完成这个需求,具体代码如下:

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
// 动画类
public class Anime {
private String name;
private String onlineYear;
private String episodes;
private String subTitle;

public Anime() {
}

public Anime(String name, String onlineYear, String episodes, String subTitle) {
this.name = name;
this.onlineYear = onlineYear;
this.episodes = episodes;
this.subTitle = subTitle;
}

public String getName() {
return name;
}

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

public String getOnlineYear() {
return onlineYear;
}

public void setOnlineYear(String onlineYear) {
this.onlineYear = onlineYear;
}

public String getEpisodes() {
return episodes;
}

public void setEpisodes(String episodes) {
this.episodes = episodes;
}

public String getSubTitle() {
return subTitle;
}

public void setSubTitle(String subTitle) {
this.subTitle = subTitle;
}

@Override
public String toString() {
return "Anime{" +
"name='" + name + '\'' +
", onlineYear='" + onlineYear + '\'' +
", episodes='" + episodes + '\'' +
", subTitle='" + subTitle + '\'' +
'}';
}
}

// 字幕的策略接口
public interface Subtitle {
Anime getAnime(Anime anime);
}

// 北宇治字幕组实现策略接口
public class KitaujiSub implements Subtitle {
@Override
public Anime getAnime(Anime anime) {
anime.setSubTitle("北宇治字幕组");
return anime;
}
}

// 樱花字幕组实现策略接口
public class SakuraSub implements Subtitle {
@Override
public Anime getAnime(Anime anime) {
anime.setSubTitle("樱花字幕组");
return anime;
}
}

// 选择字幕的上下文类
public class SubContext {

private Subtitle subtitle;
private Anime anime;

public SubContext(Subtitle subtitle,Anime anime) {
this.subtitle = subtitle;
this.anime = anime;
}

public Anime getSubtitle() {
return subtitle.getAnime(anime);
}

}

// 客户端代码
public class Main {
public static void main(String[] args) {
// 创建2个动漫对象,字幕字段的值通过策略选择完成
Anime oshinoko = new Anime();
oshinoko.setName("推しのこ");
oshinoko.setOnlineYear("2024");
oshinoko.setEpisodes("12");

Anime makehiro = new Anime();
makehiro.setName("負けヒロインは多すぎる");
makehiro.setOnlineYear("2024");
makehiro.setEpisodes("12");

// 通过上下文对象获取加工完成的动画,参数为对应加工策略和动画对象
SubContext oshinokoSub = new SubContext(new KitaujiSub(), oshinoko);
SubContext makeiros = new SubContext(new SakuraSub(), makehiro);

System.out.println(oshinokoSub.getSubtitle());
System.out.println(makeiros.getSubtitle());
}
}

// 控制台输出
-- Anime{name='推しのこ', onlineYear='2024', episodes='12', subTitle='北宇治字幕组'}
-- Anime{name='負けヒロインは多すぎる', onlineYear='2024', episodes='12', subTitle='樱花字幕组'}

上面的例子中,算法就是不同的字幕组策略,通过传入不同的算法来使动画对象获取到不同的加工结果。策略模式同样应用在线程池创建时选择的拒绝策略上,下为四个拒绝策略的源码,可以看出策略接口为RejectedExecutionHandler,四个拒绝策略分别实现了各自的算法。

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
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}

public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}

public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}

都说策略模式消除if-else,在选择策略的时候不还要用if-else判断选择哪个策略吗?

策略模式消除的是**行为执行中的if-else**,也就是说在执行某个动作的时候不再显示地判断使用哪种实现。举个例子就是支付场景下,我们会有很多支付方式,微信、支付宝、银行卡、信用卡等等,不使用策略模式的话,我们需要if判断用户选择的方式,然后执行这个方式所对应的代码。使用策略模式的情况下,上面的这些支付方式会被封装为一个个策略类,我们只需要通过上下文类引入对应策略即可完成,代码中没有了选择支付方式的if-else。消除的就是这部分的if-else。同时策略模式的引入也让代码具有了更好的维护性和扩展性,以后新增支付方式,我们只需要新增一个策略类即可,不需要修改原代码。这个时候可能就有人问:那么我们在选择策略的时候不还要使用if-else吗?关于这个问题,我们可以通过一个Map解决。具体如下:

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
public class Main {

private static final Map<String, Subtitle> SUB_MAP = new HashMap<>(){{
put("sakura", new SakuraSub());
put("kitauji", new KitaujiSub());
}};

public static void main(String[] args) {
// 创建2个动漫对象,字幕字段的值通过策略选择完成
Anime oshinoko = new Anime();
oshinoko.setName("推しのこ");
oshinoko.setOnlineYear("2024");
oshinoko.setEpisodes("12");

Anime makehiro = new Anime();
makehiro.setName("負けヒロインは多すぎる");
makehiro.setOnlineYear("2024");
makehiro.setEpisodes("12");

// 通过上下文对象获取加工完成的动画,参数为对应加工策略和动画对象
SubContext oshinokoSub = new SubContext(SUB_MAP.get("kitauji"), oshinoko);
SubContext makeiros = new SubContext(SUB_MAP.get("sakura"), makehiro);
System.out.println(oshinokoSub.getSubtitle());
System.out.println(makeiros.getSubtitle());
}
}

我们可以将所有的策略放入一个Map内,然后通过用户选择的key直接获取对应的策略,即可消除选择策略时的if-else。

策略模式与工厂方法模式有什么区别?

相信大部分人看完策略模式,会发现和工厂方法模式很像,确实这两者在结构上很像,但各自的目的又不一样。首先第一点从设计上来看,工厂方法模式属于创建型,关注的是对象的创建,而策略模式则是行为型,关注的是对算法的封装。第二点结构差异上,工厂方法包含1个接口,多个实现类和多个实现类的工厂,通过指定的工厂创建出指定的类。而策略模式包含1个策略接口、多个策略实现类和1个上下文类,通过一个上下文类选择一个策略。

适用场景:多场景支付、文件解压缩、某功能经常迭代且每次都要加入新逻辑等等

模板方法(Template Method)

模板方法通常用来定义算法的框架,将算法中的一些步骤延迟到子类,使子类可以在不改变算法结构的情况下重新定义算法中的某些步骤。模板方法的结构非常简单,只有父类和子类:

  1. 抽象类:模板方法的核心,定义了算法的框架,包含一个模板方法、一个或多个抽象方法、可选的初始化方法以及钩子方法(钩子方法可以在子类中选择是否重写,通常用来修改模板方法内的一些逻辑)

  2. 具体类:继承抽象类并实现抽象方法,完成该算法的具体实现。

我们以游戏创建角色时选择角色属性为例,具体代码如下:

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
138
139
140
141
142
143
144
145
146
147
148
// 模板方法
public abstract class BaseBoard {

public final void createPerson() {
// 初始化玩家职业
initProfession();
// 初始化玩家基础属性
initAttr();
// 初始化玩家出生地
initAddress();
// 初始化玩家等级
initLevel();
// 玩家是否选择了初始道具
if (chooseItem()) {
initItem();
}
}

protected abstract void initProfession();
protected abstract void initAttr();
protected abstract void initAddress();
protected boolean chooseItem() {
return false;
};

private void initLevel() {
System.out.println("等级:1");
}

private void initItem() {
System.out.println("初始道具:火焰壶");
}
}

// 子类 魔法师
public class Mage extends BaseBoard {
@Override
protected void initProfession() {
System.out.println("职业:魔法师");
}

@Override
protected void initAttr() {
System.out.println("属性:");
System.out.println(" 生命: 3");
System.out.println(" 力量: 0");
System.out.println(" 魔力: 12");
System.out.println(" 耐力: 5");
}

@Override
protected void initAddress() {
System.out.println("出生地:雷古拉魔法学院");
}
}

// 子类 战士
public class Warrior extends BaseBoard {
@Override
protected void initProfession() {
System.out.println("职业:战士");
}

@Override
protected void initAttr() {
System.out.println("属性:");
System.out.println(" 生命: 7");
System.out.println(" 力量: 10");
System.out.println(" 魔力: 1");
System.out.println(" 耐力: 5");
}

@Override
protected void initAddress() {
System.out.println("出生地:王城征兵所");
}

@Override
protected boolean chooseItem() {
return true;
}
}

// 子类 小偷
public class Thief extends BaseBoard {
@Override
protected void initProfession() {
System.out.println("职业:小偷");
}

@Override
protected void initAttr() {
System.out.println("属性:");
System.out.println(" 生命: 5");
System.out.println(" 力量: 3");
System.out.println(" 魔力: 5");
System.out.println(" 耐力: 7");
}

@Override
protected void initAddress() {
System.out.println("出生地:初始之地");
}
}

// 客户端
public class Main {
public static void main(String[] args) {
Thief elf = new Thief();
elf.createPerson();
System.out.println("====================================");
Warrior warrior = new Warrior();
warrior.createPerson();
System.out.println("====================================");
Mage mage = new Mage();
mage.createPerson();
System.out.println("====================================");
}
}

// 控制台输出
-- 职业:小偷
-- 属性:
-- 生命: 5
-- 力量: 3
-- 魔力: 5
-- 耐力: 7
-- 出生地:初始之地
-- 等级:1
-- ====================================
-- 职业:战士
-- 属性:
-- 生命: 7
-- 力量: 10
-- 魔力: 1
-- 耐力: 5
-- 出生地:王城征兵所
-- 等级:1
-- 初始道具:火焰壶
-- ====================================
-- 职业:魔法师
-- 属性:
-- 生命: 3
-- 力量: 0
-- 魔力: 12
-- 耐力: 5
-- 出生地:雷古拉魔法学院
-- 等级:1

上面的代码中,BaseBoard类定义了模板方法createPerson(),将所有的步骤封装在内。三个子类重写所有抽象方法,实现了具体的属性初始化逻辑。其中Warrior类又单独重写了chooseItem()方法,使其返回true,从而允许角色选择初始道具。BaseBoard中的共通方法initLevel()确保所有角色的等级都为1。

适用场景:多个子类共享相同的操作逻辑、多格式报表生成、多格式数据处理等等