Skip to main content

状态模式(行为型)

状态模式 —— 策略模式的孪生兄弟(解决的问题非常相似)

状态模式即允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

一、一台咖啡机的诞生

有一天,产品经理 kkOma 又找 Tim 提了个需求:帮我实现一台咖啡机,根据使用者的选择产生对应食材的咖啡。Tim 敏锐地感知到,kkOma 所说的用户选择的切换,本质就是状态的切换。在这个能做四种咖啡的咖啡机体内,蕴含着四种状态:

  • 美式咖啡态(american):只吐黑咖啡
  • 普通拿铁态(latte):黑咖啡加点奶
  • 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
  • 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力

这么一梳理,Tim 的思路一下子清晰了起来。作为死性不改的 if-else 侠,他再次三下五除二写出了一套功能完备的代码:

class CoffeeMaker {
constructor() {
this.state = "init";
}

// 关注咖啡机状态切换函数
changeState(state) {
// 记录当前状态
this.state = state;
if (state === "american") {
// 这里用 console 代指咖啡制作流程的业务逻辑
console.log("我只吐黑咖啡");
} else if (state === "latte") {
console.log(`给黑咖啡加点奶`);
} else if (state === "vanillaLatte") {
console.log("黑咖啡加点奶再加香草糖浆");
} else if (state === "mocha") {
console.log("黑咖啡加点奶再加点巧克力");
}
}
}

测试一下,满足要求:

const mk = new CoffeeMaker();
mk.changeState("latte"); // 输出 '给黑咖啡加点奶'

二、我 Tim 不再做 if-else 侠

鉴于 if-else 使不得,Tim 赶紧翻出了在策略模式中学到的“单一职责”和“开放封闭”原则,改造起了 KkOma 咖啡机的状态切换机制:

1、职责分离

首先,Tim 要解决最大的问题,就是咖啡制作过程不可复用:

changeState(state) {
// 记录当前状态
this.state = state;
if(state === 'american') {
// 这里用 console 代指咖啡制作流程的业务逻辑
console.log('我只吐黑咖啡');
} else if(state === 'latte') {
console.log(`给黑咖啡加点奶`);
} else if(state === 'vanillaLatte') {
console.log('黑咖啡加点奶再加香草糖浆');
} else if(state === 'mocha') {
console.log('黑咖啡加点奶再加点巧克力');
}
}

Tim 发现 changeState 函数应该只负责状态切换,而不能干涉做咖啡的过程。应该改成根据切换的状态直接调用制作工序对应的函数,从而实现职责分离:

class CoffeeMaker {
constructor() {
this.state = "init";
}
changeState(state) {
// 记录当前状态
this.state = state;
if (state === "american") {
// 这里用 console 代指咖啡制作流程的业务逻辑
this.americanProcess();
} else if (state === "latte") {
this.latteProcress();
} else if (state === "vanillaLatte") {
this.vanillaLatteProcress();
} else if (state === "mocha") {
this.mochaProcress();
}
}

americanProcess() {
console.log("我只吐黑咖啡");
}

latteProcress() {
this.americanProcess();
console.log("加点奶");
}

vanillaLatteProcress() {
this.latteProcress();
console.log("再加香草糖浆");
}

mochaProcress() {
this.latteProcress();
console.log("再加巧克力");
}
}

const mk = new CoffeeMaker();
mk.changeState("latte");

2、开放封闭

复用的问题解决了,if-else 却仍然活得好好的。

举个例子,KkOma 想增加“气泡美式”这个咖啡品种,Tim 就不得不去修改 changeState 的函数逻辑,这违反了开放封闭的原则。

同时,一个函数中收敛这么多判断,也着实不够体面。现在就要像策略模式一样,想办法用个更优雅的方式,实现咖啡机的状态和咖啡制作工序之间的映射关系:

const stateToProcessor = {
american() {
console.log("我只吐黑咖啡");
},
latte() {
this.american();
console.log("加点奶");
},
vanillaLatte() {
this.latte();
console.log("再加香草糖浆");
},
mocha() {
this.latte();
console.log("再加巧克力");
},
};

class CoffeeMaker {
constructor() {
this.state = "init";
}

// 关注咖啡机状态切换函数
changeState(state) {
// 记录当前状态
this.state = state;
// 若状态不存在,则返回
if (!stateToProcessor[state]) {
return;
}
stateToProcessor[state]();
}
}

const mk = new CoffeeMaker();
mk.changeState("latte");

当这么做时,其实已经实现了一个 js 版本的状态模式。

但这种方法仅仅是看上去完美无缺,其中却暗含一个非常重要的隐患 —— stateToProcessor 里的工序函数,感知不到咖啡机的内部状况。

3、策略与状态的辨析

策略模式是对算法的封装。算法和状态对应的行为函数虽然本质上都是行为,但是算法的独立性却高多了。

例如一个询价算法,只需读取一个数字,就能啪啪三下五除二吐出另一个数字作为返回结果 —— 它和计算主体之间可以是分离的,只要关注计算逻辑本身就可以了。

但状态不一样。例如 KkOma 的咖啡机,做咖啡要考虑的东西太多了,比如做拿铁,拿铁中的牛奶从哪来,再比如行为函数应时刻感知咖啡机每种原材料的用量、进而判断工序是否能如期执行下去等,这就决定了行为函数需要拿到 KkOma 咖啡机的各种信息,需要感知主体。

策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。

但策略模式中的行为函数是“潇洒”的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个主体,所以不同状态对应的行为函数可能并不会特别割裂。

4、进一步改造

现在当务之急是要把咖啡机和它的状态处理函数建立关联。

有一种思路是将每一个状态所对应的的行为抽象成类,然后通过传递 this 的方式来关联状态和状态主体。这种思路一般还需要实现抽象工厂,比较麻烦。而实际业务中,更常用的解决方案是:把状态-行为映射对象作为主体类对应实例的属性添加进去就 ok:

class CoffeeMaker {
constructor() {
this.state = "init";
// 初始化牛奶的存储量
this.leftMilk = "500ml";
}
stateToProcessor = {
that: this,
american() {
// 尝试在行为函数里拿到咖啡机实例的信息并输出
console.log("咖啡机现在的牛奶存储量是:", this.that.leftMilk);
console.log("我只吐黑咖啡");
},
latte() {
this.american();
console.log("加点奶");
},
vanillaLatte() {
this.latte();
console.log("再加香草糖浆");
},
mocha() {
this.latte();
console.log("再加巧克力");
},
};

// 关注咖啡机状态切换函数
changeState(state) {
this.state = state;
if (!this.stateToProcessor[state]) {
return;
}
this.stateToProcessor[state]();
}
}

const mk = new CoffeeMaker();
mk.changeState("latte");

如此一来就可以在 stateToProcessor 轻松拿到咖啡机的实例对象,进而感知咖啡机这个主体了。

三、总结

状态模式 (State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

回过头看,状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。

与策略模式的区别在于,定义里强调了“类”的概念。大多数场景下,行为的划分可以像上面一样,控制在“函数”这个粒度。