共计 6603 个字符,预计需要花费 17 分钟才能阅读完成。
说到状态模式,顾名思义,应该就是跟状态相干的设计模式了,不过,咱们还是跟后面一样,先不论状态模式是个什么货色,先从一个小小的例子登程,看看状态模式能为咱们解决什么问题。
示例
当初须要实现一个交通灯调度程序,交通灯的色彩须要在红灯 -> 绿灯 -> 黄灯 -> 红灯之间循环转换,然而不容许绿灯 -> 红灯或黄灯 -> 绿灯等状况。这属于交通规则的常识,当初咱们用程序实现它,先看看咱们最传统的思考和实现形式。
首先, 咱们会很容易想到须要定义一个交通灯色彩的枚举:
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-else
或switch-case
带来的问题,咱们曾经相当有教训了,在简略工厂模式中,咱们采纳工厂办法模式形象出生产具体类的工厂类解决了 switch-case
的问题,在上一篇的策略模式中,咱们通过将办法形象成策略类的形式,同样解决了 switch-case
的问题。这里也不例外,咱们也肯定须要形象点什么才行。然而具体形象什么呢?灯的色彩?Turn()
办法?还是别的什么?思路如同并不是那么清晰。不过呢,咱们发现其实这段代码构造跟策略模式革新前的例子极其类似,咱们无妨用策略模式革新一下,看看是否满足需要,如果不满足,看看还毛病什么,而后再进一步革新,因为咱们晓得,策略模式至多能解决 if-else
或switch-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();}
这就 状态模式 了,上面是交通灯示例最终的类图:
无限状态机
从下面的例子中,咱们可能会很容易联想到状态机,咱们也常常听到或看到无限状态机或有限状态机这样的字眼,那么无限状态机跟状态模式有什么关系呢?咱们先看看无限状态机的工作原理。无限状态机的工作原理是,产生 事件 (event) 后,依据 以后状态 (cur_state),决定执行的 动作 (action),并设置 下一个状态 (nxt_state)。从交通灯例子能够看到, 事件 (event) 就是TrafficLight
中的 Turn()
办法,由客户端触发,触发后,零碎会判断以后处于哪种灯的状态,而后执行相应的动作,实现之后再设置下一种灯状态,和无限状态机的工作原理完满对应上了。那么,二者是否等价呢?其实不然,状态模式只是实现无限状态机的一种伎俩而已,因为 if-else
版本的实现,也是无限状态机。
这里算是一个小插曲,上面咱们回归到状态模式。
定义
状态模式容许一个对象在其外部状态扭转时扭转它的行为,从而使对象看起来仿佛批改了它的类。
UML 类图
咱们将交通灯示例的类图形象一下,就能够失去如下 状态模式 的类图:
- Context:上下文环境,定义客户程序须要的接口,并保护一个具体状态角色的实例,将与状态相干的操作委托给以后的
ConcreteState
对象来解决; - State:形象状态, 定义特定状态对应行为的接口;
- ConcreteState:具体状态,实现形象状态定义的接口。
优缺点
长处
- 解决
switch-case
、if-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 分以上,奖钱。这时是策略还是状态呢?感觉如同都能够,但实际上,认真思考会发现,或者视为状态会更好,即在不同的状态会有一个对应的动作,但状态的有哪些呢?分数段?奖罚?状态又是怎么转移的呢?还得认真斟酌,这里例子简略,或者能想分明(其实不肯定),但理论我的项目中,预计就没这么容易了。
不过呢,一旦咱们辨认出了状态,而后辨认出了会依据肯定的触发条件产生状态转移,那么十有八九就能够应用状态模式了。
源码链接
更多内容,欢送关注公众号: