结构型设计模式(Structural Design Patterns) - 7种
结构型设计模式(Structural Design Patterns) - 7种
[TOC]
什么是结构型设计模式(Structural Design Patterns)
结构型设计模式(Structural Design Patterns)是一种软件设计模式,用于解决在软件系统中对象的组合、接口、类之间的关系和结构的问题。结构型设计模式可以帮助设计者创建更加灵活、可扩展和易于维护的软件系统。
结构型设计模式包括以下七种常见的模式
(桥代理组装适配器,享元回家装饰外观)
:
- 适配器模式(Adapter Pattern):将一个类的接口转换成另一个类的接口,使得原本不兼容的类能够一起工作。
- 桥接模式(Bridge Pattern):将抽象部分与其实现部分分离,使它们可以独立地变化。
- 组合模式(Composite Pattern):将对象组合成树形结构以表示"部分-整体"的层次结构,使客户端可以统一对待单个对象和组合对象。
- 装饰器模式(Decorator Pattern):动态地为对象添加额外的行为,而不需要在对象之间使用继承关系。
- 外观模式(Facade Pattern):提供一个简单的接口,隐藏一组复杂的子系统接口,使得客户端更加方便地使用这些子系统。
- 享元模式(Flyweight Pattern):共享细粒度对象,以减少内存开销。
- 代理模式(Proxy Pattern):为其他对象提供一种代理以控制对这个对象的访问。
一、适配器模式(Adapter Pattern)
适配器模式(Adapter Pattern)是一种结构型设计模式,用于将一个类的接口转换成另一个类的接口,以便两者可以相互兼容。适配器模式通常用于解决不兼容的接口之间的问题,或者用于将现有类的功能与其他类进行集成。
意图:将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适配器模式在现实生活中的应用非常广泛,例如在电子设备中的电源适配器、在软件开发中的数据库适配器等。它可以帮助解决不同接口之间的兼容性问题,实现系统的灵活扩展和复用。
Adapter Pattern适用于:
- 想使用一个已经存在的类,而它的接口不符合要求。
- 想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
- (仅适用于对象Adapter)想使用一个已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。
适配器模式包含三个关键角色:
-
目标接口(Target)
定义了客户端期望的接口,是客户端调用的接口。
-
适配器(Adapter)
是将被适配者的接口转换成目标接口的类,它实现了目标接口,并持有一个对被适配者的引用。
-
被适配者(Adaptee)
是需要被适配的类或接口,其接口与目标接口不匹配。
适配器模式包括以下几种类型:
-
类适配器
通过继承实现适配器,使得适配器类既具有目标接口的行为,又能够调用被适配类的方法。在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
using namespace std;
/*
* 类适配器
*/
// 目标接口类
class USB
{
public:
virtual void Request()
{
cout << "USB数据线" << endl;
}
~USB() { ; }
};
// 需要适配的类
class TypeC
{
public:
void SpecificRequest()
{
cout << "Type-C数据线" << endl;
}
~TypeC() { ; }
};
// C++多重继承实现类适配器
class Adapter :public USB, private TypeC
{
public:
void Request() override
{
SpecificRequest();
}
~Adapter() { ; }
};
int main()
{
USB* usb = new Adapter(); // 创建适配器对象
usb->Request();
} -
对象适配器
通过组合实现适配器,使得适配器类包含一个被适配类的对象,并实现目标接口的方法,通过调用被适配类的方法来完成适配。在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
using namespace std;
/*
* 对象适配器
*/
// 目标接口类
class USB
{
public:
virtual void Request()
{
cout << "USB数据线" << endl;
}
~USB() { ; }
};
// 需要适配的类
class TypeC
{
public:
void SpecificRequest()
{
cout << "Type-C数据线" << endl;
}
~TypeC() { ; }
};
class Adapter :public USB
{
private:
TypeC* typeC; // 定义一个需要适配的对象
public:
Adapter(TypeC* tc) :typeC(tc) { ; }
void Request() override
{
typeC->SpecificRequest(); // 需要创建适配对象调用方法
}
~Adapter() { ; }
};
int main()
{
TypeC* typeC = new TypeC();
USB* usb = new Adapter(typeC);
usb->Request();
} -
接口适配器
也称为缺省适配器模式,通过定义一个抽象适配器类,其中包含目标接口的所有方法,但方法体为空,子类可以选择性地覆盖其中的方法,从而实现适配。在C++中,可以通过接口继承和虚函数实现接口适配器模式。
适配器的优缺点:
适配器模式的优点:
- 解决接口不兼容问题:通过适配器,可以将不兼容的接口转换成兼容的接口,使得不同接口之间能够协同工作。
- 复用已有代码:可以复用已有的类或接口,而无需修改其源代码,只需实现适配器即可。
- 灵活性:可以通过适配器替换不同的实现,从而实现不同的适配逻辑。
适配器模式的缺点:
- 增加了系统的复杂性,引入了适配器类,需要进行额外的设计和编码工作。
- 在设计时需要注意选择适配器的类型(类适配器、对象适配器、接口适配器),以及适配器和目标接口的设计,以确保系统的结构清晰和易于理解。
二、桥接模式(Bridge Pattern)
桥接模式(Bridge Pattern)是一种结构型设计模式,用于将抽象和实现部分解耦,使它们可以独立地变化。桥接模式通过使用组合关系,将抽象和实现部分分离成不同的类层次结构,从而实现抽象和实现的解耦。
意图:将抽象部分与其实现部分分离,使它们都可以独立地变化。
在传统的继承关系中,抽象类和具体实现类之间存在紧耦合关系,导致当抽象类或具体实现类发生变化时,可能会影响到彼此之间的稳定性和灵活性。而桥接模式通过将抽象和实现部分分离,使它们可以独立地演化和变化,从而实现了解耦和灵活性。
Bridge Pattern适用于:
- 不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如:这种情况可能是因为在程序运行时刻实现部分应可以被选择或者切换。
- 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这是 Bridge 模式使得开发者可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
- 对 一个抽象的实现部分的修改应对客户不产生影响,即客户代码不必重新编译。
- (C++)想对客户完全隐藏抽象的实现部分。
- 有许多类要生成的类层次结构。
- 想在多个对象间共享实现(可能使用引用技术),但同时要求客户并不知道这一点。
桥接模式包含两个关键角色:
-
抽象部分(Abstraction)
抽象部分定义了高层接口,包含抽象方法,并持有一个对实现部分的引用;
-
实现部分(Implementor)
实现部分定义了底层接口,包含具体的实现方法。
- 抽象部分和实现部分通过组合关系连接在一起,而不是继承关系,从而实现了抽象和实现的解耦。
桥接模式参考代码实现
1 |
|
桥接模式的优缺点:
桥接模式的优点:
- 解耦和灵活性:将抽象和实现部分解耦,使它们可以独立地演化和变化,互相之间不会影响,从而提高了系统的灵活性和可扩展性。
- 可复用性:可以通过组合不同的抽象和实现部分,构建不同的对象组合,从而实现复用性。
- 可扩展性:可以方便地扩展抽象和实现部分,而不影响其他部分的稳定性。
- 可替代性:可以方便地替换抽象和实现部分,从而实现不同的组合,而不需要修改抽象和实现部分的代码。
桥接模式的缺点:
- 增加了系统的复杂性,需要额外的设计和编码工作。
- 在设计时需要谨慎选择抽象和实现部分的接口,以确保系统的结构清晰和易于理解。
三、组合模式(Composite Pattern)
组合模式(Composite Pattern)是一种结构型设计模式,用于将对象组织成树形结构以表示“整体-部分”的层次结构,使客户端能够以一致的方式处理单个对象和组合对象,对象的层次结构可以递归地进行组合,从而形成复杂的结构。
意图:将对象组合成树型结构以表示“部分-整体”的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。
组合模式的核心思想是将叶子节点和组合节点统一对待,使得客户端在使用组合对象时不需要关心其具体类型,而是通过统一的接口对待整体和部分。这样可以简化客户端的代码,并且在需要增加新的组合对象时不需要修改客户端代码。
Composite Pattern适用于:
- 想表示对象的部分 - 整体层次结构。
- 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
组合模式中通常有两种类型的对象:
-
叶子节点(Leaf)
表示组合中的叶子节点,它没有子节点,通常是最基本的单元。
-
组合节点(Composite)
表示组合中的组合节点,它可以包含子节点,通常实现了与叶子节点相同的接口,并在其中包含一个子节点列表。
组合模式参考代码实现
1 |
|
组合模式的优缺点:
组合模式的优点:
- 简化客户端代码:客户端可以一致地对待叶子节点和组合节点,无需进行额外的类型判断。
- 可扩展性:可以方便地增加、删除或修改组合对象,而无需修改客户端代码。
- 灵活性:可以通过组合不同的叶子节点和组合节点来构建复杂的树状结构。
组合模式的缺点:
- 可能导致系统过于复杂:如果组合对象的层次结构过于复杂,可能会导致系统的理解和维护变得困难。
- 不适合所有情况:组合模式适用于那些需要对整体和部分进行一致性处理的情况,不适用于所有情况。
四、装饰器模式(Decorator Pattern)
装饰器模式(Decorator Pattern)是一种结构型设计模式,用于动态地给对象添加新的行为或功能,而不需要修改其原有的接口或实现。
意图:动态地给一个对象添加一些额外的职责。就增加功能而言,Decorator 模式比生成子类更加灵活。
通过使用装饰器模式,可以灵活地在运行时动态地添加、删除或修改对象的行为,而不需要修改原有的对象代码。这使得装饰器模式在需要扩展对象功能,但又不希望修改现有代码的情况下,具有很好的适用性。
Decorator Pattern适用于:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 处理那些可以撤销的职责。
- 当不能采用生成子类的方式进行扩充时。一种情况是:可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是:由于类定义被隐藏,或类定义不能用于生成子类。
装饰器模式有三个关键角色:
-
抽象组件(Component)
定义了对象的基本接口,可以是抽象类或接口,它是被装饰者和装饰者共同实现的接口。
-
具体组件(Concrete Component)
实现了抽象组件接口,是被装饰的对象,即被添加新行为的对象。
-
装饰器(Decorator)
继承自抽象组件,包含了一个抽象组件的引用,并且定义了与抽象组件接口相同的接口。装饰器可以添加新的行为,也可以在执行原有行为前后添加额外的处理逻辑。
装饰器模式参考代码实现
1 |
|
装饰器模式的优缺点:
装饰器模式优点:
- 动态扩展功能:装饰器模式允许在不修改原有对象的情况下,动态地添加、删除或修改对象的行为,从而实现了功能的动态扩展,避免了继承的静态扩展方式可能引发的类爆炸问题。
- 单一职责原则:装饰器模式将功能的添加和原有对象的实现解耦,使得每个类只负责一个单一的职责,符合设计原则中的单一职责原则,提高了代码的可维护性和可扩展性。
- 灵活性和可组合性:由于装饰器模式通过组合的方式实现功能的扩展,因此可以灵活地组合多个装饰器对象,实现不同功能的组合,具有较高的灵活性和可组合性。
装饰器模式缺点:
- 多层嵌套:使用装饰器模式可能会导致多层嵌套的装饰器对象,从而增加了代码的复杂性和理解难度。
- 接口增多:每个装饰器都需要实现与抽象组件相同的接口,这可能导致接口的增多,使得系统更加复杂。
- 运行时性能损耗:由于装饰器模式采用了动态添加行为的方式,可能会在运行时引入一定的性能损耗,尤其是在装饰器链较长的情况下。
五、外观模式(Facade Pattern)
外观模式(Facade Pattern)是一种设计模式,属于结构型设计模式。它提供了一个简化复杂系统接口的高层接口,用于简化客户端与系统之间的交互。外观模式通过隐藏系统的复杂性,将一组复杂的子系统接口封装成一个简单的接口,从而提供了一个更加简单、易于使用的接口供客户端使用。
外观模式通常包含一个外观类(Facade Class),该类作为客户端与子系统之间的中介,隐藏了子系统的复杂性,并提供了一个简单的接口供客户端使用。外观类可以理解为一个门面,将复杂的子系统的接口包装在一起,为客户端提供了一个统一的入口点。
意图:为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
Facade Pattern适用于:
- 要为一个复杂子系统提供一个简单接口时,子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类,这使得子系统更具有可重用性,也更容易对子系统进行定制,但也给那些不需要定制子系统的用户带来了一些使用上的困难。Facade 可以提供一个简单的默认视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制的用户可以越过 Facade 层。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 Facade 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
- 当需要构建一个层次结构的子系统时,使用 Facade 模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,则可以让它们仅通过 Facade 进行通信,从而简化了它们之间的依赖关系。
外观模式有三个关键角色:
-
外观(Facade)
外观类是外观模式的核心,提供了一个简单的接口供客户端使用,隐藏了子系统的复杂性。外观类通常包含对子系统的一系列调用,协调和管理子系统的功能。
-
子系统(Subsystem)
子系统是一组相关的类或对象,实现了复杂的功能,但对客户端来说是不可见的。子系统由外观类所封装,外观类通过调用子系统的接口来实现对其功能的访问。
-
客户端(Client)
客户端是使用外观模式的对象,通过调用外观类提供的简化接口来访问子系统的功能。客户端不直接与子系统进行交互,而是通过外观类来间接访问子系统的功能。
外观模式参考代码实现
1 |
|
外观模式的优缺点:
外观模式的优点:
- 简化客户端代码:外观模式提供了一个简单的接口,使得客户端调用子系统的功能变得简单和方便,减少了客户端与子系统之间的耦合,简化了客户端的代码。
- 封装复杂性:外观模式将子系统的复杂性封装在一个外观类中,对客户端来说,子系统的复杂性变得不可见,从而降低了客户端理解和使用子系统的难度。
- 提高系统灵活性:由于客户端只与外观类交互,而不直接与子系统交互,因此在不影响客户端的情况下,可以灵活地修改和扩展子系统的功能。
外观模式的缺点:
- 增加了一个外观类:引入了外观类作为客户端与子系统之间的中间层,可能会增加系统的复杂性和开发成本。
- 不符合开闭原则:如果需要修改或者扩展子系统的功能,可能需要修改外观类的接口或者实现,从而违反了开闭原则。
- 依赖子系统:外观模式依赖于子系统的稳定性,如果子系统发生变化,可能会导致外观类的修改,增加了系统的维护成本。
六、享元模式(Flyweight Pattern)
享元模式(Flyweight Pattern)是一种结构型设计模式,用于通过共享对象以节省内存和提高性能。它的核心思想是共享细粒度的对象,以减少内存使用和对象创建的开销。
在享元模式中,对象分为两种类型:内部状态(Intrinsic State)和外部状态(Extrinsic State)。内部状态是可以被共享的,而外部状态是不可共享的。内部状态是对象固有的属性,它不会随着外部环境的变化而变化,因此可以被多个对象共享。外部状态是对象的一些变化较频繁的属性,它会随着外部环境的变化而变化,因此不适合被共享。
享元模式通过将内部状态和外部状态进行分离,将内部状态作为共享的部分,可以在多个对象之间共享使用,从而节省内存空间。而外部状态则通过参数的方式传递给对象,在需要时进行设置,使得多个对象可以共享内部状态,但可以有不同的外部状态。
意图:运用共享技术有效地支持大量细粒度的对象。
Flyweight Pattern适用于:
- 一个应用程序使用了大量的对象。
- 完全由于大量的对象,造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以完全相对较少的共享对象取代很多组对象。
- 如果程序不依赖于对象标识。由于 Flyweight 对象可以被共享,所以对于概念上明显有别的对象,标识测试将返回真值。
享元模式通常包含以下角色:
-
Flyweight(抽象享元类)
定义了享元对象的接口,包含了需要被共享的方法。
-
ConcreteFlyweight(具体享元类)
实现了抽象享元类的接口,实现了共享的方法。
-
UnsharedConcreteFlyweight(非共享具体享元类)
不可以被共享的具体享元类,一般不实现共享的方法。
-
FlyweightFactory(享元工厂类)
负责创建和管理享元对象,实现了享元对象的共享池,可以通过工厂方法获取享元对象。
-
Client(客户端)
使用享元对象的客户端,通过享元工厂获取享元对象,并通过外部状态对享元对象进行操作。
享元模式参考代码实现
1 |
|
享元模式的优缺点
享元模式的优点:
- 节省内存:享元模式通过共享相同的对象实例来减少内存消耗。当多个对象需要相同的数据时,可以通过共享一个享元对象实例来避免创建多个相同的对象,从而减少内存使用。
- 提高性能:由于享元模式共享对象实例,可以减少对象的创建和销毁次数,从而提高系统的性能。特别是在需要频繁创建和销毁对象的情况下,使用享元模式可以显著提升性能。
- 简化对象管理:享元模式将共享对象的管理集中化,使得对象的创建和销毁更加简化和集中化。这有助于降低系统的复杂性,减少了对象管理的开销。
- 支持大量细粒度的对象:享元模式可以支持大量细粒度的对象共享,从而可以在需要处理大量对象时,有效地减少对象的数量,从而降低系统的开销。
享元模式的缺点
- 共享对象可能导致线程安全问题:如果多个线程同时访问共享的享元对象,并对其进行修改,可能会导致线程安全问题,需要进行适当的线程同步措施。
- 对象共享可能导致状态共享:享元模式中的对象共享可能导致不同对象之间共享状态,这可能会对系统的行为产生意外的影响,需要仔细设计和管理共享状态。
- 对象的复用可能受限:某些对象可能无法被复用,因为它们包含了不同的状态或行为,无法作为共享对象进行共享。
- 增加了系统复杂性:享元模式引入了共享对象的管理逻辑,可能会增加系统的复杂性,需要额外的管理和维护工作。
七、代理模式(Proxy Pattern)
代理模式(Proxy Pattern)是一种结构型设计模式,用于为其他对象提供一种代理或者占位符,以控制对原始对象的访问。
意图:为其他对象提供一种代理以控制对这个对象的访问。
在代理模式中,代理对象(Proxy)充当了客户端与原始对象之间的中介。客户端通过代理对象来访问原始对象,而不是直接访问原始对象。代理对象负责处理与原始对象相关的一些额外功能,例如权限控制、性能优化、延迟加载、缓存等。
Proxy Pattern适用于
- 远程代理(Remote Proxy):用于访问位于远程服务器上的对象,例如网络服务、Web服务等。
- 虚拟代理(Virtual Proxy):用于延迟加载对象的创建,直到真正需要访问对象时才进行实际创建。
- 安全代理(Protection Proxy):用于控制对敏感对象的访问权限,例如权限验证、身份验证等。
- 智能代理(Smart Proxy):用于在访问对象时添加额外的行为,例如性能监控、日志记录、缓存管理等。
代理模式有四个关键角色:
-
抽象主题(Subject)
定义了真实主题和代理主题共同的接口,可以是接口(Interface)或抽象类(Abstract Class)。抽象主题定义了真实主题和代理主题之间的公共操作,客户端通过抽象主题来访问真实主题或代理主题。
-
真实主题(Real Subject)
实现了抽象主题接口,是代理模式中真正执行业务逻辑的对象。真实主题是客户端想要访问的对象。
-
代理(Proxy)
实现了抽象主题接口,是客户端访问真实主题的代理对象。代理对象通常包含一个对真实主题的引用,同时负责在访问真实主题前后执行一些额外的操作,例如权限控制、性能优化、缓存管理等。
-
客户端(Client)
通过抽象主题接口来访问真实主题或代理对象,客户端通常不直接访问真实主题,而是通过代理对象来间接访问真实主题。
代理模式参考代码实现
1 |
|
代理模式的优缺点
代理模式的优点:
- 代理对象可以控制对原始对象的访问,提供了一种间接的方式来管理对象,可以灵活地添加、删除或替换代理对象,而不会影响到客户端代码。
- 代理对象可以为原始对象添加额外的功能,例如权限控制、性能优化、缓存等,提高了系统的灵活性、可维护性和性能。
- 代理模式可以隐藏原始对象的实现细节,保护了原始对象的安全性。
- 代理模式可以在不修改原始对象的情况下对其进行扩展或改变。
代理模式的缺点:
- 代理模式引入了额外的间接层,可能会导致系统的复杂性增加。
- 代理模式可能会引入一定的性能开销,特别是在远程代理和虚拟代理中,由于涉及到网络通信或延迟加载的操作。
- 代理模式需要额外的代码来实现代理对象,增加了系统的开发和维护成本。