软件设计模式-结构型
ssk-wh Lv4

结构型设计模式关注如何将对象和类组合成更大的结构,以实现更高级别的功能。

以下是几种常见的结构型设计模式:

适配器模式(Adapter Pattern):将一个类的接口转换为另一个客户端所期望的接口格式。
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们能够独立地变化。
组合模式(Composite Pattern):将对象组合成树形结构以表示“部分-整体”的层次结构,使得客户端可以统一对待单个对象和组合对象。
装饰器模式(Decorator Pattern):动态地给一个对象添加额外的职责,同时不改变其原有的结构。
外观模式(Facade Pattern):为子系统中的一组接口提供一个一致的界面,以简化子系统的使用。
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度的对象。
代理模式(Proxy Pattern):为其他对象提供一种代理以控制这个对象的访问。

适配器模式

适配器模式(Adapter Pattern)是一种结构型设计模式,它的作用是将一个类的接口转换为另一个类的接口,以满足客户端对接口的需求。适配器模式通常用于系统接口的升级和改造,或者将一个现有的类用于另一个新的系统中。

类型

适配器模式可以分为两种类型:类适配器和对象适配器。类适配器通过多重继承实现,将适配器类继承自需要适配的类和目标接口,从而实现对目标接口的适配。对象适配器则通过组合实现,将适配器类组合一个需要适配的类的实例和目标接口的实例,从而实现对目标接口的适配。

优点

适配器模式的主要优点包括:

适配器模式可以让原本不兼容的接口进行合作,从而提高系统的灵活性和可扩展性。
适配器模式可以降低系统的耦合度,使得代码更易于维护和重构。
适配器模式可以重用现有的类,从而减少开发时间和成本。

缺点

适配器模式的主要缺点包括:

适配器模式增加了系统的复杂度,可能会影响系统的性能和可读性。
适配器模式可能需要开发新的适配器类,从而增加开发成本。

场景

在实际应用中,适配器模式可以应用于很多场景,比如:

将一个旧的类库适配为一个新的应用程序接口。
将一个第三方组件的接口适配为自己的系统接口。
将一个对象的接口适配为另一个对象的接口,以便它们可以一起工作。
将一个不同版本的接口适配为当前版本的接口,以便它们可以向后兼容。

示例

假设我们有一个 PrintInterface 接口,里面定义了一个 print 方法:

1
2
3
4
class PrintInterface {
public:
virtual void print() = 0;
};

现在,我们有两个不同的类 PrintA 和 PrintB,它们分别实现了 PrintInterface 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PrintA : public PrintInterface {
public:
void print() override {
std::cout << "PrintA is printing" << std::endl;
}
};

class PrintB : public PrintInterface {
public:
void print() override {
std::cout << "PrintB is printing" << std::endl;
}
};

然后,我们有一个 PrintAdapter 类,它实现了 PrintInterface 接口,并包含了一个 PrintA 对象的指针:

1
2
3
4
5
6
7
8
9
10
11
12
class PrintAdapter : public PrintInterface {
public:
PrintAdapter(PrintA* printA) : printA_(printA) {}

void print() override {
printA_->printA();
}

private:
PrintA* printA_;
};

注意到 PrintAdapter 类的 print 方法实际上调用了 PrintA 类的 printA 方法。这里 PrintAdapter 类就扮演了适配器的角色,将 PrintA 类的接口适配成了 PrintInterface 接口。

下面我们可以看到如何使用适配器模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() {
// 使用 PrintA 类的对象进行打印
PrintA* printA = new PrintA();
printA->print();

// 使用适配器,将 PrintA 类的对象适配成 PrintInterface 接口进行打印
PrintInterface* adapter = new PrintAdapter(printA);
adapter->print();

// 使用 PrintB 类的对象进行打印
PrintB* printB = new PrintB();
printB->print();

// 直接使用 PrintInterface 接口进行打印
PrintInterface* printInterface = new PrintB();
printInterface->print();

return 0;
}

输出结果如下:

1
2
3
4
PrintA is printing
PrintA is printing
PrintB is printing
PrintB is printing

可以看到,我们在适配器模式中使用了适配器 PrintAdapter 将 PrintA 类的接口适配成了 PrintInterface 接口,并通过 PrintInterface 接口实现了 PrintA 类的功能。同时,我们还可以直接使用 PrintB 类的对象或者 PrintInterface 接口进行打印。

