状态模式是一种行为设计模式 让你能在一个对象的内部状态变化时改变其行为 使其看上去就像改变了自身所属的类一样。状态模式与有限状态机的概念紧密相关。其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移。

     其实在之前的文章熔断器设计模式中,就是状态模式的很典型应用,系统有三种状态,闭合状态,此时系统能正常运行,当发生错误,且错误次数达到阈值时,会进入到断开状态,当断开状态持续一定时间,系统会进入到半闭合状态,此时能运行请求处理,当请求处理成功,成功次数达到阈值,则进入闭合状态,否则如果仍然失败,则会退回断开状态,系统不处理请求,直接返回错误,在那篇文章里,有详细代码演示了熔断器设计模式中如何使用状态模式。

 

▲熔断器设计模式中,系统各个状态及其转移过程   

     本文为了简单说明状态模式的使用,再举一个简单的播放器的例子来演示状态模式。

问题的产生


    假设我们要做一个播放器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 : PlayerV2
{
    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方法调用),只需要交给当前的状态去处理,每个状态不需要知道自己之前的状态是什么,只需要知道接收到什么样的输入(或者没输入)而做出相应的操作和自己下一个状态是什么即可;

    最后,通过画出系统的转换图,可以更清晰容易地实现系统状态机。

 

参考

  1. https://www.cnblogs.com/hellocsl/p/4000122.html
  2. https://refactoring.guru/design-patterns/state
  3. https://www.dofactory.com/net/state-design-pattern