Dunwu Blog

大道至简,知易行难

设计模式之外观模式

意图

外观模式 (Facade) 是一种结构型设计模式, 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

  • 外观模式为复杂子系统提供了一个简单接口,并不为子系统添加新的功能和行为。
  • 外观模式实现了子系统与客户之间的松耦合关系。
  • 外观模式没有封装子系统的类,只是提供了简单的接口。 如果应用需要,它并不限制客户使用子系统类。因此可以再系统易用性与通用性之间选择。
  • 外观模式注重的是简化接口,它更多的时候是从架构的层次去看整个系统,而并非单个类的层次。

适用场景

  • 如果你需要一个指向复杂子系统的直接接口, 且该接口的功能有限, 则可以使用外观模式。
  • 如果需要将子系统组织为多层结构, 可以使用外观。

结构

img

结构说明

  1. 外观 (Facade) 提供了一种访问特定子系统功能的便捷方式, 其了解如何重定向客户端请求, 知晓如何操作一切活动部件。

  2. 创建附加外观 (Additional Facade) 类可以避免多种不相关的功能污染单一外观, 使其变成又一个复杂结构。 客户端和其他外观都可使用附加外观。

  3. 复杂子系统 (Complex Subsystem) 由数十个不同对象构成。 如果要用这些对象完成有意义的工作, 你必须深入了解子系统的实现细节, 比如按照正确顺序初始化对象和为其提供正确格式的数据。

    子系统类不会意识到外观的存在, 它们在系统内运作并且相互之间可直接进行交互。

  4. 客户端 (Client) 使用外观代替对子系统对象的直接调用。

结构代码范式

Facade : 了解每个子系统类的功能,负责分发客户端的请求给各个子系统去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Class1 {
public void op1() {
System.out.println("方法1");
}
}

class Class2 {
public void op2() {
System.out.println("方法2");
}
}

class Class3 {
public void op3() {
System.out.println("方法3");
}
}

Subsystem Classes : 实现子系统功能。在不感知 Facade 的情况下,处理 Facade 对象分配的工作,

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
class Facade {
private Class1 one = new Class1();
private Class2 two = new Class2();
private Class3 three = new Class3();

public void op1() {
System.out.println("Facade op1()");
one.op1();
}

public void op2() {
System.out.println("Facade op2()");
two.op2();
}

public void op3() {
System.out.println("Facade op3()");
three.op3();
}

public void Method() {
System.out.println("Facade Method()");
three.op3();
two.op2();
one.op1();
}
}

【客户端】

1
2
3
4
5
6
7
8
public class FacadePattern {
public static void main(String[] args) {
Facade facade = new Facade();
facade.Method();

facade.op1();
}
}

【输出】

1
2
3
4
5
6
Facade Method()
方法3
方法2
方法1
Facade op1()
方法1

伪代码

在本例中, 外观模式简化了客户端与复杂视频转换框架之间的交互。

img

你可以创建一个封装所需功能并隐藏其他代码的外观类, 从而无需使全部代码直接与数十个框架类进行交互。 该结构还能将未来框架升级或更换所造成的影响最小化, 因为你只需修改程序中外观方法的实现即可。

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
// 这里有复杂第三方视频转换框架中的一些类。我们不知晓其中的代码,因此无法
// 对其进行简化。

class VideoFile
// ...

class OggCompressionCodec
// ...

class MPEG4CompressionCodec
// ...

class CodecFactory
// ...

class BitrateReader
// ...

class AudioMixer
// ...


// 为了将框架的复杂性隐藏在一个简单接口背后,我们创建了一个外观类。它是在
// 功能性和简洁性之间做出的权衡。
class VideoConverter is
method convert(filename, format):File is
file = new VideoFile(filename)
sourceCodec = new CodecFactory.extract(file)
if (format == "mp4")
destinationCodec = new MPEG4CompressionCodec()
else
destinationCodec = new OggCompressionCodec()
buffer = BitrateReader.read(filename, sourceCodec)
result = BitrateReader.convert(buffer, destinationCodec)
result = (new AudioMixer()).fix(result)
return new File(result)

// 应用程序的类并不依赖于复杂框架中成千上万的类。同样,如果你决定更换框架,
// 那只需重写外观类即可。
class Application is
method main() is
convertor = new VideoConverter()
mp4 = convertor.convert("funny-cats-video.ogg", "mp4")
mp4.save()

案例

使用示例: 使用 Java 开发的程序中经常会使用外观模式。 它在与复杂程序库和 API 协作时特别有用。

下面是一些核心 Java 程序库中的外观示例:

识别方法: 外观可以通过使用简单接口, 但将绝大部分工作委派给其他类的类来识别。 通常情况下, 外观管理着其所使用的对象的完整生命周期。

与其他模式的关系

  • 外观模式为现有对象定义了一个新接口, 适配器模式则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
  • 当只需对客户端代码隐藏子系统创建对象的方式时, 你可以使用抽象工厂模式来代替外观
  • 享元模式展示了如何生成大量的小型对象, 外观则展示了如何用一个对象来代表整个子系统。
  • 外观中介者模式的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。
    • 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
    • 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
  • 外观类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
  • 外观代理模式的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。

参考资料

设计模式之代理模式

意图

代理模式 (Proxy) 是一种结构型设计模式, 为其他对象提供一种代理以控制对这个对象的访问

  • 代理模式介绍了一种访问对象的间接等级。
  • 一个远程代理可以隐藏一个对象在不同地址空间的细节。
  • 一个虚拟代理可以根据需要最优化创建对象的开销。
  • 而安全代理和智能指引都允许访问对象的同时处理其他事务。

