设计模式

前言

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。

设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。

这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

image-20230719130640891

推荐视频

创建型模式

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式(Singleton)的目的是为了保证在一个进程中,某个类有且仅有一个实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
// 静态字段引用唯一实例:
private static final Singleton INSTANCE = new Singleton();

// 通过静态方法返回实例:
public static Singleton getInstance() {
return INSTANCE;
}

// private构造方法保证外部无法实例化:
private Singleton() {
}
}

遗憾的是,这种写法在多线程中是错误的,在竞争条件下会创建出多个实例。必须对整个方法进行加锁:

1
2
3
4
5
6
public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}

另一种实现Singleton的方式是利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum World {
// 唯一枚举:
INSTANCE;

private String name = "world";

public String getName() {
return this.name;
}

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

枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:

1
String name = World.INSTANCE.getName();

使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

单例模式比静态方法

单例模式比静态方法有很多优势:

  • 单例可以继承类,实现接口,而静态类不能(可以集成类,但不能集成实例成员);
  • 单例可以被延迟初始化,静态类一般在第一次加载是初始化;
  • 单例类可以被集成,他的方法可以被覆写;
  • 单例类可以被用于多态而无需强迫用户只假定唯一的实例。举个例子,你可能在开始时只写一个配置,但是以后你可能需要支持超过一个配置集,或者可能需要允许用户从外部从外部文件中加载一个配置对象,或者编写自己的。你的代码不需要关注全局的状态,因此你的代码会更加灵活。

工厂模式

假设我们希望实现一个解析字符串到NumberFactory,可以定义如下:

1
2
3
public interface NumberFactory {
Number parse(String s);
}

有了工厂接口,再编写一个工厂的实现类:

1
2
3
4
5
public class NumberFactoryImpl implements NumberFactory {
public Number parse(String s) {
return new BigDecimal(s);
}
}

而产品接口是NumberNumberFactoryImpl返回的实际产品是BigDecimal

那么客户端如何创建NumberFactoryImpl呢?通常我们会在接口Factory中定义一个静态方法getFactory()来返回真正的子类:

1
2
3
4
5
6
7
8
9
10
11
public interface NumberFactory {
// 创建方法:
Number parse(String s);

// 获取工厂实例:
static NumberFactory getFactory() {
return impl;
}

static NumberFactory impl = new NumberFactoryImpl();
}

在客户端中,我们只需要和工厂接口NumberFactory以及抽象产品Number打交道:

1
2
NumberFactory factory = NumberFactory.getFactory();
Number result = factory.parse("123.456");

Integer既是产品又是静态工厂。它提供了静态方法valueOf()来创建Integer。那么这种方式和直接写new Integer(100)有何区别呢?我们观察valueOf()方法:

1
2
3
4
5
6
7
public final class Integer {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
}

它的好处在于,valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。

抽象工厂模式

生成器模式/建造者模式

生成器模式(Builder)是使用多个“小型”工厂来最终创建出一个完整对象。

当我们使用Builder的时候,一般来说,是因为创建这个对象的步骤比较多,每个步骤都需要一个零部件,最终组合成一个完整的对象。

我们把Markdown转HTML看作一行一行的转换,每一行根据语法,使用不同的转换器:

  • 如果以#开头,使用HeadingBuilder转换;
  • 如果以>开头,使用QuoteBuilder转换;
  • 如果以---开头,使用HrBuilder转换;
  • 其余使用ParagraphBuilder转换。

这个HtmlBuilder写出来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HtmlBuilder {
private HeadingBuilder headingBuilder = new HeadingBuilder();
private HrBuilder hrBuilder = new HrBuilder();
private ParagraphBuilder paragraphBuilder = new ParagraphBuilder();
private QuoteBuilder quoteBuilder = new QuoteBuilder();

public String toHtml(String markdown) {
StringBuilder buffer = new StringBuilder();
markdown.lines().forEach(line -> {
if (line.startsWith("#")) {
buffer.append(headingBuilder.buildHeading(line)).append('\n');
} else if (line.startsWith(">")) {
buffer.append(quoteBuilder.buildQuote(line)).append('\n');
} else if (line.startsWith("---")) {
buffer.append(hrBuilder.buildHr(line)).append('\n');
} else {
buffer.append(paragraphBuilder.buildParagraph(line)).append('\n');
}
});
return buffer.toString();
}
}

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型模式,即Prototype,是指创建新对象的时候,根据现有的一个原型来创建。

因为clone()的方法签名是定义在Object中,返回类型也是Object,所以要强制转型,比较麻烦:

1
2
3
4
5
6
7
8
9
Student std1 = new Student();
std1.setId(123);
std1.setName("Bob");
std1.setScore(88);
// 复制新对象:
Student std2 = (Student) std1.clone();
System.out.println(std1);
System.out.println(std2);
System.out.println(std1 == std2); // false

实际上,使用原型模式更好的方式是定义一个copy()方法,返回明确的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
private int id;
private String name;
private int score;

public Student copy() {
Student std = new Student();
std.id = this.id;
std.name = this.name;
std.score = this.score;
return std;
}
}

