阅读 2719

重构:从kfc点单发现状态模式

什么是状态模式

对象的行为依赖于它的状态(属性),允许对象在内部状态发生改变并且可以根据它的状态改变而改变它的相关行为。

状态模式和策略模式很相似,也是将类的"状态"封装了起来,在执行动作时进行自动的转换,从而实现,类在不同状态下的同一动作显示出不同结果。它与策略模式的区别在于,这种转换是"自动","无意识"的,策略模式是在求解同一个问题的多种解法,这些不同解法之间毫无关联;状态模式则不同,状态模式要求各个状态之间有所关联,以便实现状态转移。

正文

国庆回来决定重构我司的重要项目,因其复杂的逻辑一直找不到好的角度去梳理代码,某个夜黑风高的晚上,我来到了kfc,当我看到了菜单中琳琅满目的内容陷入了沉思.....

image

这里是形式代码,不要深究

此时一份点单的内心独白

if (我的钱>50) {
    if (饿) {
      我好富有啊随便点,点2个我最喜欢的原味鸡,再来个套餐
    } else {
      自己喜欢的单品都来一样好了
    }
} else if (30<我的钱<50) {
    if (饿) {
      低于50的套餐
    } else {
      自己喜欢的单品累加不能超过50
    }
} else if (15<我的钱<30) {
    if (饿){
        低于30的套餐
    }else{
        自己喜欢的单品累加不能超过30
    }
    ......
} else if (5<我的钱<15) {
    只能根据价格来不能追求喜好了..
} else {
    做洗碗工 || 饿肚子
}

复制代码

万一钱的金额或者食物价格(状态)有所变化

1.需要修改购物条件:代码的判断条件可能需要重写

2.需要继续深入购物条件:往深层次加入条件变得复杂冗长

image

状态模式登场

第一步:抽离状态

状态模式首先需要总结出会影响行为的要素

我们把当前条件列出来我们用一个状态对象来包装它:

{
    hungry: true/false,
    money:  40,
    single:   [ 原味鸡, 鸡米花, 土豆泥, 薯条,汉堡...],
    setMeal:[ 高调奢华套餐(50¥)、性价比套餐(40¥)、不管饱套餐(25¥) ],
}
复制代码

Tips: 实际业务中可以用各种思维导图总结出当前需求有哪些状态


第二步:规划配置表

将状态与行为之间的关系封装在一个表,通过状态的变化驱动行为。

根据当前状态归纳出购买选择的配置表:

抽象出俩个单位即手上的钱和自身饥饿情况,同时他们都有自己的触发条件,也就是不同的状态。

 {
    money: { 
    
        money> 50: [...single, ...setMeal], // 喜爱的单品和套餐都能选择
        
        30 < money < 50: [...single, setMeal[0], setMeal[1]], // 价格允许的套餐和所有单品
        
        15 < money < 30: [...single, setMeal[2]], // 价格允许的套餐和所有单品
        
        5 < money < 15: [single[1], single[2], single[3]],// 价格允许的单品
        
        money < 5: 做洗碗工 || 饿肚子,
    },
    hungry: true / false,  //饿就选套餐/不饿选择单品
}

最后的购买行为:当前金钱数量决策出对应价位的菜品,再根据饥饿情况选出套餐还是单品组合

复制代码

我们可以把配置表理解成一个状态机,根据状态变化可以影响我的购买行为,如果状态(钱)有了变化(优惠券,地上捡到50元...)也能做出可预知的购买动作(毕竟已经根据钱的多少分好了行为)。

配置表可以在代码的容灾性或是后期多人维护的时候可以更加专注到状态的变化上。


第三步(拓展):拆分更小的粒度

加需求:厨房说原味鸡售罄只有鸡米花了,需要贴出告示,需要将套替中的原味鸡替换成鸡米花。

关于拆分配置的颗粒度,仔细思考外界因素对食品的影响导致购买行为出现变化,是否可以对行为再配置成可配置管理的内容。为什么kfc都是一种菜品一个容器?

  
    single:   [ 原味鸡, 鸡米花, 土豆泥, 薯条,汉堡...],
    setMeal:[ 高调奢华套餐(50¥)、性价比套餐(40¥)、不一定管饱套餐(25¥) ],
    
    改造一下整体结构,每一个单品都有它的状态,方便我们根据状态组合(万一需求又改了呢?)
    
    single:   [
       // 想想为什么我要多加价格,万一之后售价(需求)变动了呢?
        {
          name:原味鸡,
          price:12,
          stock:false, // 没货啦
        }, 
        { name:鸡米花,
          price:10,
          stock:true,
        },
        ....
        ....
        ....
    ],
    setMeal:[ 
    // 把菜单和单品之间的联系挂起来
    {
        name:高调奢华套餐:
        price:50,  
        single[0], //原味鸡
        single[1], //鸡米花
        single[2], //土豆泥
        single[3], //薯条
        single[4]  //汉堡
    },{
        name:性价比套餐,
        price:40,
        .... // 单品组合
    }{
        name:不管饱套餐
        price:25
        .... // 单品组合
    } ],
    
    