适用场景

  • 延迟初始化 (虚拟代理)。 如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。
  • 访问控制 (保护代理)。 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。
  • 本地执行远程服务 (远程代理)。 适用于服务对象位于远程服务器上的情形。
  • 记录日志请求 (日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。
  • 智能引用。 可在没有客户端使用某个重量级对象时立即销毁该对象。

结构

img

结构说明

  1. 服务接口 (Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。
  2. 服务 (Service) 类提供了一些实用的业务逻辑。
  3. 代理 (Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。
  4. 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。

结构代码范式

Subject : 定义了 RealSubject 和 Proxy 的公共接口,这样就在任何使用 RealSubject 的地方都可以使用 Proxy 。

1
2
3
abstract class Subject {
public abstract void Request();
}

RealSubject : 定义 Proxy 所代表的真实实体。

1
2
3
4
5
6
class RealSubject extends Subject {
@Override
public void Request() {
System.out.println("真实的请求");
}
}

Proxy : 保存一个引用使得代理可以访问实体,并提供一个与 Subject 的接口相同的接口,这样代理就可以用来替代实体。

1
2
3
4
5
6
7
8
9
10
11
class Proxy extends Subject {
private RealSubject real;

@Override
public void Request() {
if (null == real) {
real = new RealSubject();
}
real.Request();
}
}

伪代码

本例演示如何使用代理模式在第三方腾讯视频 (TencentVideo, 代码示例中记为 TV) 程序库中添加延迟初始化和缓存。

img

程序库提供了视频下载类。 但是该类的效率非常低。 如果客户端程序多次请求同一视频, 程序库会反复下载该视频, 而不会将首次下载的文件缓存下来复用。

代理类实现和原下载器相同的接口, 并将所有工作委派给原下载器。 不过, 代理类会保存所有的文件下载记录, 如果程序多次请求同一文件, 它会返回缓存的文件。

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
// 远程服务接口。
interface ThirdPartyTVLib is
method listVideos()
method getVideoInfo(id)
method downloadVideo(id)

// 服务连接器的具体实现。该类的方法可以向腾讯视频请求信息。请求速度取决于
// 用户和腾讯视频的互联网连接情况。如果同时发送大量请求,即使所请求的信息
// 一模一样,程序的速度依然会减慢。
class ThirdPartyTVClass implements ThirdPartyTVLib is
method listVideos() is
// 向腾讯视频发送一个 API 请求。

method getVideoInfo(id) is
// 获取某个视频的元数据。

method downloadVideo(id) is
// 从腾讯视频下载一个视频文件。

// 为了节省网络带宽,我们可以将请求结果缓存下来并保存一段时间。但你可能无
// 法直接将这些代码放入服务类中。比如该类可能是第三方程序库的一部分或其签
// 名是`final(最终)`。因此我们会在一个实现了服务类接口的新代理类中放入
// 缓存代码。当代理类接收到真实请求后,才会将其委派给服务对象。
class CachedTVClass implements ThirdPartyTVLib is
private field service: ThirdPartyTVLib
private field listCache, videoCache
field needReset

constructor CachedTVClass(service: ThirdPartyTVLib) is
this.service = service

method listVideos() is
if (listCache == null || needReset)
listCache = service.listVideos()
return listCache

method getVideoInfo(id) is
if (videoCache == null || needReset)
videoCache = service.getVideoInfo(id)
return videoCache

method downloadVideo(id) is
if (!downloadExists(id) || needReset)
service.downloadVideo(id)

// 之前直接与服务对象交互的 GUI 类不需要改变,前提是它仅通过接口与服务对
// 象交互。我们可以安全地传递一个代理对象来代替真实服务对象,因为它们都实
// 现了相同的接口。
class TVManager is
protected field service: ThirdPartyTVLib

constructor TVManager(service: ThirdPartyTVLib) is
this.service = service

method renderVideoPage(id) is
info = service.getVideoInfo(id)
// 渲染视频页面。

method renderListPanel() is
list = service.listVideos()
// 渲染视频缩略图列表。

method reactOnUserInput() is
renderVideoPage()
renderListPanel()

// 程序可在运行时对代理进行配置。
class Application is
method init() is
aTVService = new ThirdPartyTVClass()
aTVProxy = new CachedTVClass(aTVService)
manager = new TVManager(aTVProxy)
manager.reactOnUserInput()

案例

使用示例: 尽管代理模式在绝大多数 Java 程序中并不常见, 但它在一些特殊情况下仍然非常方便。 当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时, 该模式是无可替代的。

Java 标准程序库中的一些代理模式的示例:

识别方法: 代理模式会将所有实际工作委派给一些其他对象。 除非代理是某个服务的子类, 否则每个代理方法最后都应该引用一个服务对象。

注解+反射+代理消除重复代码

假设银行提供了一些 API 接口,对参数的序列化有点特殊,不使用 JSON,而是需要我们把参数依次拼在一起构成一个大字符串。

按照银行提供的 API 文档的顺序,把所有参数构成定长的数据,然后拼接在一起作为整个字符串。因为每一种参数都有固定长度,未达到长度时需要做填充处理:

  • 字符串类型的参数不满长度部分需要以下划线右填充,也就是字符串内容靠左;
  • 数字类型的参数不满长度部分以 0 左填充,也就是实际数字靠右;
  • 货币类型的表示需要把金额向下舍入 2 位到分,以分为单位,作为数字类型同样进行
    左填充。

对所有参数做 MD5 操作作为签名(为了方便理解,Demo 中不涉及加盐处理)。

问题版本

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
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;

public class BankService {

public static String createUser(String name, String identity, String mobile, int age) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
//字符串靠左,多余的地方_填充
stringBuilder.append(String.format("%-10s", name).replace(' ', '_'));
//字符串靠左,多余的地方_填充
stringBuilder.append(String.format("%-18s", identity).replace(' ', '_'));
//数字靠右,多余的地方用0填充
stringBuilder.append(String.format("%05d", age));
//字符串靠左,多余的地方_填充
stringBuilder.append(String.format("%-11s", mobile).replace(' ', '_'));
//最后加上MD5作为签名
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
return Request.Post("http://localhost:45678/reflection/bank/createUser")
.bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}

public static String pay(long userId, BigDecimal amount) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
//数字靠右,多余的地方用0填充
stringBuilder.append(String.format("%020d", userId));
//金额向下舍入2位到分,以分为单位,作为数字靠右,多余的地方用0填充
stringBuilder.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue()));
//最后加上MD5作为签名
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
return Request.Post("http://localhost:45678/reflection/bank/pay")
.bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
}
}

在以上的代码版本中,存在以下问题:

  • 三种标准数据类型的处理逻辑有重复,稍有不慎就会出现 Bug;
  • 处理流程中字符串拼接、加签和发请求的逻辑,在所有方法重复;
  • 实际方法的入参的参数类型和顺序,不一定和接口要求一致,容易出错;
  • 代码层面针对每一个参数硬编码,无法清晰地进行核对,如果参数达到几十个、上百个,出错的概率极大。

优化版本

针对上面代码版本中的问题,可以使用 注解+反射+代理模式 解决重复代码。

【注解一】

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface BankAPI {

String desc() default "";

String url() default "";

}

【注解二】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface BankAPIField {

int order() default -1;

int length() default -1;

String type() default "";

}

【抽象类】

1
abstract class AbstractAPI {}

【代理类】

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
@Slf4j
public class BetterBankService {

public static String createUser(String name, String identity, String mobile, int age) throws IOException {
CreateUserAPI createUserAPI = new CreateUserAPI();
createUserAPI.setName(name);
createUserAPI.setIdentity(identity);
createUserAPI.setAge(age);
createUserAPI.setMobile(mobile);
return remoteCall(createUserAPI);
}

public static String pay(long userId, BigDecimal amount) throws IOException {
PayAPI payAPI = new PayAPI();
payAPI.setUserId(userId);
payAPI.setAmount(amount);
return remoteCall(payAPI);
}

private static String remoteCall(AbstractAPI api) throws IOException {
//从BankAPI注解获取请求地址
BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class);
bankAPI.url();
StringBuilder stringBuilder = new StringBuilder();
Arrays.stream(api.getClass().getDeclaredFields()) //获得所有字段
.filter(field -> field.isAnnotationPresent(BankAPIField.class)) //查找标记了注解的字段
.sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order())) //根据注解中的order对字段排序
.peek(field -> field.setAccessible(true)) //设置可以访问私有字段
.forEach(field -> {
//获得注解
BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class);
Object value = "";
try {
//反射获取字段值
value = field.get(api);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//根据字段类型以正确的填充方式格式化字符串
switch (bankAPIField.type()) {
case "S": {
stringBuilder.append(
String.format("%-" + bankAPIField.length() + "s", value.toString()).replace(' ', '_'));
break;
}
case "N": {
stringBuilder.append(
String.format("%" + bankAPIField.length() + "s", value.toString()).replace(' ', '0'));
break;
}
case "M": {
if (!(value instanceof BigDecimal)) {
throw new RuntimeException(
String.format("{} 的 {} 必须是BigDecimal", api, field));
}
stringBuilder.append(String.format("%0" + bankAPIField.length() + "d",
((BigDecimal) value).setScale(2, RoundingMode.DOWN)
.multiply(new BigDecimal("100"))
.longValue()));
break;
}
default:
break;
}
});
//签名逻辑
stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));
String param = stringBuilder.toString();
long begin = System.currentTimeMillis();
//发请求
String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url())
.bodyString(param, ContentType.APPLICATION_JSON)
.execute().returnContent().asString();
log.info("调用银行API {} url:{} 参数:{} 耗时:{}ms", bankAPI.desc(), bankAPI.url(), param,
System.currentTimeMillis() - begin);
return result;
}

}

【注解修饰的 API 接口一】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import lombok.Data;

@BankAPI(url = "/bank/createUser", desc = "创建用户接口")
@Data
public class CreateUserAPI extends AbstractAPI {

@BankAPIField(order = 1, type = "S", length = 10)
private String name;
@BankAPIField(order = 2, type = "S", length = 18)
private String identity;
@BankAPIField(order = 4, type = "S", length = 11)
private String mobile;
@BankAPIField(order = 3, type = "N", length = 5)
private int age;

}

【注解修饰的 API 接口二】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import lombok.Data;

import java.math.BigDecimal;

@BankAPI(url = "/bank/pay", desc = "支付接口")
@Data
public class PayAPI extends AbstractAPI {

@BankAPIField(order = 1, type = "N", length = 20)
private long userId;
@BankAPIField(order = 2, type = "M", length = 10)
private BigDecimal amount;

}

