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

    1. class CoffeeMaker {
    2. constructor() {
    3. /**
    4. 这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
    5. **/
    6. // 初始化状态,没有切换任何咖啡模式
    7. this.state = 'init';
    8. }
    9. // 关注咖啡机状态切换函数
    10. changeState(state) {
    11. // 记录当前状态
    12. this.state = state;
    13. if(state === 'american') {
    14. // 这里用 console 代指咖啡制作流程的业务逻辑
    15. console.log('我只吐黑咖啡');
    16. } else if(state === 'latte') {
    17. console.log(`给黑咖啡加点奶`);
    18. } else if(state === 'vanillaLatte') {
    19. console.log('黑咖啡加点奶再加香草糖浆');
    20. } else if(state === 'mocha') {
    21. console.log('黑咖啡加点奶再加点巧克力');
    22. }
    23. }
    24. }

    “单一职责”和“开放封闭”原则

    改造咖啡机的状态切换机制

    职责分离

    首先,映入眼帘最大的问题,就是咖啡制作过程不可复用:

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

    这个 changeState 函数,它好好管好自己的事(状态切换)不行吗?怎么连做咖啡的过程也写在这里面?这不合理。
    别的不说,就说香草拿铁吧:它是啥高深莫测的新品种么?它不是,它就是拿铁加点糖浆。那我至于把做拿铁的逻辑再在香草拿铁里写一遍么——完全不需要!直接调用拿铁制作工序对应的函数,然后末尾补个加糖浆的动作就行了——可惜,我们现在所有的制作工序都没有提出来函数化,而是以一种极不优雅的姿势挤在了 changeState 里面,谁也别想复用谁。太费劲了,咱们赶紧给它搞一搞职责分离: ```javascript class CoffeeMaker { constructor() { / 这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑 / // 初始化状态,没有切换任何咖啡模式 this.state = ‘init’; } changeState(state) { // 记录当前状态 this.state = state; if(state === ‘american’) {

    1. // 这里用 console 代指咖啡制作流程的业务逻辑
    2. this.americanProcess();

    } else if(state === ‘latte’) {

    1. this.latteProcress();

    } else if(state === ‘vanillaLatte’) {

    1. this.vanillaLatteProcress();

    } else if(state === ‘mocha’) {

    1. 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’);

  1. 输出结果符合预期:<br />我只吐黑咖啡 <br />加点奶
  2. <a name="QhSrE"></a>
  3. ### 开放封闭
  4. <br />现在咱们假如要增加”气泡美式“这个咖啡品种,就不得不去修改 changeState 的函数逻辑,这违反了开放封闭的原则。<br />同时,一个函数里收敛这么多判断,也着实不够体面。咱们现在要像策略模式一样,想办法把咖啡机状态和咖啡制作工序之间的映射关系(也就是咱们上节谈到的分发过程)用一个更优雅地方式做掉。如果你策略模式掌握得足够好,你会第一时间反映出对象映射的方案:
  5. ```javascript
  6. const stateToProcessor = {
  7. american() {
  8. console.log('我只吐黑咖啡');
  9. },
  10. latte() {
  11. this.american();
  12. console.log('加点奶');
  13. },
  14. vanillaLatte() {
  15. this.latte();
  16. console.log('再加香草糖浆');
  17. },
  18. mocha() {
  19. this.latte();
  20. console.log('再加巧克力');
  21. }
  22. }
  23. class CoffeeMaker {
  24. constructor() {
  25. /**
  26. 这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
  27. **/
  28. // 初始化状态,没有切换任何咖啡模式
  29. this.state = 'init';
  30. }
  31. // 关注咖啡机状态切换函数
  32. changeState(state) {
  33. // 记录当前状态
  34. this.state = state;
  35. // 若状态不存在,则返回
  36. if(!stateToProcessor[state]) {
  37. return ;
  38. }
  39. stateToProcessor[state]();
  40. }
  41. }
  42. const mk = new CoffeeMaker();
  43. mk.changeState('latte');

