一、为什么需要设计模式?
1.1、设计模式的定义
设计模式大概有23种。
设计模式是指在软件开发中,经过验证的,用于解决在特定环境下,重复出现的,特定问题的解决方案。
从定义可以看出,设计模式的使用有很多的局限性。一定要明确它解决什么问题,再使用它。当不清楚设计模式解决什么问题时不要轻易使用。
通俗的讲,设计模式是解决软件开发过程中一些问题的固定套路。不要过度的封装或使用设计模式,除非明确了需求的具体变化方向,而且变化方向的点是反复的出现,才会使用设计模式;即慎用设计模式。
设计模式要到达一定的工程代码量才能精通。但是,了解设计模式是需要的。
1.2、设计模式由来
设计模式的由来可以追溯到20世纪80年代,由计算机科学家埃里希·伽玛(Erich Gamma)等人首次提出。他们将设计模式定义为可重复利用的解决方案,用于常见问题和设计挑战。
设计模式的出现是为了解决软件开发中的一些常见问题,帮助开发人员更高效地编写可维护和可扩展的代码。通过使用设计模式,开发人员可以借鉴先前的成功经验,避免重复发明轮子,同时提高代码的可读性和可理解性。
设计模式的目标是提供经过验证和经过时间考验的解决方案,以解决特定情境中的常见问题。设计模式不是一种具体的算法或代码片段,而是一种在特定情境下的解决方案模板。它们可以应用于各种编程语言和开发环境中。
设计模式通常分为三种类型:创建型模式、结构型模式和行为型模式。
- 创建型模式关注对象的创建机制;
- 结构型模式关注对象之间的关系和组织方式;
- 行为型模式关注对象之间的交互和通信。
一些常见的设计模式包括单例模式、工厂模式、观察者模式、策略模式等。
一句话来说,就是:满足设计原则后,慢慢迭代出来的。
1.3、 设计模式解决的问题
使用设计模式的前提条件:具体的需求既有稳定点又有变化点。
(1)稳定点,即不会变的东西。如果全是稳定点,不需要设计模式。
(2)变化点,即经常发生变化。如果全是变化点,发生的改变没有具体的方向,这也不需要设计模式。比如游戏开发,使用脚本语言解决全是变化的点,因为脚本不需要重新编译,热更新就可以。
设计模式具体解决问题的场景:希望修改少量的代码,就可以适应需求的变化。比如,整洁的房间有一个好动的猫,如何保证房间的整洁?把猫关到笼子中,使猫在有限范围内活动。
也就是使用设计模式,让变化点在有限范围内变化。
二、设计模式基础
设计模式和开发语言相关的,利用语言的特性实现设计模式。
对于C++而言,设计模式的基础是:
(1)面向对象的思想。面向对象的三个特征,封装(目的是隐藏实现细节,实现模块化)、继承(目的是希望无需修改原有类的基础上,通过继承来实现功能的扩展;C++可以多继承)、多态(静态多态是函数重载,同一个函数名但参数不同来同时表现出不同的形态;动态的多态是继承中虚函数的重写)。设计函数很多依赖于动态的多态
(2)设计原则。
2.1、C++多态之虚函数重写
假设一个基类,有两个虚函数:
class Base{
public:
virtual void func1(){}
virtual void func2(){}
int a;
};
其虚函数表和内存布局为:
此时有一个子类继承Base:
class Subject : public Base{
public:
virtual void func2(){}
virtual void func3(){}
int b;
};
其虚函数表和内存布局为:
从内存布局可以看到,有虚函数就会为该类生成虚函数表指针,虚函数表是编译的时候编译器自动帮我们自动生成的。虚函数表其实是一个一维数组,数组的元素保存的虚函数地址,通过偏移就可以调用到相对应的函数。
对于Base类而言,虚函数表有func1和func2;Subject继承Base,它的虚函数表中也会有Base的虚函数,而且虚函数表中Base的虚函数在Subject的虚函数前面。
如果Subject没有重写Base虚函数,那么虚函数表中保存的虚函数地址是一样的(如示例中的func1)。
如果Subject重写Base虚函数,那么虚函数表中会发生替换,将Subject重新的虚函数地址替换掉Base中相应虚函数的地址(如示例中的func2)。
如果Subject自己有新的虚函数,则也要加入虚函数表中。
2.2、多态的体现
(1)早绑定。假如有Base *p=new Subject;如果Subject没有重写Base虚函数,那么会将Subject类型转换为Base类型,这就是早绑定。
(2)晚绑定。假如有Base *p=new Subject;如果Subject重写了Base虚函数,那么p实际指向的是Subject对象,这就是晚绑定。
2.3、扩展方式
(1)继承。
(2)组合。
2.4、多态组合
// 继承
class Subject : public Base{
};
// 组合
class Subject{
private:
Base base;
};
设计模式中的组合通常是指组合基类指针。好处是可以扩展Base的功能,通过多态方式让组合解耦合。
// 组合基类指针
class Subject{
private:
Base *base;
};
三、设计原则
设计原则是设计模式还没产生它就存在了。设计原则是多代程序员总结的开发原则。
3.1、依赖倒置
实现要依赖接口,接口又可以转换为抽象,即具体实现的代码需要依赖这个抽象。具体使用接口(客户)也要依赖这个抽象。
高层模块不应该依赖低层模块,两者都应该依赖抽象;
抽象不应该依赖具体实现,具体实现应该依赖于抽象;
自动驾驶系统公司是高层,汽车生产厂商为低层,它们不应该互相依赖,一方变动另一方也会跟着变动;而应该抽象一个自动驾驶行业标准,高层和低层都依赖它;这样以来就解耦了两方的变动;自动驾驶系统、汽车生产厂商都是具体实现,它们应该都依赖自动驾驶行业标准(抽象)。
3.2、开放封闭
一个类应该对扩展(组合和继承)开放,对修改关闭。针对封装和多态。
3.3、面向接口
不将变量类型声明为某个特定的具体类,而是声明为某个接口;客户程序无需获知对象的具体类型,只需要知道对象所具有的接
口;减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案;主要针对封装。
3.4、封装变化点
将稳定点和变化点分离,扩展修改变化点;让稳定点和变化点的实现层次分离。主要针对封装和多态。
3.5、单一职责
一个类应该仅有一个引起它变化的原因。主要针对封装。
3.6、里氏替换
子类型必须能够替换掉它的父类型;主要出现在子类覆盖父类实现,原来使用父类型的程序可能出现错误;覆盖了父类方法却没有实现父类方法的职责。
主要针对多态中的虚函数重写。
3.7、接口隔离
(1)不应该强迫客户依赖于它们不用的方法;
(2)一般用于处理一个类拥有比较多的接口,而这些接口涉及到很多职责;
(3)客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。
通过限定词隔离。类与类之间依赖接口,通过接口隔离类。
3.8、组合优于继承
继承耦合度高,组合耦合度低。
3.9、最小知道原则
让用户尽量不选择它不需要的接口。
四、模板方法模式
4.1、定义
定义一个操作中的算法的骨架 ,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
从定义中可以分析出该设计模式解决的问题。
(1)稳定点:算法架构。
(2)变化点:子流程需要变化。
举个例子:
某个品牌动物园,有一套固定的表演流程,但是其中有若干个表演子流程可创新替换,以尝试迭代更新表演流程。
4.2、代码结构
(1)基类中有骨架流程接口。
(2)所有子流程对子类开放并且是虚函数。
(3)多态的使用方式。
通过以上三点可以肯定是模板方法模式。
如果代码没有设计模式,也要符合设计原则。目的是:
(1)设计模式是由设计原则演变来的;
(2)符合设计原则的代码,只需要修改少量代码就能够演变成设计模式。
#include <iostream>
using namespace std;
class ZooShow {
public:
ZooShow(int type = 1) : _type(type) {}
public:
void Show() {
if (Show0())
PlayGame();
Show1();
Show2();
Show3();
}
private:
void PlayGame() {
cout << "after Show0, then play game" << endl;
}
bool Show0() {
if (_type == 1) {
//
return true;
} else if (_type == 2 ) {
// ...
} else if (_type == 3) {
}
cout << _type << " show0" << endl;
return true;
}
void Show1() {
if (_type == 1) {
cout << _type << " Show1.1" << endl;
} else if (_type == 2) {
cout << _type << " Show1.2" << endl;
} else if (_type == 3) {
}
cout << _type << " Show1" << endl;
}
void Show2() {
if (_type == 20) {
}
cout << "base Show2" << endl;
}
void Show3() {
if (_type == 1) {
cout << _type << " Show3.1" << endl;
} else if (_type == 2) {
cout << _type << " Show3.2" << endl;
}
cout << _type << " Show3" << endl;
}
private:
int _type;
};
int main () {
ZooShow *zs = new ZooShow(1);
zs->Show();
return 0;
}
以上代码满足的设计原则:
(1)接口隔离原则。类封装的时候使用权限限定词(统一或固定的流程用public,子流程使用private限定使用户不能单独的调用子流程);类与类依赖通过接口实现(依赖注入)。
(2)最少知道原则。用户只能看到show(),其他的子流程不可见。
以上代码破坏了哪些设计原则:
(1)单一职责原则。稳定点是show(),子流程是变化点;迭代通过_type指定,随着迭代次数的增加,代码不断膨胀,当要知道某次迭代由哪些流程构成时极为困难。这些接口中有多个变化方向(随_type变化),所以不满足单一职责原则。
(2)开闭原则。接口中有很多的if判断,每次迭代更新版本都修改了类,说明类不稳定。
因此,符合设计模式的代码如下:
#include <iostream>
using namespace std;
// 开闭
class ZooShow {
public:
void Show() {
// 如果子表演流程没有超时的话,进行一个中场游戏环节;如果超时,直接进入下一个子表演流程
if (Show0())
PlayGame();
Show1();
Show2();
Show3();
}
private:
void PlayGame() {
cout << "after Show0, then play game" << endl;
}
bool expired;
// 对其他用户关闭,但是子类开放的
protected:
virtual bool Show0() {
cout << "show0" << endl;
if (! expired) {
return true;
}
return false;
}
virtual void Show2() {
cout << "show2" << endl;
}
virtual void Show1() {
}
virtual void Show3() {
}
};
// 框架
// 模板方法模式
class ZooShowEx10 : public ZooShow {
protected:
virtual void Show0() {
if (! expired) {
return true;
}
return false;
}
}
class ZooShowEx1 : public ZooShow {
protected:
virtual bool Show0() {
cout << "ZooShowEx1 show0" << endl;
if (! expired) { // 里氏替换
return true;
}
return false;
}
virtual void Show2(){
cout << "show3" << endl;
}
};
class ZooShowEx2 : public ZooShow {
protected:
virtual void Show1(){
cout << "show1" << endl;
}
virtual void Show2(){
cout << "show3" << endl;
}
};
class ZooShowEx3 : public ZooShow {
protected:
virtual void Show1(){
cout << "show1" << endl;
}
virtual void Show3(){
cout << "show3" << endl;
}
virtual void Show4() {
//
}
};
/*
*/
int main () {
ZooShow *zs = new ZooShowEx10; // 晚绑定
// ZooShow *zs1 = new ZooShowEx1;
// ZooShow *zs2 = new ZooShowEx2;
zs->Show();
return 0;
}
(1)子流程可以被子类反问和修改,使用virtual关键字子流程,修改限定词为protected:使子类可以修改而用户不能访问。
(2)子类不能修改主流程,只能修改子流程。
(3)因此,每次迭代更新都没有修改主类的代码,子类通过重写虚函数来更新子流程。
(4)满足里氏替换原则。show0()有一个隐含职责,如果没有超时expired则需要进入PlayGame(),即示例代码中的返回true和false的选择。
virtual void Show0() {
if (! expired) {
return true;
}
return false;
}
4.3、符合的设计原则
(1)单一职责。
(2)开闭原则。不能改变基类代码,只能虚函数重写。
(3)依赖倒置。所有子类的接口都要依赖虚函数实现。即实现依赖接口,子类扩展时需要依赖基类的虚函数实现,使用者只依赖接口。
(4)封装变化点。通过protected限定符限制住变化的地方,暴露给子类去扩展。
(5)接口隔离。
(6)最小知道原则。用户不需要知道对它没有用的接口。
4.4、扩展
实现子类继承基类,重写子流程。通过多态调用方式使用。
// 模板方法模式
class ZooShowEx10 : public ZooShow {
protected:
virtual void Show0() {
if (! expired) {
return true;
}
return false;
}
}
// 调用
int main () {
ZooShow *zs = new ZooShowEx10; // 晚绑定
// ZooShow *zs1 = new ZooShowEx1;
// ZooShow *zs2 = new ZooShowEx2;
zs->Show();
return 0;
}
4.5、经典应用场景
模板方法是很常用的设计模式,基本所有使用设计模式的项目都会用得到。
4.6、要点
(1)最常用的设计模式,子类可以复写父类子流程,使父类的骨架流程丰富;
(2)反向控制流程的典型应用;
(3)父类 protected 保护子类需要复写的子流程;这样子类的子流程只能父类来调用。
4.7、本质
通过固定算法骨架来约束子类的行为。
五、观察者模式
5.1、定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
5.2、解决的问题
(1)稳定点:“一”对“多”(变化)的依赖关系,“一”变化“多”跟着变化。
(2)变化点:“多”增加,“多”减少。
5.3、代码结构
举个例子:
气象站发布气象资料给数据中心,数据中心经过处理,将气象信息更新到两个不同的显示终端(A 和B等等)。
class DisplayA {
public:
void Show(float temperature);
};
class DisplayB {
public:
void Show(float temperature);
};
class DisplayC {
public:
void Show(float temperature);
}
class WeatherData {
};
class DataCenter {
public:
void TempNotify() {
DisplayA *da = new DisplayA;
DisplayB *db = new DisplayB;
DisplayC *dc = new DisplayC;
// DisplayD *dd = new DisplayD;
float temper = this->CalcTemperature();
da->Show(temper);
db->Show(temper);
dc->Show(temper);
dc->Show(temper);
}
private:
float CalcTemperature() {
WeatherData * data = GetWeatherData();
// ...
float temper/* = */;
return temper;
}
WeatherData * GetWeatherData(); // 不同的方式
};
int main() {
DataCenter *center = new DataCenter;
center->TempNotify();
return 0;
}
以上代码每次新增设备,都需要修改代码。这使稳定点变为不稳定,不符合设计原则 / 设计模式。
#include <list>
#include <algorithm>
using namespace std;
//
class IDisplay {
public:
virtual void Show(float temperature) = 0;
virtual ~IDisplay() {}
};
class DisplayA : public IDisplay {
public:
virtual void Show(float temperature) {
cout << "DisplayA Show" << endl;
}
private:
void jianyi();
};
class DisplayB : public IDisplay{
public:
virtual void Show(float temperature) {
cout << "DisplayB Show" << endl;
}
};
class DisplayC : public IDisplay{
public:
virtual void Show(float temperature) {
cout << "DisplayC Show" << endl;
}
};
class DisplayD : public IDisplay{
public:
virtual void Show(float temperature) {
cout << "DisplayC Show" << endl;
}
};
class WeatherData {
};
// 应对稳定点,抽象
// 应对变化点,扩展(继承和组合)
class DataCenter {
public:
void Attach(IDisplay * ob) {
// 添加设备
}
void Detach(IDisplay * ob) {
// 移除设备
}
void Notify() {// 一变化,多跟着变化
float temper = CalcTemperature();
for (auto iter : obs) {
iter.Show(temper);
}
}
// 接口隔离
private:
WeatherData * GetWeatherData();
float CalcTemperature() {
WeatherData * data = GetWeatherData();
// ...
float temper/* = */;
return temper;
}
std::list<IDisplay*> obs;
};
int main() {
// 单例模式
DataCenter *center = new DataCenter;
// ... 某个模块
IDisplay *da = new DisplayA();
center->Attach(da);
// ...
IDisplay *db = new DisplayB();
center->Attach(db);
IDisplay *dc = new DisplayC();
center->Attach(dc);
center->Notify();
//-----
center->Detach(db);
center->Notify();
//....
center->Attach(dd);
center->Notify();
return 0;
}
通过抽象的方式让稳定的变得更稳定。通过扩展方式(继承和组合)应对变化点。
5.4、符合的设计原则
(1)面向接口编程。
(2)接口隔离。类与类依赖接口隔离。
(3)封装变化点。如上述代码的Attach()和Detach()。
5.5、扩展
(1)继承实现接口。
(2)调用Attach()。
(3)调用Detach()。
5.6、要点
(1)观察者模式使得我们可以独立地改变目标与观察者,从而使二者之间的关系松耦合;
(2)观察者自己决定是否订阅通知,目标对象并不关注谁订阅了;
(3)观察者不要依赖通知顺序,目标对象也不知道通知顺序;
(4)常用在基于事件的ui框架中,也是 MVC 的组成部分;
(5)常用在分布式系统中、actor框架中。
5.7、本质
触发联动
八、策略模式
8.1、定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。该模式使得算法可独立于使用它的客户程序而变化。
(1)稳定点:客户程序与算法的调用关系。
(2)变化点:新增算法和算法内容变化。
8.2、代码结构
(1)基类有接口。
(2)客户程序与算法的调用关系有一个类或接口,有调用封装。
(3)通过依赖注入调用。
(4)具体的调用。
举个例子:
某商场节假日有固定促销活动,为了加大促销力度,现提升国庆节促销活动规格。
这个的稳定点是【固定促销活动】,变化点是促销力度。
enum VacationEnum {
VAC_Spring,
VAC_QiXi,
VAC_Wuyi,
VAC_GuoQing,
VAC_ShengDan,
};
class Promotion {
VacationEnum vac;
public:
double CalcPromotion(){
if (vac == VAC_Spring {
// 春节
}
else if (vac == VAC_QiXi) {
// 七夕
}
else if (vac == VAC_Wuyi) {
// 五一
}
else if (vac == VAC_GuoQing) {
// 国庆
}
else if (vac == VAC_ShengDan) {
}
}
};
牢记,通过抽象解决稳定点,通过扩展(扩展和组合)解决变化点。
class Context {
};
// 稳定点:抽象去解决它
// 变化点:扩展(继承和组合)去解决它
class ProStategy {
public:
virtual double CalcPro(const Context &ctx) = 0;
virtual ~ProStategy();
};
// cpp
class VAC_Spring : public ProStategy {
public:
virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_QiXi : public ProStategy {
public:
virtual double CalcPro(const Context &ctx){}
};
class VAC_QiXi1 : public VAC_QiXi {
public:
virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_Wuyi : public ProStategy {
public:
virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_GuoQing : public ProStategy {
public:
virtual double CalcPro(const Context &ctx){}
};
class VAC_Shengdan : public ProStategy {
public:
virtual double CalcPro(const Context &ctx){}
};
class Promotion {
public:
Promotion(ProStategy *sss) : s(sss){}
~Promotion(){}
double CalcPromotion(const Context &ctx){
return s->CalcPro(ctx);
}
private:
ProStategy *s;
};
int main () {
Context ctx;
ProStategy *s = new VAC_QiXi1();
Promotion *p = new Promotion(s);
p->CalcPromotion(ctx);
return 0;
}
8.3、符合的设计原则
(1)接口隔离。依赖注入,通过一个接口解决两个类的依赖。
private:
ProStategy *s;
通过函数或者构造函数传参进来的这种方式叫做依赖注入。
Promotion(ProStategy *sss) : s(sss){}
~Promotion(){}
(2)面向接口编程。
(3)开闭原则。
8.4、扩展
实现一个类继承基类,通过依赖注入的方式调用相对应函数。
8.5、要点
(1)策略模式提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换;
(2)策略模式消除了条件判断语句;也就是在解耦合。
8.6、本质
分离算法,选择实现;
总结
详述设计模式的定义、由来、解决的问题;C++多态;设计原则以及三个设计模式 模板方法模式、观察者模式、策略模式。
对于C++的private权限,一般只能自己可以访问,如果其他的类要访问其private的内容,只能通过友缘类来访问。
class a{
friend class b;
private:
// ...
}
模板方法模式 有骨架接口,子流程通过virtual关键字暴露给子类重写,调用时晚绑定;即 子类可以重写父类的子流程,使父类流程丰富;本质是通过固定骨架约束子类的行为。这是最常用的设计模式。
观察者模式 可以独立的改变目标和观察者,使两种之间的关系松耦合;定义对象间一对多的依赖关系,”一“变化“多”也跟着变化;本质是触发联动。
策略模式 通过依赖注入实现算法的替换;实现一个类继承基类,通过依赖注入的方式调用相对应函数。