与其他模式的关系

  • 适配器模式能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰模式则能为对象提供加强的接口。
  • 外观模式代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
  • 装饰代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。

参考资料

设计模式之享元模式

意图

享元模式 (Flyweight) 是一种结构型设计模式,它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

适用场景

  • 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。

结构

img

结构说明

  1. 享元模式只是一种优化。 在应用该模式之前, 你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题, 并且确保该问题无法使用其他更好的方式来解决。
  2. 享元 (Flyweight) 类包含原始对象中部分能在多个对象中共享的状态。 同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
  3. 情景 (Context) 类包含原始对象中各不相同的外在状态。 情景与享元对象组合在一起就能表示原始对象的全部状态。
  4. 通常情况下, 原始对象的行为会保留在享元类中。 因此调用享元方法必须提供部分外在状态作为参数。 但你也可将行为移动到情景类中, 然后将连入的享元作为单纯的数据对象。
  5. 客户端 (Client) 负责计算或存储享元的外在状态。 在客户端看来, 享元是一种可在运行时进行配置的模板对象, 具体的配置方式为向其方法中传入一些情景数据参数。
  6. 享元工厂 (Flyweight Factory) 会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需调用工厂并向其传递目标享元的一些内在状态即可。 工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。

结构代码范式

Flyweight : 它是所有具体享元类的超类或接口,通过这个接口,Flyweight 可以接受并作用于外部状态。

1
2
3
abstract class Flyweight {
public abstract void operation(int extrinsicstates);
}

ConcreteFlyweight : 是继承 Flyweight 超类或实现 Flyweight 接口,并为内部状态增加存储空间。

1
2
3
4
5
6
class ConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicstates) {
System.out.println("共享的Flyweight : " + extrinsicstates);
}
}

UnsharedConcreteFlyweight : 指那些不需要共享的 Flyweight 子类,因为 Flyweight 接口共享成为可能,但它并不强制共享。

1
2
3
4
5
6
class UnsharedConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicstates) {
System.out.println("不共享的Flyweight : " + extrinsicstates);
}
}

FlywightFactory :是一个享元工厂,用来创建并管理 Flyweight 对象。它主要是用来确保合理地共享 Flyweight ,当用户请求一个 Flyweight 时, FlyweightFactory 对象提供一个已创建的实例或创建一个(如果对象不存在的话)。

