17. 设计模式-状态模式

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

示例

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

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

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()的方法,这会致使整个项目扩展维护起来极其困难,由于,它严重违背了开闭原则ide

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

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

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;
    }
}

一切看起来彷佛都很完美,完美无缺。再来看看如何使用:code

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();
}

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

有限状态机

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

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

定义

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

UML类图

咱们将交通灯示例的类图抽象一下,就能够获得以下状态模式的类图:

  • 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分以上,奖钱。这时是策略仍是状态呢?感受好像均可以,但实际上,仔细思考会发现,或许视为状态会更好,即在不一样的状态会有一个对应的动做,但状态的有哪些呢?分数段?奖罚?状态又是怎么转移的呢?还得仔细斟酌,这里例子简单,或许能想清楚(其实不必定),但实际项目中,估计就没这么容易了。

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

源码连接
更多内容,欢迎关注公众号:
image

相关文章
相关标签/搜索