状态模式

状态模式是一种非同寻常的优秀模式,它也许是解决某些需求场景的最好方法。虽然状态模式并非一种简单到一目了然的模式(它每每还会带来代码量的增长),但你一旦明白了状态模式的精髓,之后必定会感谢它带给你的无与伦比的好处。javascript

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

1. 初识状态模式

咱们来想像这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不一样的状态下,表现出来的行为是不同的。java

如今用代码来描述这个场景,首先定义一个 Light 类,能够预见,电灯对象 light 将从 Light 类建立而出,light 对象将拥有两个属性,咱们用 state 来记录电灯当前的状态,用 button 表示具体的开关按钮。下面来编写这个电灯程序的例子。程序员

1. 1 电灯程序

首先给出不用状态模式的电灯程序实现:算法

var Light = function () {
    this.state = 'off'; //给电灯设置初始状态
    this.button = null; //电灯开关按钮
};

接下来定义 Light.prototype.init 方法,该方法负责在页面中建立一个真实的 button 节点,假设这个 button 就是电灯的开关按钮,当 button 的 onclick 事件被触发时,就是电灯开关被按下的时候,代码以下:设计模式

Light.prototype.init = function () {
    var button = document.createElement('button');
    self = this;
    button.innerHTML = '开关';
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
        self.buttonWasPressed();
    }
};

当开关被按下时,程序会调用 self.buttonWasPressed 方法,开关按下以后的全部行为,都被封装在这个方法里,代码以下:性能优化

Light.prototype.buttonWasPressed = function () {
    if (this.state === 'off'){
        console.log('开灯');
        this.state = 'on';
    } else if (this.state === 'on'){
        console.log('关灯');
        this.state = 'off';
    }
};

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

OK ,如今能够看到,咱们已经编写了一个强壮的状态机,这个状态机的逻辑简单又缜密,看起来这段代码设计得无懈可击,这个程序没有任何 bug 。实际上这种代码咱们已经编写过无数次,好比要交替切换一个 button 的 class ,跟此例同样,每每先用一个变量 state 来记录按钮的当前行为,在事件发生时,再根据这个状态来决定下一步的行为。闭包

使人遗憾的是,这个世界上的电灯并不是只有一种。许多酒店里有另一种电灯,这种电灯也只有一个开关,但它表现的是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯。如今必须改造上面的代码来完成这种新型电灯的制造:app

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';
    }
};

如今这个反例先告一段落,咱们来考虑一下上述程序的缺点。函数

  • 很明显 buttonWasPressed 方法是违反开放——封闭原则的,每次新增或者修改 light 的状态,都须要改动 buttonWasPressed 方法中的代码,这使得 buttonWasPressed 成为了一个不稳定的方法。
  • 全部跟状态有关的行为,都被封装在 buttonWasPressed 方法里,若是之后这个电灯又增长了强强光,超强光和终极强光,那咱们将没法预计这个方法将膨胀到什么地步。固然为了简化示例,此处在状态发生改变的时候,只是简单的打印一条 log 和改变 button 的 innerHTML 。在实际开发中,要处理的事情可能比这多得多,也就是说,buttonWasPressed 方法要比如今庞大的多。
  • 状态的切换很是不明显,仅仅表现为对 state 变量赋值,好比 this.state = 'weakLight' 。在实际开发中,这样的操做很容易被程序员不当心漏掉。咱们也没有办法一目了然的明白电灯一共有多少状态,除非耐心的读完 buttonWasPressed 方法里的全部代码。当状态的种类多起来的时候,某一次切换的过程就好像被埋藏在一个巨大方法里的某个阴暗角落里。
  • 状态之间的切换关系,不过是往 buttonWasPressed 方法里堆砌 if ,else 语句,增长或者修改一个状态可能须要改变若干个操做,这使得 buttonWasPressed 更加难以阅读和维护。

1. 2 状态模式改进电灯程序

