C++ 设计模式学习之路

设计模式概述

设计模式由来

  每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心。通过这种方式,你可以无数次地使用那些已有的解决方案,无需在重复相同的工作。《建筑的永恒之道》

  设计模式是在特定的环境下人们解决某类重复出现问题的一套成功或者有效的方案。

  设计模式(Design pattern)通常被有经验的面向对象软件开发人员使用。设计模式是软件开发人员在开发过程中面临的一般问题的解决方案。这些方案是众多软件开发人员经过长时间的试验和错误总结出来的。

  设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

设计模式的起源

  在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 的书,该书首次提到了软件开发中设计模式的概念。

  四位作者合称 GOF(四人帮,全拼 Gang of Four)。他们所提出的设计模式主要是基于以下的面向对象设计原则:

  • 对接口编程而不是对实现编程
  • 优先使用对象组合而不是继承

设计模式的类型

  根据设计模式的参考书 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 中所提到的,总共有 23 种设计模式。这些模式可以分为三大类:创建型模式(Creational Patterns)结构型模式(Structural Patterns)行为型模式(Behavioral Patterns)

设计模式目前种类:GoF 的 23 种 + “简单工厂模式” = 24 种

设计模式总览表

image.png

用一个图片来整体描述一下设计模式之间的关系:
image.png

设计模式的区别

1. 创建型模式

  创建型模式就是抽象了实例化的过程,创建对象的模式。它帮助一个系统独立于如何创建、组合和表示它的那些对象。创建型模式将创建对象的过程进行了抽象,也可以理解为将创建对象的过程进行了封装,作为客户程序仅仅需要去使用对象,而不关注创建对象过程中的逻辑。

  对象的创建会消耗掉系统的很多资源,所以单独对对象的创建进行研究,高效地创建对象就是创建型模式要探讨的问题。

2. 结构模式

  在解决了对象的创建问题之后,对象的组成以及对象之间的依赖关系就成了开发人员关注的焦点,因为如何设计对象的结构、继承和依赖关系会影响到后续程序的维护性、代码的健壮性、耦合性等。对象结构的设计很容易体现出设计人员水平的高低。

3. 行为模式

  在对象的结构和对象的创建问题都解决了之后,就剩下对象的行为问题了,如果对象的行为设计的好,那么对象的行为就会更清晰,它们之间的协作效率就会提高。
  行为型模式涉及到算法和对象间职责的分配,行为模式描述了对象和类的模式,以及它们之间的通信模式,行为型模式刻划了在程序运行时难以跟踪的复杂的控制流可分为行为类模式和行为对象模式。

  • 行为模式使用继承机制在类间分派行为
  • 行为对象模式使用对象聚合来分配行为。一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任何一个对象都无法单独完成的任务。

面向对象设计原则

  面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式种,它们是从许多设计方案中总结出的指导性原则。原则的目的:高内聚,低耦合

1. 单一职责原则(Single Responsibility Principle)

  类的职责单一,对外只提供一种功能,而引起类变化的原因都只有一个

2. 开闭原则(Open Close Principle)

  对扩展开放,对修改关闭。对类的改动是通过增加代码进行的,而不是修改源代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

//抽象一个计算机类
class AbstractCalculator {
public:
	virtual int GetResult() = 0;
	virtual void SetOperatorNumber(int x, int y) = 0;
};
//加法类
class PlusCalculator:public AbstractCalculator {
public:
	PlusCalculator() = default;
	PlusCalculator(int x, int y):a(x), b(y){}
	virtual int GetResult () {
		return a + b;
	}
	virtual void SetOperatorNumber(int x, int y) {
		a = x;
		b = y;
	}
private:
	int a;
	int b;
};
//减法类
class MinuteCalculator :public AbstractCalculator {
public:
	MinuteCalculator() = default;
	MinuteCalculator(int x, int y) :a(x), b(y) {}
	void SetOperatorNumber(int x, int y){
		a = x;
		b = y;
	}
	virtual int GetResult() {
		return a - b;
	}
private:
	int a;
	int b;
};

void test01() {
	AbstractCalculator *caculator = new PlusCalculator;
	caculator->SetOperatorNumber(10, 20);
	cout << caculator->GetResult() << endl;
}

  当计算器需要支持新的操作例如,增加一个乘法功能时,不需要对原来的代码进行修改,因为以前的代码是稳定运行的,如果贸然添加会造成不可控的问题,在上述问题中只需要添加如下的代码即可。

class MultCalculator :public AbstractCalculator {
public:
	virtual void SetOperatorNumber(int x, int y) {
		a = x;
		b = y;
	}
	virtual int GetResult() {
		return a * b;
	}
private:
	int a;
	int b;
};

3. 里氏代换原则(Liskov Substitution Principle)

  里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

4. 依赖倒转原则(Dependence Inversion Principle)

  依赖于抽象(接口),不要依赖具体的实现(类),针对接口编程
