结构型设计模式介绍了如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。
部分插图来自: https://refactoringguru.cn/design-patterns/catalog
一、适配器模式
适配器模式作为两个不兼容的接口之间的桥梁, 它使接口不兼容的对象能够相互合作。
适配器模式主要包括以下角色:
- 目标接口:当前系统业务所期待的接口,由于与客户端不兼容,因此无法直接调用其功能。即上图中的圆孔,下例中的狗。
- 适配者类:被访问和适配的现存组件库中的组件接口。即上图中的方钉,下例中的猫。
- 适配器类:它是一个转换器,通过继承或封装适配者,接受客户端基于适配者发起的调用,并将其转换为适用于目标接口的调用。即上图中的方顶转圆钉适配器,它封装了一个方钉,却又假扮成一个圆钉,在比如下例中伪装成狗的猫。
适配器模式优点: 只要客户端代码通过客户端接口与适配器进行交互,就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器,满足开闭原则。
适配器模式缺点:因为需要新增一系列接口和类,会增加代码的复杂度。有时直接更改服务类使其与其他代码兼容会更简单。
适配器模式的实现主要分为两种:
- 类适配器:适配器同时继承两个对象的接口,其中目标接口的相关函数需要为虚函数,以便适配器对其进行重写。
- 对象适配器:适配器实现其中一个对象(目标接口)的接口,并对另一个对象(适配者)进行封装。
a.类适配器
#include <iostream>
#include <string>
using namespace std;
/* 适配者 狸花猫 */
class Cat {
public:
string m_name = "DragonLi";
string getName() {
return m_name;
}
};
/* 目标接口 萨摩耶 */
class Dog {
public:
string m_name = "Samoyed";
virtual string getName() {
return m_name;
}
};
/* 适配器 伪装成狗的猫 */
class Adapter : public Dog, public Cat {
public:
/* 重写的是萨摩耶的 getName 方法 */
string getName() override {
return Cat::m_name;
}
};
/* 宠物狗商店 */
class DogStore {
public:
static string showDog(Dog *dog) {
return "A dog: " + dog->getName();
}
};
int main() {
/* 从宠物狗商店中直接得到宠物狗 */
string dogName = DogStore::showDog(new Dog);
cout << dogName << endl;
/* 从宠物狗商店中通过适配器得到宠物猫 */
string fakeDogName = DogStore::showDog(new Adapter);
cout << fakeDogName << endl;
/* 从宠物狗商店中直接得到宠物猫 */
// string catName = DogStore::showDog(new Cat); // Cannot initialize a parameter of type 'Dog *' with an rvalue of type 'Cat *'
// cout << catName << endl;
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
A dog: Samoyed
A dog: DragonLi
atreus@MacBook-Pro %
b.对象适配器
#include <iostream>
#include <string>
using namespace std;
/* 适配者 狸花猫 */
class Cat {
public:
string m_name = "DragonLi";
string getName() {
return m_name;
}
};
/* 目标接口 萨摩耶 */
class Dog {
public:
string m_name = "Samoyed";
virtual string getName() {
return m_name;
}
};
/* 适配器 伪装成狗的猫 */
class Adapter : public Dog, public Cat {
public:
Cat *m_cat;
explicit Adapter(Cat *cat) {
m_cat = cat;
}
/* 重写的是萨摩耶的 getName 方法 */
string getName() override {
return m_cat->getName();
}
~Adapter() {
delete m_cat ;
}
};
/* 宠物狗商店 */
class DogStore {
public:
static string showDog(Dog *dog) {
return "A dog: " + dog->getName();
}
};
int main() {
/* 从宠物狗商店中直接得到宠物狗 */
string dogName = DogStore::showDog(new Dog);
cout << dogName << endl;
/* 从宠物狗商店中通过适配器得到宠物猫 */
string fakeDogName = DogStore::showDog(new Adapter(new Cat));
cout << fakeDogName << endl;
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
A dog: Samoyed
A dog: DragonLi
atreus@MacBook-Pro %
二、桥接模式
桥接模式将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,用组合关系代替继承关系,从而降低了抽象和实现的耦合度。
桥接模式主要包含以下角色:
- 抽象化角色 :提供高层控制逻辑的抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化角色 :抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化角色 :实现化角色的抽象类,供扩展抽象化角色调用。抽象化角色仅能通过在这里声明的方法与具体实现化角色交互。
- 具体实现化角色 :实现化角色接口的具体实现。
桥接模式优点:
- 客户端代码仅与高层抽象部分进行互动,不会接触到平台的详细信息。
- 可以新增抽象部分和实现部分,且它们之间不会相互影响,满足开闭原则。
- 抽象部分专注于处理高层逻辑,实现部分处理平台细节,满足单一职责原则。
桥接模式缺点:对高内聚的类使用该模式可能会让代码更加复杂。
#include <iostream>
#include <string>
using namespace std;
/* 宠物基类 实现化角色 */
class Pet {
protected:
string m_name;
public:
virtual string getName() = 0;
};
/* 猫咪类 具体实现化角色 */
class Cat : public Pet {
public:
string getName() override {
m_name = "Cat";
return m_name;
}
};
/* 狗狗类 具体实现化角色 */
class Dog : public Pet {
public:
string getName() override {
m_name = "Dog";
return m_name;
}
};
/* 颜色基类 抽象化角色 */
class Color {
protected:
string m_color;
Pet *m_pet;
public:
explicit Color(Pet *pet) {
m_pet = pet;
}
virtual string getColorAndName() = 0;
};
/* 黑色 扩展抽象化角色 */
class Black : public Color {
public:
explicit Black(Pet *pet) : Color(pet) {
}; // 显式调用基类构造函数
string getColorAndName() override {
m_color = "black";
return m_color + " " + m_pet->getName();
}
};
/* 白色 扩展抽象化角色 */
class Write : public Color {
public:
explicit Write(Pet *pet) : Color(pet) {
};
string getColorAndName() override {
m_color = "write";
return m_color + " " + m_pet->getName();
}
};
int main() {
Color *black = new Black(new Cat); // 抽象的颜色必须与具体的宠物进行结合才能被实现
cout << black->getColorAndName() << endl;
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
black Cat
atreus@MacBook-Pro %
三、组合模式
组合模式可以将对象组合成树状结构,用来表示部分以及整体层次,并且使用单一的对象来使用它们。
组合模式主要包含以下角色:
- 组件接口:此接口描述了树中简单项目(树叶)和复杂项目(树枝)所共有的操作。
- 树枝节点:包含了叶节点或其他树枝节点等子项目的单位。树枝节点并不知道其子项目所属的具体类,它只能通过通用的组件接口与其子项目交互。树枝节点在接收到请求后会将工作分配给自己的子项目,处理中间结果,然后将最终结果返回给客户端。
- 叶子节点:是树的基本结构且不包含子项目。叶节点会完成大部分的实际工作,因为它们无法将工作指派给其他部分。
组合模式优点:
- 可以利用多态和递归机制更方便地使用复杂树结构。
- 无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分,满足开闭原则。
组合模式缺点:对于功能差异较大的类,提供公共接口或许会有困难。有些时候为了使用组合模式,甚至需要过度一般化组件接口,者会使其变得令人难以理解。
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
#define FOUR_SPACE " "
/* 组件接口 海军小队和军舰都是海军的组件 */
class Component {
protected:
string m_name;
vector<Component *> m_list;
public:
explicit Component(string name) : m_name(std::move(name)) {
}
virtual ~Component() {
for (auto i: m_list) {
delete i;
}
}
virtual void getInfo(int n) = 0;
/* 该方法仅能由海军小队子类进行重写 */
virtual void add(Component *component) {
cout << "Illegal operation" << endl;
}
};
/* 海军小队类 树枝节点 可能包含其他小队或军舰 */
class NavySquad : public Component {
public:
explicit NavySquad(string name) : Component(std::move(name)) {
}
void getInfo(int n) override {
int tmp = n;
while (tmp--) {
cout << FOUR_SPACE;
}
cout << "|-" << m_name << endl;
for (auto i: m_list) {
i->getInfo(n + 1);
}
}
void add(Component *component) override {
m_list.emplace_back(component);
}
};
/* 军舰类 叶子节点 */
class Warship : public Component {
public:
explicit Warship(string name) : Component(std::move(name)) {
}
void getInfo(int n) override {
int tmp = n;
while (tmp--) {
cout << FOUR_SPACE;
}
cout << "|-" << m_name << endl;
}
};
int main() {
Component *navy = new NavySquad("navy");
Component *navy_squad1 = new NavySquad("navy_squad1");
Component *navy_squad2 = new NavySquad("navy_squad2");
Component *navy_squad3 = new NavySquad("navy_squad3");
Component *warship1 = new Warship("warship1");
Component *warship2 = new Warship("warship2");
Component *warship3 = new Warship("warship3");
Component *warship4 = new Warship("warship4");
Component *warship5 = new Warship("warship5");
navy->add(navy_squad1);
navy->add(navy_squad2);
navy_squad1->add(warship1);
navy_squad1->add(warship2);
navy_squad2->add(navy_squad3);
navy_squad2->add(warship3);
navy_squad3->add(warship4);
navy_squad3->add(warship5);
navy->getInfo(0);
delete navy;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
|-navy
|-navy_squad1
|-warship1
|-warship2
|-navy_squad2
|-navy_squad3
|-warship4
|-warship5
|-warship3
atreus@MacBook-Pro %
四、装饰模式
装饰模式允许通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为,即在不改变现有对象结构的情况下,动态地给该对象增加一些职责。
装饰模式主要包含以下角色:
- 抽象构件:定义一个抽象接口以规范被封装的对象。
- 具体构件:实现抽象构件,它定义了基础行为,但装饰类可以改变这些行为。
- 抽象装饰:继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰:定义了可动态添加到具体构件的额外行为,具体装饰类会重写抽象装饰的方法,并在调用父类方法之前或之后进行额外的行为。
装饰模式优点:
- 无需创建新子类即可扩展对象的行为,且可以在运行时添加或删除对象的功能。
- 可以将实现了许多不同行为的一个大类拆分为多个较小的类,满足单一职责原则。
装饰模式缺点:
- 实现行为不受装饰栈顺序影响的装饰比较困难。
- 各层的初始化配置代码看上去可能会很糟糕。
#include <iostream>
#include <string>
using namespace std;
/* 猫基类 抽象构件 */
class Cat {
public:
virtual void bark() = 0;
virtual ~Cat() = default;
};
/* 狸花猫 具体构件 */
class DragonLi : public Cat {
public:
void bark() override {
cout << "DragonLi: meow~";
}
};
/* 缅因猫 具体构件 */
class MaineCoon : public Cat {
public:
void bark() override {
cout << "MaineCoon: meow~";
}
};
/* 抽象装饰 */
class Decorator : public Cat {
protected:
Cat *m_cat;
public:
explicit Decorator(Cat *cat) : m_cat(cat) {
}
void bark() override {
m_cat->bark();
}
~Decorator() override {
delete m_cat;
}
};
/* 狸花猫装饰类 具体装饰 */
class DragonLiDecorator : public Decorator {
public:
explicit DragonLiDecorator(Cat *cat) : Decorator(cat) {
}
void bark() override {
cout << "DragonLiDecorator(";
m_cat->bark();
cout << ")" << endl;
}
};
/* 缅因猫装饰类 具体装饰 */
class MaineCoonDecorator : public Decorator {
public:
explicit MaineCoonDecorator(Cat *cat) : Decorator(cat) {
}
void bark() override {
cout << "MaineCoonDecorator(";
m_cat->bark();
cout << ")" << endl;
}
};
int main() {
Decorator *dagonLiDecorator = new DragonLiDecorator(new DragonLi);
dagonLiDecorator->bark();
delete dagonLiDecorator;
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
DragonLiDecorator(DragonLi: meow~)
atreus@MacBook-Pro %
五、外观模式
外观模式能为程序库、框架或其他复杂类提供一个简单且一致的接口,外部应用程序不用关心内部子系统的具体的细节,是对迪米特法则(一个类对于其他类知道的越少越好)的典型应用。
外观模式主要包含以下角色:
- 外观角色:为多个子系统对外提供一个共同的接口。
- 子系统角色:实现系统的部分功能,客户可以通过外观角色访问它。
外观模式优点: 大大降低了应用程序的复杂度,提高了程序的可维护性。
外观模式缺点: 外观可能成为与程序中所有类都耦合的上帝对象。
#include <iostream>
/* 炮弹事件类 子系统角色基类 并非必须 */
class ShellEvent {
public:
virtual void executeEvent() = 0;
};
/* 发射事件类 子系统角色 */
class Fire : public ShellEvent {
public:
void executeEvent() override {
std::cout << "The shell have been fired." << std::endl;
}
};
/* 飞行事件类 子系统角色 */
class Fly : public ShellEvent {
public:
void executeEvent() override {
std::cout << "The shell is flying." << std::endl;
}
};
/* 命中事件类 子系统角色 */
class Hit : public ShellEvent {
public:
void executeEvent() override {
std::cout << "The shell has hit." << std::endl;
}
};
/* 外观类 外观角色 */
class Facade {
private:
Fire *m_fire;
Fly *m_fly;
Hit *m_hit;
public:
Facade() {
m_fire = new Fire();
m_fly = new Fly();
m_hit = new Hit();
}
void execute() {
m_fire->executeEvent();
m_fly->executeEvent();
m_hit->executeEvent();
}
~Facade() {
delete m_fire;
delete m_fly;
delete m_hit;
}
};
int main() {
Facade facade;
facade.execute();
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
The shell have been fired.
The shell is flying.
The shell has hit.
atreus@MacBook-Pro %
六、享元模式
享元模式摒弃了在对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让我们能在有限的内存容量中载入更多对象。
内部状态和外部状态:
- 内部状态:不会随着环境的改变而改变的可共享部分,即下例中的猫的名字。
- 外部状态:随环境改变而改变的不可以共享的部分,即下例中猫的颜色。
享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要包含以下角色:
- 抽象享元角色:通常是一个抽象类,声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 具体享元角色:实现了抽象享元类,称为享元对象。具体享元类为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元角色:并不是所有抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类。当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
享元模式优点:如果程序中有很多相似对象,那么将节省大量内存。
享元模式缺点:
- 可能需要牺牲执行速度来换取内存,因为每次调用享元方法时都需要重新计算部分数据,比如下例中的颜色如果不是字符串而是一个对象,那么每次调用猫咪时都要处理这个颜色对象。
- 代码会变得更复杂。
#include <iostream>
#include <string>
using namespace std;
/* 猫基类 抽象享元角色 */
class Cat {
public:
virtual string getName() = 0;
void bark(const string &color) {
cout << color << " " << getName() << ": meow~ " << endl;
}
virtual ~Cat() = default;
};
/* 狸花猫 具体享元角色 */
class DragonLi : public Cat {
public:
string getName() override {
return "DragonLi";
}
};
/* 缅因猫 具体享元角色 */
class MaineCoon : public Cat {
public:
string getName() override {
return "MaineCoon";
}
};
/* 猫咪工厂类 享元工厂角色 */
class CatFactory {
private:
CatFactory() = default;
~CatFactory() {
delete m_dragonLi;
delete m_maineCoon;
}
Cat *m_dragonLi = new DragonLi;
Cat *m_maineCoon = new MaineCoon;
public:
static CatFactory *getInstance() {
static CatFactory instance;
return &instance;
}
Cat *CreateCat(const std::string &category) {
if (category == "DragonLi") {
return m_dragonLi;
} else {
return m_maineCoon;
}
}
};
int main() {
CatFactory *catFactory = CatFactory::getInstance();
Cat *dragonLi1 = catFactory->CreateCat("DragonLi");
dragonLi1->bark("black");
Cat *maineCoon = catFactory->CreateCat("MaineCoon");
maineCoon->bark("white");
Cat *dragonLi2 = catFactory->CreateCat("DragonLi");
dragonLi2->bark("grey");
cout << "dragonLi1 == dragonLi2 ? " << (dragonLi1 == dragonLi2) << endl;
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
black DragonLi: meow~
white MaineCoon: meow~
grey DragonLi: meow~
dragonLi1 == dragonLi2 ? 1
atreus@MacBook-Pro %
七、代理模式
代理模式使我们能够提供对象的替代品或其占位符。代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。
代理模式主要包含以下角色:
- 抽象主题类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理类:提供了与真实主题相同的接口,它的内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
代理模式优点:
- 可以在客户端毫无察觉的情况下控制服务对象。
- 不管服务对象是否存在,有没有准备好,代理都能正常工作。
- 可以在不对服务或客户端做出修改的情况下创建新代理,满足开闭原则。
代理模式缺点:
- 代码可能会变得复杂,因为需要新建许多类。
- 服务响应可能会延迟。
#include <iostream>
/* 猫基类 */
class Cat {
public:
virtual void bark() = 0;
virtual ~Cat() = default;
};
/* 狸花猫 */
class DragonLi : public Cat {
public:
void bark() override {
std::cout << "DragonLi: meow~" << std::endl;
}
~DragonLi() override {
std::cout << "~DragonLi()" << std::endl;
}
};
class CatProxy : public Cat {
private:
Cat *m_cat;
public:
CatProxy() {
m_cat = new DragonLi;
}
void bark() override {
std::cout << "Get a cat by Proxy: ";
m_cat->bark();
}
~CatProxy() override {
delete m_cat;
}
};
int main() {
CatProxy catProxy;
catProxy.bark();
return 0;
}
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
Get a cat by Proxy: DragonLi: meow~
~DragonLi()
atreus@MacBook-Pro %