啰嗦了一大堆,如今咱们来学习状态模式改进电灯的程序。有意思的是,一般咱们谈到封装,通常都会优先封装对象的行为,而不是对象的状态。但在状态模式中恰好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,因此 button 被按下的时候,只须要在上下文中,把这个请求委托给当前的状态便可,该状态对象会负责渲染它自身的行为。

同时咱们还能够把状态的切换规则实现分布在状态类中,这样就有效的消除了本来存在的大量条件分支语句。

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

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

var WeakLightState = function (light) {
    this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
    console.log('强光');
    this.light.setState(this.light.strongLightState);
};

var StrongLightState = function (light) {
    this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
    console.log('关灯');
    this.light.setState(this.light.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;
    button.innerHTML = '开关';
    this.button = document.body.appendChild(button);
    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 条件分支语句来控制状态之间的转换。

当咱们须要为 light 对象增长一种新的状态时,只须要增长一个新的状态类,再稍稍改变一些现有的代码便可。假设如今 light 对象多了一种超强光的状态,那就先增长 SuperStrongLightState 类:

var SuperStrongLightState = function (light) {
    this.light = light;
}
SuperStrongLightState.prototype.buttonWasPressed = function () {
    console.log('关灯');
    this.light.setState(this.light.offLightState)
}

而后再 Light 构造函数里新增一个 superStrongLightState 对象:

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

最后改变状态类之间的切换规则,从 StrongLightState ☛ OffLightState 变为 StrongLightState ☛ SupStrongLightState ☛ OffLightState:

var StrongLightState = function (light) {
    this.light = light;
};

StrongLightState.prototype.buttonWasPressed = function () {
    console.log('超强光');
    this.light.setState(this.light.superStrongLightState);
};

2. 状态模式的定义

经过电灯的例子,相信咱们对于状态模式已经有了必定程度的了解。如今回头来看 GoF 中对状态模式的定义:容许一个对象在其内部状态改变时改变它的行为,对象看起来彷佛修改了它的类。

咱们以逗号分隔,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不一样的行为变化。电灯的例子足以说明这一点,在 off 和 on 这两种不一样的状态下,咱们点击同一个按钮,获得的行为反馈是大相径庭的。

第二部分是从客户的角度来看,咱们使用的对象,在不一样的状态下具备大相径庭的行为,这个对象看起来是从不一样的类中实例化而来的,实际上这是使用了委托的效果。

3. 缺乏抽象类的变通方法

咱们看到,在状态类中将定义一些共同行为方法,Context 最终会将请求委托给状态对象的这些方法,在这个例子里,这个方法就是 buttonWasPressed 。不管增长了多少种状态类,它们都必须实现 buttonWasPressed 方法。

在 Java 中,全部的状态类必须继承自一个 State 抽象父类,固然若是没有共同的功能值得放入抽象父类,也能够选择实现 State 接口。这样作的缘由一方面是咱们曾屡次提过的向上转型,另外一方面是保证全部的状态子类都实现了 buttonWasPressed 方法。遗憾的是,JavaScript 既不支持抽象类,也没有接口的概念。因此在使用状态模式的时候要格外当心,若是咱们编写一个状态子类时,忘记了给这个状态子类实现 buttonWasPressed 方法,则会在状态切换的时候抛出异常。由于 Context 老是把请求委托给状态对象的 buttonWasPressed 方法。

不论怎么严格要求程序员,也许都避免不了犯错的那一天,毕竟若是没有编译器的帮助,只依靠程序员自觉以及一点好运气,是不靠谱的。这里建议的解决方案跟模板方法模式中同样,让抽象父类的抽象方法直接抛出一个异常,这个异常至少会在程序运行期间就被发现:

var State = function () {};
State.prototype.buttonWasPressed = function () {
    throw new Error('父类的 buttonWasPressed 方法必须被重写');
};

var OffLightState = function (light) {
    this.light = light;
};
OffLightState.prototype = new State();  //继承抽象父类
//重写父类方法
OffLightState.prototype.buttonWasPressed = function () {
    console.log('弱光');
    this.light.setState(this.light.weakLightState);
};

4. 状态模式的优缺点

优势以下:

  • 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。经过增长新的状态类,很容易增长新的状态和转换。
  • 避免 Context 无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了 Context 中本来过多的条件分支。
  • 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然。
  • Context 中的请求动做和状态类中封装的行为能够很是容易的独立变化而互不影响。

状态模式的缺点是会在系统中定义过多的状态类,编写 20 个状态类是一项枯燥乏味的工做,并且系统中会所以而增长很多对象。另外,因为逻辑分散在状态类中,虽然避开了不受欢迎的条件分支语句,但也形成了逻辑分散的问题,咱们没法在一个地方就看出整个状态机的逻辑。

5. 状态模式的性能优化点

有两种选择来管理 state 对象的建立和销毁。第一种是仅当 state 对象被须要时才建立并随后销毁,另外一种是一开始就建立好全部的状态对象,而且始终不销毁它们。若是 state 对象比较庞大,能够用第一种方式来节省内存,这样能够避免建立一些不会用到的对象并及时的回收它们。但若是状态的改变很频繁,最好一开始就把这些 state 对象都建立出来,也没有必要销毁它们,由于可能很快将再次用到它们。

6. 状态模式和策略模式的关系

状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为,它们的类图看起来几乎如出一辙,但在乎图上有很大不一样,所以它们是两种迥然不一样的模式。

策略模式和状态模式的相同点是,它们都有一个上下文,一些策略类或状态类,上下文把请求委托给这些类来执行。

它们之间的区别是策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,因此客户必须熟知这些策略类的做用,以便客户能够随时主动切换算法;但在状态模式中,状态和状态对应的行为时早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来讲,并不须要了解这些细节。这正是状态模式的做用所在。

7. JavaScript 版本的状态机

前面的示例是模拟传统面向对象语言的状态模式实现,咱们为每种状态都定义一个状态子类,而后再 Context 中持有这些状态对象的引用,以便把 currState 设置为当前的状态对象。

状态模式是状态机的实现之一,但在 JavaScript 这种“无类”语言中,没有规定让状态对象必定要从类中建立而来。另一点,JavaScript 能够很是方便的使用委托技术,并不须要事先让一个对象持有另外一个对象。下面的状态机选择了经过 Function.prototyp.call 方法直接把请求委托给某个字面量对象来执行。

下面改写电灯的例子,来展现这种更加轻巧的作法:

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

var FSM = {
    off: {
        buttonWasPressed: function () {
            console.log('关灯');
            this.currState = FSM.on;
        }
    },
    on: {
        buttonWasPressed: function () {
            console.log('开灯');
            this.currState = FSM.off;
        }
    }
};

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

接下来尝试另一种方法,即利用下面的 delegate 函数来完成这个状态机编写。这是面向对象设计和闭包互换的一个例子,前者把变量保存为对象的属性,然后者把变量封闭在闭包造成的环境中:

var delegate = function (client, delegation) {
    return {
        buttonWasPressed: function () {
            return delegation.buttonWasPressed.apply(client, arguments);
        }
    }
};

var Light = function () {
    this.offState = delegate(this, FSM.off);
    this.onState = delegate(this, FSM.on);
    this.currState = this.offState; //设置当前状态
    this.button = null;
};
Light.prototype.init = function () {
    var button = document.createElement('button'),
    self = this;
    button.innerHTML = "开关";
    this.button = document.body.appendChild(button);
    this.button.onclick = function () {
        self.currState.buttonWasPressed();
    };
};

var FSM = {
    off: {
        buttonWasPressed: function () {
            console.log('关灯');
            this.currState = this.onState;
        }
    },
    on: {
        buttonWasPressed: function () {
            console.log('开灯');
            this.currState = this.offState;
        }
    }
};

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

参考书目:《JavaScript设计模式与开发实践》

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息