1
2
3
4
5
6
7
8
9
10
11
12
13
class FlywightFactory {
private Hashtable<String, Flyweight> flyweights = new Hashtable<String, Flyweight>();

public FlywightFactory() {
flyweights.put("X", new ConcreteFlyweight());
flyweights.put("Y", new ConcreteFlyweight());
flyweights.put("Z", new ConcreteFlyweight());
}

public Flyweight getFlyweight(String key) {
return ((Flyweight)flyweights.get(key));
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FlyweightPattern {
public static void main(String[] args) {
int extrinsicstates = 1;
FlywightFactory factory = new FlywightFactory();

Flyweight fx = factory.getFlyweight("X");
fx.operation(extrinsicstates);

Flyweight fy = factory.getFlyweight("Y");
fy.operation(++extrinsicstates);

Flyweight fz = factory.getFlyweight("Z");
fz.operation(++extrinsicstates);

Flyweight uf = new UnsharedConcreteFlyweight();
uf.operation(++extrinsicstates);
}
}

输出

1
2
3
4
共享的Flyweight : 1
共享的Flyweight : 2
共享的Flyweight : 3
不共享的Flyweight : 4

伪代码

img

在本例中, 享元模式能有效减少在画布上渲染数百万个树状对象时所需的内存。

该模式从主要的 Tree 类中抽取内在状态, 并将其移动到享元类 树种类Tree­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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 享元类包含一个树的部分状态。这些成员变量保存的数值对于特定树而言是唯一
// 的。例如,你在这里找不到树的坐标。但这里有很多树木之间所共有的纹理和颜
// 色。由于这些数据的体积通常非常大,所以如果让每棵树都其进行保存的话将耗
// 费大量内存。因此,我们可将纹理、颜色和其他重复数据导出到一个单独的对象
// 中,然后让众多的单个树对象去引用它。
class TreeType is
field name
field color
field texture
constructor TreeType(name, color, texture) { ... }
method draw(canvas, x, y) is
// 1. 创建特定类型、颜色和纹理的位图。
// 2. 在画布坐标 (X,Y) 处绘制位图。

// 享元工厂决定是否复用已有享元或者创建一个新的对象。
class TreeFactory is
static field treeTypes: collection of tree types
static method getTreeType(name, color, texture) is
type = treeTypes.find(name, color, texture)
if (type == null)
type = new TreeType(name, color, texture)
treeTypes.add(type)
return type

// 情景对象包含树状态的外在部分。程序中可以创建数十亿个此类对象,因为它们
// 体积很小:仅有两个整型坐标和一个引用成员变量。
class Tree is
field x,y
field type: TreeType
constructor Tree(x, y, type) { ... }
method draw(canvas) is
type.draw(canvas, this.x, this.y)

// 树(Tree)和森林(Forest)类是享元的客户端。如果不打算继续对树类进行开
// 发,你可以将它们合并。
class Forest is
field trees: collection of Trees

method plantTree(x, y, name, color, texture) is
type = TreeFactory.getTreeType(name, color, texture)
tree = new Tree(x, y, type)
trees.add(tree)

method draw(canvas) is
foreach (tree in trees) do
tree.draw(canvas)

案例

使用示例: 享元模式只有一个目的: 将内存消耗最小化。 如果你的程序没有遇到内存容量不足的问题, 则可以暂时忽略该模式。

享元模式在核心 Java 程序库中的示例:

识别方法: 享元可以通过构建方法来识别, 它会返回缓存对象而不是创建新的对象。

与其他模式的关系

  • 你可以使用享元模式实现组合模式树的共享叶节点以节省内存。
  • 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。
  • 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。

参考资料

设计模式之桥接模式

意图

桥接模式 (Bridge) 是一种结构型设计模式, 可将抽象部分与实现部分分离,使它们都可以独立的变化。

如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。抽象化角色和具体化角色都应该可以被子类扩展。在这种情况下,桥接模式可以灵活地组合不同的抽象化角色和具体化角色,并独立化地扩展。

设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。

适用场景

  • 如果你想要拆分或重组一个具有多重功能的庞杂类(例如能与多个数据库服务器进行交互的类),可以使用桥接模式。
  • 如果你希望在几个独立维度上扩展一个类, 可使用该模式。
  • 如果你需要在运行时切换不同实现方法, 可使用桥接模式。

结构

img

结构说明

  1. 抽象部分 (Abstraction) 提供高层控制逻辑, 依赖于完成底层实际工作的实现对象。
  2. 实现部分 (Implementation) 为所有具体实现声明通用接口。 抽象部分仅能通过在这里声明的方法与实现对象交互。
    • 抽象部分可以列出和实现部分一样的方法, 但是抽象部分通常声明一些复杂行为, 这些行为依赖于多种由实现部分声明的原语操作。
  3. 具体实现 (Concrete Implementations) 中包括特定于平台的代码。
  4. 精确抽象 (Refined Abstraction) 提供控制逻辑的变体。 与其父类一样, 它们通过通用实现接口与不同的实现进行交互。
  5. 通常情况下, 客户端 (Client) 仅关心如何与抽象部分合作。 但是, 客户端需要将抽象对象与一个实现对象连接起来。

结构代码范式

【Implementor】定义实现接口

1
2
3
4
interface Implementor {
// 实现抽象部分需要的某些具体功能
public void operationImpl();
}

【Abstraction】定义抽象接口

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class Abstraction {
// 持有一个 Implementor 对象,形成聚合关系
protected Implementor implementor;

public Abstraction(Implementor implementor) {
this.implementor = implementor;
}

// 可能需要转调实现部分的具体实现
public void operation() {
implementor.operationImpl();
}
}

【ConcreteImplementor】实现 Implementor 中定义的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ConcreteImplementorA implements Implementor {
@Override
public void operationImpl() {
// 真正的实现
System.out.println("具体实现A");
}
}

class ConcreteImplementorB implements Implementor {
@Override
public void operationImpl() {
// 真正的实现
System.out.println("具体实现B");
}
}

【RefinedAbstraction】扩展 Abstraction 类。

1
2
3
4
5
6
7
8
9
10
11
12
class RefinedAbstraction extends Abstraction {

public RefinedAbstraction(Implementor implementor) {
super(implementor);
}

public void otherOperation() {
// 实现一定的功能,可能会使用具体实现部分的实现方法,
// 但是本方法更大的可能是使用 Abstraction 中定义的方法,
// 通过组合使用 Abstraction 中定义的方法来完成更多的功能。
}
}

【客户端】

1
2
3
4
5
6
7
8
public class BridgePattern {
public static void main(String[] args) {
Implementor implementor = new ConcreteImplementorA();
RefinedAbstraction abstraction = new RefinedAbstraction(implementor);
abstraction.operation();
abstraction.otherOperation();
}
}

【输出】

1
2
具体实现A
其他操作

伪代码

img

遥控器基类声明了一个指向设备对象的引用成员变量。 所有遥控器通过通用设备接口与设备进行交互, 使得同一个遥控器可以支持不同类型的设备。

你可以开发独立于设备类的遥控器类, 只需新建一个遥控器子类即可。 例如, 基础遥控器可能只有两个按钮, 但你可在其基础上扩展新功能, 比如额外的一节电池或一块触摸屏。

客户端代码通过遥控器构造函数将特定种类的遥控器与设备对象连接起来。

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
// “抽象部分”定义了两个类层次结构中“控制”部分的接口。它管理着一个指向“实
// 现部分”层次结构中对象的引用,并会将所有真实工作委派给该对象。
class RemoteControl is
protected field device: Device
constructor RemoteControl(device: Device) is
this.device = device
method togglePower() is
if (device.isEnabled()) then
device.disable()
else
device.enable()
method volumeDown() is
device.setVolume(device.getVolume() - 10)
method volumeUp() is
device.setVolume(device.getVolume() + 10)
method channelDown() is
device.setChannel(device.getChannel() - 1)
method channelUp() is
device.setChannel(device.getChannel() + 1)


// 你可以独立于设备类的方式从抽象层中扩展类。
class AdvancedRemoteControl extends RemoteControl is
method mute() is
device.setVolume(0)


// “实现部分”接口声明了在所有具体实现类中通用的方法。它不需要与抽象接口相
// 匹配。实际上,这两个接口可以完全不一样。通常实现接口只提供原语操作,而
// 抽象接口则会基于这些操作定义较高层次的操作。
interface Device is
method isEnabled()
method enable()
method disable()
method getVolume()
method setVolume(percent)
method getChannel()
method setChannel(channel)


// 所有设备都遵循相同的接口。
class Tv implements Device is
// ...

class Radio implements Device is
// ...


// 客户端代码中的某个位置。
tv = new Tv()
remote = new RemoteControl(tv)
remote.togglePower()

radio = new Radio()
remote = new AdvancedRemoteControl(radio)

案例

使用示例: 桥接模式在处理跨平台应用、 支持多种类型的数据库服务器或与多个特定种类 (例如云平台和社交网络等) 的 API 供应商协作时会特别有用。

识别方法: 桥接可以通过一些控制实体及其所依赖的多个不同平台之间的明确区别来进行识别。

Java 中桥接模式应用最经典的代表无疑是日志组件 slf4j 的桥接 jar 包。

假如,你正在开发应用程序所调用的组件当中已经使用了 common-logging,这时你需要 jcl-over-slf4j.jar 把日志信息输出重定向到 slf4j-api,slf4j-api 再去调用 slf4j 实际依赖的日志组件。这个过程称为桥接。下图是官方的 slf4j 桥接策略图:

img

与其他模式的关系

  • 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
  • 桥接状态模式策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 你可以将抽象工厂模式桥接搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。
  • 你可以结合使用生成器模式桥接模式主管类负责抽象工作, 各种不同的生成器负责实现工作。

参考资料

设计模式之装饰模式

意图

装饰模式 (Decorator) 是一种结构型设计模式,动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator 模式相比生成子类更为灵活。

  • 装饰对象和真实对象有相同的接口。这样客户端对象就能以和真实对象相同的方式和装饰对象交互。
  • 装饰对象包含一个真实对象的引用(reference)。
  • 装饰对象接受所有来自客户端的请求。它把这些请求转发给真实的对象。
  • 装饰对象可以在转发这些请求以前或以后增加一些附加功能。这样就确保了在运行时,不用修改给定对象的结构就可以在外部增加附加的功能。在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展。

适合场景

  • 如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式。
  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式。

结构

img

结构说明

  1. 部件 (Component) 声明封装器和被封装对象的公用接口。
  2. 具体部件 (Concrete Component) 类是被封装对象所属的类。 它定义了基础行为, 但装饰类可以改变这些行为。
  3. 基础装饰 (Base Decorator) 类拥有一个指向被封装对象的引用成员变量。 该变量的类型应当被声明为通用部件接口, 这样它就可以引用具体的部件和装饰。 装饰基类会将所有操作委派给被封装的对象。
  4. 具体装饰类 (Concrete Decorators) 定义了可动态添加到部件的额外行为。 具体装饰类会重写装饰基类的方法, 并在调用父类方法之前或之后进行额外的行为。
  5. 客户端 (Client) 可以使用多层装饰来封装部件, 只要它能使用通用接口与所有对象互动即可。

结构代码范式

Component : 定义一个对象接口,可以给这些对象动态地添加职责。

1
2
3
interface Component {
public void operation();
}

ConcreteComponent : 实现 Component 定义的接口。

1
2
3
4
5
6
class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("初始行为");
}
}

Decorator : 装饰抽象类,继承了 Component, 从外类来扩展 Component 类的功能,但对于 Component 来说,是无需知道 Decorator 的存在的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Decorator implements Component {
// 持有一个 Component 对象,和 Component 形成聚合关系
protected Component component;

// 传入要进一步修饰的对象
public Decorator(Component component) {
this.component = component;
}

@Override
// 调用要修饰对象的原方法
public void operation() {
component.operation();
}
}

ConcreteDecorator : 具体的装饰对象,起到给 Component 添加职责的功能。

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
class ConcreteDecoratorA extends Decorator {
private String addedState = "新属性1";

public ConcreteDecoratorA(Component component) {
super(component);
}

public void operation() {
super.operation();
System.out.println("添加属性: " + addedState);
}
}

class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}

public void operation() {
super.operation();
AddedBehavior();
}

public void AddedBehavior() {
System.out.println("添加行为");
}
}

【客户端】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DecoratorPattern {
public static void main(String[] args) {
Component component = new ConcreteComponent();
component.operation();

System.out.println("======================================");
Decorator decoratorA = new ConcreteDecoratorA(component);
decoratorA.operation();

System.out.println("======================================");
Decorator decoratorB = new ConcreteDecoratorB(decoratorA);
decoratorB.operation();
}
}

【输出】

1
2
3
4
5
6
7
8
初始行为
======================================
初始行为
添加属性: 新属性1
======================================
初始行为
添加属性: 新属性1
添加行为

伪代码

img

在本例中, 装饰模式能够对敏感数据进行压缩和加密, 从而将数据从使用数据的代码中独立出来。

程序使用一对装饰来封装数据源对象。 这两个封装器都改变了从磁盘读写数据的方式:

  • 当数据即将被写入磁盘前, 装饰对数据进行加密和压缩。 在原始类对改变毫无察觉的情况下, 将加密后的受保护数据写入文件。
  • 当数据刚从磁盘读出后, 同样通过装饰对数据进行解压和解密。 装饰和数据源类实现同一接口, 从而能在客户端代码中相互替换。
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
// 装饰可以改变组件接口所定义的操作。
interface DataSource is
method writeData(data)
method readData():data

