设计模式之创建型设计模式详解
- 一、设计模式是什么?
- 二、模板方法
- 2.1、代码结构
- 2.2、符合的设计原则
- 2.3、如何扩展代码
- 2.4、小结
- 三、观察者模式
- 3.1、代码结构
- 3.2、符合的设计原则
- 3.3、如何扩展代码
- 3.4、小结
- 四、策略模式
- 4.1、代码结构
- 4.2、符合的设计原则
- 4.3、如何扩展代码
- 4.4、小结
- 总结
一、设计模式是什么?
设计模式总共有23种,那什么是设计模式呢?设计模式是指在软件开发中,经过验证的,用于解决在特定环境下,重复出现的,特定问题的解决方案。
从这个定义可以看出,设计模式有很多的限定词,比如“特定”、“重复特定”等。那说明什么问题呢?说明设计模式在使用的时候它有很多的局限性。所以,学习设计模式一定要切入它的一个本质,也就是它解决一个什么问题,然后再去使用它。当我们不清楚这个设计模式解决什么问题的时候,不要轻易的去使用设计模式,所以设计模式是适用的好。
设计模式的定义换一句都能够听懂的话,就是设计模式是解决软件开发过程中一些问题的固定套路,解决问题的固定套路。因此,不要过度的去封装或者去使用设计模式,除非我们已经确定了这个设计模式,就是明确了我们的这个具体需求的变化方向;而且这个变化方向的这个点呢,经常的出现,反复的出现,那么我们才会去使用设计模式,就是要使用恰当。还有一个,就是设计模式类似于一个哲学,或者说类似武侠小说里的一个武功秘籍,一定要具备一定的工程代码量的才能够精通。但是,学习设计模式还是有必要的,我们要提前知道设计模式。
设计模式分为创建型、行为型、结构型等等,接下来将讲解几个常见的创建型设计模式。
二、模板方法
模板方法是使用最频繁,并且是符合设计模式思想的一个设计模式。
(1)定义:定义一个操作中的算法的骨架 ,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
模板方法的定义很抽象,记住它是没有任何作用的,关键是怎么去分析它,要去分析它的稳定点和变化点。其实模板方法的定义中已经包含了它的稳定点和变化点,所以只需要理解模板方法里面描述的稳定点是什么,变化点是什么?才是能够帮助我们具体应用它。
(2)解决的问题:
- 稳定点:算法骨架。模板方法的稳定点是“定义一个操作的算法的骨架”。关键字就是骨架,不改变一个算法的结构好就是稳定点。
- 变化点:子流程需要变化。从定义上可以看出,模板方法的子类它需要重新定义该算法的某些特定步骤,这个是它的变化点。也就是说某一个算法的骨架由多个流程构成,模板方法解决的一个问题是这个流程通常是通过一个接口暴露出来,要在子类当中去重写,重写里面的一些若干子类。比如说一段流程可以用另外一个方法来实现,希望不改变一个算法的结构(即先后顺序),子类要做的是去重写,修改它若干的一些流程,修改完之后它未来的调用方式是先调用子类的第一个流程,然后调用子类的第二个流程,再调第三个流程,依次类推。所以变化点就是可能对一个或者是多个子流程进行变化。
学习某一个设计模式,不需要记住它的定义,要记的是它的稳定点是什么,它的变化点是什么,要以简短的语句来描述它,这样才能学会它。
2.1、代码结构
首先来了解一个具体的场景,这里举一个最简单的例子,帮助理解模板方法:
某个品牌动物园,有一套固定的表演流程,但是其中有若干个表演子流程可创新替换,以尝试迭代更新表演流程。
可以看到这个例子跟上面的定义极其的相似,通过这个语句里应该很快就能够发现出来它的稳定点:有一套固定的表演流程。品牌动物园里面去看这个表演可能有十几个节目,动物园不提供单个表演流程收费,而是一整套流程收费,哪怕是中间进来,也要把整个流程的票价买全。然后这个品牌动物园也是有市场竞争的,需要去替换某一些子流程,比如说有一些国庆节,想去进行一个优化以便跟国庆相关一些,这时可以安插进去,此外还需要去进行迭代创新。所以,它的变化点就是里面的子流程呢,需要去替换。
接下来看一下代码实现,首先是不使用设计模式的代码实现,如果代码没有设计模式,也要符合设计原则。目的是:
(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限定使用户不能单独的调用子流程);类与类依赖通过接口实现(依赖注入)。这里它有两个意思,第一个是指类封装的时候怎么实现这个类封装当中的接口隔离呢?主要是通过权限限定词(动物园有一个固定的表演流程,这个表演流程是以统一的方式提供的,把这个统一的流程用show()
来进行一个封装,并且是public限定词来进行封装它)。
(2)最少知道原则。用户只能看到show(),其他的子流程不可见。示例中有五个表演子流程,这些表演子流程不应该让用户去选择他们不需要的接口,所以用private
来进行限定。
以上代码破坏了哪些设计原则:
(1)单一职责原则。稳定点是show()
,子流程是变化点;迭代通过_type指定,随着迭代次数的增加,代码不断膨胀,当要知道某次迭代由哪些流程构成时极为困难。这些接口中有多个变化方向(随_type
变化),所以不满足单一职责原则。
(2)开闭原则。接口中有很多的if判断,每次迭代更新版本都修改了类,说明类不稳定。每一次迭代更新都需要去修改该类,所以应该要对扩展开放。C++的扩展方式:
- 通过继承的方式,继承无需修改原有类的基础上,通过继承的实现对功能的扩展。
- 通过组合的方式,通常面对对象当中的组合是指的多态的组合;主要通过组合基类的指针。
通过传入一个_type
来解决需求是不可取的,应该去扩展它。因此,符合设计模式的代码如下:
#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;
}
设计符合设计模式的代码时,首先希望这些Show0(),Show1,Show2,...
是需要暴露给子类去使用的,允许子类去重写它。前面的代码中通过private
限定词使子类无法够访问到Show0(),Show1,Show2,...
,private
只能自己可以使用,当然也可以打破这种限制(额外的知识点,可以用friend
,即友元类,比如说friend class A;
,此时这个类A对象是可以访问private的)。
所以,要修改这个限定词,让用户不能够访问它(如果用户可以访问那些接口,那么用户就可能会自由组合下面的接口;改成public
的话,用户可能会单独去调用它,也有可能独的去组合里面的调用),但又希望让子类可以去修改它,因此可以修改为protected
,protected
可以让子类去访问它。,注意,流程中有一个PlayGame()
,它是一个中间的、可有可无的一个流程,所以在这里用private
来修饰,因为不希望子类去重写它,并没有进行一个改变,它依然是private
限定词来进行限定。
protect
里面可以让子类去重写的一些接口,并且使用virtual
关键字修饰,virtual
关键字主要目的是为了使用多态,多态可以去通过继承,然后重写它的功能。也就是说接口对子类是开放的。
假设要修改这个Show0()
,子类直接去实现这个Show0()
,去修改它的子流程就行了,通过这种方式就可以直接使用它。在示例中使用一种多态的方式来使用它,子类指针通过晚绑定(早绑定是指里面没有虚函数的时候,它是一个早绑定,会把这个类强制转化为这个类,因为类很容易转换;晚绑定是指里面有一个virtual
关键字)会走虚函数,通过虚函数表指针对晚绑定到实际指向的对象。
通过这种方式来进行重写,比如说要有第二次更新迭代修改Show0()
和show2()
、第三次迭代要修改Show1()
和show2()
、第四次阶代修改Show1()
、show3()
和show4()
,就可以扩展代码。通过这种方式扩展代码之后基类就变得非常的稳定,因为每次迭代更新都没有修改里面的代码,只是在上面增加代码,就相当于整洁的房间有好动的猫,这好动的猫就是这四个函数,现在用笼子把这个猫关起来了;代码上通过扩展的方式来限定它变化的方向,利用继承并使用多态的方式进行扩展功能,这样一来变化的方向就在一个朝着一个方向变化。
在这里还涉及一个设计原则:里氏替换原则。主要是指多态当中的虚函数复写的流程,可以看到示例中的Show0()
有一个隐藏职责,如果这个表演流程没有超时的话,就会进行一个游戏环节;如果这个表演流程由于某些原因超时了则直接进入下一个表演流程Show1()
,不会进入中场互动游戏流程。示例中故意增加了这一个需求,主要解释一下里氏替换原则。里氏替换原则要求在多态当中的虚函数复写,即一定要实现父类方法的职责,示例中Show0()
的职责就是看有没有超时,示例中定义了expired
来实现。如果子类要去覆写这个Show0()
,要注意到它里面有一个隐含的职责,要遵循它(里氏替换 ),即实现某一个功能时不能只关注子流程的功能而忘记他的隐藏职责,如果直接一个return true
,就会不管超没超时都去玩游戏。所以去复写它的方法的时候一定要实现他的职责,这就是里氏替换(一定要把隐含职责实现起来,没有超时去玩游戏,超时了则进入下一个游戏)。
通过这样子的修改,代码就满足了设计原则。在迭代出一个设计模式的时候,只要让他满足一些一些设计原则就可以自动演变成设计模式,只需要修改少量的代码(第一个把这个限定词改成protected
,第二个在前面加一个virtual
关键字)就实现了模板方法。
模板方法的代码结构:
- 在基类中用一个
public
,并定义出一个骨架流程接口。 - 有一个
protected
限定词,所有的子流程对子类开放,并且是虚函数。 - 多态的使用方式。也就是用基类指针指向子类对象。
注意,在工作当中发现有一个public
,并提供了一个骨架流程接口,比如说某一个方法,骨架流程当中它里面的一些流程是对子类使用的protected
的关键字,并且看到虚函数,90%是模板方法。判定方式:
- 有一个
protected
限定词,就是对子类开放的。 - 一些流程是虚函数,并且子类当中覆写这些虚函数。
- 用基类指针指向一个子类的对象。
根据以上三点就可以确定90%是模板方法,一定要记住它的代码结构是什么样子,好方便去看代码的时候,一眼就能看出来它是什么设计模式。
2.2、符合的设计原则
模板方法符合哪些设计原则呢?最开始的时候只符合少量的设计原则,慢慢的它就迭代出模板方法。模板方法符合的设计原则如下:
- 单一职责。基类非常的稳定,只有一个职责,职责就是骨架,骨架接口都用
protected
进行或者是private
进行限定了,限定以后用户是不能够访问它的。 - 开闭原则。对扩展开放,对修改关闭。要改变它只能通过继承的方式,通过覆写方式来实现,利用继承的方式进行扩展来开放。
- 依赖倒置。子类的扩展时需要依赖基类的虚函数实现,使用者只依赖接口。依赖导致有两层含义,一个是依赖接口,第二个是依赖虚函数。所有的子类的这一个类的实现,都需要依赖基类的虚函数来进行实现,还要实现它隐含职责。即具体的子类实现依赖具体的某一个接口或者是函数。
- 封装了变化点。通过
protected
让它限定住变化的地方,让子类去扩展。 - 接口隔离。有两种,分别是:类与类之间的一个隔离和类的封装的本身通过限定词去进行隔离。
- 最小知道原则。对于用户而言,只需要知道一个接口,然后直接调用。
2.3、如何扩展代码
这个模板方法分析特别的详细,读者可以按照上述方式去分析设计模式。扩展方式主要通过写一个类去继承基类来修改里面的子流程,这对于初学者(代码量还不够的朋友)一定要掌握,即怎么去使用这个设计模式(设计模式已经写好了,怎么在上面扩展代码)需要掌握的。示例:
// 模板方法模式
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;
}
扩展代码的方法步骤:
- 实现子类继承基类,复写子流程。
- 通过多态调用方式去使用它。
2.4、小结
经典应用场景:模板方法是使用最频繁的,几乎每一个项目都会使用这个模板方法,项目中这个设计模式肯定是必然会出现。
要点:
(1)最常用的设计模式,子类可以复写父类子流程,使父类的骨架流程丰富;
(2)反向控制流程的典型应用;
(3)父类 protected 保护子类需要复写的子流程;这样子类的子流程只能父类来调用。
本质:通过固定算法骨架来约束子类的行为。
结构图:
思维导图:
三、观察者模式
(1)观察者模式同样是使用的比较频繁的一种设计模式,首先看一下它的定义:定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。这是设计模式的作者给的定义,不用记住它,要做的是去分析它的稳定点和变化点,即它解决的问题是什么。
(2)解决的问题:
分析一下定义:“对象间的一种一对多”这句话要注意一个对多个的依赖关系,“以便当一个对象发生改变时”中的“一个对象”是指“一对多”当中的“一”,“所有依赖它的对象都得到通知并自动更新”中“所有依赖它的对象”是指这个“多”。它的稳定点显然是这种一对多的依赖关系,它主要描述的就是“一”变化的时候“多”跟着变化,这种关系是稳定的,也是它的一个职责。它的变化点是指“多”增加和“多”减少。注意这个“一”对“多”没明确多少个,就是它变化的地方,观察者模式要解决的问题就是这个变化点,让这个变化点在有限范围内变化。
- 稳定点:“一”对“多”(变化)的依赖关系,“一”变化“多”跟着变化。
- 变化点:“多”增加,“多”减少。
分析设计模式就是分析它的稳定点和分析它的变化点。
3.1、代码结构
代码结构是最重要的,要知道怎么用代码来实现需求,实现稳定点和变化点。首先有这样一个需求:
气象站发布气象资料给数据中心,数据中心经过处理,将气象信息更新到两个不同的显示终端(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;
}
先实现不同的终端啊,比如说有DisplayA 、DisplayB 、DisplayC 三个终端,还有一个气象中心类、一个数据中心类,数据中心做数据处理,把数据发给气象中心,然后气象中心会去计算数据,计算这个数据之后产生的所有变化都发送给终端,终端依赖于数据的变更。不符合设计模式的情况下首先new
一个数据中心,所有的终端也new
出来,然后用数据中心计算一下资料产生数据(包括比如说天气、建议穿多少衣服、适不适合洗车等等这些其他的数据),计算完之后会有一个具体的数据,相对应的所有的终端都去更新这个数据。
这种实现方式有什么问题呢?能不能够实现变化的需求呢?可以看到,当终端增加之后,总是要修改代码TempNotify
添加一个接口,这样一来接口就变得不稳定了,因为每一次增加数据或者修改数据接口都要跟着变化。
因此,以上代码每次新增设备,都需要修改代码。这使稳定点变为不稳定,不符合设计原则 / 设计模式。 符合设计模式的实现如下:
#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;
}
为了满足设计模式,可以使用面向接口编程,首先提供一个接口Idisplay
,所有的终端都要继承自这个具体的接口;具体的终端的改变,不应该影响数据中心接口的稳定性,不稳定主要来源于具体终端的增加和减少,设计模式就是要让这个稳定点的变得稳定,可以通过给它增加一个存储容器;比如说有一个容器std::list<IDisplay*> obs;
,把所有的终端都存储好,增加一个接口就往里面添加一个终端来监听,终端想不再监听这个去数据中心的数据变化就调用Detach
,这些接口是对用户开放的,用户可以往里面增加终端,也可以往里面解除终端,示例中还有一个非常重要的功能就是Notify()
进行广播数据,数据发生变化时通知所有的终端跟着变化。
现在为了让这些不同的终端能够进行统一管理,示例中添加了IDisplay
类,用来描述这个具体终端的功能(这里是显示的功能),它是没有具体实现的,是一个纯虚函数;具体的终端如果要往里面增加,就去继承它,继承它之后实现显示的功能,因为每一个终端它的显示的方式不一样,这样一来解决了这个具体的问题,让原来的不稳定现在变得稳定。
应对稳定点通过抽象的方式来解决,应对变化点通过各种的方式来进行解决和扩展(多态和组合)。注意,稳定点也是需要抽象的,示例中是怎么让这个数据中心变得稳定呢?因为稳定是“一”(数据中心)对“多”(list
)的依赖关系,就可以依赖具体的接口。
未使用设计模式之前,示例是不稳定的,它不稳定的原因是因为终端端的增加或者是减少要修改代码;设计模式通过抽象它的稳定点(“一”对“多”的依赖关系),“一”是数据中心,“多”是把它抽象成一个具体的接口好,因为设备可能每个都不一样,但是他们都有一个相同的地方(显示功能),然后让他们的“一”对“多”的依赖建立在接口上面,并且用一个容器std::list<IDisplay*>
去保存它。因为它的“多”会有的时候增加,有的时候减少。示例中提供Attach
和Detach
给用户使用,以方便动态的添加或减少设备;另外也添加了Notify
接口为“多”服务,里面使用循环来遍历Show
接口。
3.2、符合的设计原则
这里分析一下观察者模式符合哪些设计原则:
- 面向接口编程。以上述例子分析,针对稳定点,代码声明了某一个接口,使程序根本不关注是哪一个终端设备,这个接口中有一个“显示”功能的,只需要知道对象所具有的接口就行了;因为所有的终端都有一个显示功能,数据中心只关注终端有没有显示功能,具体是什么对象,它根本不关注,只需要有一个显示功能就行了,数据中心把具体的数据算完之后直接调用这一个显示接口就行了,即 把终端的所有的显示接口都调用一下,那么这个一对多的关系就跟着变化了。针对变化点("多"可能增加好,"多"可能减少),每个终端都是自己调用(终端自己去变化),。
- 接口隔离。类与类依赖接口隔离。它有两种,一种是类的封装(通过限定值来实现这个接口隔离),类的封装当中用限定词(
public
protect
private
)去隔离具体的实现,决定哪些数据要暴露给用户使用,哪些不暴露给用户使用,不应该让用户依赖他们不需要的接口;另外一种是指类与类之间的关系好,即类与类的依赖应该依赖具体的接口,通过接口来进行隔离两个类。这里所说的符合接口隔离原则,主要是指第二种。示例中的“一”对“多”是数据中心与不同的设备所属的两个类,他们的依赖是通过一个具体的接口,这个接口起到了一个结耦合的作用,这个也是所说的组合接口。 - 封装变化点。如上述代码的
Attach()
和Detach()
。前面的模板方法是用protect
来进行限定实现封装变化点,使其他人不能够访问,但是可以让子类去重写,通过重写的方式来进行扩展功能;在这里的封装变化点因为变化点是“多”的增加和“多”的减少,因此通过提供两个接口(一个是Attach
解决“多”增加的问题,Detach
解决“多”减少的问题)实现封装变化点。
3.3、如何扩展代码
如何扩展这个代码呢?即如何扩展这个观察者模式呢?以上述例子分析:
首先是终端需要继承class IDisplay
接口,比如说现在增加了一个终端,只需要写少量的代码就可应对需求的变化:增加一个具体的类,这个类继承class IDisplay
接口,然后在某一个逻辑当中去写一个数据中心center(这个center在实际开发过程当中肯定是个单例模式),它会首先需要把终端给添加进来好,接下来数据中心center调用Notify
,主要加的代码只有两部分。
(1)继承实现接口。
(2)添加终端(调用Attach),即“多”增加。
(3)移除终端(调用Detach),即“多”减少。
3.4、小结
(1)要点:
- 观察者模式使得我们可以独立地改变目标与观察者,从而使二者之间的关系松耦合;
- 观察者自己决定是否订阅通知,目标对象并不关注谁订阅了;
- 观察者不要依赖通知顺序,目标对象也不知道通知顺序;
- 常用在基于事件的ui框架中,也是 MVC 的组成部分;
- 常用在分布式系统中、actor框架中;
(2)本质:触发联动。
(3)结构图:
观察者模式的定义不需要去记住,需要知道的是它解决什么问题;它的一个稳定点是什么?稳定点是"一"对"多"的依赖关系,"一"变化"多"也跟着变化;它的变化点是什么呢?它的变化点是这个“多”,它可能会增加,也有可能会减少。基于这种场景,既包含这个稳定点,又包含这种变化点的场景好,就是使用观察的模式的场景。
观察者模式的代码结构是通过有一个“一”的类,还有一个“多”的类,“多”是通过接口的方式来实现,“一”的类里面有一个容器容纳所有的“多”,“一”对“多”的依赖关系就通过这个接口来进行隔离。比如示例中因为只关注这个具体的“显示”的功能,并不关注终端是哪一个对象或者是哪一个类,只关注有没有“显示”功能。另外会有一个具体的接口抽象出这个“变化”,用一个接口来实现,不同的变化点就继承这一个接口,实现它的“显示”功能。
使用:会有一个单例(“一”对“多”中的“一”),需要加“显示”功能,“一” 定时会去调用Notify
,或者有变化的时候主动调Notify
,“多”不用关心这个,这个是“一”的职责不是“多”的职责。
怎么去扩展代码呢?扩展代码我们只需要写两部分代码:
- 继承接口。
- 在某个模块中
Attach
到“一”当中。
思维导图:
四、策略模式
(1)定义:定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。该模式使得算法可独立于使用它的客户程序而变化。
经过前面两种设计模式的讲解,我们应该知道学习一个策略模式的固定套路:分析稳定点和变化点。这个定义不是很重要,看不看得懂也没关系,没看懂也没关系。它解决什么问题呢?本质上就是它的稳定点是什么,它的变化点是什么。
(2)解决的问题:
- 稳定点:客户程序与算法的调用关系。定义中的“该模式使得算法可独立于使用它的客户程序而变化”就是稳定点,这个稳定点是指客户程序去调用这个具体接口的一种方式,这种调用关系是一个稳定点。客户程序反正是要调用某一个算法,客户程序自己来提供一些参数。
- 变化点:新增算法和算法内容变化。有很多很多不同的算法,可以去相互替换,客户程序不需要去关注不同的变化,反正是要调用这样的一个算法,然后客户端自己提供一些参数,得到符合所需要的一个结果。这一系列的方法他们是一种平行的关系,他们都是可替换的一个关系。这里解释这么多可能也没什么效果,后面看代码就明白了。
策略模式的稳定点只会是一对一的关系,调用某一个算法,但是有很多的候选项,传入哪一个就调哪一个,非常的简单。
4.1、代码结构
首先看一个背景:
某商场节假日有固定促销活动,为了加大促销力度,现提升国庆节促销活动规格。
这个背景的稳定点是“促销活动”,这个促销活动会去调用一个促销活动的算法,比如说买800送100买、买900送多少200,活动打几折等等,这个就是具体的一些算法。这个活动是整个商场的活动,可能去轮番的选择,总之会选择一个算法来做促销。
变化点就是某商场它有一个统一的活动(可能是打折、满减、送卡,送礼品等等),那么现在为了增加增加促销力度,提升某一个规格,或者增加一个活动(比如增加一个圣诞节、参与某一个什么活动)这些就是变化点。相对应的稳定点就是节假日会调用一个算法来实现这个促销活动。
我们来看具体的一个实现,当没有使用这个设计模式的时候的实现如下:
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) {
}
}
};
一个Promotion
的促销活动,里面有一个算法,根据不同的活动(春节活动、七夕活动、五一活动、国庆活动、圣诞活动等等)会有相对应的算法,里面很多if else
的使用,根据不同的这个规格去调用不同的算法,这些算法都是并行的一种关系,但是最终只会调用一个算法,这个就是一个普通的一个实现。
策略模式就是用来消除if else
的使用。看一下符合设计模式的一种方式,首先对于稳定点是要用抽象去解决,对于变化点通过扩展的方式去解决它(即继承和组合,组合是指多态的组合,不是指组合对象)。符合设计模式的实现如下:
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;
}
这里的稳定点是这个调用关系,提供了这样的一个接口class ProStategy
来抽象这个稳定点,这个接口就是具体的一个策略类。另外还有一个具体的Promotion
,是“促销活动”,目的应该是让Promotion
这个类稳定。前面的代码中这个类不稳定的原因是根据不同的活动来进行if else
判断,现在让这个类和具体的活动用接口进行隔离,要符合一个设计原则需要进行一个抽象,采用接口ProStategy *s
隔离的方式隔离这个促销类跟促销活动算法类。
这里还采用一种依赖注入的方式,引入了一个新的技术点:接口隔离有两种,一个是类的封装,一个是类与类依赖一个接口;类与类依赖一个接口前面讲解过是采用一个容器容器存储接口,在这里是采用的另外一种解决这个接口隔离的问题,也就是依赖注入。前面讲过的通过具体的函数其实也是依赖注入,通过函数把具体的接口的传进来,解决类与类之间的一个隔离的问题,也就是两个类的依赖性的只建立在一个接口上面,它也是一种依赖注入。通过这种依赖注入可以让整个类变得稳定了。未来增加任何的促销活动都不需要修改促销类,它永远是稳定的,因为它只需要调用一下算法,关于算法是怎么改变的,可以通过注入的方式来告诉这个促销类应该使用哪一个算法。
这样一来变化点都不会影响稳定点了。增加算法通过继承具体的接口ProStategy
,然后再去实现这一个CalcPro
函数。未来的变化点当中可能还要去进行修改算法内容,可以直接在自己的实现中修改CalcPro
函数就行了,不会影响其他的接口,也不会影响Promotion
促销类。
依赖注入:有一个具体的接口指针,通过函数或者构造函数传参进来的这种方式叫做依赖注入。
private:
ProStategy *s;
// ...
Promotion(ProStategy *sss) : s(sss){}
~Promotion(){}
代码结构小结:
(1)基类有接口。
(2)客户程序与算法的调用关系有一个类或接口,有调用封装。
(3)通过依赖注入调用。
(4)具体的调用。
4.2、符合的设计原则
- 接口隔离。解决的是一个类与类之间的一个接口隔离,这里的解决方案采用依赖注入,这是一个新的词,专门用于解决两个类只依赖一个接口,通过一个地方解决两个类的依赖。
- 面向接口编程。只关注接口所需的的功能就行了,接口里面实现了其他的功能根本不关心,比如国庆可能还实现了其他的职责,加了任何方法都跟促销没什么关系,可以加任意的方法,因为只关注的是某一个接口,这个接口就是某一个具体的算法,即面向接口编程。
- 开闭原则。对扩展开放,对修改关闭;类是没法修改的,只通过依赖注入的方式来进行改变,可以用构造函数也可以使用具体的某一个函数接口实现。
4.3、如何扩展代码
- 继承接口。写一个接口扩展它,然后实现具体的算法(解决新加算法的问题)。
- 通过依赖注入调相对应的函数。使用时new一个具体的一个算法,然后通过依赖注入的方式加到
promotion
当中去,promotion
调用这个接口。
4.4、小结
要点:
- 策略模式提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
- 策略模式消除了条件判断语句;也就是在解耦合。
本质: 分离算法,选择实现。
结构图:
思维导图:
总结
本文介绍了设计模式的概念及其在软件开发中的应用。首先,解释了设计模式是什么,它是一种解决特定问题的可复用且经过验证的解决方案。然后,重点介绍了三种常见的设计模式:模板方法、观察者模式和策略模式。
在模板方法部分,详细说明了代码结构和模板方法的作用。模板方法是一种行为型设计模式,它定义了一个算法的骨架,将一些步骤延迟到子类中实现。文章还讨论了符合的设计原则,并提供了如何扩展代码的实用建议。
接下来,介绍了观察者模式的代码结构和设计原则。观察者模式是一种发布-订阅模式,它用于定义对象之间的一对多依赖关系。文章强调了观察者模式的灵活性和可扩展性,并提供了如何扩展代码的示例。
最后,讨论了策略模式的代码结构和设计原则。策略模式是一种行为型设计模式,它定义了一系列可互换的算法,并使其能够独立于客户端而变化。文章强调了策略模式的可维护性和可测试性,并给出了扩展代码的建议。
通过阅读本文,读者将对设计模式有一个清晰的理解,并了解到如何应用模板方法、观察者模式和策略模式来解决实际的软件开发问题。无论是初学者还是有经验的开发人员,都能从本文中获得有益的知识和实用的技巧。