state-pattern
基本常识:策略模式和状态模式是双胞胎,在出生时才分开。你已经知道了,策略模式是围绕可以互换的算法来创建成功业务的。然而,状态走的是更崇高的路,它通过改变对象内部的状态来帮助对象控制自己的行为。
它常常告诉它的对象客户跟着我念:我很棒,我很聪明,我最优秀
引入
- 首先,找出所有的状态
- 接下来,创建一个实例变量来持有目前的状态,然后定义每个状态的值
- 将所有系统中可以发生的动作整合起来
增加新的状态造成的混乱
我们要做的事情是:首先,我们定义一个 State 接口。在这个接口内,糖果机的每个动作都有一个对应的方法
然后为机器中的每个状态实现状态类。这些类将负责在对应的状态下进行机器的行为
最后,我们要摆脱旧的条件代码,取而代之的方式是,将动作委托到状态类
现在我们要把一个状态的所有行为放在一个类中, 这么一来我们将行为局部化了, 并使得事情更容易改变和理解
定义状态接口和类
完整的糖果机类
每一个状态对修改关闭, 而糖果机对扩展开放,因为可以加入新的状态类
创建一个新的代码基和类结构,这更能映射万能糖果公司的图,而且更容易阅读和理解
定义
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类
这个描述中的第一部分附有相当多的涵义, 是吧? 因为这个模式将状态封装成为独立的类, 并将动作委托到代表当前状态的对象, 我们知道行为会随着内部状态而改变.
糖果机提供了一个很好的例子: 当糖果机是在 NoQuarterState 或 HasQuarterState 两种不同的状态时,你投入 25 分钱,就会得到不同的行为 (机器接受 25 分钱和机器拒绝 25 分钱)
而这个定义中的第二部分呢? 一个对象看起来好像修改了它的类是什么意思呢?
从客户的视角来看:如果说你使用的对象能够完全改变它的行为, 那么你会觉得, 这个对象实际上是从别的类实例化而来的. 然而实际上, 你知道我们是在使用组合通过简单引用不同的状态对象来造成类改变的假象。
FAQ
策略模式的类图和状态机模式的类图长得一模一样
但是这两个模式的差别在于它们意图
以状态模式而言, 我们将一群行为封装在状态对象中, context 的行为随时可以委托到那些状态对象中的一个. 随着时间的流逝,当前状态在状态对象集合中游走改变,以反映出 context 内部的状态,因此, context 的行为也会跟着改变。但是 context 的客户对于状态对象了解不多,甚至根本是浑然不觉。
而以策略模式而言,客户通常主动指定 Context 所要组合的策略对象是哪一个。现在,固然策略模式让我们具有弹性,能够在运行时改变策略,但对于某个 context 对象来说,通常都只有一个最适当的策略对象。比方说,在第 1 章,有些鸭子(例如绿头鸭)被设置成利用典型的长翔行为进行飞翔,而有些鸭子(例如像皮鸭和诱饵鸭)使用的飞翔行为只能让它们紧贴地面.
一般来说,我们把策略模式想成是除了继承之外的一种弹性替代方案。如果你使用继承定义了一个类的行为,你将被这个行为困住,甚至要修改它都很难。有了策略模式,你可以通过组合不同的对象来改变行为。
我们把状态模式想成是不用在 context 中放置许多条件判断的替代方案。通过将行为包装进状态对象中.你可以通过在 context 内简单地改 变状态对象来改变 context 的行为。
在 GumballMachine 中,状态决定了下一个 状态应该是什么。ConcreteState 总是决定接下来的状态是什么吗
不,并非总是如此,Coniext 也可以决定状态转换的流向。
一般来讲,当状态转换是固定的时候.就适合放在 Context 中;然而,当转换是更动态的时候,通常就会放在状态类中(例如,在 GumballMachine 中.由运行时糖果的数目来决定状态矣转换到 NoQuarter 还是 SoldOut)。 将状态转换放在状态类中的缺点是:状态类之间产生了 依赖。在我们的 GumballMachine 实现中,我们试图通过 使用 Context 上的 gelter 方法把依赖减到最小,而不是显式硬编码具体状态类。
请注意,在做这个决策的同时,也等于是在为另一件事情做决簸:当系统进化时,究竟哪个类是对修改封闭( Context 还是状态类)的
放在 context 中时什么场景呢, 还是不太理解
比如说, 在执行了某个 action 之后, 一定会转换到某个状态, 这个状态的转换就可以放在 context 类中
客户会直接和状态交互吗?
不会。状态是用在 Context 中来代表它的内部状态以及行为的,所以只有 Context 才会对状态提出请求。客户不会直接改变 Context 的状态。全盘了解状态是 Context 的工作,客户根本不了解,所以不会直接和状态联系。
如果在我的程序中 Contexl 有许多实例,这些实例之间可以共享状态对象吗
是的,绝对可以,事实上这是很常见的做法。但唯一的前提是,你的状态对象不能持有它们自己的内部状态;否则就不能共享。
想要共享状态,你需要把每个状态都指定到静态的实例变更中。如果你的状态需要利用到 Context 中的方法或者实例变量,你还必须在每个 handler。方法内传入一个 context 的引用
使用状态模式似乎总是增加我们设计中类的 数目。请看GumballMachine的例子,新版本比旧版本多 出了许多类
没微,在个别的状态类中时装状态行为,结果总是增加这个设计中类的数目。这就是为了要获取弹性而付出的代价。除非你的代码是一次性的,可以用完就扔掉(是呀!才怪!),那么其实状态模式的设计是绝对值 得的。其实真正支要的是你暴露给客户的类数目,而且我们有办法将这些额外的状态类全都隐藏起来。
让我们看一下另一种做法:如果你有一个应用,它有很多状态,但是你决定不将这些状态封装在不同的对象 中,那么你就会得到巨大的、整块的条件语句。
这会让你的代码不容易维护和理解。通过使用许多对象,你可以让状态变得很干净,在以后理解和维护它们时,就可以省下很多的工夫。
状态模式类图显示 State 是一个抽象类,但 你不是使用接口实现糖果机状态的吗?
是的。如果我们没有共同的功能可以放进抽 象矣中,就会使用接口。在你实现状态模式时,很可能 想使用抽象类。这么一来,当你以后需卖在抽象类中加 入新的方法时就很容易,不需要打破具体状态的实现。
总感觉这里介绍的状态机和 xState 的不太一样呢
设计模式里的状态机, 通常描述的是一个明确的主体, 这个主体在不同的状态下都会执行相近的行为, 但是不同状态下的相同行为会导致不同的结果
而 xState 里的状态机, 更像是利用了状态机的思想来描述应用的状态流转的过程, 不同的状态下可执行的行为差异非常大
可以把 xState 拆分成多个状态机, 每个状态机更接近我们传统意义上的状态机
xStete 关于状态机和状态图的介绍
可以更好的理解状态机相关的概念和复杂状态机所需要的新概念
状态图(statecharts)是一种图形语言,它用来描述过程中的状态。
你可能也用过类似的图,来设计用户流程图、规划数据库、或者构建 APP 架构。状态图(statecharts)是换种方式,用一堆盒子和箭头,来给人展示什么叫流程。不过,有了 XState,我们就能用代码来管理应用逻辑了。
状态 States
我们用圆角矩形盒子来展示状态。为狗的过程,绘制状态图,首先会想到两种状态:
狗总是 睡着(asleep) 或 醒着(awake)。狗不能同时睡着和醒着,狗也不可能不睡不醒。只有这两种状态,没其它的了,这就是我们说的有限数量的状态。
转换与事件 Transitions and Events
狗在 睡着 和 醒着 之间的变化,是通过转换来表示的,它用一个箭头表示,从一个状态指向过程序列中的下一个状态。
转换(transition)是由导致状态更改的 事件(event) 引起的。用事件来标记转换。
转换和事件是 确定性 的。 确定性意味着每个转换和事件总是指向相同的下一个状态,并且每次进程运行时总是从给定的起始条件产生相同的结果。 你永远不会把狗摇醒后,它还 睡着 ,或打晕它 它还 醒着 吧。
小狗具有两个有限状态,和两个转换的过程,就是一个 有限状态机。 状态机用于描述某事物的行为。 状态机描述事物的状态,以及这些状态之间的转换。 它是一个有限状态机,因为它具有有限数量的状态。(缩写为 FSM)
初始状态 Initial State
任何具有状态的事物,都会有一个 初始状态,即进程存在的默认状态,直到发生事件,从而改变事物的状态。
初始状态用实心圆圈表示,箭头从圆圈指向初始状态。
用状态图来描述遛狗的过程,初始状态会是 等待(waiting) 走路。
最终状态 Final State
大多数具有状态的进程都会有一个 最终状态,即进程完成时的最后一个状态。 最终状态由状态圆角矩形框上的双边框表示。
在遛狗状态图中,最终状态是 溜狗完成(walk complete)。
复合状态 Compound States
复合状态是可以包含更多状态的状态,也称为子状态。 这些子状态只能在父级复合状态发生时发生。在遛狗(on a walk)状态中,可以有 走路中(walking)、 奔跑中(running) 和 停下来闻闻好闻的气味(stopping to sniff good smells) 几个子状态。
复合状态由标记的圆角矩形框表示,该框充当其子状态的容器。
复合状态还应指定哪个子状态是初始状态。 在 on a walk 状态下,初始状态为 walking。
复合状态使状态图能够处理比日常状态机更复杂的情况。
原子状态 Atomic States
原子状态是没有任何子状态的状态。等待(Waiting), 遛狗完成(walk complete), 走路(walking), 奔跑(running) 和 停下来闻闻好闻的(stopping to sniff good smells) 都是原子状态。
并行状态 Parallel States
并行状态是一种复合状态,其中所有子状态(也称为区域)同时处于活动状态。 这些区域在复合状态容器内由虚线分隔。
在 on a walk 复合状态内,可能有两个区域。 一个区域包含狗的 walking、 running 和 stopping to sniff good smells 的活动子状态,另一个区域包含狗的尾巴 摇动(wagging) 和 不摇动(not wagging) 状态。 狗可以走路和摇尾巴,跑和摇尾巴,或者在摇尾巴的同时停下来闻,它也可以在不摇尾巴的情况下进行任何这些活动。
两个区域还应该指定哪个子状态是初始状态。 在我们的 tail 区域,初始状态是 not wagging。
自转换 Self-transition
自转换是指事件发生但转换返回到相同状态时。 转换箭头退出并重新进入相同的状态。
描述自我转变的一种有用方法是在过程中“一直做某事,但一直没变化”。
在狗讨好的过程中,会有一个 讨好(begging) 状态和一个 获得好处(gets treat) 事件。 而对于爱吃的狗来说,无论你经历了多少次得到 gets treat 事件,狗都会回到 begging 状态。
计划状态图 Planning Statecharts
状态图的好处之一是,在将状态图放在一起的过程中,你可以发觉过程中的所有可能状态。 这种探索将帮助你避免代码中的错误,因为能让你覆盖到所有的事件变化。
而且由于状态图是可执行的,它们既可以作为图表,也可以作为代码,从而减少在图表和编码环境之间引入差异或错误解释的可能性。
为登录状态机计划一个状态图 Planning a Statechart for a Login Machine
要绘制登录状态机的状态图,首先要列出流程中的基本事件。 想想你的登录过程会 做 什么:
- 登进 log in
- 登出 log out
然后列出由于这些事件而存在的 状态:
- 已登进 logged in
- 已登出 logged out
一旦有了一些事件和状态,状态图就开始了。
不要忘记 初始状态。 在这种情况下,logged out 状态是初始状态,因为任何新用户都会进入未登录过程。
延迟转换 Delayed Transitions
作为安全措施,某些登进和登出的过程,会在固定时间后,登出非活动用户。
活动(active) 和 空闲(idle) 状态仅在用户登进时发生,因此它们成为 登进(logged in) 复合状态中的子状态。
logged in 复合状态中的初始状态是 active,因为它是 log in 事件的直接结果,登录是用户活动的标志。
延迟转换(delayed transition) 是一种在处于某种状态,达到指定时间长度后,发生的转换。 延迟的转换被标记为“之后”和一个固定的持续时间,以指示在转换到下一个指示状态之前应该经过多长时间。
在登进状态图中,60000 毫秒或 1 分钟的延迟转换跟随 active 状态来确定用户是否 idle。 如果在转换达到一分钟之前有 activity 事件,则流程返回 active 状态。
如果用户保持 idle 状态,则在空闲状态之后会延迟 180000 毫秒(或 3 分钟)转换到 自动登出(auto logged out) 状态。
动作 Actions
状态图使用,在状态图之外系统触发的 actions。 动作通常也称为 作用(effects) 或 副作用(side-effects)。 “副作用”听起来像是一个消极或不重要的术语,但引发动作,是使用状态图的主要目的。
动作事件,对后续的其余部分没有影响,事件只是被触发,流程还是原来设置的那样,走下一步。 例如,登录状态图可能会执行更改用户界面的操作。
可以在进入或退出状态或转换时触发 动作。状态的操作包含在状态容器内,带有“entry /” 或 “exit /”标签,具体取决于动作是在进入还是退出状态时触发。
在登录状态图中,idle 状态有一个进入动作来警告用户他们可能会被登出。
xState
基本使用
vanilla js 需要先用 interpret 通过 machine 创建 service, 然后在 service 的基础上通过 send 进行状态流转
react 中可以直接使用 useMachine
这两种用法和 api 的差异, 感觉怪怪的
xState FAQ
涉及的概念太多了
useActor 和 useSelector 看不懂是啥意思, 不够直观
19 年的时候已经 17k Star 了 23 年才 22K
基本已经退出历史舞台了, 虽然设计很有想法, 但是上手成本太大了. 不利于推广