状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。状态模式与有限状态机的概念紧密相关。其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移。
其实在之前的文章熔断器设计模式中,就是状态模式的很典型应用,系统有三种状态,闭合状态,此时系统能正常运行,当发生错误,且错误次数达到阈值时,会进入到断开状态,当断开状态持续一定时间,系统会进入到半闭合状态,此时能运行请求处理,当请求处理成功,成功次数达到阈值,则进入闭合状态,否则如果仍然失败,则会退回断开状态,系统不处理请求,直接返回错误,在那篇文章里,有详细代码演示了熔断器设计模式中如何使用状态模式。
▲熔断器设计模式中,系统各个状态及其转移过程
本文为了简单说明状态模式的使用,再举一个简单的播放器的例子来演示状态模式。
问题的产生
假设我们要做一个播放器App,播放器一般有三个状态,比如播放,停止,暂停三个功能。比如像我这个15年前的魅族E3 MP3那样。
▲至今仍在服役的魅族E3 MP3,顶部有播放控制按钮
播放器是个有限状态机,他有三个状态,并且状态之间会转移,我们抽象出来如下状态转移图。
当播放器处于停止状态,如果点击播放按钮,他会进入播放状态;如果当前播放器处于播放状态,点击暂停,会进入暂停状态,点击停止,会进入停止状态;如果当前播放器处于暂停状态,点击停止则会进入停止状态,点击播放则会进入播放状态。
非状态模式实现
首先定义一个播放器接口,里面包含当前播放器状态,和三个操作的方法。
public interface IPlayer
{
public PlayerState CurrentState { get; }
public void Play();
public void Pause();
public void Stop();
}
然后新建一个Player类,实现该接口:
public class Player : IPlayer
{
private PlayerState currentState = PlayerState.Stopped;
public PlayerState CurrentState => currentState;
public void Play()
{
switch (CurrentState)
{
case PlayerState.Playing:
Console.WriteLine("curent state is palying, do nothing.");
break;
case PlayerState.Paused:
case PlayerState.Stopped:
Console.WriteLine("play now.");
break;
}
currentState = PlayerState.Playing;
}
public void Pause()
{
switch (CurrentState)
{
case PlayerState.Paused:
Console.WriteLine("curent state is paused, do nothing.");
break;
case PlayerState.Playing:
Console.WriteLine("pause vedio now.");
break;
case PlayerState.Stopped:
Console.WriteLine("curent state is stopped,do noting.");
break;
}
currentState = PlayerState.Paused;
}
public void Stop()
{
switch (CurrentState)
{
case PlayerState.Paused:
case PlayerState.Playing:
Console.WriteLine("stop vedio now.");
break;
case PlayerState.Stopped:
Console.WriteLine("curent state is stopped,do noting.");
break;
}
currentState = PlayerState.Stopped;
}
}
在Start,Stop,Pause中,根据当前的状态CurrentState,来判断当前该执行那个操作,比如如果当前状态是Pause或者Stop,那么调用Play方法的时候,就执行播放逻辑,并将当前状态设置为播放中。
使用方法如下:
Console.WriteLine("请输入命令:1-停止 2-播放 3-暂停 Q-退出");
string cmd = Console.ReadLine();
IPlayer player = new Player();
while (true)
{
if (cmd == "1")
{
player.Stop();
}
else if (cmd == "2")
{
player.Play();
}
else if (cmd == "3")
{
player.Pause();
}
else if (cmd == "Q")
{
break;
}
else
{
Console.WriteLine("unrecognize command!");
Console.WriteLine("请输入命令:1-停止 2-播放 3-暂停 Q-退出");
}
cmd = Console.ReadLine();
}
这里可以看到,在每个方法中,都需要判断当前的状态,分别处理。这里使用的switch,也可以用if else,当看到整个代码里到处都充斥着“switch”或“if else”时,这就是“bad smell”了,表示结构可能需要优化。
更进一步,假设需求发生了变化,老板发现做单纯的MP3不赚钱了,需要做视频播放器,如果不增加新的需求,原来的代码还可以用,假如需要增加在播放视频的时候,插播广告的功能。那么,就需要增加一个播放广告的状态AD和播放广告ShowAD的方法了。
首先,需要修改IPlayer方法,增加一个ShowAD方法:
public interface IPlayer
{
public PlayerState CurrentState { get; }
public void Play();
public void Pause();
public void Stop();
public void ShowAD();
}
然后在Player里实现ShowAD方法:
public void ShowAD()
{
switch (CurrentState)
{
case PlayerState.ShowADing:
Console.WriteLine("curent state is AD,do noting.");
break;
case PlayerState.Paused:
Console.WriteLine("curent state is paused , do noting.");
break;
case PlayerState.Playing:
Console.WriteLine("show advertisement now.");
break;
case PlayerState.Stopped:
Console.WriteLine("curent state is stopped ,do noting.");
break;
}
currentState = PlayerState.ShowADing;
}
你以为这就完了吗?并不是,因为PlayerState里增加了ShowADing状态,所以,其他的方法比如Play()、Pause()、Stop()方法里,统统都需要增加ShowADing的Case,这样改动非常大,不友好,容易996。
状态模式实现
要解决上述困境,就需要引入状态模式。状态模式的定义是:允许对象在内部状态改变时改变它的行为,对象看起来就好像修改了它的类。这么说很抽象,这里的Context就相当于我们的VedioPlayer类。
我们还是以上面播放器的例子来说明,首先还是实现播放,暂停,停止状态,此时的状态转换图如下:
首先,先抽象一个Player作为Context。
public abstract class Player
{
public abstract void Request(int flag);
public abstract void SetState(State state);
public abstract void Play();
public abstract void Pause();
public abstract void Stop();
}
Request用来处理外部请求,比如播放,暂停,停止等;SetState用来设置内部当前状态,这个State不是枚举,是一个表示状态的抽象类,实现如下:
public abstract class State
{
protected const int PLAY_OR_PAUSE = 0;
protected const int STOP = 1;
protected Player player;
public State(Player pl)
{
player = pl;
}
public abstract void Handle(int action);
public override string ToString()
{
return $"current state: {this.GetType().Name}";
}
}
接下来,对照状态转换图,继承自State,新建不同的具体状态类:
public class PlayingState : State
{
public PlayingState(Player player) : base(player)
{
}
public override void Handle(int action)
{
switch (action)
{
case PLAY_OR_PAUSE:
player.Pause();
player.SetState(new PausedState(player));
break;
case STOP:
player.Stop();
player.SetState(new StoppedState(player));
break;
default:
throw new ArgumentException("error action:" + action + ",current state:" + this.GetType().Name);
}
}
}
首先是播放状态PlayingState的实现,当前是播放状态,如果是暂停请求,则调用play的Pause方法,并将当前状态设置为暂停。如果是停止请求,则调用play的Stop方法,并将当前状态设置为停止状态。可以看到这里没有播放请求,因为已经处于播放状态,如果在此调用播放请求,就会报错。接下来是PausedState的实现:
class PausedState : State
{
public PausedState(Player pl) : base(pl)
{
}
public override void Handle(int action)
{
switch (action)
{
case PLAY_OR_PAUSE:
player.Play();
player.SetState(new PlayingState(player));
break;
case STOP:
player.Stop();
player.SetState(new StoppedState(player));
break;
default:
throw new ArgumentException("error action:" + action + ",current state:" + this.GetType().Name);
}
}
}
同上面的逻辑,在暂停状态下,只处理“播放”和“停止”请求,其他的都抛异常。最后处理StopedState:
class StoppedState : State
{
public StoppedState(Player pl) : base(pl)
{
}
public override void Handle(int action)
{
switch (action)
{
case PLAY_OR_PAUSE:
player.Play();
player.SetState(new PlayingState(player));
break;
default:
throw new ArgumentException("error action:" + action + ",current state:" + this.GetType().Name);
}
}
}
在停止状态下,只处理“播放”请求,其他的都是非法请求,可以抛异常,也可以忽略。
最后,我们新建一个表示视频播放器的类VideoPlayer,实现Player抽象类。
class VideoPlayer : Player
{
private State currentState;
public VideoPlayer()
{
currentState = new StoppedState(this);
}
public override void Pause()
{
Console.WriteLine("Pause Video");
}
public override void Play()
{
Console.WriteLine("Play Video");
}
public override void Stop()
{
Console.WriteLine("Stop Video");
}
public override void SetState(State state)
{
currentState = state;
}
public override void Request(int flag)
{
Console.WriteLine("before action:" + currentState.GetType().Name);
currentState.Handle(flag);
Console.WriteLine("after action:" + currentState.GetType().Name);
}
}
用法如下:
Console.WriteLine("请输入命令:0-播放、暂停 1-停止 Q-退出");
string cmd = Console.ReadLine();
Player player = new VideoPlayer();
while (true)
{
if (cmd == "0")
{
player.Request(0);
}
else if (cmd == "1")
{
player.Request(1);
}
else if (cmd == "Q")
{
break;
}
else
{
Console.WriteLine("unrecognize command!");
Console.WriteLine("请输入命令:0-播放、暂停 1-停止 Q-退出");
}
cmd = Console.ReadLine();
}
输出结果如下:
请输入命令:0-播放、暂停 1-停止 Q-退出
0
before action:StoppedState
Play Video
after action:PlayingState
1
before action:PlayingState
Stop Video
after action:StoppedState
0
before action:StoppedState
Play Video
after action:PlayingState
0
before action:PlayingState
Pause Video
after action:PausedState
1
before action:PausedState
Stop Video
after action:StoppedState
0
before action:StoppedState
Play Video
after action:PlayingState
0
before action:PlayingState
Pause Video
after action:PausedState
现在,需求变更,假设需要增加一个播放广告的需求,那么整个状态变更图变为:
我们需要做的是,修改Player抽象类,增加ShowAD()方法,然后添加ShowADingState状态类:
class ShowADingState : State
{
public ShowADingState(Player pl) : base(pl)
{
}
public override void Handle(int action)
{
switch (action)
{
case PLAY_OR_PAUSE:
player.Play();
player.SetState(new PlayingState(player));
break;
default:
throw new ArgumentException("error action:" + action + ",current state:" + this.GetType().Name);
}
}
}
每个状态不需要知道自己之前的状态是什么,只需要知道接收到什么样的输入而做出相应的操作和下一个状态。现在,可以看到,如果当前状态是ShowADingState,调用该方法就会调用player的显示广告方法,然后将当前状态设置为播放状态进入到下一个状态。这里定义了从广告状态进入到播放状态的过程,缺少从播放状态转移到播放广告状态过程,从状态图可以看到,播放状态和广告状态是可以相互转换的,所以在PlayingState还需要做一点修改,如下:
public class PlayingState : State
{
public PlayingState(Player player) : base(player)
{
}
public override void Handle(int action)
{
switch (action)
{
case PLAY_OR_PAUSE:
player.Pause();
player.SetState(new PausedState(player));
break;
case STOP:
player.Stop();
player.SetState(new StoppedState(player));
break;
case SHOW_AD:
player.ShowAD();
player.SetState(new ShowADingState(player));
break;
default:
throw new ArgumentException("error action:" + action + ",current state:" + this.GetType().Name);
}
}
}
可以看到,这种方法,教之前需要改动的地方很少,不用动所有的方法,只需要修改关联的状态处理即可。
现在,修改一下调用方法,增加播放广告的操作:
Console.WriteLine("请输入命令:0-播放、暂停 1-停止 2-广告 Q-退出");
string cmd = Console.ReadLine();
Player player = new VideoPlayer();
while (true)
{
if (cmd == "0")
{
player.Request(0);
}
else if (cmd == "1")
{
player.Request(1);
}
else if (cmd == "2")
{
player.Request(2);
}
else if (cmd == "Q")
{
break;
}
else
{
Console.WriteLine("unrecognize command!");
Console.WriteLine("请输入命令:0-播放、暂停 1-停止 2-广告 Q-退出");
}
cmd = Console.ReadLine();
}
输出结果如下:
请输入命令:0-播放、暂停 1-停止 2-广告 Q-退出
0
before action:StoppedState
Play Video
after action:PlayingState
1
before action:PlayingState
Stop Video
after action:StoppedState
0
before action:StoppedState
Play Video
after action:PlayingState
2
before action:PlayingState
Show ADing
after action:ShowADingState
0
before action:ShowADingState
Play Video
after action:PlayingState
总结
正如状态模式的定义那样:状态模式允许对象在内部状态改变时改变它的行为,对象看起来就好像修改了它的类(每个状态可以做出不一样的动作),在上面的状态模式实现例子中,每一个单独的状态,会根据请求的操作,将当前Context的状态修改为下一个状态,这种状态的变更是在各个状态类里面实现的,而不是在Context类里手动转换;
另外拥有多个状态的对象(Context)只需要实现需要的操作,比如本例中的播放,暂停,停止,放广告等,对于外部的输入请求(request方法调用),只需要交给当前的状态去处理,每个状态不需要知道自己之前的状态是什么,只需要知道接收到什么样的输入(或者没输入)而做出相应的操作和自己下一个状态是什么即可;
最后,通过画出系统的转换图,可以更清晰容易地实现系统状态机。
参考
- https://www.cnblogs.com/hellocsl/p/4000122.html
- https://refactoring.guru/design-patterns/state
- https://www.dofactory.com/net/state-design-pattern