复制代码

完成需求: 这样子可以直接将原味鸡换成鸡米花

now: single[0] = single[1]
复制代码

案例

我需要重构的项目有很多不同的单位,他们所展示的表单内容有部分是相同的,有部分是不同的,这个时候我可以通过状态模式去配置。

原代码:

<el-form-item label="应用名称">
    <el-input v-model="form.name"/>
</el-form-item>
<el-form-item label="发部分支">
    <el-input v-model="form.branch"/>
</el-form-item>
<el-form-item v-if="env" label="发布环境">
    <el-input v-model="form.env"/>
</el-form-item>
<el-form-item v-if="appType==='node'|| appType==='static'" label="npm install">
    <el-input v-model="form.npm"/>
</el-form-item>
<el-form-item v-if="appType==='android'" label="打包加固">
    <el-input v-model="form.package"/>
</el-form-item>
<el-form-item v-if="appType!=='static'&& env!='dev'" label="发布暂停">
    <el-input v-model="form.isStop"/>
</el-form-item>

..... // 省略各种条件
复制代码

改进代码:

1.抽取状态: 

    环境:env:['prod', 'dev', 'test'], 应用:appType:['node','android','static']

2.配置状态机:

    // configState.js
    
    const configState = {
        env:{
            env:['env','dev','test'],
            appType:['node','android','static'],
        },
        npm:{
            env:['env'],
            appType:['node','static'],
        },
        package:{
            env:['env','dev','test'],
            appType:['android'],
        },
         isStop:{
            env:['env','test'],
            appType:['android','node'],
        },
    }
    
    
3.抽象行为:
    
    import { configState } from "./configState";
    
    // 抽象当前展示行为
    
    const state = fn(env, appType) {
        
        /*
        * 此处代码省略!!!
        * 遍历configState,若env和appType数组中均含有则为true
        */
        
        return {
            env:true,
            npm:true,
            package:false,
            isStop:true,
        },
    }
    
    
    <el-form-item label="应用名称">
        <el-input v-model="form.name"/>
    </el-form-item>
    <el-form-item label="发部分支">
        <el-input v-model="form.branch"/>
    </el-form-item>
    <el-form-item v-if="state.env" label="发布环境">
        <el-input v-model="form.env"/>
    </el-form-item>
    <el-form-item v-if="state.npm" label="npm install">
        <el-input v-model="form.npm"/>
    </el-form-item>
    <el-form-item v-if="state.package" label="打包加固">
        <el-input v-model="form.package"/>
    </el-form-item>
    <el-form-item v-if="state.isStop" label="发布暂停">
        <el-input v-model="form.isStop"/>
    </el-form-item>

复制代码

总结

场景

  1. 代码中包含大量与对象状态有关的条件语句如if...else, switch..case等。
  2. 逻辑之间混乱相隔较远需要抽象在一个地方统一管理
  3. 业务条件变动频繁(程序员不背锅)

优点:

  1. 场景变化或是增多,状态切换的逻辑被分布在状态类中通过增加新的状态类,很容易增加新的状态和转换。

  2. 解耦,状态和动作类中的行为可以非常容易地独立变化而互不影响。

  3. 内聚,状态机的存在可以让我们更聚焦在状态的控制上。

缺点:

  1. 有多少的状态就得有多少的单例方法,因此会创建大量的相关行为。

  2. 代码结构变得复杂,需要提供专门的状态机文件。

实现

  1. 状态拥有者的实体模型。

  2. 状态接口(也可使用抽象类),定义业务方法。

  3. 状态的各个具体实现类,分别实现业务方法。


关键在于抽象状态与行为的联动,总结可变与可变的代码。

所有设计模式都有它在生活中的案例,life is design

最后因为我太墨迹被收银员小姐姐赶了出去什么都没有吃上......

关注下面的标签,发现更多相似文章
评论