配套视频课程已更新完毕,大家可通过以下两种方式观看视频讲解:

关注公众号:爱编程的大丙,或者进入大丙课堂学习。


1. 厨师山治

山治是文斯莫克家族的第三子,基因改造人,由于小时候未能觉醒能力而被父亲文斯莫克·伽治放逐到东海。遇见恩师哲普后在海上餐厅巴拉蒂担任厨师。为了寻找传说之海ALL BLUE而随草帽一伙踏入伟大航路。

image-20220925154732063

在海上航行的每一天里,最忙的应该就是山治了。他需要准备食材、做早饭、做午饭、做下午茶、做晚饭,一整天都在不同的状态之间忙碌。

在设计模式中有一种和山工作治状态类似的模式叫做状态模式。状态模式就是在一个类的内部会有多种状态的变化,因为状态变化从而导致其行为的改变,在类的外部看上去这个类就像是自身发生了改变一样。

在日常生活中由于内部属性的变化导致外在样貌或者行为发生改变的例子比比皆是,比如:

  • 人在幼年、童年、少年、中年、老年各个使其的形态都是不一样的

  • 工作期间,上午、中午、下午、傍晚、深夜的工作状态也不一样

  • 人的心情不同时,会有喜、怒、哀、乐

  • 手机在待机、通话、充电、游戏时的状态也不一样

  • 文章的发表会有草稿、审阅、发布状态

    文档对象的全部状态

状态模式和策略模式比较类似,策略模式中的各个策略是独立的不关联的,但是状态模式下的对象的各种状态可以是独立的也可以是相互依赖的,比如上面关于文章的发布的例子:

  • 普通用户的文章草稿发表之后被审阅,审阅失败重新变成草稿
  • 管理用户的文章操作发布成功变成已发表状态, 发布失败重新变成草稿

2. 山治的一天

假设这一天草帽一伙在一直在海上航行,没有遇到海军也没有遇到极端天气,对于船上的成员来说这又是可以大饱口福的一天。

2.1 开始工作

山治作为厨师一整天都在忙碌,我们在这里简单的把他的工作状态划分一下:上午的状态、中午的状态、下午的状态、晚上的状态。不论哪种状态他都是在认真的在完成自己的本职工作,只不过在不同的时间点工作的内容是不一样的。所以,我们可以给这些状态定义一个基类:

1
2
3
4
5
6
7
8
9
// State.h
// 抽象状态
class Sanji;
class AbstractState
{
public:
virtual void working(Sanji* sanji) = 0;
virtual ~AbstractState() {}
};

由于这个状态是属于山治的,所以在这个抽象的状态类中通过提供的工作函数working()的参数指定了这个状态的所有者,在这里只是对山治类 Sanji做了一个声明第3行,尽量不要在这个头文件中包含山治的头文件,否则会造成头文件重复包含(因为山治类和状态类需要相互引用对方)。

有了上面的抽象的状态类,就可以基于这个基类把山治全天对应的状态类的子类依次定义出来了:

头文件 State.h

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
// 上午状态
class ForenoonState : public AbstractState
{
public:
void working(Sanji* sanji) override;
};

// 中午状态
class NoonState : public AbstractState
{
public:
void working(Sanji* sanji) override;
};

// 下午状态
class AfternoonState : public AbstractState
{
public:
void working(Sanji* sanji) override;
};

// 晚上状态
class EveningState : public AbstractState
{
public:
void working(Sanji* sanji) override;
};

源文件 State.cpp

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include "State.h"
#include "Sanji.h"
using namespace std;

void ForenoonState::working(Sanji* sanji)
{
int time = sanji->getClock();
if (time < 8)
{
cout << "当前时间<" << time << ">点, 准备早餐, 布鲁克得多喝点牛奶..." << endl;
}
else if (time > 8 && time < 11)
{
cout << "当前时间<" << time << ">点, 去船头钓鱼, 储备食材..." << endl;
}
else
{
sanji->setState(new NoonState);
sanji->working();
}
}

void NoonState::working(Sanji* sanji)
{
int time = sanji->getClock();
if (time < 13)
{
cout << "当前时间<" << time << ">点, 去厨房做午饭, 给路飞多做点肉..." << endl;
}
else
{
sanji->setState(new AfternoonState);
sanji->working();
}
}