桥接模式

桥接模式是一种结构型设计模式,旨在将一个大类或一组紧密相关的类分解为抽象和实现两个独立的层次结构,并使用一个桥接接口将它们连接起来。桥接模式允许这些层次结构独立变化,从而可以更加灵活地设计代码。

在桥接模式中,有两个独立变化的维度:抽象和实现。抽象可以是一个类,也可以是一个接口,定义了具体实现需要的方法,而实现则是指具体实现的类或接口。桥接模式通过一个桥接接口来将抽象和实现连接起来。

优点

桥接模式的主要优点包括:

提高了代码的可扩展性和可维护性。
允许抽象和实现可以独立扩展,从而避免了类的爆炸式增长。
使得代码更加灵活,可以根据不同的需求进行组合。

缺点

桥接模式的主要缺点包括:

增加了代码的复杂性,需要额外的抽象层次和桥接接口。
对于简单的情况,使用桥接模式可能会增加不必要的复杂性。

示例

假设我们正在设计一个手机品牌和手机软件之间的关系。假设我们有两个手机品牌:华为和小米,并且有两个手机软件:微信和支付宝。我们希望能够让任何一种手机品牌都可以运行这两个软件,而且软件也可以在不同品牌的手机上运行。

使用桥接模式,我们可以将手机品牌和软件分别抽象为两个类。首先,定义一个手机品牌抽象类,它有一个纯虚函数RunSoftware:

1
2
3
4
5
class PhoneBrand {
public:
virtual ~PhoneBrand() {}
virtual void RunSoftware() = 0;
};

然后,我们创建两个具体的手机品牌类,HuaweiPhone和XiaomiPhone,它们都实现了PhoneBrand类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class HuaweiPhone : public PhoneBrand {
public:
void RunSoftware() {
std::cout << "Run software on Huawei phone" << std::endl;
}
};

class XiaomiPhone : public PhoneBrand {
public:
void RunSoftware() {
std::cout << "Run software on Xiaomi phone" << std::endl;
}
};

接下来,我们定义一个手机软件抽象类,它也有一个纯虚函数Run:

1
2
3
4
5
class PhoneSoftware {
public:
virtual ~PhoneSoftware() {}
virtual void Run() = 0;
};

然后,我们创建两个具体的手机软件类,WeChatSoftware和AlipaySoftware,它们也都实现了PhoneSoftware类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class WeChatSoftware : public PhoneSoftware {
public:
void Run() {
std::cout << "Run WeChat on phone" << std::endl;
}
};

class AlipaySoftware : public PhoneSoftware {
public:
void Run() {
std::cout << "Run Alipay on phone" << std::endl;
}
};

最后,我们创建一个桥接类Phone,它将手机品牌和手机软件连接在一起。它有两个成员变量:一个是指向PhoneBrand对象的指针,一个是指向PhoneSoftware对象的指针。Phone类有一个RunSoftware函数,它通过调用PhoneBrand对象的RunSoftware函数和PhoneSoftware对象的Run函数来运行软件:

1
2
3
4
5
6
7
8
9
10
11
12
class Phone {
public:
Phone(PhoneBrand* brand, PhoneSoftware* software)
: brand_(brand), software_(software) {}
void RunSoftware() {
brand_->RunSoftware();
software_->Run();
}
private:
PhoneBrand* brand_;
PhoneSoftware* software_;
};

现在,我们可以创建各种不同品牌和不同软件的手机了。例如,要创建一个运行在华为手机上的微信,可以这样写:

1
2
3
PhoneBrand* huawei = new HuaweiPhone();
PhoneSoftware* wechat = new WeChatSoftware();
Phone* phone = new Phone(huawei, wechat);

组合模式

组合模式是一种结构型设计模式,它允许你将对象组合成树形结构来表现“整体/部分”的层次结构,使得客户端能够使用统一的方式处理单个对象和对象组合。

组合模式是一种结构型设计模式,它允许你将对象组合成树形结构来表现“整体/部分”的层次结构,使得客户端能够使用统一的方式处理单个对象和对象组合。

在组合模式中,每个组件(component)都是一个对象,可以是叶子节点(leaf)或者组合节点(composite)。叶子节点表示最终的对象,没有子节点;而组合节点可以包含一组子节点,这些子节点可以是叶子节点,也可以是组合节点,这样就形成了一棵树形结构。

