说到状态模式,顾名思义,应该就是跟状态相关的设计模式了,不过,我们还是跟前面一样,先不管状态模式是个什么东西,先从一个小小的例子出发,看看状态模式能为我们解决什么问题。

示例

现在需要实现一个交通灯调度程序,交通灯的颜色需要在红灯->绿灯->黄灯->红灯之间循环转换,但是不允许绿灯->红灯或黄灯->绿灯等情况。这属于交通规则的常识,现在我们用程序实现它,先看看我们最传统的思考和实现方式。

首先,我们会很容易想到需要定义一个交通灯颜色的枚举:

public enum LightColor
{
    Green,
    Red,
    Yellow
}

然后,定义一个交通灯的类,在交通灯类中处理颜色转换及相应的业务逻辑,代码如下:

public class TrafficLight
{
    private LightColor _lightColor;
    public TrafficLight()
    {
        _lightColor = LightColor.Red;
    }

    public void Turn()
    {
        if (_lightColor == LightColor.Red)
        {
            Console.WriteLine("红灯停");
            _lightColor = LightColor.Green;
        }
        else if (_lightColor == LightColor.Green)
        {
            Console.WriteLine("绿灯行");
            _lightColor = LightColor.Yellow;
        }
        else if (_lightColor == LightColor.Yellow)
        {
            Console.WriteLine("黄灯亮了等一等");
            _lightColor = LightColor.Red;
        }
    }
}

最后,再不妨调用运行一下:

static void Main(string[] args)
{
    TrafficLight light = new TrafficLight();
    light.Turn();
    light.Turn();
    light.Turn();
    light.Turn();
}

显而易见,这段代码是完全满足需求的,并且逻辑严谨,调用方式也极其简单,如果需求不变,这或许就是最好的实现方式了。但是经过前面设计原则的熏陶,我们知道,需求不变是不可能的。因此,我们很容易就会发现这段代码存在的问题,充斥着if-else的条件分支,这就意味着扩展困难。这里例子简单,可能并不明显,但是真实项目中必然会有更多的条件分支和更多类似Turn()的方法,这会导致整个项目扩展维护起来极其困难,因为,它严重违背了开闭原则

其实,对于解决if-elseswitch-case带来的问题,我们已经相当有经验了,在简单工厂模式中,我们采用工厂方法模式抽象出生产具体类的工厂类解决了switch-case的问题,在上一篇的策略模式中,我们通过将方法抽象成策略类的方式,同样解决了switch-case的问题。这里也不例外,我们也一定需要抽象点什么才行。但是具体抽象什么呢?灯的颜色?Turn()方法?还是别的什么?思路好像并不是那么清晰。不过呢,我们发现其实这段代码结构跟策略模式改造前的例子极其相似,我们不妨用策略模式改造一下,看看能否满足需求,如果不满足,看看还缺点什么,然后再进一步改造,因为我们知道,策略模式至少能解决if-elseswitch-case的问题。

我们看看策略模式改造后的代码,先将Turn()方法抽象成策略类:

public interface ITurnStrategy
{
    void Turn();
}

public class GreenLightTurnStrategy : ITurnStrategy
{
    public void Turn()
    {
        Console.WriteLine("绿灯行");
    }
}

public class RedLightTurnStrategy : ITurnStrategy
{
    public void Turn()
    {
        Console.WriteLine("红灯停");
    }
}

public class YellowLightTurnStrategy : ITurnStrategy
{
    public void Turn()
    {
        Console.WriteLine("黄灯亮了等一等");
    }
}

再看看改造后的TrafficLight类:

public class TrafficLight
{
    private ITurnStrategy _turnStrategy;

    public TrafficLight(ITurnStrategy turnStrategy)
    {
        _turnStrategy = turnStrategy;
    }

    public void Turn()
    {
        if (_turnStrategy != null)
        {
            _turnStrategy.Turn();
        }
    }

    public void ChangeTurnStrategy(ITurnStrategy turnStrategy)
    {
        _turnStrategy = turnStrategy;
    }
}

一切看起来似乎都很完美,天衣无缝。再来看看如何使用:

static void Main(string[] args)
{
    TrafficLight light = new TrafficLight(new RedLightTurnStrategy());
    light.Turn();
    light.ChangeTurnStrategy(new GreenLightTurnStrategy());
    light.Turn();
    light.ChangeTurnStrategy(new YellowLightTurnStrategy());
    light.Turn();
    light.Turn();
}

一用就发现了问题,调用变复杂了。其实,为了能让系统更容易扩展,调用时复杂一点也没什么,但是,另一个致命的问题却不能忽视,我们希望灯颜色切换是由内部一套固定机制控制,而不是调用方来决定,如果用户想换什么颜色就换什么颜色,交通规则岂不乱套了?显然,策略模式是不满足需求的,我们其实希望light.ChangeTurnStrategy()这个动作,由系统自己内部完成。

既然不满足需求,那么问题到底出在哪呢?回过头来再梳理一下,我们发现或许我们的思路一开始就出现了偏差,交通灯能换颜色吗?显然是不能的,因为每个灯的颜色是固定的,我们所谓的换颜色,实际上换的是灯,难道要用工厂方法模式来创造不同颜色的灯?显然也不合适,三个灯一开始就在那里,只是循环切换而已,不存在创建的过程。实际上,我们或许应该换一种思路,这里明显体现的是交通灯的三种状态,每一种状态下对应一种需要处理的行为动作,同时,也只有状态才有切换的过程。

换一种思路后,我们看问题的角度就不一样了,看看改变思路后的代码:

public abstract class TrafficLightState
{
    public abstract void Handle(TrafficLight light);
}

public class GreenState : TrafficLightState
{
    public override void Handle(TrafficLight light)
    {
        Console.WriteLine("绿灯行");
        light.SetState(new YellowState());
    }
}

public class RedState : TrafficLightState
{
    public override void Handle(TrafficLight light)
    {
        Console.WriteLine("红灯停");
        light.SetState(new GreenState());
    }
}

public class YellowState : TrafficLightState
{
    public override void Handle(TrafficLight light)
    {
        Console.WriteLine("黄灯亮了等一等");
        light.SetState(new RedState());
    }
}

public class TrafficLight
{
    private TrafficLightState _currentState;

    public TrafficLight()
    {
        _currentState = new RedState();
    }

    public void Turn()
    {
        if (_currentState != null)
        {
            _currentState.Handle(this);
        }
    }

    public void SetState(TrafficLightState state)
    {
        _currentState = state;
    }
}

其实,可以发现,除了类名和方法名变了,代码跟策略模式几乎一模一样(具体演化过程,文字难以表达清楚,可以看一下我在B站或者公众号上的视频),但含义却是天差地远,这里不是直接将方法抽象成策略对象,而是抽象不同的状态,因此用了抽象类,而非接口(也可以用接口,但是我们通常会将方法抽象成接口,而将对象或属性抽象成类);并为每个状态提供处理该状态下对应行为的接口方法,而不是直接提供具体行为的接口方法。

另外,参数也有所不同,TrafficLightState中需要持有对TrafficLight的引用,因为需要在具体的状态类中处理TrafficLight的状态转移。改造后的代码再次完美满足需求,调用方又变得简单了,状态的转移再次回归了主权:

static void Main(string[] args)
{
    TrafficLight light = new TrafficLight();
    light.Turn();
    light.Turn();
    light.Turn();
    light.Turn();
}

这就状态模式了,下面是交通灯示例最终的类图:
设计模式-状态模式-LMLPHP

有限状态机

从上面的例子中,我们可能会很容易联想到状态机,我们也经常听到或看到有限状态机或无限状态机这样的字眼,那么有限状态机跟状态模式有什么关系呢?我们先看看有限状态机的工作原理。有限状态机的工作原理是,发生 事件(event) 后,根据 当前状态(cur_state) ,决定执行的 动作(action) ,并设置 下一个状态(nxt_state)。从交通灯例子可以看到,事件(event) 就是TrafficLight中的Turn()方法,由客户端触发,触发后,系统会判断当前处于哪种灯的状态,然后执行相应的动作,完成之后再设置下一种灯状态,和有限状态机的工作原理完美对应上了。那么,二者是否等价呢?其实不然,状态模式只是实现有限状态机的一种手段而已,因为if-else版本的实现,也是有限状态机。

这里算是一个小插曲,下面我们回归到状态模式。

定义

状态模式允许一个对象在其内部状态改变时改变它的行为,从而使对象看起来似乎修改了它的类。

UML类图

我们将交通灯示例的类图抽象一下,就可以得到如下状态模式的类图:
设计模式-状态模式-LMLPHP

  • Context:上下文环境,定义客户程序需要的接口,并维护一个具体状态角色的实例,将与状态相关的操作委托给当前的 ConcreteState对象来处理;
  • State:抽象状态,定义特定状态对应行为的接口;
  • ConcreteState:具体状态,实现抽象状态定义的接口。

优缺点

