JavaScript设计模式-状态模式

概念

  状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

描述

  通常谈到封装,一般都会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
  许多酒店里有另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯,我们通常都会这么写代码。
【初始版本】

Light.prototype.buttonWasPressed = function(){
    if ( this.state === 'off' ){
        console.log( '弱光' )
        this.state = 'weakLight'
    }else if ( this.state === 'weakLight' ){
        console.log( '强光' )
        this.state = 'strongLight'
    }else if ( this.state === 'strongLight' ){
        console.log( '关灯' )
        this.state = 'off'
    }
}

现在考虑一下上述程序的缺点
  1、违反开放——封闭原则的,每次新增或者修改light的状态,buttonWasPressed成为了一个非常不稳定的方法。如果以后这个电灯又增加了强强光、超强光和终极强光,那将无法预计这个方法将膨胀到什么地步。
  2、状态的切换非常不明显,仅仅表现为对state变量赋值,比如this.state='weakLight',在实际开发中,这样的操作很容易被程序员不小心漏掉,也没有办法一目了然地明白电灯一共有多少种状态,除非耐心地读完buttonWasPressed方法里的所有代码。

【状态模式】
  下面进入状态模式的代码编写阶段,首先将定义3个状态类,分别是offLightStateWeakLightStatestrongLightState。这3个类都有一个原型方法buttonWasPressed,代表在各自状态下,按钮被按下时将发生的行为,代码如下:

// offLightState
var OffLightState = function( light ){
    this.light = light
}
OffLightState.prototype.buttonWasPressed = function(){
    console.log( '弱光' ) // offLightState 对应的行为
    this.light.setState( this.light.weakLightState ) // 切换状态到weakLightState
}
// WeakLightState:
var WeakLightState = function( light ){
    this.light = light
}
WeakLightState.prototype.buttonWasPressed = function(){
    console.log( '强光' ) // weakLightState 对应的行为
    this.light.setState( this.light.strongLightState ) // 切换状态到strongLightState
}
// StrongLightState:
var StrongLightState = function( light ){
    this.light = light
}
StrongLightState.prototype.buttonWasPressed = function(){
    console.log( '关灯' ) // strongLightState 对应的行为
    this.light.setState( this.light.offLightState ) // 切换状态到offLightState
}

  接下来改写Light类,现在使用更加立体化的状态对象来记录当前的状态而不再使用一个字符串。在Light类的构造函数里为每个状态类都创建一个状态对象,这样一来可以很明显地看到电灯一共有多少种状态,代码如下:

var Light = function(){
    this.offLightState = new OffLightState( this )
    this.weakLightState = new WeakLightState( this )
    this.strongLightState = new StrongLightState( this )
    this.button = null
}

  在button按钮被按下的事件里,Context也不再直接进行任何实质性的操作,而是通过self.currState.buttonWasPressed()将请求委托给当前持有的状态对象去执行,代码如下:

Light.prototype.init = function(){
    var button = document.createElement( 'button' ),
        self = this
    this.button = document.body.appendChild( button )
    this.button.innerHTML = '开关'
    this.currState = this.offLightState // 设置当前状态
    this.button.onclick = function(){
        self.currState.buttonWasPressed()
    }
}

  最后还要提供一个Light.prototype.setState方法,状态对象可以通过这个方法来切换light对象的状态。状态的切换规律事先被完好定义在各个状态类中。在Context中再也找不到任何一个跟状态切换相关的条件分支语句:

Light.prototype.setState = function( newState ){
    this.currState = newState;
}

  现在可以进行一些测试:

var light = new Light()
light.init()

  使用状态模式代码量增加了好多,但好处很明显,它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类之中,便于阅读和管理代码。另外,状态之间的切换都被分布在状态类内部,这使得无需编写过多的if、else条件分支语言来控制状态之间的转换。