组合模式中的两个关键角色是Component和Composite。Component是一个抽象类,定义了组合对象和叶子对象的公共接口;而Composite是组合节点的具体实现,包含一个子对象的列表,并实现了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
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
#include <iostream>
#include <list>
#include <string>
using namespace std;

class FileSystemComponent {
public:
virtual ~FileSystemComponent() {}
virtual void display() = 0;
virtual void add(FileSystemComponent *component) {}
virtual void remove(FileSystemComponent *component) {}
};

class File : public FileSystemComponent {
private:
string name;

public:
File(string name) {
this->name = name;
}

void display() override {
cout << name << endl;
}
};

class Directory : public FileSystemComponent {
private:
string name;
list<FileSystemComponent*> components;

public:
Directory(string name) {
this->name = name;
}

void display() override {
cout << name << " (directory)" << endl;
for (auto component : components) {
component->display();
}
}

void add(FileSystemComponent *component) override {
components.push_back(component);
}

void remove(FileSystemComponent *component) override {
components.remove(component);
}
};

int main() {
// Create some files and directories
File *file1 = new File("file1.txt");
File *file2 = new File("file2.txt");
Directory *dir1 = new Directory("dir1");
Directory *dir2 = new Directory("dir2");

// Build the tree structure
dir1->add(file1);
dir2->add(file2);
dir2->add(dir1);

// Display the tree
dir2->display();

// Clean up
delete dir2;
delete file1;
delete file2;

return 0;
}

在这个示例中,FileSystemComponent是组合模式中的Component接口,File和Directory是具体实现的组件。File表示叶子节点,只有一个文件名属性和一个display方法;Directory表示组合节点,包含一个子对象的列表,以及add、remove和display方法。在main函数中,我们创建了一些文件和目录,将它们组合成树形结构,并通过调用根节点的display方法来打印整个文件系统的层次结构。

装饰器模式

装饰模式是一种结构型设计模式,它允许你在不改变对象接口的前提下,动态地向对象添加行为。这种模式通过创建一个装饰对象来包装原始对象,然后在装饰对象上添加功能来扩展原始对象的行为。

在装饰模式中,有四个主要角色:

抽象组件(Component):定义了被装饰对象的接口,并声明了装饰方法。

具体组件(ConcreteComponent):实现了抽象组件的接口,并提供了默认的实现。

抽象装饰器(Decorator):继承了抽象组件,并包含了一个抽象组件的引用,以便对其进行装饰。

具体装饰器(ConcreteDecorator):继承了抽象装饰器,并实现了其装饰方法,以扩展原始对象的功能。

使用装饰模式,你可以动态地向对象添加功能,而不需要修改原始对象的代码。这种方式比继承更灵活,因为你可以在运行时动态地添加或删除装饰器,从而影响对象的行为。

示例

假设有一个基础类 Shape,它定义了一个抽象方法 draw,用于绘制一个形状。现在我们想要在基础类的基础上,动态地添加一个边框的功能,可以使用装饰模式。

1
2
3
4
5
6
7
8
9
10
11
class Shape {
public:
virtual void draw() = 0;
};

class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

然后定义一个装饰者类 ShapeDecorator,它也继承自 Shape,但包含一个指向基础组件的指针 component_。装饰者类也实现了 draw 方法,但是在调用 component_->draw() 之前或之后添加了额外的功能。在这个例子中,我们添加了一个边框。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ShapeDecorator : public Shape {
public:
ShapeDecorator(Shape* component) : component_(component) {}

void draw() override {
component_->draw();
drawBorder();
}

virtual void drawBorder() = 0;

protected:
Shape* component_;
};

class BorderDecorator : public ShapeDecorator {
public:
BorderDecorator(Shape* component) : ShapeDecorator(component) {}

void drawBorder() override {
std::cout << "Drawing a border." << std::endl;
}
};

现在,我们可以使用 Rectangle 类创建一个矩形,并使用 BorderDecorator 类将其装饰,从而添加边框的功能。

1
2
3
4
5
6
7
int main() {
Shape* rect = new Rectangle();
rect->draw();

Shape* rectWithBorder = new BorderDecorator(new Rectangle());
rectWithBorder->draw();
}

