前言

状态模式是一种行为设计模式,用于在一个对象的内部状态改变时改变其行为。这种模式通过将每个状态封装成独立的类,并将动作委托到代表当前状态的对象,从而实现状态与行为的分离。

状态模式的组成三个主要部分:

  1. 环境类(Context):维护一个指向当前状态对象的引用,并将所有与该状态相关的工作委托给它。
  2. 抽象状态类(State):定义一个接口以封装与环境类的一个特定状态相关的行为。
  3. 具体状态类(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());

我实现了三种状态:起始状态、单词状态、结束状态

通过让状态自己去控制下一个状态来实现分析,这种方式显然可读性和扩展性都比上一版的要好很多了,且很多地方代码逻辑更加完善。

分类: JavaScript设计模式与开发实践 标签: JavaScript模式状态模式

评论

暂无评论数据

暂无评论数据

目录