优点

  • 解决switch-caseif-else带来的难以维护的问题,这个很明显,没什么好说的;
  • 结构清晰,提高了扩展性,不难发现,Context类简洁清晰了,扩展时,几乎不用改变,而且每个状态子类也简洁清晰了,扩展时也只需要极少的改变。
  • 通过单例或享元可使状态在多个上下文间共享。

这个问题需要单独说,我们不难发现,状态模式虽然解决了很多问题,但是每次状态的切换都需要创建一个新的状态类,而原本它仅仅是一个小小的枚举值而已,这样一对比,对象重复的创建资源开销是否过于巨大?其实,要解决对象重复创建的问题,我们知道,单例模式和享元模式都是不错的选择,具体选用哪一个,就要看状态类的数量和个人的喜好了。

下面是采用享元模式改进的代码,首先是熟悉的享元工厂,代码很简单:

public class LightStateFactory
{
    private static readonly IDictionary<Type, TrafficLightState> _lightStates
           = new Dictionary<Type, TrafficLightState>();

    private static readonly object _locker = new object();
    public static TrafficLightState GetLightState<TLightState>() where TLightState : TrafficLightState
    {
        Type type = typeof(TLightState);
        if (!_lightStates.ContainsKey(type))
        {
            lock (_locker)
            {
                if (!_lightStates.ContainsKey(type))
                {
                    TrafficLightState typeface = Activator.CreateInstance(typeof(TLightState)) as TrafficLightState;
                    _lightStates.Add(type, typeface);
                }
            }
        }

        return _lightStates[type];
    }
}

使用就更简单了,将创建状态对象的地方换成享元工厂创建就可以了,代码片段如下:

public override void Handle(TrafficLight light)
{
    Console.WriteLine("红灯停");
    light.SetState(LightStateFactory.GetLightState<GreenState>());
}

这里需要特别提一下,由于状态是单例的,可以在多个上下文间共享,而任何时候,涉及到全局共享就不得不考虑并发的问题。因此,除非明确需要共享,否则状态类中不应持有其它的资源,不然可能产生并发问题。同样的原因,状态类也不要通过属性或字段的方式持有对Context的引用,这也是我采用局部变量对TrafficLight进行传参的原因。

缺点

  • 随着状态的扩展,状态类数量会增多,这个老生常谈了,几乎所有解决类似问题的设计模式都存在这个缺点;
  • 增加了系统复杂度,使用不当将会导致逻辑的混乱,因为,状态类毕竟增多了嘛,而且还涉及到状态的转移,思维可能就更乱了;
  • 不完全满足开闭原则,因为扩展时,除了新增或删除对应的状态子类外,还需要修改涉及到的相应状态转移的其它状态类,不过相对于原来的实现,这里已经改善很多了。

与策略模式区别

策略模式

  • 强调可以互换的算法;
  • 用户直接与具体算法交互,决定算法的替换,需要了解算法本身;
  • 策略类不需要持有Context的引用。

状态模式

  • 强调改变对象内部的状态来帮助控制自己的行为;
  • 状态是对象内部流转,用户不会直接跟状态交互,不需要了解状态本身;
  • 状态类需要持有Context的引用,用来实现状态转移。

总结

其实,从类图和实现方式上可以看出,状态模式和策略模式真的很像,但是由于策略模式更具有一般性,因此更容易想到。而且,我们也知道状态模式和策略模式都能解决if-else带来的问题,关键就在于策略和状态的识别,就如上述交通灯例子,刚开始识别成策略也很难发现有什么不对。再举一个更通俗的例子,老师会根据同学的考试成绩对同学给出不同的奖惩方案,如成绩低于60分的同学罚款,成绩高于90分的同学奖钱,但是怎么奖怎么罚,都是老师决定的(不然全考90分以上,老师得哭)。这里是普通的条件分支,没有枚举,但是我们依然可以看出,这里体现的是根据不同的分数段采取不同的策略,可以采用策略模式。再例如,同样是考试成绩,父母对你设置一个指标,考了60以下,罚钱,考了90分以上,奖钱。这时是策略还是状态呢?感觉好像都可以,但实际上,仔细思考会发现,或许视为状态会更好,即在不同的状态会有一个对应的动作,但状态的有哪些呢?分数段?奖罚?状态又是怎么转移的呢?还得仔细斟酌,这里例子简单,或许能想清楚(其实不一定),但实际项目中,估计就没这么容易了。

不过呢,一旦我们识别出了状态,然后识别出了会根据一定的触发条件发生状态转移,那么十有八九就可以使用状态模式了。

源码链接

05-01 17:05