【设计模式】3. 结构型模式
# 结构型模式
- 适配器 ★★★
- 桥接 ★
- 组合 ★★
- 装饰 ★★
- 外观 ★★
- 亨元 ★
- 代理 ★
# 适配器模式 ★★★
场景:在程序中整合一个第三方智能分析函数库。 但是遇到了一个问题, 那就是分析函数库只兼容 JSON 格式的数据。可以为分析函数库中的每个类创建将 XML 转换为 JSON 格式的适配器, 然后让客户端仅通过这些适配器来与函数库进行交流。 当某个适配器被调用时, 它会将传入的 XML 数据转换为 JSON 结构, 并将其传递给被封装分析对象的相应方法。
# 1. 特点
适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。
适合应用场景
- 你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。
- 如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
优缺点
- 优点
- 单一职责原则你可以将接口或数据转换代码从程序主要业务逻辑中分离。
- 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
- 缺点
- 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。
- 优点
与其他模式的关系
- 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
- 适配器可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
- 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
- 外观模式为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
- 桥接、 状态模式和策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
# 2. 示例
package main
import "fmt"
// 客户端
type Client struct {
}
// 客户端将Lightning接口插入电脑
func (c *Client) InsertLightningConnectorIntoComputer(com Computer) {
fmt.Println("Client inserts Lightning connector into computer.")
com.InsertIntoLightningPort()
}
// 电脑接口
type Computer interface {
InsertIntoLightningPort()
}
// mac 电脑
type Mac struct {
}
func (m *Mac) InsertIntoLightningPort() {
fmt.Println("Lightning connector is plugged into mac machine.")
}
// windows 电脑
type Windows struct{}
func (w *Windows) insertIntoUSBPort() {
fmt.Println("USB connector is plugged into windows machine.")
}
// windows 适配器
type WindowsAdapter struct {
windowMachine *Windows
}
func (w *WindowsAdapter) InsertIntoLightningPort() {
fmt.Println("Adapter converts Lightning signal to USB.")
w.windowMachine.insertIntoUSBPort()
}
// main
func main() {
client := &Client{}
mac := &Mac{}
client.InsertLightningConnectorIntoComputer(mac)
windowsMachine := &Windows{}
windowsMachineAdapter := &WindowsAdapter{
windowMachine: windowsMachine,
}
client.InsertLightningConnectorIntoComputer(windowsMachineAdapter)
}
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
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
# 桥接模式 ★
# 1. 特点
桥接模式是一种结构型设计模式, 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。
适合应用场景
- 如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。
- 如果你希望在几个独立维度上扩展一个类, 可使用该模式。
- 如果你需要在运行时切换不同实现方法, 可使用桥接模式。
优缺点
- 优点
- 你可以创建与平台无关的类和程序。
- 客户端代码仅与高层抽象部分进行互动, 不会接触到平台的详细信息。
- 开闭原则。 你可以新增抽象部分和实现部分, 且它们之间不会相互影响。
- 单一职责原则。 抽象部分专注于处理高层逻辑, 实现部分处理平台细节。
- 缺点
- 对高内聚的类使用该模式可能会让代码更加复杂。
- 优点
与其他模式的关系
- 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
- 桥接、 状态模式和策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 你可以将抽象工厂模式和桥接搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。
- 你可以结合使用生成器模式和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。
# 2. 示例
略
# 组合模式 ★★
# 1. 特点
组合模式是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。
适合应用场景
- 如果你需要实现树状对象结构, 可以使用组合模式。
- 如果你希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式。
优缺点
- 优点
- 你可以利用多态和递归机制更方便地使用复杂树结构。
- 开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分。
- 缺点
- 对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解。
- 优点
与其他模式的关系
- 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 你可以在创建复杂组合树时使用生成器模式, 因为这可使其构造步骤以递归的方式运行。
- 责任链模式通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。
- 你可以使用迭代器模式来遍历组合树。
- 你可以使用访问者模式对整个组合树执行操作。
- 你可以使用享元模式实现组合树的共享叶节点以节省内存。
- 组合和装饰模式的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
# 2. 示例
package main
import "fmt"
type Component interface {
search(string)
}
// 文件夹
type Folder struct {
components []Component
name string
}
func (f *Folder) search(keyword string) {
fmt.Printf("Serching recursively for keyword %s in folder %s\n", keyword, f.name)
for _, composite := range f.components {
composite.search(keyword)
}
}
func (f *Folder) add(c Component) {
f.components = append(f.components, c)
}
// 文件
type File struct {
name string
}
func (f *File) search(keyword string) {
fmt.Printf("Searching for keyword %s in file %s\n", keyword, f.name)
}
func (f *File) getName() string {
return f.name
}
// main
func main() {
file1 := &File{name: "File1"}
file2 := &File{name: "File2"}
file3 := &File{name: "File3"}
folder1 := &Folder{
name: "Folder1",
}
folder1.add(file1)
folder2 := &Folder{
name: "Folder2",
}
folder2.add(file2)
folder2.add(file3)
folder2.add(folder1)
folder2.search("rose")
}
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
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
# 装饰模式 ★★
# 1. 特点
- 装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
- 适合应用场景
- 如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式。
- 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式。
- 优缺点
- 优点
- 你无需创建新子类即可扩展对象的行为。
- 你可以在运行时添加或删除对象的功能。
- 你可以用多个装饰封装对象来组合几种行为。
- 单一职责原则。 你可以将实现了许多不同行为的一个大类拆分为多个较小的类。
- 缺点
- 在封装器栈中删除特定封装器比较困难。
- 实现行为不受装饰栈顺序影响的装饰比较困难。
- 各层的初始化配置代码看上去可能会很糟糕。
- 优点
- 与其他模式的关系
- 装饰可让你更改对象的外表, 策略模式则让你能够改变其本质。
- 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。
- 责任链模式和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。
- 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
- 适配器模式可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
# 2. 示例
package main
import "fmt"
type IPizza interface {
getPrice() int
}
type VeggeMania struct {
}
func (p *VeggeMania) getPrice() int {
return 15
}
type CheeseTopping struct {
pizza IPizza
}
func (c *CheeseTopping) getPrice() int {
pizzaPrice := c.pizza.getPrice()
return pizzaPrice + 10
}
type TomatoTopping struct {
pizza IPizza
}
func (c *TomatoTopping) getPrice() int {
pizzaPrice := c.pizza.getPrice()
return pizzaPrice + 7
}
func main() {
pizza := &VeggeMania{}
//Add cheese topping
pizzaWithCheese := &CheeseTopping{
pizza: pizza,
}
//Add tomato topping
pizzaWithCheeseAndTomato := &TomatoTopping{
pizza: pizzaWithCheese,
}
fmt.Printf("Price of veggeMania with tomato and cheese topping is %d\n", pizzaWithCheeseAndTomato.getPrice())
}
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
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
# 外观模式 ★
# 1. 特点
- 外观模式是一种结构型设计模式, 能为程序库、 框架或其他复杂类提供一个简单的接口。这种方式看上去与中介者模式非常相似。
- 适合应用场景
- 如果你需要一个指向复杂子系统的直接接口, 且该接口的功能有限, 则可以使用外观模式。
- 如果需要将子系统组织为多层结构, 可以使用外观。
- 优缺点
- 优点
- 你可以让自己的代码独立于复杂子系统。
- 缺点
- 外观可能成为与程序中所有类都耦合的上帝对象。
- 优点
- 与其他模式的关系
- 外观模式为现有对象定义了一个新接口, 适配器模式则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
- 当只需对客户端代码隐藏子系统创建对象的方式时, 你可以使用抽象工厂模式来代替外观。
- 享元模式展示了如何生成大量的小型对象, 外观则展示了如何用一个对象来代表整个子系统。
- 外观和中介者模式的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。
- 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
- 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。 外观类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
- 外观与代理模式的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
# 2. 示例
略。。。
# 亨元模式 ★
# 1. 特点
- 享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。
- 适合应用场景
- 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。
- 优缺点
- 优点
- 如果程序中有很多相似对象, 那么你将可以节省大量内存。
- 缺点
- 你可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据。
- 代码会变得更加复杂。 团队中的新成员总是会问: “为什么要像这样拆分一个实体的状态?”。
- 优点
- 与其他模式的关系
- 你可以使用享元模式实现组合模式树的共享叶节点以节省内存。
- 享元展示了如何生成大量的小型对象, 外观模式则展示了如何用一个对象来代表整个子系统。
# 2. 示例
略
# 代理模式 ★
# 1. 特点
- 代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符(字符串占位符)。 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。
- 适合应用场景
- 延迟初始化 (虚拟代理)。 如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。
- 访问控制 (保护代理)。 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。
- 本地执行远程服务 (远程代理)。 适用于服务对象位于远程服务器上的情形。
- 记录日志请求 (日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时。
- 缓存请求结果 (缓存代理)。 适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。
- 智能引用。 可在没有客户端使用某个重量级对象时立即销毁该对象。
- 优缺点
- 优点
- 你可以在客户端毫无察觉的情况下控制服务对象。
- 如果客户端对服务对象的生命周期没有特殊要求, 你可以对生命周期进行管理。
- 即使服务对象还未准备好或不存在, 代理也可以正常工作。
- 开闭原则。 你可以在不对服务或客户端做出修改的情况下创建新代理。
- 缺点
- 代码可能会变得复杂, 因为需要新建许多类。
- 服务响应可能会延迟。
- 优点
- 与其他模式的关系
- 适配器模式能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰模式则能为对象提供加强的接口。
- 外观模式与代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。
- 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。
# 2. 示例
略
上次更新: 2023/08/27, 21:33:49