// 具体组件提供操作的默认实现。这些类在程序中可能会有几个变体。
class FileDataSource implements DataSource is
constructor FileDataSource(filename) { ... }

method writeData(data) is
// 将数据写入文件。

method readData():data is
// 从文件读取数据。

// 装饰基类和其他组件遵循相同的接口。该类的主要任务是定义所有具体装饰的封
// 装接口。封装的默认实现代码中可能会包含一个保存被封装组件的成员变量,并
// 且负责对其进行初始化。
class DataSourceDecorator implements DataSource is
protected field wrappee: DataSource

constructor DataSourceDecorator(source: DataSource) is
wrappee = source

// 装饰基类会直接将所有工作分派给被封装组件。具体装饰中则可以新增一些
// 额外的行为。
method writeData(data) is
wrappee.writeData(data)

// 具体装饰可调用其父类的操作实现,而不是直接调用被封装对象。这种方式
// 可简化装饰类的扩展工作。
method readData():data is
return wrappee.readData()

// 具体装饰必须在被封装对象上调用方法,不过也可以自行在结果中添加一些内容。
// 装饰必须在调用封装对象之前或之后执行额外的行为。
class EncryptionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. 对传递数据进行加密。
// 2. 将加密后数据传递给被封装对象 writeData(写入数据)方法。

method readData():data is
// 1. 通过被封装对象的 readData(读取数据)方法获取数据。
// 2. 如果数据被加密就尝试解密。
// 3. 返回结果。

// 你可以将对象封装在多层装饰中。
class CompressionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. 压缩传递数据。
// 2. 将压缩后数据传递给被封装对象 writeData(写入数据)方法。

method readData():data is
// 1. 通过被封装对象的 readData(读取数据)方法获取数据。
// 2. 如果数据被压缩就尝试解压。
// 3. 返回结果。


// 选项 1:装饰组件的简单示例
class Application is
method dumbUsageExample() is
source = new FileDataSource("somefile.dat")
source.writeData(salaryRecords)
// 已将明码数据写入目标文件。

source = new CompressionDecorator(source)
source.writeData(salaryRecords)
// 已将压缩数据写入目标文件。

source = new EncryptionDecorator(source)
// 源变量中现在包含:
// Encryption > Compression > FileDataSource
source.writeData(salaryRecords)
// 已将压缩且加密的数据写入目标文件。


// 选项 2:客户端使用外部数据源。SalaryManager(工资管理器)对象并不关心
// 数据如何存储。它们会与提前配置好的数据源进行交互,数据源则是通过程序配
// 置器获取的。
class SalaryManager is
field source: DataSource

constructor SalaryManager(source: DataSource) { ... }

method load() is
return source.readData()

method save() is
source.writeData(salaryRecords)
// ...其他有用的方法...


// 程序可在运行时根据配置或环境组装不同的装饰堆桟。
class ApplicationConfigurator is
method configurationExample() is
source = new FileDataSource("salary.dat")
if (enabledEncryption)
source = new EncryptionDecorator(source)
if (enabledCompression)
source = new CompressionDecorator(source)

logger = new SalaryManager(source)
salary = logger.load()
// ...

案例

使用示例: 装饰模式在 Java 代码中可谓是标准配置, 尤其是在与流式加载相关的代码中。

Java 核心程序库中有一些关于装饰的示例:

识别方法: 装饰可通过以当前类或对象为参数的创建方法或构造函数来识别。

与其他模式的关系

  • 适配器模式可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
  • 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
  • 责任链模式装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。
    • 责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。
  • 组合模式装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
    • 装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
    • 但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。
  • 大量使用组合装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。
  • 装饰可让你更改对象的外表, 策略模式则让你能够改变其本质。
  • 装饰代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。

参考资料

设计模式之适配器模式

意图

适配器模式 (Adapter)是一种结构型设计模式, 它能使不兼容的对象能够相互合作。

适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。

适配器不仅可以转换不同格式的数据, 其还有助于采用不同接口的对象之间的合作。 它的运作方式如下:

  • 适配器实现与其中一个现有对象兼容的接口。
  • 现有对象可以使用该接口安全地调用适配器方法。
  • 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。

适用场景

  • 当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。
  • 如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。

结构

适配器实现了其中一个对象的接口, 并对另一个对象进行封装。

img

