• javascript设计模式学习之十六——状态模式


    一、状态模式的定义

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

    状态模式中有意思的一点是,一般我们谈到封装,都是优先封装对象的行为,而非对象的状态。但在状态模式中刚好相反,状态模式的关键是把事务的每种状态都封装为单独的类,跟此种状态有关的行为都封装在这个类的内部。与此同时,我们还可以把状态的切换规则实现分布在状态类中,这样就有效消除了原本存在的大量条件分支语句。

    二、状态模式的应用案例——文件上传

    在现实中,状态模式的应用案例有很多,如文件上传程序有文件扫描、正在上传、暂停、上传成功、上传失败这几种状态,音乐播放器有加载中、正在播放、暂停、播放完毕这几种状态。点击同一个按钮,在上传中和暂停状态下的行为表现是不一样的,同时他们的class也不一样。

    在文件上传中,涉及到两个控制按钮:第一个用于暂停和继续上传,第二个用于删除文件;文件在不同的状态下,点击这两个按钮,发生的行为也不同:

    1)文件在扫描过程中,是不能进行任何操作的,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成后,根据文件的md5进行判断,如果文件已经存在于服务器中,则直接跳转到上传完成状态。如果文件的大小超过允许上传的最大值,或者文件已经损坏,则跳转到上传失败状态。

    2)上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传。

    3)扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件。

    文件上传是一个异步的过程,在上传过程中,上传插件会不停地调用javascript提供的一个全局函数window.external.upload,来通知javascript目前的上传进度,控件会把当前的文件状态作为参数state塞进window.external.upload.这里使用setTimeout来模拟文件的上传进度,以下代码模拟上传插件的实现:

    //上传插件对象
    var plugin = (function() {
        var plugin = document.createElement('embed');
        plugin.style.display = 'none';
        plugin.type = 'application/txftn-webkit';
        //插件扫描
        plugin.sign = function() {
            console.log('plugin开始进行文件扫描');
        };
        plugin.pause = function() {
            console.log('plugin暂停文件扫描');
        };
        plugin.uploading = function() {
            console.log('plugin开始文件上传');
        };
        plugin.del = function() {
            console.log('plugin删除文件上传');
        };
        plugin.done = function() {
            console.log('plugin文件上传完成');
        };
        document.body.appendChild(plugin);
        return plugin;
    })();

    如果编写传统代码实现文件上传,代码如下:

    //采用传统方法实现文件上传
    //实现上传类
    var Upload = function(fileName) {
        this.fileName = fileName;
        this.plugin = plugin;
        this.button1 = null; //控制继续、暂停上传等
        this.button2 = null; //控制上传文件删除等
        this.curState = 'sign'; //设置初始状态为文件扫描
    };
    Upload.prototype.init = function() {
        this.div = document.createElement('div');
        this.div.innerHTML = '<span>文件名称:' + this.fileName + '</span><button id="button1">扫描中</button><button id="button2">删除</button>';
        document.body.appendChild(this.div);
        this.button1 = document.getElementById('button1');
        this.button2 = document.getElementById('button2');
        this.bindEvent();
    };
    Upload.prototype.changeState = function(state) { //负责切换状态的具体行为
        this.curState=state;
        switch (state) {
            case 'sign':
                this.plugin.sign();
                this.button1.innerHTML = '扫描中,任何操作无效';
                break;
            case 'uploading':
                this.plugin.uploading();
                this.button1.innerHTML = '正在上传,点击暂停';
                break;
            case 'pause':
                this.plugin.pause();
                this.button1.innerHTML = '已暂停,点击继续上传';
                break;
            case 'done':
                this.plugin.done();
                this.button1.innerHTML = '文件上传完成';
                break;
            case 'error':
                this.button1.innerHTML = '文件上传失败';
                break;
            case 'del':
                this.plugin.del();
                this.div.parentNode.removeChild(this.div);
                console.log('删除完成');
                break;
        }
    
    };
    Upload.prototype.bindEvent = function() {
        var self = this;
        this.button1.onclick = function() {
            if (self.curState === 'sign') {
                console.log('扫描中,点击无效...');
            } else if (self.curState === 'uploading') { //上传中,切换到暂停样式
                self.changeState('pause');
            } else if (self.curState === 'pause') { //暂停中,切换到上传模式
                self.changeState('uploading');
            } else if (self.curState === 'done') {
                console.log('文件已经上传,点击无效...');
            } else if (self.curState === 'error') {
                console.log('文件上传失败,点击无效...');
            }
        };
        this.button2.onclick = function() {
            if (self.curState === 'done' || self.curState === 'error' || self.curState === 'pause') {
                self.changeState('del'); //以上三种状态下可以删除
            } else if (self.curState === 'sign') {
                console.log('文件正在扫描中,不能删除');
            } else if (self.curState === 'uploading') {
                console.log('文件正在上传中,不能删除');
            }
        };
    };

     上面完成了一个简单的文件上传程序。当然这是一个反例,程序中充斥着大量的if-else语句,状态和行为都被耦合到一个巨大的方法里,难以进行修改和扩展。

    测试:

    var uploadObj=new Upload('java疯狂讲义');
    uploadObj.init();
    
    window.external.upload=function(state){
        uploadObj.changeState(state);
    };
    window.external.upload('sign');
    setTimeout(function(){
        window.external.upload('uploading');
    },1000);
    setTimeout(function(){
        window.external.upload('done');
    },5000);

    运用状态模式重构以上代码:

    //上传插件对象
    var plugin = (function() {
        var plugin = document.createElement('embed');
        plugin.style.display = 'none';
        plugin.type = 'application/txftn-webkit';
        //插件扫描
        plugin.sign = function() {
            console.log('plugin开始进行文件扫描');
        };
        plugin.pause = function() {
            console.log('plugin暂停文件扫描');
        };
        plugin.uploading = function() {
            console.log('plugin开始文件上传');
        };
        plugin.del = function() {
            console.log('plugin删除文件上传');
        };
        plugin.done = function() {
            console.log('plugin文件上传完成');
        };
        document.body.appendChild(plugin);
        return plugin;
    })();
    
    
    //使用状态模式重构文件上传代码
    var Upload = function() {
        this.plugin = plugin;
        this.fileName = fileName;
        this.button1 = null;
        this.button2 = null;
        //构建各个状态类
        this.signState = new SignState(this);
        this.uploadingState = new UploadingState(this);
        this.pauseState = new PauseState(this);
        this.doneState = new DoneState(this);
        this.errorState = new ErrorState(this);
        this.curState = this.signState;
    };
    
    Upload.prototype.init = function() {
        this.div = document.createElement('div');
        this.div.innerHTML = '<span>文件名称:' + this.fileName + '</span><button id="button1">扫描中</button><button id="button2">删除</button>';
        document.body.appendChild(this.div);
        this.button1 = document.getElementById('button1');
        this.button2 = document.getElementById('button2');
        this.bindEvent();
    };
    Upload.prototype.bindEvent = function() {
        var self = this;
        this.button1.onclick = function() {
            self.curState.clickHandler1();
        };
        this.button2.onclick = function() {
            self.curState.clickHandler2();
        };
    };
    Upload.prototype.sign=function(){
        this.plugin.sign();
        this.curState=this.signState;
    };
    Upload.prototype.uploading=function(){
        this.button1.innerHTML='正在上传,点击暂停';
        this.plugin.uploading();
        this.curState=this.uploadingState;
    };
    Upload.prototype.pause=function(){
        this.button1.innerHTML='已暂停,点击继续上传';
        this.plugin.pause();
        this.curState=this.pauseState;
    };
    Upload.prototype.done=function(){
        this.button1.innerHTML='上传完成';
        this.plugin.done();
        this.curState=this.doneState;
    };
    Upload.prototype.error=function(){
        this.button1.innerHTML='上传失败';
        this.curState=this.errorState;
    };
    Upload.prototype.del=function(){
        this.plugin.del();
        this.div.parentNode.removeChild(this.div);
    };
    
    //接下来编写各个状态类的实现
    var StateFactory = (function() {
        //这个相当于抽象类
        var State = function() {};
        State.prototype.clickHandler1 = function() {
            throw new Error('子类必须重写父类的clickHandler1方法');
        };
        Stat8e.prototype.clickHandler2 = function() {
            throw new Error('子类必须重写父类的clickHandler2方法');
        };
        return function(param) {
            var F = function(uploadObj) {
                this.uploadObj = uploadObj;
            };
            F.prototype = new State();
            for (var i in param) {
                F.prototype[i] = param[i];
            }
            return F;
        };
    })();
    var signState = StateFactory({
        clickHandler1: function() {
            console.log('扫描中,点击无效');
        },
        clickHandler2: function() {
            console.log('文件正在扫描,不能删除');
        }
    });
    var UploadingState = StateFactory({
        clickHandler1: function() {
            this.uploadObj.pause();
        },
        clickHandler2: function() {
            console.log('文件正在上传,不能删除');
        }
    });
    var PauseState = StateFactory({
        clickHandler1: function() {
            this.uploadObj.uploading();
        },
        clickHandler2: function() {
            this.uploadObj.del();
        }
    });
    var DoneState = StateFactory({
        clickHandler1: function() {
            console.log('文件上传完成,点击无效');
        },
        clickHandler2: function() {
            this.uploadObj.del();
        }
    });
    var ErrorState = StateFactory({
        clickHandler1: function() {
            console.log('文件上传失败,点击无效');
        },
        clickHandler2: function() {
            this.uploadObj.del();
        }
    });
    window.external.upload=function(){
        
    };

    状态模式控制电灯案例: 

    下面已切换电灯状态这个相对简单的例子来进一步说明状态模式:

    //状态模式学习
    //以控制灯泡开关的状态为例,电灯开关开着的时候,按下开关,电灯会切换到关闭状态,反之,则切换到开启状态
    var Light=function(){
        this.state='off';
        this.button=null;
        this.stateSpan=document.getElementById('lightState');
    };
    Light.prototype.init=function(){
        var button=document.createElement('button');
        button.innerHTML='开关';
        this.button=document.body.appendChild(button);
        this.bindEvent();
    };
    Light.prototype.bindEvent=function(){
        var self=this;
        this.button.onclick=function(e){
            self.buttonWasPressed();
        };
    };
    
    Light.prototype.buttonWasPressed=function(){
        if(this.state==='off'){
            console.log('电灯打开了');
            this.stateSpan.innerHTML='打开';
            this.state='on';
        }else if(this.state==='on'){
            this.stateSpan.innerHTML='关闭';
            console.log('电灯关闭了');
            this.state='off';
        }
    };
    
    //测试
    var light=new Light();
    light.init();

    以上代码中的buttonWasPressed方法显然违背了封闭-开放原则,电灯的状态除了开,关之外,还可能有多种,如强光,弱光等,每一次新增或者修改状态都需要修改buttonWasPressed方法,该方法将变得极为庞大和臃肿,以至于极难维护。

    //使用状态模式进行代码重构
    //
    function OnState(light){
        this.light=light;
    }
    OnState.prototype.buttonWasPressed=function(){
        this.light.lightSpan.innerHTML='关闭';
        this.light.curState=this.light.offState;
    };
    
    function OffState(light){
        this.light=light;
    }
    OffState.prototype.buttonWasPressed=function(){
        this.light.lightSpan.innerHTML='打开';
        this.light.curState=this.light.onState;
    };
    var Light=function(){
        this.button=null;
        this.lightSpan=document.getElementById('lightState');
        this.onState=new OnState(this);
        this.offState=new OffState(this);
        this.curState=this.offState;
    };
    Light.prototype.init=function(){
        var button=document.createElement('button');
        button.innerHTML='开关';
        this.button=document.body.appendChild(button);
        var self=this;
        this.button.onclick=function(){
            self.buttonWasPressed();
        };
    };
    Light.prototype.buttonWasPressed=function(){
        this.curState.buttonWasPressed();
    };
    
    
    
    //测试
    var light=new Light();
    light.init();

     由此可见,使用状态模式的好处很明显,可以使得每种状态和它所对应的行为之间的关系局部化,当light需要增添一个新的状态的时候,无需编写过多的if-else语句,只需要新增一个状态类,再稍稍更改现有的代码即可。

    同样也可以看出状态模式的缺点,其缺点在于会使得系统中增添很多的状态类。因此系统会增减不少对象,同事由于避开了if-else语句,也造成了逻辑分散的问题,无法再一个地方看出整个状态机的逻辑。

    其实,上面的例子里,再javascript这种无类的语言中,没有规定让状态对象一定要从某个类中创建出来,通过Function.prototype.call方法可以直接将请求委托给某个字面量兑现来执行。

     
    var Light=function(){
        this.button=null;
        this.lightSpan=document.getElementById('lightState');
        this.curState=FSM.off;    
    };
     
    var FSM={
        'on':{
            'buttonWasPressed':function(){
                this.curState=FSM.off;
                this.lightSpan.innerHTML='关闭';
            }
        },
        'off':{
            'buttonWasPressed':function(){
                this.curState=FSM.on;
                this.lightSpan.innerHTML='打开';
            }
        }
    };
    Light.prototype.init=function(){
        var button=document.createElement('button');
        button.innerHTML='开关';
        var self=this;
        this.button=document.body.appendChild(button);
        button.onclick=function(){
            self.curState.buttonWasPressed.call(self);
        };
    };
    
    
    var light=new Light();
    light.init();
  • 相关阅读:
    live555源码研究(三)------UsageEnvironment类
    live555源码研究(二)------TaskScheduler类
    live555源码研究(一)------live555MediaServer的启动过程和基本类图
    (转)视频监控相关文章
    【流媒體】live555—VS2008 下live555编译、使用及测试
    【转】PostgreSQL IP地址访问配置
    red5研究(一):下载,工程建立、oflaDemo安装、demo测试
    SVN服务器的搭建和使用
    【转】linux下cvs配置
    【转】js正则表达式语法
  • 原文地址:https://www.cnblogs.com/bobodeboke/p/5719104.html
Copyright © 2020-2023  润新知