image.png

  传统的过程式设计倾向于使高层次的模块依赖于低层次的模块,抽象成依赖于具体实现层。当底层模块的被改动时,将会影响整个系统的开发,牵一发而动全身,不利于系统的维护,且从图中可以看出程序的耦合性很强。


  举例说明如下:银行办公可能有 存款、转账、支付等业务, 在 #if 0 中,实现了一个 BankWorker 类,其中有银行工作人员可以办理的工作,为了方便使用,中层业务模块将每个职能变成一个函数方便高层的业务逻辑调用。就目前来看,业务逻辑是较为清晰的,也正是符合了传统的设计方式,高层次模块依赖于中层模块,中层模块依赖底层的模块,因为它们都是针对具体的对象进行操作的,后期如果需要添加一些其他的功能,例如添加一个炒股的功能,首先要在底层模块的 BankWorker 中添加一个 炒股功能的模块,然后中层模块需要添加一个新的功能用于实现新功能方法的调用,其次高层业务模块需要使用新的接口才能调用这个功能,这就是所谓的牵一发而动全身。其次,BankWorker 这个类实现了三个功能,不符合 单一职责原则,且当添加功能的时候需要对原来的代码进行改变,不符合 开闭原则,而在使用依赖倒转原则后都会有所改变,具体介绍如下。

#if 0
//银行工作人员类
class BankWorker {
public:
	void SaveService() { cout << "办理存款业务..." << endl; }
	void PayService() { cout << "办理支付业务..." << endl; }
	void TranferService() { cout << "办理转账业务..." << endl; }
};

//中层模块
void DoSave(BankWorker* worker) { worker->SaveService(); }
void DoPay(BankWorker* worker) { worker->PayService(); }
void DoTranfer(BankWorker* worker) { worker->TranferService(); }

//高层业务逻辑
void test1() {
	BankWorker* worker = new BankWorker;
	DoSave(worker);
	delete worker;
}
#endif

![image.png](https://neo00.top/upload/2020/08/image-1bc7a95b63504a0b913b436918a79144.png)   在如下代码中,通过多态的机制,中层模块不再依赖于具体的对象,而是依赖于一个抽象的类,大大减少了底层模块于中层模块的耦合程度,当需要添加功能的时候,只需要像开闭原则那样添加即可,且中层模块不需要改动,高层模块至于条于新的对象建立关系,~~怎么感觉这一步增大了耦合程度呢?~~
//底层模块
class AbstractBankWorker {
public:
	virtual void doWork() = 0;
};

class SaveWorker :public AbstractBankWorker {
public:
	virtual void doWork() { cout << "办理存款业务..." << endl; }
};

class PayWorker :public AbstractBankWorker {
public:
	virtual void doWork() { cout << "办理支付业务..." << endl; }
};

class TranferWorker :public AbstractBankWorker {
public:
	virtual void doWork() { cout << "办理转账业务..." << endl; }
};

//中层模块
void doBusiness(AbstractBankWorker* worker) {
	worker->doWork();
}

//高层模块
void test2() {
	AbstractBankWorker* worker = new TranferWorker;
	doBusiness(worker);
	delete worker;
	worker = nullptr;
}

5. 接口隔离原则(Interface Segregation Principle)

  不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外开放的功能,不应该把所有操作都封装到一个接口去,降低类之间的耦合度。

6. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)

  如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合就降低了这种依赖关系。**对于继承和组合,优先使用组合。**在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另一个模块的内部实现细节,这样当一个模块内部发生改变时,不会影响到其他模块的使用。(黑盒原理)

  举例说明如下,当一个人有很多车时,所有的车都可以抽象成一个类,里面都一个启动的属性。人可以继承不同的车辆,使用不同车辆的启动方法,这样就是实现了复用。但是,一次只能继承一种车的类型,这样无法实现接口的统一,所以此处不能使用继承,而是组合。使用组合,让人拥有一辆抽象的车,通过多态进行调用,就形成了统一的接口。

class AbstractCar {  //抽象车类,有一个启动属性
public:
	virtual void run() = 0;
};

class Dazhong :public AbstractCar { //不同的车辆有不同的启动方法
public:
	virtual void run() { cout << "大众启动..." << endl; }
};

class Tuolaji :public AbstractCar {
public:
	virtual void run() { cout << "拖拉机启动..." << endl; }
};

#if 0
//使用继承的方式实现代码复用,只能针对实体编程,如果选择多继承则会出现二义性的问题,虚继承当然更不行
class Person :public Dazhong {
public:
	void Doufeng() {
		run();
	}
};

class PersonB :public Tuolaji {
public:
	void Doufeng() {
		run();
	}
};
#endif

//使用组合的方式,实现的接口的统一,一个人可以驾驶不同的车辆
class Person {
public:
	void SetCar(AbstractCar* car) { this->car = car; }
	void Doufeng() { 
		car->run(); //多态
		delete car;
		car = nullptr;
	}
private:
	AbstractCar* car = nullptr;
};