结构说明

  1. 客户端 (Client) 是包含当前程序业务逻辑的类。
  2. 客户端接口 (Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。
  3. 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。
  4. 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。
  5. 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。

结构代码范式

【Target】

定义用户实际需要的接口

1
2
3
abstract class Target {
public abstract void Request();
}

【Adaptee】

定义一个需要适配的接口

1
2
3
4
5
class Adaptee {
public void SpecificRequest() {
System.out.println("特殊请求");
}
}

【Adapter】

通过在内部包装一个 Adaptee 对象,把源接口转换成目标接口。

1
2
3
4
5
6
7
8
class Adapter extends Target {
private Adaptee adaptee = new Adaptee();

@Override
public void Request() {
adaptee.SpecificRequest();
}
}

【客户端】

1
2
3
4
5
6
public class AdapterPattern {
public static void main(String[] args) {
Target target = new Adapter();
target.Request();
}
}

【输出】

1
特殊请求

伪代码

img

适配器假扮成一个圆钉 (Round­Peg), 其半径等于方钉 (Square­Peg) 横截面对角线的一半 (即能够容纳方钉的最小外接圆的半径)。

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
// 假设你有两个接口相互兼容的类:圆孔(Round­Hole)和圆钉(Round­Peg)。
class RoundHole is
constructor RoundHole(radius) { ... }

method getRadius() is
// 返回孔的半径。

method fits(peg: RoundPeg) is
return this.getRadius() >= peg.getRadius()

class RoundPeg is
constructor RoundPeg(radius) { ... }

method getRadius() is
// 返回钉子的半径。


// 但还有一个不兼容的类:方钉(Square­Peg)。
class SquarePeg is
constructor SquarePeg(width) { ... }

method getWidth() is
// 返回方钉的宽度。


// 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
// 配器对象作为圆钉。
class SquarePegAdapter extends RoundPeg is
// 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。
private field peg: SquarePeg

constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg

method getRadius() is
// 适配器会假扮为一个圆钉,
// 其半径刚好能与适配器实际封装的方钉搭配起来。
return peg.getWidth() * Math.sqrt(2) / 2


// 客户端代码中的某个位置。
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

案例

适配器模式在 Java 代码中很常见。

Java 核心程序库中有一些标准的适配器:

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

与其他模式的关系

  • 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
  • 适配器可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
  • 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
  • 外观模式为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
  • 桥接状态模式策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

参考资料

设计模式之组合模式

意图

组合模式 (Component) 是一种结构型设计模式,将对象组合成树形结构以表示“部分-整体”的层次结构。

组合模式使得用户对单个对象和组合对象的使用具有唯一性

适用场景

组合模式的适用场景:

  • 想要表示对象的部分-整体层次结构。
  • 想要客户端忽略组合对象与单个对象的差异,客户端将统一地使用组合结构中的所有对象。

关于分级数据结构的一个普遍性的例子是你每次使用电脑时所遇到的 文件系统

文件系统由目录和文件组成。每个目录都可以装内容。目录的内容可以是文件,也 可以是目录。

按照这种方式,计算机的文件系统就是以递归结构来组织的。如果你想要描述这样的数据结构,那么你可以使用组合模式。

结构

img

结构说明

  1. 组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作。
  2. 叶节点 (Leaf) 是树的基本结构, 它不包含子项目。一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。
  3. 容器 (Container)——又名 “组合 (Composite)”——是包含叶节点或其他容器等子项目的单位。 容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互。容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。
  4. 客户端 (Client) 通过组件接口与所有项目交互。 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互。

结构代码范式

Component : 组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理 Component 的子部件。

1
2
3
4
5
6
7
8
9
10
11
abstract class Component {
protected String name;

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

public abstract void Add(Component c);
public abstract void Remove(Component c);
public abstract void Display(int depth);
}

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
class Leaf extends Component {

public Leaf(String name) {
super(name);
}

@Override
public void Add(Component c) {
System.out.println("Can not add to a leaf");
}

@Override
public void Remove(Component c) {
System.out.println("Can not remove from a leaf");
}

@Override
public void Display(int depth) {
String temp = "";
for (int i = 0; i < depth; i++)
temp += '-';
System.out.println(temp + name);
}

}

Composite : 定义枝节点行为,用来存储子部件,在 Component 接口中实现与子部件相关的操作。例如 Add 和 Remove。

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
class Composite extends Component {

private List<Component> children = new ArrayList<Component>();

public Composite(String name) {
super(name);
}

@Override
public void Add(Component c) {
children.add(c);
}

@Override
public void Remove(Component c) {
children.remove(c);
}

@Override
public void Display(int depth) {
String temp = "";
for (int i = 0; i < depth; i++)
temp += '-';
System.out.println(temp + name);

for (Component c : children) {
c.Display(depth + 2);
}
}

}

Client : 通过 Component 接口操作结构中的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CompositePattern {

public static void main(String[] args) {
Composite root = new Composite("root");
root.Add(new Leaf("Leaf A"));
root.Add(new Leaf("Leaf B"));

Composite compX = new Composite("Composite X");
compX.Add(new Leaf("Leaf XA"));
compX.Add(new Leaf("Leaf XB"));
root.Add(compX);

Composite compXY = new Composite("Composite XY");
compXY.Add(new Leaf("Leaf XYA"));
compXY.Add(new Leaf("Leaf XYB"));
compX.Add(compXY);

root.Display(1);
}

}

伪代码

在本例中, 我们将借助组合模式帮助你在图形编辑器中实现一系列的几何图形。

img

组合图形Compound­Graphic 是一个容器, 它可以由多个包括容器在内的子图形构成。 组合图形与简单图形拥有相同的方法。 但是, 组合图形自身并不完成具体工作, 而是将请求递归地传递给自己的子项目, 然后 “汇总” 结果。

通过所有图形类所共有的接口, 客户端代码可以与所有图形互动。 因此, 客户端不知道与其交互的是简单图形还是组合图形。 客户端可以与非常复杂的对象结构进行交互, 而无需与组成该结构的实体类紧密耦合。

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
// 组件接口会声明组合中简单和复杂对象的通用操作。
interface Graphic is
method move(x, y)
method draw()

// 叶节点类代表组合的终端对象。叶节点对象中不能包含任何子对象。叶节点对象
// 通常会完成实际的工作,组合对象则仅会将工作委派给自己的子部件。
class Dot implements Graphic is
field x, y

constructor Dot(x, y) { ... }

method move(x, y) is
this.x += x, this.y += y

method draw() is
// 在坐标位置(X,Y)处绘制一个点。

// 所有组件类都可以扩展其他组件。
class Circle extends Dot is
field radius

constructor Circle(x, y, radius) { ... }

method draw() is
// 在坐标位置(X,Y)处绘制一个半径为 R 的圆。

// 组合类表示可能包含子项目的复杂组件。组合对象通常会将实际工作委派给子项
// 目,然后“汇总”结果。
class CompoundGraphic implements Graphic is
field children: array of Graphic

// 组合对象可在其项目列表中添加或移除其他组件(简单的或复杂的皆可)。
method add(child: Graphic) is
// 在子项目数组中添加一个子项目。

method remove(child: Graphic) is
// 从子项目数组中移除一个子项目。

method move(x, y) is
foreach (child in children) do
child.move(x, y)

// 组合会以特定的方式执行其主要逻辑。它会递归遍历所有子项目,并收集和
// 汇总其结果。由于组合的子项目也会将调用传递给自己的子项目,以此类推,
// 最后组合将会完成整个对象树的遍历工作。
method draw() is
// 1. 对于每个子部件:
// - 绘制该部件。
// - 更新边框坐标。
// 2. 根据边框坐标绘制一个虚线长方形。


// 客户端代码会通过基础接口与所有组件进行交互。这样一来,客户端代码便可同
// 时支持简单叶节点组件和复杂组件。
class ImageEditor is
field all: CompoundGraphic

method load() is
all = new CompoundGraphic()
all.add(new Dot(1, 2))
all.add(new Circle(5, 3, 10))
// ...

// 将所需组件组合为复杂的组合组件。
method groupSelected(components: array of Graphic) is
group = new CompoundGraphic()
foreach (component in components) do
group.add(component)
all.remove(component)
all.add(group)
// 所有组件都将被绘制。
all.draw()

案例

使用实例: 组合模式在 Java 代码中很常见,常用于表示与图形打交道的用户界面组件或代码的层次结构。

下面是一些来自 Java 标准程序库中的组合示例:

识别方法: 组合可以通过将同一抽象或接口类型的实例放入树状结构的行为方法来轻松识别。

与其他模式的关系

  • 桥接模式状态模式策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 你可以在创建复杂组合树时使用生成器模式, 因为这可使其构造步骤以递归的方式运行。
  • 责任链模式通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。
  • 你可以使用迭代器模式来遍历组合树。
  • 你可以使用访问者模式对整个组合树执行操作。
  • 你可以使用享元模式实现组合树的共享叶节点以节省内存。
  • 组合装饰模式的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
    • 装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
    • 但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。
  • 大量使用组合装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。

参考资料

设计模式之模板方法模式

意图

模板方法模式 (Template Method) 是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

模板方法模式是所有模式中最为常见的几个模式之一,是基于继承代码复用的基本技术。,没有关联关系。 因此,在模板方法模式的类结构图中,只有继承关系

模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。

代表这些具体逻辑步骤的方法称做**基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method)**,这个设计模式的名字就是从此而来。

在模板方法模式中,首先父类会定义一个算法的框架,即实现算法所必须的所有方法。

  • 其中,具有共性的代码放在父类的具体方法中。

  • 各个子类特殊性的代码放在子类的具体方法中。但是父类中需要有对应抽象方法声明。

  • 钩子方法可以让子类决定是否对算法的不同点进行挂钩。

适用场景

  • 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
  • 当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。

结构

img

结构说明

  1. 抽象类 (Abstract­Class) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象类型, 也可以提供一些默认实现。
  2. 具体类 (Concrete­Class) 可以重写所有步骤, 但不能重写模板方法自身。

结构代码范式

AbstractClass : 抽象类,定义并实现一个模板方法。这个模板方法定义了算法的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类去实现。顶级逻辑也有可能调用一些具体方法。

1
2
3
4
5
6
7
8
9
abstract class AbstractClass {
public abstract void PrimitiveOperation1();
public abstract void PrimitiveOperation2();

public void TemplateMethod() {
PrimitiveOperation1();
PrimitiveOperation2();
}
}

ConcreteClass : 实现实现父类所定义的一个或多个抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ConcreteClassA extends AbstractClass {
@Override
public void PrimitiveOperation1() {
System.out.println("具体A类方法1");
}

@Override
public void PrimitiveOperation2() {
System.out.println("具体A类方法2");
}
}

class ConcreteClassB extends AbstractClass {
@Override
public void PrimitiveOperation1() {
System.out.println("具体B类方法1");
}

@Override
public void PrimitiveOperation2() {
System.out.println("具体B类方法2");
}
}

客户端

1
2
3
4
5
6
7
8
public class TemplateMethodPattern {
public static void main(String[] args) {
AbstractClass objA = new ConcreteClassA();
AbstractClass objB = new ConcreteClassB();
objA.TemplateMethod();
objB.TemplateMethod();
}
}

伪代码

本例中的模板方法模式为一款简单策略游戏中人工智能的不同分支提供 “框架”。

模板方法模式示例的结构

一款简单游戏的 AI 类。

