7 行为型-状态模式

7.1 一台咖啡机的诞生

作为一个具备强大抽象思维能力的程序员,李雷没有辜负自己这么多年来学过的现代前端框架。他敏锐地感知到,韩梅梅所说的这些不同的”选择“间的切换,本质就是状态的切换。在这个能做四种咖啡的咖啡机体内,蕴含着四种状态:

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

嘿嘿,这么一梳理,李雷的思路一下子清晰了起来。作为死性不改的 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'); // 输出 '给黑咖啡加点奶'

鉴于 if-else 使不得,李雷赶紧翻出了他在策略模式中学到的“单一职责”和“开放封闭”原则,比猫画虎地改造起了自己的咖啡机:

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

1. 职责分离

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

    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 函数,它好好管好自己的事(状态切换)不行吗?怎么连做咖啡的过程也写在这里面?这不合理。

    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');

输出结果符合预期:

    我只吐黑咖啡
    加点奶

7.3 开放封闭

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

现在咱们假如要增加”气泡美式“这个咖啡品种,就不得不去修改 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 里的工序函数,感知不到咖啡机的内部状况。

7.4 进一步改造

按照我们这一通描述,当务之急是要把咖啡机和它的状态处理函数建立关联。

如果你读过一些早期的设计模式教学资料,有一种思路是将每一个状态所对应的的一些行为抽象成类,然后通过传递 this 的方式来关联状态和状态主体。

这种思路也可以,不过它一般还需要你实现抽象工厂,比较麻烦。实际业务中这种做法极为少见。我这里要给大家介绍的是一种更方便也更常用的解决方案——非常简单,把状态- 行为映射对象作为主体类对应实例的一个属性添加进去就行了:

    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 轻松拿到咖啡机的实例对象,进而感知咖啡机这个主体了。

7.5 状态模式复盘

和策略模式一样,咱们仍然是敲完代码之后,一起来复盘一下状态模式的定义:

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

这个定义比较粗糙,可能你读完仍然 get 不到它想让你干啥。这时候,我们就应该把目光转移到它解决的问题上来:

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

仔细回忆一下我们这节做的事情,也确实就是这么回事儿。

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

Last Updated:
Contributors: leeguooooo