Appearance
行为型-状态模式——自助咖啡机背后的力量
状态模式
状态模式和策略模式宛如一对孪生兄弟——它们长得很像、解决的问题也可以说没啥本质上的差别。虽然现在的你可能和本书的主人公李雷一样,对状态模式怀揣着一种“我没见过你所以我觉得你一定很牛x”的敬畏之心。不过没关系,随着我们本节学习过程的展开,你会慢慢体会到状态模式带来的快乐~
咖啡奇缘一杯咖啡带来的思考
话说最近李雷公司楼下的咖啡机在搞年终促销:平时99820块一杯的香草拿铁,现在不要998只要9块9,价格堪称史低。虽然李雷本身没有喝咖啡的习惯,但是本着“有便宜不占就是吃大亏”的原则,他决定开始喝咖啡——并且要一直喝到这9.9的特价拿铁卖空为止。
每天一杯咖啡,让李雷体会到了做一个精致都市丽人996之余的快乐:
这一天,李雷像往常一样来到了咖啡机面前,大方地用支付宝刷了一个9块9,然后就进入了快乐的等咖啡环节。
这时,他的对口产品经理韩梅梅也过来买咖啡了。韩梅梅看到李雷也喜欢喝香草拿铁,觉得他是一个有品位的男人,便啧啧称赞:“哇,李雷,这是你的香草拿铁啊!”。
李雷莞尔一笑,尽力克制住自己内心对咖啡的渴望,缓缓地展示出了他的绅士风度:“不,这是你的优乐美香草拿铁。我可以等下一杯。“
韩梅梅喜上眉梢,立刻把刚打好的咖啡牢牢地攥在了自己手里。为了表达对李雷的感谢,她决定陪李雷打完下一杯咖啡。
咖啡机轰轰作响,进入了第二杯咖啡的制作步骤。
积极工作的咖啡机引发了韩梅梅的思考,她感慨道:“咖啡机其实也是一个产品。它在不同的选择下有着不同的任务:当我们选择香草拿铁时,它进入香草拿铁的制作工序;当我们选择美式时,它进入美式的制作工序。眼前一台小小的机器,可以根据用户的口味产出四种咖啡,想想也好厉害啊!李雷,听说你们程序员都是万能的,你能把这个过程用程序实现一下吗?”
李雷扶了扶墙、擦了擦额头上的汗,确认自己记住了韩梅梅刚刚语言中描述的需求点,大声吼出了下面这句话:
“我们公司又tmd不做咖啡机!我这就给您整一个!”
说罢,李雷便忘记了眼前打好的第二杯咖啡,直接飞回了工位。
(此处让我们恭喜韩梅梅智取两杯香草拿铁)
一台咖啡机的诞生
作为一个具备强大抽象思维能力的程序员,李雷没有辜负自己这么多年来学过的现代前端框架。他敏锐地感知到,韩梅梅所说的这些不同的”选择“间的切换,本质就是状态的切换。在这个能做四种咖啡的咖啡机体内,蕴含着四种状态:
- 美式咖啡态(american):只吐黑咖啡
- 普通拿铁态(latte):黑咖啡加点奶
- 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力
嘿嘿,这么一梳理,李雷的思路一下子清晰了起来。作为死性不改的 if-else 侠,他再次三下五除二写出了一套功能完备的代码:
javascript
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('黑咖啡加点奶再加点巧克力');
}
}
}
测试一下,完美无缺:
javascript
const mk = new CoffeeMaker();
mk.changeState('latte'); // 输出 '给黑咖啡加点奶'
不,我李雷必不可能再做 if-else 侠
望着眼前的代码,他陷入了沉思。他回忆起了那年狂写 if-else 后差点被测试同学毒打的惨痛经历(这段经历很沉重,希望正在读这篇文章的你也没忘,要是你忘了,赶紧跳转回上一个小节复习一下)。
鉴于 if-else 使不得,李雷赶紧翻出了他在策略模式中学到的“单一职责”和“开放封闭”原则,比猫画虎地改造起了自己的咖啡机:
改造咖啡机的状态切换机制
职责分离
首先,映入李雷眼帘最大的问题,就是咖啡制作过程不可复用:
javascript
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('黑咖啡加点奶再加点巧克力');
}
}
李雷发现,这个 changeState 函数,它好好管好自己的事(状态切换)不行吗?怎么连做咖啡的过程也写在这里面?这不合理。
别的不说,就说咱李雷和韩梅梅都欲罢不能的香草拿铁吧:它是啥高深莫测的新品种么?它不是,它就是拿铁加点糖浆。那我至于把做拿铁的逻辑再在香草拿铁里写一遍么——完全不需要!直接调用拿铁制作工序对应的函数,然后末尾补个加糖浆的动作就行了——可惜,我们现在所有的制作工序都没有提出来函数化,而是以一种极不优雅的姿势挤在了 changeState 里面,谁也别想复用谁。太费劲了,咱们赶紧给它搞一搞职责分离:
javascript
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');
输出结果符合预期:
我只吐黑咖啡
加点奶
开放封闭
复用的问题解决了,if-else 却仍然活得好好的。
现在咱们假如要增加”气泡美式“这个咖啡品种,就不得不去修改 changeState 的函数逻辑,这违反了开放封闭的原则。
同时,一个函数里收敛这么多判断,也着实不够体面。咱们现在要像策略模式一样,想办法把咖啡机状态和咖啡制作工序之间的映射关系(也就是咱们上节谈到的分发过程)用一个更优雅地方式做掉。如果你策略模式掌握得足够好,你会第一时间反映出对象映射的方案:
javascript
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 里的工序函数,感知不到咖啡机的内部状况。
策略与状态的辨析
怎么理解这个问题?大家知道,策略模式是对算法的封装。算法和状态对应的行为函数虽然本质上都是行为,但是算法的独立性可高多了。
比如说我一个询价算法,我只需要读取一个数字,我就能啪啪三下五除二给你吐出另一个数字作为返回结果——它和计算主体之间可以是分离的,我们只要关注计算逻辑本身就可以了。
但状态可不一样了。拿咱们咖啡机来说,为了好懂,咱写代码的时候把真正咖啡的制作工序用 console 来表示了。但大家都知道,做咖啡要考虑的东西可太多了。 比如咱们做拿铁,拿铁里的牛奶从哪来,是不是从咖啡机的某个储物空间里去取?再比如我们行为函数是不是应该时刻感知咖啡机每种原材料的用量、进而判断自己的工序还能不能如期执行下去?这就决定了行为函数必须能很方便地拿到咖啡机这个主体的各种信息——它必须得对主体有感知才行。
策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。
但策略模式中的行为函数是”潇洒“的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。
进一步改造
按照我们这一通描述,当务之急是要把咖啡机和它的状态处理函数建立关联。
如果你读过一些早期的设计模式教学资料,有一种思路是将每一个状态所对应的的一些行为抽象成类,然后通过传递 this 的方式来关联状态和状态主体。
这种思路也可以,不过它一般还需要你实现抽象工厂,比较麻烦。实际业务中这种做法极为少见。我这里要给大家介绍的是一种更方便也更常用的解决方案——非常简单,把状态-行为映射对象作为主体类对应实例的一个属性添加进去就行了:
javascript
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');
输出结果为:
咖啡机现在的牛奶存储量是: 500ml
我只吐黑咖啡
加点奶
如此一来,我们就可以在 stateToProcessor 轻松拿到咖啡机的实例对象,进而感知咖啡机这个主体了。
状态模式复盘
和策略模式一样,咱们仍然是敲完代码之后,一起来复盘一下状态模式的定义:
状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
这个定义比较粗糙,可能你读完仍然 get 不到它想让你干啥。这时候,我们就应该把目光转移到它解决的问题上来:
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
仔细回忆一下我们这节做的事情,也确实就是这么回事儿。
唯一的区别在于,定义里强调了”类“的概念。但我们的示例中,包括大家今后的实践中,一个对象的状态如果复杂到了你不得不给它的每 N 种状态划分为一类、一口气划分很多类这种程度,我更倾向于你去反思一个这个对象是不是做太多事情了。事实上,在大多数场景下,我们的行为划分,都是可以像本节一样,控制在”函数“这个粒度的。
尾声
从此李雷和韩梅梅每天都一起喝一杯香草拿铁,过上了幸福的生活^_^。
可能很多同学会觉得这一节李雷和韩梅梅的画风有点清奇(事实也确实如此)。
这也算是作者有意为之——作为本书最后一个更新上来的小节,它完成于圣诞前夕,也算是作为一个节日彩蛋送给大家。希望大家在学到实实在在的技术之余,也能感受到阅读的快乐(沾沾节庆的喜气,哈哈~)。
不过更重要的原因,倒不是节日不节日的,而是因为前几天我在我答不完的读者提问中,看到了这么一个清新脱俗的问题:
真是非常有趣,无意中甩出的一个梗,竟然会有人这么在意(他加了我以后就给我说了这么一件事)。干脆一不做二不休,在状态模式给他俩一个美好结局了,哈哈。
这里也希望大家在即将到来的2020年里,学好技术,用好设计模式、升职加薪走上人生巅峰(要是能像李雷一样凭借优秀的自身实力脱个单,那就更好了哈~~)。
(阅读过程中有任何想法或疑问,或者单纯希望和笔者交个朋友啥的,欢迎大家添加我的微信xyalinode与我交流)