游戏中所有的种族都有几乎同类的单位和建筑。 因此你可以在不同的种族上复用相同的 AI 结构, 同时还需要具备重写一些细节的能力。 通过这种方式, 你可以重写半兽人的 AI 使其更富攻击性, 也可以让人类侧重防守, 还可以禁止怪物建造建筑。 在游戏中新增种族需要创建新的 AI 子类, 还需要重写 AI 基类中所声明的默认方法。

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
// 抽象类定义了一个模板方法,其中通常会包含某个由抽象原语操作调用组成的算
// 法框架。具体子类会实现这些操作,但是不会对模板方法做出修改。
class GameAI is
// 模板方法定义了某个算法的框架。
method turn() is
collectResources()
buildStructures()
buildUnits()
attack()

// 某些步骤可在基类中直接实现。
method collectResources() is
foreach (s in this.builtStructures) do
s.collect()

// 某些可定义为抽象类型。
abstract method buildStructures()
abstract method buildUnits()

// 一个类可包含多个模板方法。
method attack() is
enemy = closestEnemy()
if (enemy == null)
sendScouts(map.center)
else
sendWarriors(enemy.position)

abstract method sendScouts(position)
abstract method sendWarriors(position)

// 具体类必须实现基类中的所有抽象操作,但是它们不能重写模板方法自身。
class OrcsAI extends GameAI is
method buildStructures() is
if (there are some resources) then
// 建造农场,接着是谷仓,然后是要塞。

method buildUnits() is
if (there are plenty of resources) then
if (there are no scouts)
// 建造苦工,将其加入侦查编组。
else
// 建造兽族步兵,将其加入战士编组。

// ...

method sendScouts(position) is
if (scouts.length > 0) then
// 将侦查编组送到指定位置。

method sendWarriors(position) is
if (warriors.length > 5) then
// 将战斗编组送到指定位置。

// 子类可以重写部分默认的操作。
class MonstersAI extends GameAI is
method collectResources() is
// 怪物不会采集资源。

method buildStructures() is
// 怪物不会建造建筑。

method buildUnits() is
// 怪物不会建造单位。

案例

模板方法模式应用场景十分广泛。

在《Head First》的模板方法模式章节里列举了一个十分具有代表性的例子。

现实生活中,茶和咖啡是随处可见的饮料。冲泡一杯茶或冲泡一杯咖啡的过程是怎样的?

我们来整理一下流程。

  • 泡茶: 烧开水 ==> 冲泡茶叶 ==> 倒入杯中 ==> 添加柠檬
  • 泡咖啡: 烧开水 ==> 冲泡咖啡 ==> 倒入杯中 ==> 添加糖和牛奶

由以上处理步骤不难发现,准备这两种饮料的处理过程非常相似。我们可以使用模板类方法去限定制作饮料的算法框架。

其中相同的具有共性的步骤(如烧开水、倒入杯中),直接在抽象类中给出具体实现。

而对于有差异性的步骤,则在各自的具体类中给出实现。

【抽象类】

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
abstract class Beverage {

// 模板方法,决定了算法骨架。相当于TemplateMethod()方法
public void prepareBeverage() {
boilWater();
brew();
pourInCup();
if (customWantsCondiments())
{
addCondiments();
}
}

// 共性操作,直接在抽象类中定义
public void boilWater() {
System.out.println("烧开水");
}

// 共性操作,直接在抽象类中定义
public void pourInCup() {
System.out.println("倒入杯中");
}

// 钩子方法,决定某些算法步骤是否挂钩在算法中
public boolean customWantsCondiments() {
return true;
}

// 特殊操作,在子类中具体实现
public abstract void brew();

// 特殊操作,在子类中具体实现
public abstract void addCondiments();

}

【具体类】

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
class Tea extends Beverage {

@Override
public void brew() {
System.out.println("冲泡茶叶");
}

@Override
public void addCondiments() {
System.out.println("添加柠檬");
}

}

class Coffee extends Beverage {

@Override
public void brew() {
System.out.println("冲泡咖啡豆");
}

@Override
public void addCondiments() {
System.out.println("添加糖和牛奶");
}

}

【客户端】

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {

System.out.println("============= 准备茶 =============");
Beverage tea = new Tea();
tea.prepareBeverage();

System.out.println("============= 准备咖啡 =============");
Beverage coffee = new Coffee();
coffee.prepareBeverage();

}

输出

1
2
3
4
5
6
7
8
9
10
============= 准备茶 =============
烧开水
冲泡茶叶
倒入杯中
添加柠檬
============= 准备咖啡 =============
烧开水
冲泡咖啡豆
倒入杯中
添加糖和牛奶

案例

使用示例: 模版方法模式在 Java 框架中很常见。 开发者通常使用它来向框架用户提供通过继承实现的、 对标准功能进行扩展的简单方式。

这里是一些核心 Java 程序库中模版方法的示例:

识别方法: 模版方法可以通过行为方法来识别, 该方法已有一个在基类中定义的 “默认” 行为。

消除 if … else 和重复代码

假设要开发一个购物车功能,针对不同用户进行不同的处理:

  • 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣;
  • VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
  • 内部用户可以免运费,无商品折扣。

问题 1.0 版本

普通用户购物车

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

public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();

//把Map的购物车转换为Item列表
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

//处理运费和商品优惠
itemList.stream().forEach(item -> {
//运费为商品总价的10%
item.setDeliveryPrice(
item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
//无优惠
item.setCouponPrice(BigDecimal.ZERO);
});

//计算纯商品总价
cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
//计算运费总价
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算总优惠
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//应付总价=商品总价+运费总价-总优惠
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

}

VIP 用户购物车

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

public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();

List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

itemList.stream().forEach(item -> {
//运费为商品总价的10%
item.setDeliveryPrice(
item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
//购买两件以上相同商品,第三件开始享受一定折扣
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
});

cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

}

内部用户购物车

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

public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();

List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

itemList.stream().forEach(item -> {
//免运费
item.setDeliveryPrice(BigDecimal.ZERO);
//无优惠
item.setCouponPrice(BigDecimal.ZERO);
});

cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {
String userCategory = Db.getUserCategory(userId);

if (userCategory.equals("Normal")) {
NormalUserCart normalUserCart = new NormalUserCart();
return normalUserCart.process(userId, items);
}

if (userCategory.equals("Vip")) {
VipUserCart vipUserCart = new VipUserCart();
return vipUserCart.process(userId, items);
}

if (userCategory.equals("Internal")) {
InternalUserCart internalUserCart = new InternalUserCart();
return internalUserCart.process(userId, items);
}

return null;
}

对比一下代码量可以发现,三种购物车 70% 的代码是重复的。原因很简单,虽然不同类型用户计算运费和优惠的方式不同,但整个购物车的初始化、统计总价、总运费、总优惠和支付价格的逻辑都是一样的。

修正版本

1.0 版本的问题在于:相同的代码应该只在一处出现。

如果我们熟记抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑呢?

其实,这个模式就是模板方法模式。

下面展示基于 工厂模式+模板方法模式 优化重复代码。

【抽象类】

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
public abstract class AbstractCart {

public Cart process(long userId, Map<Long, Integer> items) {

Cart cart = new Cart();

List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

itemList.stream().forEach(item -> {
processCouponPrice(userId, item);
processDeliveryPrice(userId, item);
});

cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

protected abstract void processCouponPrice(long userId, Item item);

protected abstract void processDeliveryPrice(long userId, Item item);

}

【普通用户购物车】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {

@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}

@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(item.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity()))
.multiply(new BigDecimal("0.1")));
}

}

【VIP 用户购物车】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {

@Override
protected void processCouponPrice(long userId, Item item) {
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
}

}

【内部用户购物车】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {

@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}

@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(BigDecimal.ZERO);
}

}

【客户端】

1
2
3
4
5
6
@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
String userCategory = Db.getUserCategory(userId);
AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
return cart.process(userId, items);
}

与其他模式的关系

  • 工厂方法模式模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。
  • 模板方法基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略模式基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。

参考资料

网络协议之 ICMP

ICMP 简介

ICMP 全名为(INTERNET CONTROL MESSAGE PROTOCOL)网络控制消息协议。

ICMP 的协议号为1

ICMP 报文就像是 IP 报文的小弟,总顶着 IP 报文的名头出来混。因为 ICMP 报文是在 IP 报文内部的,如图:

img

ICMP 类型