void AfternoonState::working(Sanji* sanji)
{
int time = sanji->getClock();
if (time < 15)
{
cout << "当前时间<" << time << ">点, 准备下午茶, 给罗宾和娜美制作爱心甜点..." << endl;
}
else if (time > 15 && time < 18)
{
cout << "当前时间<" << time << ">点, 和乔巴去船尾钓鱼, 储备食材..." << endl;
}
else
{
sanji->setState(new EveningState);
sanji->working();
}
}

void EveningState::working(Sanji* sanji)
{
int time = sanji->getClock();
if (time < 19)
{
cout << "当前时间<" << time << ">点, 去厨房做晚饭, 让索隆多喝点汤..." << endl;
}
else
{
cout << "当前时间<" << time << ">点, 今天过得很高兴, 累了睡觉了..." << endl;
}
}

在状态类的源文件中包含了山治类的头文件,因为在这些状态类的子类中重写working()函数的时候,需要通过山治类对象调用他的成员函数。通过上面的代码可以看到山治在不同的时间状态下所做的事情是不一样的。

另外我们可以看到状态模式下各个模式之间是可以有依赖关系的,这一点和策略模式是有区别的,策略模式下各个策略都是独立的,当前策略不知道有其它策略的存在。

2.2 山治

上面定义的一系列的状态都是属于山治这个对象的,只不过通过状态模式来处理山治全天的工作状态变化的时候,把他们都分离出去了,成了独立的个体,从逻辑上讲他们之间是包含和被包含的关系,从UML类图的角度来讲他们之间是组合(整体和部分)关系。关于山治这个类我们可以这样定义:

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
// Sanji.h
#pragma once
#include "State.h"

class Sanji
{
public:
Sanji()
{
m_state = new ForenoonState;
}
void working()
{
m_state->working(this);
}
void setState(AbstractState* state)
{
if (m_state != nullptr)
{
delete m_state;
}
m_state = state;
}
void setClock(int time)
{
m_clock = time;
}
int getClock()
{
return m_clock;
}
~Sanji()
{
delete m_state;
}
private:
int m_clock = 0; // 时钟
AbstractState* m_state = nullptr;
};

在山治类中有两个私有成员变量:

  1. m_clock:通过这整形的时钟变量来描述一天中当前这个时刻的时间点
  2. m_state:通过这个状态指针来保存当前描述山治状态的对象

关于山治类的成员函数有以下这么几个:

  1. working() :工作函数,在不同的时间状态下,工作的内容也不同
  2. setClock() :设置当前的时间
  3. getClock() :得到当前的时间
  4. setState() :设置山治当前的状态

2.3 工作日志

我们可以修改山治类内部的时钟的值来模拟时间的流逝,这样山治的状态就会不停地发生变化了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
Sanji* sanji = new Sanji;
// 时间点
vector<int> data{7, 10, 12, 14, 16, 18, 22};
for (const auto& item : data)
{
sanji->setClock(item);
sanji->working();
}
delete sanji;

return 0;
}

最后我们来看一下山治这一整天都干了些什么吧:

1
2
3
4
5
6
7
当前时间<7>点, 准备早餐, 布鲁克得多喝点牛奶...
当前时间<10>点, 去船头钓鱼, 储备食材...
当前时间<12>点, 去厨房做午饭, 给路飞多做点肉...
当前时间<14>点, 准备下午茶, 给罗宾和娜美制作爱心甜点...
当前时间<16>点, 和乔巴去船尾钓鱼, 储备食材...
当前时间<18>点, 去厨房做晚饭, 让索隆多喝点汤...
当前时间<22>点, 今天过得很高兴, 累了睡觉了...

3. 结构图

最后画一下状态模式对应的UML类图(学会状态模式之后,要先画UML类图再写程序。

image-20220925230648189

如果对象需要根据当前自身状态进行不同的行为, 同时状态的数量非常多且与状态相关的代码会频繁变更或者类对象在改变自身行为时需要使用大量的条件语句时,可使用状态模式。