void test1() {
	Person* p1 = new Person;
	p1->SetCar(new Dazhong);
	p1->Doufeng();

	p1->SetCar(new Tuolaji);
	p1->Doufeng();
	delete p1;
}

7. 迪米特法则(Law Of Demeter)

  又叫最少知识原则,一个对象应该对其他对象尽可能少的了解,从而降低各个对象之间的耦合性,提高系统的可维护性。

  举例说明如下,当一个人想要购买一个楼盘时,有很多不同的楼盘可供选择,而此时客户需要参观了解每个楼盘,以至于耦合性极度升高,如下图所示。
image.png
  但是客户如果只是想买一个高品质的楼房,那么就没必要和其余类型的楼房打交道,不需要了解所有的楼房,此时中介应运而生。
image.png
  客户只需要和中介产生一个耦合关系即可完成这件事情,不需要知道楼盘具体的情况,也就是迪米特法则,感觉有点像依赖倒转原则

class AbstractBuiliding {  //创建一个抽象楼盘类,会有很多楼盘,类似前面的开闭原则
public:
	virtual void sale() = 0;
	virtual string& getQulity() = 0;
};

class BuildingA :public AbstractBuiliding {
public:
	BuildingA() { mQulity = "高品质"; }
	virtual void sale() { cout << "楼盘A" << mQulity << "被售卖!" << endl; }
	virtual string& getQulity() { return mQulity; }
protected:
	string mQulity;
};

class BuildingB :public AbstractBuiliding {
public:
	BuildingB() { mQulity = "低品质"; }
	virtual void sale() { cout << "楼盘B" << mQulity << "被售卖!" << endl; }
	virtual string& getQulity() { return mQulity; }
protected:
	string mQulity;
};

class Mediator { //创建一个抽象中介,也有可能会有很多中介,这里暂时不深究
public:
	Mediator() {
		AbstractBuiliding* building = new BuildingA;
		vBuilding.push_back(building);
		building = new BuildingB;
		vBuilding.push_back(building);
	}

	AbstractBuiliding* findBuilding(string quality) {
		for (auto it = vBuilding.begin(); it != vBuilding.end(); it++) {
			if ((*it)->getQulity() == quality) {
				return *it;
			}
		}
		return nullptr;
	}

	~Mediator() {
		for (auto it = vBuilding.begin(); it != vBuilding.end(); it++) {
			if (*it) {
				delete* it;
				*it = nullptr;
			}
		}
	}
protected:
	vector<AbstractBuiliding*> vBuilding;
};

void test01() {
	Mediator* mediator = new Mediator;  //至于需要建立一次关系即可。
	AbstractBuiliding* building = mediator->findBuilding("高品质");
	if (building) {
		building->sale();
	}
	else {
		cout << "没有合适的楼盘" << endl;
	}
}

有一个叫“漂流石”的网友一篇笔记对六大设计原则的解释特别简单精辟,拿来借鉴一下:

开闭原则:实现热插拔,提高扩展性。

里氏代换原则:实现抽象的规范,实现子父类互相替换;

依赖倒转原则:针对接口编程,实现开闭原则的基础;

接口隔离原则:降低耦合度,接口单独设计,互相隔离;

迪米特法则,又称不知道原则:功能模块尽量独立;

合成复用原则:尽量使用聚合,组合,而不是继承;

单一职责原则:每一个类应该专注于做一件事情。

设计模式学习之路

创建型模式

  创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。

 创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。

简单工厂模式(Simple Factory)
工厂方法模式(Factory Method)
抽象工厂模式(Abstract Factory)
[建造者模式(Builder)]
单例模式(Singleton)

结构型模式

  结构型模式(Structural Pattern)描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。

结构型模式可以分为类结构型模式和对象结构型模式:

  • 类结构型模式关心类的组合,由多个类可以组合成一个更大的

  系统,在类结构型模式中一般只存在继承关系和实现关系 对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。 根据 合成复用原则,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。

适配器模式(Adapter)
[桥接模式(Bridge)]
[组合模式(Composite)]
装饰模式(Decorator)
外观模式(Facade)
[享元模式(Flyweight)]
代理模式(Proxy)

行为型模式

  行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。

  通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象 之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。

行为型模式分为类行为型模式和对象行为型模式两种:

  • 类行为型模式:类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。
  • 对象行为型模式:对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据 合成复用原则,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。

[职责链模式(Chain of Responsibility)]
命令模式(Command)
[解释器模式(Interpreter)]
[迭代器模式(Iterator)]
[中介者模式(Mediator)]
[备忘录模式(Memento)]
观察者模式(Observer)
[状态模式(State)]
策略模式(Strategy)
模板方法模式(Template Method)
[访问者模式(Visitor)]

相关参考

C++设计模式(全26讲)
图说设计模式
设计模式学习
菜鸟教程