JavaScript设计模式与开发实践笔记
第16章 状态模式
状态模式的关键是区分事物内部的状态,事物内部的改变往往会带来事物的行为改变。
场景
有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又被打开。同一个开关按钮,不同状态下表现的行为不一样。
现在用代码描述这个场景,Light代表一个电灯类,light是从Light创建出的实例,且拥有两个属性,用state记录电灯的状态,用button记录开关按钮。
16.1.1 第一个示例:电灯程序
1 | class Light { |
上面的代码虽然设计得很好,逻辑既简单又缜密,但是不利于扩展。想想世界上灯泡并非一种,如果引进新的灯泡就需要改动原先的代码。综述上面的例子犯了一下缺点:
- 很明显
buttonWasPressed
方法是违反开放-封闭原则的,每次新增或者修改light的状态,都需要改动buttonWasPressed
方法中的代码,这使得buttonWasPressed
成为了一个非常不稳定的方法。 - 所以跟状态有关的行为,都被封装在
buttonWasPressed
方法里,如果以后电灯又增加了强光,超强光,终极强光,那我们无法预料到这个方法将膨胀到什么地方。 - 状态的切换非常不明显,仅仅表现为对state变量的赋值,比如
this.state='on'
- 状态之间的切换关系,不过是往
buttonWasPressed
方法里堆砌if、else
语句,增加或者修改了一个状态可能需要改变若干个操作,这使得buttonWasPressed
更加难以阅读和维护。
16.1.2 状态模式改进电灯程序
通常我们谈到封装,一般会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所有button被按下的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。
下面进入状态莫斯的代码编写阶段,首先将定义3个状态类,分别是OffLightState、WeakLightState、StrongLightState。这三个类都有一个原型方法buttonWasPressed
,代表在各自状态下,按钮被按下时将发生的行为,代码如下:
1 | // OffLightState |
接下来改写Light类,现在不再使用一个字符串来记录当前的状态,而是使用更加立体化的状态对象。我们在Light类的构造函数里为每个状态类都创建一个状态对象,这样一来我们可以很明显地看到电灯一共有多少种状态,代码如下:
1 | class Light { |
使用状态模式的好处是可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类中,便于阅读和管理代码。
16.2 状态模式的定义
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
我们以逗号分隔,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。电灯的例子足以说明这一点,在off和on这两种不同的状态下,我们点击同一个按钮,得到的行为反馈是截然不同的。
第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化来的,实际上这是使用了委托的效果。
16.3 状态模式的通用结构
16.4 缺少抽象类的变通方式
定义一个State的抽象父类,把共同的功能放进抽象父类中。
1 | class State { |
16.5 另一个状态模式示例—文件上传
16.5.1 更复杂的切换条件
相对于电灯的例子,文件上传不同的地方在于,现在我们将面临更加复杂的条件切换关系。在电灯的例子中,电灯的状态总是从关到开再到关,或者从关到弱光、弱光到强光,强光再到关。看起来总是遵循规矩的A->B->C->A
,所以即使不使用状态模式来编写电灯的程序,而是使用原始的if、else
来控制状态的切换,我们也不至于在逻辑编写中迷失自己,因为状态的切换总死遵循一些简单的规律,而文件上传的状态切换要复杂得多,控制文件上传的流程需要两个节点,第一个用于暂停和继续上传,第二个用于删除文件。
现在看看文件在不同的状态下,点击这两个按钮将分别发生什么行为。
- 文件在扫描状态中,是不能进行任何操作的,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的md5值判断,若确认该文件已经文在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳转到上传失败状态。剩下的情况才进入上传中状态。
- 上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传。
- 扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件。
16.5.2 一些准备工作
上传是一个异步过程,所有控件会不停的调用JavaScript提供的一个全局函数window.external.upload
,来通知JavaScript的上传进度,把控件当前的文件状态作为参数state
塞进window.external.upload
。setTimeout
负责模拟上传进度,window.external.upload
在此例中只负责打印log:
1 | window.external.upload = function(state) { |
16.5.3 开始编写代码
1 | class Upload { |
16.5.4 状态模式重构文件上传程序
第一步仍然是提供window.external.upload
函数,在页面中模拟创建上传插件,这部分代码没有改变:
1 | window.external.upload = function(state) { |
第二部,改造Upload构造函数,在构造函数中为每种状态子类都创建一个实例对象:
1 | class Upload { |
第三步,init方法无需改变,仍然负责往页面中创建跟上传流程有关的DOM节点,并开始绑定按钮的事件:
1 | init() { |
第四步,负责具体的按钮事件实现,在点击了按钮之后,Context
并不做任何具体的操作,而是把请求委托给当前的状态类来执行:
1 | bindEvent() { |
第五步,使用StateFactory编写各个状态类的实现。
1 | class State { |
16.6 状态模式的优缺点
状态模式的优点如下:
- 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
- 避免
Context
无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context
中原本过多的条件分支。 - 用对象代替字符串来记录当前的状态,使得状态的切换更加一目了然。
- Context中的请求动作和状态类中的封装行为可以非常容易地独立变化而互不影响。
状态模式的缺点是会在系统中定义许多状态类,且逻辑分散,不能一眼看出整个状态机的逻辑。
16.7 状态模式的性能优化点
- state对象的创建销毁时机
- 利用
亨元模式
使得各个Context
共享一个state
对象
16.8 状态模式和策略模式的关系
相同点:他们都有上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
区别:使用场景不一样,策略模式多用于客户封装了一系列算法,在需要的时候切换使用;而状态模式中状态和状态对应的行为是早已封装好的,状态之间的切换也是早就规定好了,”改变行为”这件事发生在状态模式内部,对客户来说不需要了解其实现细节。