应用

  不论是文件上传,还是音乐、视频播放器,都可以找到一些明显的状态区分。比如文件上传程序中有扫描、正在上传、暂停、上传成功、上传失败这几种状态,音乐播放器可以分为加载中、正在播放、暂停、播放完毕这几种状态。点击同一个按钮,在上传中和暂停状态下的行为表现是不一样的,同时它们的样式class也不同。
  相对于电灯的例子,文件上传不同的地方在于,现在将面临更加复杂的条件切换关系。在电灯的例子中,电灯的状态总是从关到开再到关,或者从关到弱光、弱光到强光、强光再到关。看起来总是循规蹈矩的A→B→C→A,所以即使不使用状态模式来编写电灯的程序,而是使用原始的if、else来控制状态切换,也不至于在逻辑编写中迷失自己,因为状态的切换总是遵循一些简单的规律。而文件上传的状态切换相比要复杂得多,控制文件上传的流程需要两个节点按钮,第一个用于暂停和继续上传,第二个用于删除文件。
  文件在扫描状态中,是不能进行任何操作的,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的md5值判断,若确认该文件已经存在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳往上传失败状态。剩下的情况下才进入上传中状态。上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传。扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件。
  浏览器插件来帮助完成文件上传。插件类型根据浏览器的不同,有可能是ActiveXObject,也有可能是WebkitPlugin。上传是一个异步的过程,所以控件会不停地调用JS提供的一个全局函数window.external.upload,来通知JS目前的上传进度,控件会把当前的文件状态作为参数state塞进window.external.upload。

  我们用状态模式来模拟文件的上传过程,无法提供一个完整的上传插件,window.external.upload函数在此例中只负责打印一些log:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<script>
    window.external.upload = function( state ){
        console.log( state ) // 可能为sign、uploading、done、error
    }
    let plugin = (function(){ 
        let plugin = document.createElement( 'embed' )
        plugin.style.display = 'none'
        plugin.type = 'application/txftn-webkit'
        plugin.sign = function(){
            console.log( '开始文件扫描' )
        }
        plugin.pause = function(){
            console.log( '暂停文件上传' )
        }
        plugin.uploading = function(){
            console.log( '开始文件上传' )
        }
        plugin.del = function(){
            console.log( '删除文件上传' )
        }
        plugin.done = function(){
            console.log( '文件上传完成' )
        }
        document.body.appendChild( plugin )
        return plugin
    })()

    class Upload { 
        constructor(fileName) {
            this.plugin = plugin
            this.fileName = fileName
            this.button1 = null
            this.button2 = null
            this.signState = new SignState( this ) // 设置初始状态为waiting
            this.uploadingState = new UploadingState( this )
            this.pauseState = new PauseState( this )
            this.doneState = new DoneState( this )
            this.errorState = new ErrorState( this )
            this.currState = this.signState // 设置当前状态
        }
        init() {
            this.dom = document.createElement( 'div' )
            this.dom.innerHTML =
                `<span>文件名称:${this.fileName}</span>
                 <button data-action="button1">扫描中</button>
                 <span>文件名称:${this.fileName}</span>
                 <button data-action="button2">删除</button>`
            document.body.appendChild( this.dom )
            this.button1 = this.dom.querySelector( '[data-action="button1"]' )
            this.button2 = this.dom.querySelector( '[data-action="button2"]' )
            this.bindEvent()
        }
        bindEvent() {
            let _this = this
            this.button1.onclick = function(){
                _this.currState.clickHandler1()
            }
            this.button2.onclick = function(){
                _this.currState.clickHandler2()
            }
        }
        sign() {
            this.plugin.sign()
            this.currState = this.signState
        }
        uploading() {
            this.button1.innerHTML = '正在上传,点击暂停'
            this.plugin.uploading()
            this.currState = this.uploadingState
        }
        pause() {
            this.button1.innerHTML = '已暂停,点击继续上传'
            this.plugin.pause()
            this.currState = this.pauseState
        }
        done() {
            this.button1.innerHTML = '上传完成'
            this.plugin.done()
            this.currState = this.doneState
        }
        error() {
            this.button1.innerHTML = '上传失败'
            this.currState = this.errorState
        }
        del() {
            this.plugin.del()
            this.dom.parentNode.removeChild( this.dom )
        }
    }

    let StateFactory = (function(){
        let State = function(){}
        State.prototype.clickHandler1 = function(){
            throw new Error( '子类必须重写父类的clickHandler1 方法' )
        }
        State.prototype.clickHandler2 = function(){
            throw new Error( '子类必须重写父类的clickHandler2 方法' )
        }
        return function( param ){
            let F = function( uploadObj ){
                this.uploadObj = uploadObj
            }
            F.prototype = new State()
            for ( let i in param ){
                F.prototype[ i ] = param[ i ]
            }
            return F
        }
    })()

    let SignState = StateFactory({
        clickHandler1: function(){
            console.log( '扫描中,点击无效...' )
        },
        clickHandler2: function(){
            console.log( '文件正在上传中,不能删除' )
        }
    })

    let UploadingState = StateFactory({
        clickHandler1: function(){
            this.uploadObj.pause()
        },
        clickHandler2: function(){
            console.log( '文件正在上传中,不能删除' )
        }
    })

    let PauseState = StateFactory({
        clickHandler1: function(){
            this.uploadObj.uploading()
        },
        clickHandler2: function(){
            this.uploadObj.del()
        }
    })

    let DoneState = StateFactory({
        clickHandler1: function(){
            console.log( '文件已完成上传, 点击无效' )
        },
        clickHandler2: function(){
            this.uploadObj.del()
        }
    })

    let ErrorState = StateFactory({
        clickHandler1: function(){
            console.log( '文件上传失败, 点击无效' );
        },
        clickHandler2: function(){
            this.uploadObj.del()
        }
    })

    let uploadObj = new Upload( 'JavaScript' )
    uploadObj.init()
    window.external.upload = function( state ) {
        uploadObj[state]()
    }
    window.external.upload( 'sign' )
    setTimeout(function(){
        window.external.upload( 'uploading' ) // 1 秒后开始上传
    }, 1000 )
    setTimeout(function(){
        window.external.upload( 'done' ) // 5 秒后上传完成
    }, 5000 )
</script>
</body>
</html>

小结

  从上面的两个例子可以总结出状态模式的优缺点。
  状态模式的优点:
  1、状态模式定义了状态与行为之间的关系,并将它们封装在一个类里,通过增加新的状态类,很容易增加新的状态和转换.
  2、避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
  3、用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  4、Context中的请求动作和状态类中封装的行为可以非常容易地独立变化而互不影响。
  状态模式的缺点是会在系统中定义许多状态类,编写状态类是一项枯燥乏味的工作,而且系统中会因此而增加不少对象。另外,由于逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也造成了逻辑分散的问题,无法在一个地方就看出整个状态机的逻辑。

参考文献

《JavaScript设计模式与开发实践》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容