结构型模式

代理模式

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

静态代理模式:

角色分析:

1、抽象角色:一般会使用接口或抽象类来解决

2、真实角色:被代理的角色

3、代理角色:代理真实角色,代理真实角色后我们会进行一些附属操作

4、访问角色:访问代理对象的人

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 interface Rent {
void rent();
}
//房东
public class Master implements Rent{
@Override
public void rent() {
System.out.println("Master rent");
}
}
//中介
public class Proxy implements Rent{
private Master master;

public Proxy() {
}

public Proxy(Master master) {
this.master = master;
}

@Override
public void rent() {
see();
master.rent();
fare();
}
//看房
public void see(){
System.out.println("see");
}
//收费
public void fare(){
System.out.println("fare");
}
}
//测试类
public class Consumer {
public static void main(String[] args) {
Master master = new Master();
//进行代理
Proxy proxy = new Proxy(master);
//不需要通过对象,直接通过代理完成响应业务
proxy.rent();
}
}

得到测试结果

see
Master rent
fare

可以从上述代码看出,我们通过创建中介这一代理完成了租房,并且还有看房、收费的附属操作。我们不需要使用房东对象,通过使用代理中介就可以完成操作。

代理模式优点:

可以使真实角色的操作更加纯粹!不用去关注一些公共的业务,公共也就可以交给代理角色,实现了业务的分工,公共业务发生扩展的时候,方便集中管理!
代理模式缺点:

一个真实角色就会产生一个代理角色;代码量会翻倍开发效率会变低,也许,这样无法理解到代理模式的好处。举个例子也许能更好理解,比如说我们想要在原有固定功能上新增业务,按照开闭原则我们是不能对原有代码进行修改的。但是我们可以通过代理模式,增加代理,在实现原有功能的情况下写入新的功能,创建对象时也就可以使用代理,完成操作。

动态代理模式:

虽然静态代理模式可以很好的解决开闭原则,但是每有一个真实角色就会产生一个代理,代码量翻倍过于臃肿,开发效率较低。因此,我们就使用动态代理模式进行设计。

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 IUserService {
void add();
void delete();
void update();
void query();

}
//实现类
public class UserServiceImpl implements IUserService {
@Override
public void add() {
System.out.println("add");
}

@Override
public void delete() {
System.out.println("delete");
}

@Override
public void update() {
System.out.println("update");
}

@Override
public void query() {
System.out.println("query");
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//自动生成动态代理类模板
public class ProxyInvocationHandler implements InvocationHandler {
//被代理接口
private Object target;

public void setTarget(Object target) {
this.target = target;
}
//得到代理类
public Object getProxy() {
return Proxy.newProxyInstance(getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
public void log(String s) {
System.out.println("[debug]:" + s);
}
//得到代理类
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log(method.getName());
Object result = method.invoke(target, args);
return result;
}
}
//测试类
public class Consumer {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
ProxyInvocationHandler handler = new ProxyInvocationHandler();
//设置代理对象
handler.setTarget(userService);
//生成代理类
IUserService proxy = (IUserService)handler.getProxy();
proxy.add();
proxy.query();
}
}

得到测试结果

1
2
3
4
[debug]:add
add
[debug]:query
query

通过测试我们可以顺利的使用动态代理模式完成一系列操作,当我们想要添加附属操作时,我们只需要在模板中进行添加。
优点:
①可以使真实角色的操作更加纯粹!不用去关注一些公共的业务。
②公共也就可以交给代理角色!实现了业务的分工。
③公共业务发生扩展的时候,方便集中管理。
④一个动态代理类代理的是一个接口,一般就是对应的一类业务。
⑤一个动态代理类可以代理多个类,只要是实现了同一个接口即可!

装饰者模式

动态的将新功能附加到对象上。在对象功能的拓展方面,比继承更有弹性。同时装饰者模式也体现了开闭原则。

角色分析:

  1. 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  2. 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  3. 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  4. 具体装饰(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
//定义抽象类
public abstract class Drink {
public abstract double cost();
}
//定义两个抽象类的实现类
public class Juice extends Drink{
@Override
public double cost() {
System.out.println("juice: "+16);
return 16;
}
}
public class Milk extends Drink{
@Override
public double cost() {
System.out.println("milk: "+12);
return 12;
}
}
//定义装饰抽象类
public abstract class Decorator extends Drink {
public abstract double cost();
}
//定义两个装饰具体实现类
public class Chocolate extends Decorator{
private final static double COST = 4;
private Drink drink;

public Chocolate(Drink drink) {
this.drink = drink;
}

@Override
public double cost() {
System.out.println("chocolate:"+4);
return COST+drink.cost();
}
}
public class Pudding extends Decorator{
private final static double COST = 5;
private Drink drink;

public Pudding(Drink drink) {
this.drink = drink;
}

@Override
public double cost() {
System.out.println("pudding:"+5);
return COST+drink.cost();
}
}
//测试类
public class Test {
public static void main(String[] args) {
Drink milk = new Milk();
milk = new Pudding(milk);
milk = new Chocolate(milk);
System.out.println(milk.cost());
}
}

得到测试结果

chocolate:4
pudding:5
milk: 12
21.0

适配器模式

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。*

适配器模式在Java标准库中有广泛应用。

比如我们持有数据类型是String[],但是需要List接口时,可以用一个Adapter:

1
2
String[] exist = new String[] {"Good", "morning", "Bob", "and", "Alice"};
Set<String> set = new HashSet<>(Arrays.asList(exist));

注意到List<T> Arrays.asList(T[])就相当于一个转换器,它可以把数组转换为List

享元模式

享元(Flyweight)的核心思想很简单:

如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。

享元模式在Java标准库中有很多应用。

我们知道,包装类型如ByteInteger都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以Integer为例,如果我们通过Integer.valueOf()这个静态工厂方法创建Integer实例,当传入的int范围在-128~+127之间时,会直接返回缓存的Integer实例:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) throws InterruptedException {
Integer n1 = Integer.valueOf(100);
Integer n2 = Integer.valueOf(100);
System.out.println(n1 == n2); // true
}
}

在实际应用中,享元模式主要应用于缓存,即客户端如果重复请求某些对象,不必每次查询数据库或者读取文件,而是直接返回内存中缓存的数据。

我们以Student为例,设计一个静态工厂方法,它在内部可以返回缓存的对象:

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
public class Student {
// 持有缓存:
private static final Map<String, Student> cache = new HashMap<>();

// 静态工厂方法:
public static Student create(int id, String name) {
String key = id + "\n" + name;
// 先查找缓存:
Student std = cache.get(key);
if (std == null) {
// 未找到,创建新对象:
System.out.println(String.format("create new Student(%s, %s)", id, name));
std = new Student(id, name);
// 放入缓存:
cache.put(key, std);
} else {
// 缓存中存在:
System.out.println(String.format("return cached Student(%s, %s)", std.id, std.name));
}
return std;
}

private final int id;
private final String name;

public Student(int id, String name) {
this.id = id;
this.name = name;
}
}

在实际应用中,我们经常使用成熟的缓存库,例如GuavaCache,因为它提供了最大缓存数量限制、定时过期等实用功能。

外观模式/门面模式

为子系统中的一组接口提供一个一致的界面。Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式,即Facade,是一个比较简单的模式。

它的基本思想如下:

如果客户端要跟许多子系统打交道,那么客户端需要了解各个子系统的接口,比较麻烦。如果有一个统一的“中介”,让客户端只跟中介打交道,中介再去跟各个子系统打交道,对客户端来说就比较简单。

所以Facade就相当于搞了一个中介。

我们以注册公司为例,假设注册公司需要三步:

  1. 向工商局申请公司营业执照;
  2. 在银行开设账户;
  3. 在税务局开设纳税号。

以下是三个系统的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 工商注册:
public class AdminOfIndustry {
public Company register(String name) {
...
}
}

// 银行开户:
public class Bank {
public String openAccount(String companyId) {
...
}
}

// 纳税登记:
public class Taxation {
public String applyTaxCode(String companyId) {
...
}
}

如果子系统比较复杂,并且客户对流程也不熟悉,那就把这些流程全部委托给中介:

1
2
3
4
5
6
7
8
9
10
public class Facade {
public Company openCompany(String name) {
Company c = this.admin.register(name);
String bankAccount = this.bank.openAccount(c.getId());
c.setBankAccount(bankAccount);
String taxCode = this.taxation.applyTaxCode(c.getId());
c.setTaxCode(taxCode);
return c;
}
}

这样,客户端只跟Facade打交道,一次完成公司注册的所有繁琐流程:

1
Company c = facade.openCompany("Facade Software Ltd.");

很多Web程序,内部有多个子系统提供服务,经常使用一个统一的Facade入口,例如一个RestApiController,使得外部用户调用的时候,只关心Facade提供的接口,不用管内部到底是哪个子系统处理的。

更复杂的Web程序,会有多个Web服务,这个时候,经常会使用一个统一的网关入口来自动转发到不同的Web服务,这种提供统一入口的网关就是Gateway,它本质上也是一个Facade,但可以附加一些用户认证、限流限速的额外服务。

行为型模式

观察着模式/发布-订阅模式

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

观察者模式(Observer)又称发布-订阅模式(Publish-Subscribe:Pub/Sub)。它是一种通知机制,让发送通知的一方(被观察方)和接收通知的一方(观察者)能彼此分离,互不影响。

Electron开发中互动消息接收后,多个窗口都要进行响应时就会用。

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 Store {
private List<ProductObserver> observers = new ArrayList<>();
private Map<String, Product> products = new HashMap<>();

// 注册观察者:
public void addObserver(ProductObserver observer) {
this.observers.add(observer);
}

// 取消注册:
public void removeObserver(ProductObserver observer) {
this.observers.remove(observer);
}

public void addNewProduct(String name, double price) {
Product p = new Product(name, price);
products.put(p.getName(), p);
// 通知观察者:
observers.forEach(o -> o.onPublished(p));
}

public void setProductPrice(String name, double price) {
Product p = products.get(name);
p.setPrice(price);
// 通知观察者:
observers.forEach(o -> o.onPriceChanged(p));
}
}

责任链模式

一种处理请求的模式,它让多个处理器都有机会处理该诘求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。
责任链模式的主要角色

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(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
//抽象处理者
public abstract class Handler {
private Handler next;
public void setNext(Handler next) { this.next = next; }
public Handler getNext() { return next; }

//处理请求
public abstract void handleRequest(int info);
}
//具体处理者1
public class Handler1 extends Handler{
@Override
public void handleRequest(int info) {
if (info <10){
System.out.println("Handler1完成处理");
}else {
if (getNext()!=null){
getNext().handleRequest(info);
}else {
System.out.println("没有处理者进行处理");
}
}
}
}
//具体处理者2
public class Handler2 extends Handler{
@Override
public void handleRequest(int info) {
if (info <20&&info>10){
System.out.println("Handler2完成处理");
}else {
if (getNext()!=null){
getNext().handleRequest(info);
}else {
System.out.println("没有处理者进行处理");
}
}
}
}
// 测试类
public class Test {
public static void main(String[] args) {
Handler handler1 = new Handler1();
Handler handler2 = new Handler2();
handler1.setNext(handler2);
handler1.handleRequest(5);
handler1.handleRequest(15);
handler1.handleRequest(25);
}
}

得到测试结果:

Handler1完成处理
Handler2完成处理
没有处理者进行处理

通过测试结果我们看到,5交给了Handler1处理,15交给了Handler2处理,而25则没有处理者处理。请求者根本不需要参与处理,只需要提交数据就可以完成功能的处理,完全不需要管是哪个处理者进行处理的。当我们想要继续添加处理者时,这只需要再次添加就可以了,也不会对之前的代码造成影响。