输出结果为:

1
2
3
Drawing a rectangle.
Drawing a rectangle.
Drawing a border.

外观模式

外观模式(Facade Pattern)是一种结构型设计模式,它为复杂系统提供一个简单的接口,隐藏了系统的复杂性,使得客户端可以更加方便地使用系统。

外观模式可以将一个复杂系统分成多个子系统,然后为每个子系统提供一个简单的接口,使得客户端可以通过这个接口来访问子系统,而无需了解子系统的具体实现。

外观模式的核心思想是封装,即将复杂的系统封装成一个简单的接口。这种封装可以使得系统更加易于维护和扩展。

举个例子,假设我们要开发一个游戏,游戏中包含多个子系统,比如音频系统、视频系统、输入系统、游戏逻辑系统等。如果客户端要使用这些子系统,就需要了解这些子系统的接口和实现方式,这样会增加客户端的复杂度。但是如果我们使用外观模式,将所有子系统封装成一个简单的接口,客户端只需要调用这个接口,就可以实现游戏的各种功能,而无需了解子系统的具体实现。

在实际开发中,外观模式经常用于封装底层库或框架的复杂性,提供简单的接口给客户端使用。

示例

下面是一个用 C++ 实现的外观模式的示例,该示例模拟了一个音乐播放器,使用外观模式将不同的子系统(音乐库、播放列表、音量控制器)组合在一起,对外提供简单的接口,方便用户使用。

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
#include <iostream>
#include <string>
#include <vector>

/* 子系统1:音乐库 */
class MusicLibrary {
public:
void loadMusic(std::string musicName) {
std::cout << "Loading music: " << musicName << std::endl;
// 模拟加载音乐的过程
}
};

/* 子系统2:播放列表 */
class PlayList {
public:
void addMusicToPlayList(std::string musicName) {
std::cout << "Adding music to playlist: " << musicName << std::endl;
// 模拟将音乐添加到播放列表的过程
}
};

/* 子系统3:音量控制器 */
class VolumeController {
public:
void setVolume(int volume) {
std::cout << "Setting volume to: " << volume << std::endl;
// 模拟设置音量的过程
}
};

/* 外观类:音乐播放器 */
class MusicPlayer {
public:
void playMusic(std::string musicName) {
musicLibrary.loadMusic(musicName);
playList.addMusicToPlayList(musicName);
volumeController.setVolume(50);
std::cout << "Now playing: " << musicName << std::endl;
}
private:
MusicLibrary musicLibrary;
PlayList playList;
VolumeController volumeController;
};

/* 客户端 */
int main() {
MusicPlayer musicPlayer;
musicPlayer.playMusic("Yesterday");
return 0;
}

在上面的示例中,MusicLibrary、PlayList、VolumeController 三个类分别代表了音乐库、播放列表、音量控制器三个子系统。MusicPlayer 类是外观类,将三个子系统组合在一起,并对外提供简单的 playMusic 接口,方便用户使用。客户端只需要调用 MusicPlayer 的 playMusic 方法,即可完成一系列操作。

享元模式

享元模式是一种结构型设计模式,它通过将对象的状态分为内部状态和外部状态,从而减少应用程序中的对象数量。内部状态是与对象的特定实例无关的状态,而外部状态则取决于对象的特定实例和上下文。

在享元模式中,共享对象的内部状态,而外部状态则由客户端来维护和传递。这使得应用程序可以减少内存使用和对象创建的数量,从而提高性能和降低内存占用。

例如,一个文本编辑器可以使用享元模式来共享相同的字体对象。字体的内部状态是字体的名称、字形和大小等,而外部状态则是应用程序中每个字符的位置和颜色。

在实现享元模式时,可以使用工厂类来维护和管理共享对象池,确保对象的共享和创建的正确性。

优点

享元模式的优点包括:

减少对象的数量,从而减少内存使用和提高性能。
可以在运行时动态添加和删除享元对象,从而提高灵活性。
通过共享对象的内部状态,可以减少应用程序的复杂性。

缺点

其缺点包括:

由于共享对象的内部状态是不可变的,因此无法为不同的应用程序场景创建不同的状态组合。
由于共享对象的内部状态是不可变的,因此必须确保对象在创建后不会被修改。

示例