输出结果符合预期:
我只吐黑咖啡 加点奶
当我们这么做时,其实已经实现了一个 js 版本的状态模式。
但这里有一点大家需要引起注意:这种方法仅仅是看上去完美无缺,其中却暗含一个非常重要的隐患——stateToProcessor 里的工序函数,感知不到咖啡机的内部状况。

策略与状态的辨析

怎么理解这个问题?大家知道,策略模式是对算法的封装。算法和状态对应的行为函数虽然本质上都是行为,但是算法的独立性可高多了。
比如说我一个询价算法,我只需要读取一个数字,我就能啪啪三下五除二给你吐出另一个数字作为返回结果——它和计算主体之间可以是分离的,我们只要关注计算逻辑本身就可以了。
但状态可不一样了。拿咱们咖啡机来说,为了好懂,咱写代码的时候把真正咖啡的制作工序用 console 来表示了。但大家都知道,做咖啡要考虑的东西可太多了。 比如咱们做拿铁,拿铁里的牛奶从哪来,是不是从咖啡机的某个储物空间里去取?再比如我们行为函数是不是应该时刻感知咖啡机每种原材料的用量、进而判断自己的工序还能不能如期执行下去?这就决定了行为函数必须能很方便地拿到咖啡机这个主体的各种信息——它必须得对主体有感知才行。
策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。
但策略模式中的行为函数是”潇洒“的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。

进一步改造

按照我们这一通描述,当务之急是要把咖啡机和它的状态处理函数建立关联。
如果你读过一些早期的设计模式教学资料,有一种思路是将每一个状态所对应的的一些行为抽象成类,然后通过传递 this 的方式来关联状态和状态主体。
这种思路也可以,不过它一般还需要你实现抽象工厂,比较麻烦。实际业务中这种做法极为少见。我这里要给大家介绍的是一种更方便也更常用的解决方案——非常简单,把状态-行为映射对象作为主体类对应实例的一个属性添加进去就行了:

  1. class CoffeeMaker {
  2. constructor() {
  3. /**
  4. 这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑
  5. **/
  6. // 初始化状态,没有切换任何咖啡模式
  7. this.state = 'init';
  8. // 初始化牛奶的存储量
  9. this.leftMilk = '500ml';
  10. }
  11. stateToProcessor = {
  12. that: this,
  13. american() {
  14. // 尝试在行为函数里拿到咖啡机实例的信息并输出
  15. console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
  16. console.log('我只吐黑咖啡');
  17. },
  18. latte() {
  19. this.american()
  20. console.log('加点奶');
  21. },
  22. vanillaLatte() {
  23. this.latte();
  24. console.log('再加香草糖浆');
  25. },
  26. mocha() {
  27. this.latte();
  28. console.log('再加巧克力');
  29. }
  30. }
  31. // 关注咖啡机状态切换函数
  32. changeState(state) {
  33. this.state = state;
  34. if (!this.stateToProcessor[state]) {
  35. return;
  36. }
  37. this.stateToProcessor[state]();
  38. }
  39. }
  40. const mk = new CoffeeMaker();
  41. mk.changeState('latte');

输出结果为:
咖啡机现在的牛奶存储量是: 500ml 我只吐黑咖啡 加点奶
如此一来,我们就可以在 stateToProcessor 轻松拿到咖啡机的实例对象,进而感知咖啡机这个主体了。

状态模式复盘

和策略模式一样,咱们仍然是敲完代码之后,一起来复盘一下状态模式的定义:
状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
这个定义比较粗糙,可能你读完仍然 get 不到它想让你干啥。这时候,我们就应该把目光转移到它解决的问题上来:
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。

唯一的区别在于,定义里强调了”类“的概念。但我们的示例中,包括大家今后的实践中,一个对象的状态如果复杂到了你不得不给它的每 N 种状态划分为一类、一口气划分很多类这种程度,我更倾向于你去反思一个这个对象是不是做太多事情了。事实上,在大多数场景下,我们的行为划分,都是可以像本节一样,控制在”函数“这个粒度的。