JavaScript 状态模式
前言
状态模式是一种行为设计模式,用于在一个对象的内部状态改变时改变其行为。这种模式通过将每个状态封装成独立的类,并将动作委托到代表当前状态的对象,从而实现状态与行为的分离。
状态模式的组成三个主要部分:
- 环境类(Context):维护一个指向当前状态对象的引用,并将所有与该状态相关的工作委托给它。
- 抽象状态类(State):定义一个接口以封装与环境类的一个特定状态相关的行为。
- 具体状态类(Concrete State):实现抽象状态类的接口的类,为具体状态提供了行为的实现。
说白了就是将状态和状态具体的逻辑封装成一个个状态对象,通过抽象状态类统一接口方便调用,我们通过事件或者其他去触发的是环境类上当前状态对象的接口,状态对象会自动设置下一个状态对象是什么。
通过将具体的处理拆分成不同的状态对象,从而实现环境类与具体逻辑的松耦合,环境类只需要维护状态对象即可。
电灯的例子
我们来想象这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时 按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同 的状态下,表现出来的行为是不一样的。
var Light = function() {
this.state = 'off'; // 给电灯设置初始状态 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.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();
可以看到我们内部维护了一个状态值state
,通过这个状态值控制buttonWasPressed
方法的具体行为,这种方式看起来无懈可击,且没有任何bug。
但是当我们灯的功能更多的时候,比如现在的灯都会有亮度模式:弱光、强光、关闭;那么buttonWasPressed
可能就会变成这样:
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
的方法会随着增大,使得它非常不稳定,不符合开闭原则。
如果再增加更多的状态,按照实际的业务需求,肯定会比上述代码复杂的多,维护会很困难,而且状态的变化是通过if else堆砌出来的,如果需要调转顺序,强光先,弱光后,在关灯,就需要改变若干个操作和代码顺序。
使用状态模式改进
// 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
};
var Light = function() {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
};
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 = function(newState) {
this.currState = newState;
};
var light = new Light();
light.init();
我们将状态和状态对应的行为封装成状态对象,通过状态对象自己去关联下一个状态,从而将业务逻辑剥离出来,便于扩展。
如果我们需要新增一个新的状态,只需要再创建一个状态对象,在对应的状态对象里实现管理,环境类Light
里面再持有一个新的状态即可。
可以看到改动的部分很少且简单,具体的复杂逻辑都被放在了对应的状态对象中了。
缺少抽象类的变通方式
在之前的文章中也有提到,JavaScript本身是没有抽象类的,在状态模式中没法很好的约束状态对象的实现,如果依赖程序员的自觉性,那显然是不现实的。
场景的做法就是做一个父类,父类规定的方法直接throw出一个错误,这样子类在调用的时候触发报错,就能及时避免子类没有实现父类方法的情况。
var State = function() {};
State.prototype.buttonWasPressed = function() {
throw new Error('父类的 buttonWasPressed 方法必须被重写');
};
var SuperStrongLightState = function(light) {
this.light = light;
};
SuperStrongLightState.prototype = new State(); // 继承抽象父类
SuperStrongLightState.prototype.buttonWasPressed = function() { // 重写 buttonWasPressed 方法
console.log('关灯');
this.light.setState(this.light.offLightState);
};
无类实现
在ES5版本的时候确定了一个特性,就是JavaScript是不需要类的,我们可以使用很多种方式来创建状态对象,不通过class继承的方式。
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); // 把请求委托给 FSM 状态机
}
};
var FSM = {
off: {
buttonWasPressed: function() {
console.log('关灯');
this.button.innerHTML = '下一次按我是开灯';
this.currState = FSM.on;
}
},
on: {
buttonWasPressed: function() {
console.log('开灯');
this.button.innerHTML = '下一次按我是关灯';
this.currState = FSM.off;
}
}
};
var light = new Light();
light.init();
通过FSM状态机来维护不同的状态对象。
我们还可以通过函数闭包的方式解决上面按钮事件的this问题:
var delegate = function(client, delegation) {
return {
buttonWasPressed: function() { // 将客户的操作委托给 delegation 对象
return delegation.buttonWasPressed.apply(client, arguments);
}
}
};
var FSM = {
off: {
buttonWasPressed: function() {
console.log('关灯');
this.button.innerHTML = '下一次按我是开灯';
this.currState = this.onState;
}
},
on: {
buttonWasPressed: function() {
console.log('开灯');
this.button.innerHTML = '下一次按我是关灯';
this.currState = this.offState;
}
}
};
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 light = new Light();
light.init();
例子:分析英文字符串中的单词数量
这个是我面试时被问到的,当时也没有写出来,一开始我还认为面试不就问问前端日常的一些知识嘛,什么vue的数据响应式怎么实现的,组件的通信,请求的封装,我觉得我都没啥问题,结果来了个这,当时给我干蒙了。
后面自己去查了点资料,问了下ai,实现了一个这样的函数:
// const boxText = "Hello, I am a 22-year-old with 2.3 million dollars.";
const boxText = `This is an example text. It includes numbers like 12, 34.56,
hyphens-between-words, and words split across lines. aaaa .,'s !!!! a-s 6-6`;
function test(text) {
const textArr = text.split("");
const list = [];
let status = 0; //0开始、1是一个单词;
let prevText = "";
let word = "";
textArr.forEach((t, index) => {
switch (status) {
case 0:
if (isEnglishLetters(t) || isDigital(t)) {
status = 1;
prevText += t;
word += t;
} else {
status = 0;
}
break;
case 1:
if (isDelimiter(t)) {
list.push(word);
prevText = "";
word = "";
status = 0;
} else if (t === ".") {
if (isEnglishLetters(prevText)) {
list.push(word);
prevText = "";
word = "";
status = 0;
} else if (
isDigital(prevText) &&
isDigital(textArr[index + 1])
) {
prevText = t;
word += t;
} else {
list.push(word);
status = 0;
prevText = "";
word = "";
}
} else if (t === "-") {
if (
isEnglishLettersAndDigital(prevText) &&
isEnglishLettersAndDigital(textArr[index + 1])
) {
prevText = t;
word += t;
} else {
list.push(word);
prevText = "";
word = "";
status = 0;
}
} else {
// 正常字符
status = 1;
prevText = t;
word += t;
}
break;
}
});
// 最后一次
if (word.trim() !== "") {
list.push(word);
}
prevText = "";
word = "";
status = 0;
return list;
}
/** 是否是字母 */
function isEnglishLetters(text) {
return /^[a-zA-Z]$/.test(text);
}
/** 是否是数字 */
function isDigital(text) {
return /^[0-9]$/.test(text);
}
/** 是否是子母或者数字 */
function isEnglishLettersAndDigital(text) {
return isEnglishLetters(text) || isDigital(text);
}
/** 是否是分隔符 */
function isDelimiter(text) {
return /^[,;!?\s]$/.test(text);
}
const list = test(boxText);
console.log("🚀 ~ list:", list);
这种方式就是通过一个状态值来判断下一次的处理方式,这种通过状态来处理的逻辑,显然是可以通过状态模式来进行优化的,下面是我优化后的代码:
const boxText = "Hello, I am a 22-year-old with 2.3 million dollars.";
// const boxText = `This is an example text. It includes numbers like 12, 34.56,
// hyphens-between-words, and words split across lines. aaaa .,'s !!!! a-s 6-6`;
class State {
analysis() {
throw new Error("Method not implemented.");
}
/** 是否是字母 */
isEnglishLetters(text) {
return /^[a-zA-Z]$/.test(text);
}
/** 是否是数字 */
isDigital(text) {
return /^[0-9]$/.test(text);
}
/** 是否是子母或者数字 */
isEnglishLettersAndDigital(text) {
return this.isEnglishLetters(text) || this.isDigital(text);
}
/** 是否是分隔符 */
isDelimiter(text) {
return /^[,;!?\s]$/.test(text);
}
}
class StartState extends State {
constructor(context) {
super();
this.context = context;
}
analysis(char, index) {
if (this.isEnglishLetters(char) || this.isDigital(char)) {
this.context.prevText = char;
this.context.word += char;
// 设置下一个状态
this.context.setState(this.context.wordState);
}
}
}
class WordState extends State {
constructor(context) {
super();
this.context = context;
}
analysis(char, index) {
if (this.isDelimiter(char)) {
this.delimiterAnalysis(char, index);
} else if (char === ".") {
this.dotAnalysis(char, index);
} else if (char === "-") {
this.dashAnalysis(char, index);
} else {
this.wordAnalysis(char, index);
}
}
/** 当前单词是分隔符号 */
delimiterAnalysis(char, index) {
this.context.wordArr.push(this.context.word);
this.context.word = "";
this.context.prevText = char;
// 下一个状态
this.context.setState(this.context.startState);
}
/** 当前单词是. */
dotAnalysis(char, index) {
// 判断上一个字符是不是字母,如果是表示单词结束
const isPrevEnglishLetters = this.isEnglishLetters(
this.context.prevText
);
if (isPrevEnglishLetters) {
this.context.wordArr.push(this.context.word);
this.context.word = "";
this.context.prevText = char;
// 下一个状态
this.context.setState(this.context.startState);
return;
}
// 判断上一个和下一个字符是不是数字
const isPrevDigital = this.isDigital(this.context.prevText);
const isNextDigital = this.isDigital(this.context.textArr[index + 1]);
if (isPrevDigital && isNextDigital) {
this.context.word += char;
this.context.prevText = char;
// 下一个状态
this.context.setState(this.context.wordState);
return;
}
// 不满足上述任何条件,理应也是结束
this.context.wordArr.push(this.context.word);
this.context.word = "";
this.context.prevText = char;
// 下一个状态
this.context.setState(this.context.startState);
}
/** 当前单词是- */
dashAnalysis(char, index) {
// 判断上一个和下一个字符是不是字母或者数字,例:22-year-old、hyphens-between-words
const isPrevEnglishLettersAndDigital =
this.isEnglishLettersAndDigital(this.context.prevText);
const isNextEnglishLettersAndDigital =
this.isEnglishLettersAndDigital(this.context.textArr[index + 1]);
if (
isPrevEnglishLettersAndDigital &&
isNextEnglishLettersAndDigital
) {
this.context.word += char;
this.context.prevText = char;
// 下一个状态
this.context.setState(this.context.wordState);
return;
}
// 不满足上述任何条件,理应也是结束
this.context.wordArr.push(this.context.word);
this.context.word = "";
this.context.prevText = char;
// 下一个状态
this.context.setState(this.context.startState);
}
// 正常字符串情况
wordAnalysis(char, index) {
this.context.word += char;
this.context.prevText = char;
// 下一个状态
this.context.setState(this.context.wordState);
}
}
class EndState extends State {
constructor(context) {
super();
this.context = context;
}
analysis() {
if (this.context.word.trim() !== "") {
this.context.wordArr.push(this.context.word);
this.context.word = "";
this.context.prevText = "";
// 下一个状态
this.context.setState(this.context.startState);
}
}
}
class Context {
constructor() {
this.startState = new StartState(this);
this.wordState = new WordState(this);
this.endState = new EndState(this);
this.textArr = [];
this.wordArr = [];
this.prevText = "";
this.word = "";
}
start(text) {
this.textArr = text.split("");
this.wordArr = [];
this.prevText = "";
this.word = "";
this.currentState = this.startState;
this.textArr.forEach((t, index) => {
this.currentState.analysis(t, index);
});
this.currentState = this.endState;
this.currentState.analysis();
}
result() {
return {
words: this.wordArr,
count: this.wordArr.length,
};
}
/** 设置状态 */
setState(state) {
this.currentState = state;
}
}
const context = new Context();
context.start(boxText);
console.log("🚀 ~ context.result():", context.result());
我实现了三种状态:起始状态、单词状态、结束状态
通过让状态自己去控制下一个状态来实现分析,这种方式显然可读性和扩展性都比上一版的要好很多了,且很多地方代码逻辑更加完善。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据