ICMP 报文主要有两大功能:查询报文和差错报文。

目的不可达(Destination Unreachable Message)

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| unused |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Internet Header + 64 bits of Original Data Datagram |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

日常生活中,邮寄包裹会经过多个传递环节,任意一环如果无法传下去,都会返回寄件人,并附上无法邮寄的原因。同理,当路由器收到一个无法传递下去的 IP 报文时,会发送 ICMP目的不可达报文(Type 为 3)给 IP 报文的源发送方。报文中的 Code 就表示发送失败的原因。

Code

0 = net unreachable;

1 = host unreachable;

2 = protocol unreachable;

3 = port unreachable;

4 = fragmentation needed and DF set;

5 = source route failed.

超时(Time Exceeded Message)

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| unused |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Internet Header + 64 bits of Original Data Datagram |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

网络传输 IP 数据报的过程中,如果 IP 数据包的 TTL 值逐渐递减为 0 时,需要丢弃数据报。这时,路由器需要向源发送方发送 ICMP 超时报文(Type 为 11),Code 为 0,表示传输过程中超时了。

一个 IP 数据报可能会因为过大而被分片,然后在目的主机侧把所有的分片重组。如果主机迟迟没有等到所有的分片报文,就会向源发送方发送一个 ICMP 超时报文,Code 为 1,表示分片重组超时了。

参数错误报文(Parameter Problem Message)

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Pointer | unused |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Internet Header + 64 bits of Original Data Datagram |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

当路由器或主机处理数据报时,发现因为报文头的参数错误而不得不丢弃报文时,需要向源发送方发送参数错误报文(Type 为 12)。当 Code 为 0 时,报文中的 Pointer 表示错误的字节位置。

源冷却(Source Quench Message)

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| unused |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Internet Header + 64 bits of Original Data Datagram |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

路由器在处理报文时会有一个缓存队列。如果超过最大缓存队列,将无法处理,从而丢弃报文。并向源发送方发一个 ICMP 源冷却报文(Type 为 4),告诉对方:“嘿,我这里客满了,你迟点再来。”

重定向(Redirect Message)

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Gateway Internet Address |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Internet Header + 64 bits of Original Data Datagram |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

想像一下,在公司中,有人来你的项目组问你某某某在哪儿。你一想,我们组没有这人啊。你肯定就会说,我们组没有这号人,你去其他组看看。当路由收到 IP 数据报,发现数据报的目的地址在路由表上没有,它就会发 ICMP 重定向报文(Type 为 5)给源发送方,提醒它想要发送的地址不在,去其他地方找找吧。

请求回显或回显应答(Echo or Echo Reply Message)

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Identifier | Sequence Number |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Data …

+-+-+-+-+-

**Type(8)请求回显报文(Echo);Type(0)是回显应答报文(Echo Reply)**。

请求回显或回显应答报文属于查询报文。Ping 就是用这种报文进行查询和回应。

时间戳或时间戳请求(Timestamp or Timestamp Reply Message)

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Identifier | Sequence Number |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Originate Timestamp |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Receive Timestamp |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Transmit Timestamp |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

时间戳报文是用来记录收发以及传输时间的报文。Originate Timestamp 记录的是发送方发送报文的时刻;Receive Timestamp记录的是接收方收到报文的时刻;Transmit Timestamp表示回显这最后发送报文的时刻。

信息请求或信息响应

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Type | Code | Checksum |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

| Identifier | Sequence Number |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这种报文是用来找出一个主机所在的网络个数(一个主机可能会在多个网络中)。报文的 IP 消息头的目的地址会填为全 0,表示 this,源地址会填为源 IP 所在的网络 IP。

总结

图:ICMP 知识点思维导图

img

参考

树和二叉树

什么是树

在计算机科学中,(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由 n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

它具有以下的特点:

  • 每个节点都只有有限个子节点或无子节点。
  • 树有且仅有一个根节点。
  • 根节点没有父节点;非根节点有且仅有一个父节点。
  • 每个非根节点可以分为多个不相交的子树。
  • 树里面没有环路。

树的术语

  • 节点的度:一个节点含有的子树的个数称为该节点的度;
  • 树的度:一棵树中,最大的节点度称为树的度;
  • 叶子节点终端节点:度为零的节点;
  • 非终端节点分支节点:度不为零的节点;
  • 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
  • 子节点:一个节点含有的子树的根节点称为该节点的子节点;
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点;
  • 堂兄弟节点:父节点在同一层的节点互为堂兄弟;
  • 节点的祖先:从根到该节点所经分支上的所有节点;
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
  • 森林:由 m(m>=0)棵互不相交的树的集合称为森林;

  • 节点的高度:节点到叶子节点的最长路径。高度是从下往上度量。
  • 节点的深度:根节点到该节点的最长路径。深度是从上往下度量。
  • 节点的层次:节点的深度 + 1。
  • 树的高度:根节点的高度。

树的性质

  • 树中的节点数等于所有节点的度数加 1。
  • 度为 m 的树中第 i 层上至多有 $$m^{i-1}$$ 个节点($$i ≥ 1$$)。
  • 高度为 h 的 m 次树至多有 $$(m^h-1)/(m-1)$$ 个节点。
  • 具有 n 个节点的 m 次树的最小高度为 $$\log_m{(n(m-1)+1)}$$ 。

树的种类

无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树

有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;

  • 二叉树:每个节点最多含有两个子树的树称为二叉树;
    • 完全二叉树:对于一颗二叉树,假设其深度为 d(d>1)。除了第 d 层外,其它各层的节点数目均已达最大值,且第 d 层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;
  • 满二叉树:所有叶节点都在最底层的完全二叉树;
  • 平衡二叉树AVL 树):当且仅当任何节点的两棵子树的高度差不大于 1 的二叉树;
  • 排序二叉树(二叉查找树(英语:Binary Search Tree)):也称二叉搜索树、有序二叉树;
  • 霍夫曼树带权路径最短的二叉树称为哈夫曼树或最优二叉树;
  • B 树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个子树。

二叉树

二叉树中的每个节点最多有两个子节点,分别是左子节点右子节点

二叉树的性质

  1. 二叉树第 i 层上的结点数目最多为 2i-1 (i≥1)。
  2. 深度为 k 的二叉树至多有 2k-1 个结点(k≥1)。
  3. 包含 n 个结点的二叉树的高度至少为 **log2(n+1)**。
  4. 在任意一棵二叉树中,若终端结点的个数为 n0,度为 2 的结点数为 n2,则 n0=n2+1。

满二叉树

除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树

完全二叉树

叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树

特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。

存储一棵二叉树,有两种方法,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。

二叉链式存储法

每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。

顺序存储法

如果节点 X 存储在数组中下标为 i 的位置,下标为 2 _ i 的位置存储的就是左子节点,下标为 2 _ i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。

如果是非完全二叉树,其实会浪费比较多的数组存储空间。所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是 为什么完全二叉树要求最后一层的子节点都靠左的原因。

二叉树的遍历

二叉树的遍历有三种方式:

  • 前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
  • 中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
  • 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

二叉查找树

二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。

二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。

二叉查找树的查找

首先,我们看如何在二叉查找树中查找一个节点。我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。

二叉查找树的插入

如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。

二叉查找树的删除

第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。

第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。

第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。

二叉查找树的时间复杂度

不管操作是插入、删除还是查找,**时间复杂度其实都跟树的高度成正比,也就是 O(log n)**。

二叉查找树的形态各式各样。比如这个图中,对于同一组数据,我们构造了三种二叉查找树。它们的查找、插入、删除操作的执行效率都是不一样的。图中第一种二叉查找树,根节点的左右子树极度不平衡,已经退化成了链表,所以查找的时间复杂度就变成了 O(n)。

为什么需要二叉查找树

第一,哈希表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。

第二,哈希表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。

第三,笼统地来说,尽管哈希表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。

第四,哈希表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

最后,为了避免过多的散列冲突,哈希表装载因子不能太大,特别是基于开放寻址法解决冲突的哈希表,不然会浪费一定的存储空间。

参考资料