下面是一个简单的 C++ 示例代码,演示如何使用享元模式来实现对网站用户信息的管理。在该示例中,User 是一个用户类,代表网站上的用户。UserFactory 类是一个享元工厂类,它维护了一个内部状态的池子,每个状态都与一个 User 对象相对应。

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
#include <iostream>
#include <map>
#include <string>

using namespace std;

// 用户类
class User {
public:
User(const string& name, int age, const string& address) :
name_(name), age_(age), address_(address) {}

const string& GetName() const { return name_; }
int GetAge() const { return age_; }
const string& GetAddress() const { return address_; }

private:
string name_;
int age_;
string address_;
};

// 享元工厂类
class UserFactory {
public:
// 获取一个用户对象,如果该状态的用户对象不存在,则创建一个新的对象
User* GetUser(const string& name, int age, const string& address) {
// 先在内部状态的池子中查找是否已有对应的状态
auto iter = user_pool_.find(name);
if (iter != user_pool_.end()) {
// 如果已有,则返回对应的用户对象
return iter->second;
}
else {
// 如果没有,则创建一个新的用户对象,并将其加入池子中
auto user = new User(name, age, address);
user_pool_[name] = user;
return user;
}
}

private:
map<string, User*> user_pool_;
};

int main() {
UserFactory factory;

// 获取一个名为 "Alice" 的用户对象
auto user1 = factory.GetUser("Alice", 25, "Shanghai");
cout << user1->GetName() << endl;
cout << user1->GetAge() << endl;
cout << user1->GetAddress() << endl;

// 获取另一个名为 "Alice" 的用户对象,这时会直接从内部状态池中获取
auto user2 = factory.GetUser("Alice", 30, "Beijing");
cout << user2->GetName() << endl;
cout << user2->GetAge() << endl;
cout << user2->GetAddress() << endl;

return 0;
}

在该示例中,UserFactory 类维护了一个内部状态的池子,每个状态都与一个 User 对象相对应。当调用 UserFactory::GetUser() 方法获取一个用户对象时,会先在内部状态的池子中查找是否已有对应的状态,如果已有,则返回对应的用户对象;否则,会创建一个新的用户对象,并将其加入池子中,然后返回这个新创建的用户对象。

在客户端代码中,每次需要获取一个用户对象时,都可以通过 UserFactory 类来获取,而无需直接创建新的 User 对象。这样,就可以有效地减少创建对象的开销,提高程序的性能和效率。

代理模式

代理模式是一种结构型设计模式,它允许通过创建代理对象来控制对原始对象的访问。代理对象在客户端和目标对象之间起到中介作用,隐藏了目标对象的复杂性和提供了额外的控制和安全性。

代理模式通常涉及三个角色:目标对象、代理对象和客户端。目标对象是需要被代理的对象,代理对象控制着对目标对象的访问,并且可以在适当的时候创建、销毁或修改目标对象。客户端通过代理对象来访问目标对象,而不是直接访问目标对象。

代理模式可以分为静态代理和动态代理。静态代理需要手动编写代理类,而动态代理使用反射技术来在运行时动态地创建代理对象。

示例

下面是一个简单的C++示例代码,展示了静态代理模式的实现,其中目标对象是一个简单的计算器类,代理对象通过计算器类提供的接口对其进行封装。

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
#include <iostream>

// 目标对象
class Calculator {
public:
virtual ~Calculator() {}
virtual int add(int a, int b) = 0;
};

// 具体目标对象
class ConcreteCalculator : public Calculator {
public:
int add(int a, int b) override {
return a + b;
}
};

// 代理对象
class CalculatorProxy : public Calculator {
public:
CalculatorProxy(Calculator* calc) : m_calc(calc) {}

int add(int a, int b) override {
// 在调用目标对象的方法之前或之后执行其他操作
std::cout << "Before add operation..." << std::endl;

int result = m_calc->add(a, b);

std::cout << "After add operation..." << std::endl;

return result;
}

private:
Calculator* m_calc;
};

int main() {
// 创建目标对象
Calculator* calc = new ConcreteCalculator();

// 创建代理对象并将目标对象传递给它
Calculator* proxy = new CalculatorProxy(calc);

// 通过代理对象调用目标对象的方法
int result = proxy->add(5, 10);

std::cout << "Result: " << result << std::endl;

// 释放内存
delete proxy;
delete calc;

return 0;
}
 Comments