消灭前端项目中的“魔数”

4,804 阅读9分钟

什么是魔数

代码中出现但没有解释的数字常量或字符串。如果在某个程序中你使用了魔数,那么在几个月或几年后你将很可能不知道它的含义是什么。

magic number,魔数,也称魔法值、魔法字符串、魔术数字等

前端中,比较常见的场景,在与后台定义接口时往往会使用一些约定好的数字或字符串代表一些业务状态;例如:一些系统中可能会使用01来指代系统中的性别,这些值往往与业务逻辑强相关,且反复出现;

if (gender === '0') {
	// dosomething
}

if (gender === '1') {
    // dosomething
}

如果采用硬编码的方式写入到代码里,对于代码作者以外的程序员,很难理解这个值的作用和意义;如果使用注释对这些逻辑进行标注,反复出现则需要每次都进行标注。而且使用字面量时编辑器也没法给出联想和提示,值一旦有拼写错误,这种bug会很难调试。

不要使用魔数

魔数在编程中往往代表着不好的编码习惯,缺点也很明显:

  1. 数值的意义难以了解。

  2. 数值需要变动时,可能要改不只一个地方。

使用常量

对于上面性别的这个案例,可以通过定义常量的方式消除魔数。

const female = '0' // 女性
const male = '1' // 男性

if (gender === female) {
	// dosomething
}

if (gender === male) {
    // dosomething
}

见名知意,一个合理的变量名本身就是很好的注释;

现代编辑器也可以在书写代码的时候进行联想和提示,避免出错,使用时如果变量名拼错程序会直接抛出异常;即使需要注释,也只需要在定义常量是注释一次即可,编辑器都提供了跳转变量定义位置的功能。

举个复杂点的例子

前面的性别的案例只是一个比较简单的场景,在实际的项目中可能更复杂,需要使用到魔数的场景也更多;下面通过一个订单管理功能,使用Vue来实现,举个复杂点的例子;

案例

一个订单在在不同的时间会有多种状态,与定义接口时,使用了数字(或者字符串)对不同的状态进行了约定。根据订单状态展示不同的操作按钮。

状态 接口值 操作
待付款 0 去付款
待发货 1 催发货
待收货 2 收货
待评价 3 去评价

实现一个根据状态对订单列表进行筛选查询的功能(进入页面默认筛选待评价的订单)。

<template>
  <div class="container">
    <div>
      <ElSelect v-model="form.status" style="margin-right: 20px">
        <ElOption label="待付款" value="0"/>
        <ElOption label="待发货" value="1"/>
        <ElOption label="待收货" value="2"/>
        <ElOption label="待评价" value="3"/>
      </ElSelect>
      <ElButton type="primary" @click="handleSearch">
        查询
      </ElButton>
    </div>
    <hr>

    <ElTable :data="orderList" type="index">
      <ElTableColumn prop="id" label="订单号"/>
      <ElTableColumn prop="address" label="收货地址"/>
      <ElTableColumn prop="phone" label="手机号"/>
      <ElTableColumn label="订单状态">
        <template v-slot="{row}">
          <span v-if="row.status === '0'">待付款</span>
          <span v-if="row.status === '1'">待发货</span>
          <span v-if="row.status === '2'">待收货</span>
          <span v-if="row.status === '3'">待评价</span>
        </template>
      </ElTableColumn>

      <ElTableColumn label="操作">
        <template v-slot="{row}">
          <ElButton v-if="row.status === '0'" type="text">
            去付款
          </ElButton>

          <ElButton v-if="row.status === '1'" type="text">
            催发货
          </ElButton>

          <ElButton v-if="row.status === '2'" type="text">
            收货
          </ElButton>

          <ElButton v-if="row.status === '3'" type="text">
            去评价
          </ElButton>
        </template>
      </ElTableColumn>
    </ElTable>
  </div>
</template>

<script>

  export default {
    name: 'OrderList',

    data() {
      return {
        form: {
          status: '3'
        },

        orderList: []
      }
    },

    methods: {
      handleSearch() {
        // 模拟异步查询
        setTimeout(() => {
          this.orderList = [
            {id: 202004000, address: '北京市', phone: 1311112223, status: '0'},
            {id: 202004001, address: '上海市', phone: 1311112223, status: '1'},
            {id: 202004002, address: '天津市', phone: 1311112224, status: '2'},
            {id: 202004003, address: '重庆市', phone: 1311112225, status: '3'}
          ].filter(item => item.status === this.form.status)
        }, 500)
      }
    },

    created() {
      this.handleSearch()
    }
  }
</script>

<style>
  .container {
    margin: 70px;
  }
</style>

上面的实现中在涉及到订单状态的代码逻辑中,都采用了硬编码的方式,魔数由此产生;

一些逻辑的实现虽然可以通过其他更好的方式实现,比如列表中订单状态的回显,可以提取到js逻辑中,在Vue中可以放到过滤器(filter)或者函数中进行逻辑判断(可以选择使用switch case);

<template>
	....

	<ElTableColumn label="订单状态">
     	<template v-slot="{row}">
            {{ row.status | code2Text }}
        </template>
    </ElTableColumn>

	...
</template>
...
filters: {
    // 状态码转换为文字
    code2Text(code) {
        switch (code) {
            case '0':
                return '待付款'
            case '1':
                return '待发货'
            case '2':
                return '待收货'
            case '3':
                return '待收货'
        }
    }
}
...

不管是在模板中还是js代码中,不管使用if判断还是switch,本质上没有太大的区别;

目前实现中存在的问题

针对目前实现的代码,我们先总结下存在的问题,再一步步进行解决;

  1. 反复出现的订单状态魔数:渲染搜索框的options、列表中的数据状态中、操作按钮的显示逻辑中、包括默认的搜索条件status: '3'都有使用到;有些情况下可能配合文案还比较好阅读,但是如果项目有国际化或在没有文案的逻辑中,可读性极差,代码作者再写完几天后可能也记不清每个code有什么意义了。如果每个地方都进行注释的话又很啰嗦;
  2. 如果某天后台约定修改了某个状态对应的编码;比如:待收货的对应编码改为'5',需要改动的地方就会很多;

开始优化,消灭魔数,定义常量

  1. 定义常量
const status = {
    pendingPayment: '0', // 待付款
    waiting4Delivery: '1', // 待发货
    waiting4Receipt: '2', // 待收货
    waiting4Evaluation: '3' // 待评价
};

export default {
    name: 'OrderList',

    data() {
      return {
       // ...
       status // 在data中定义一下,这样模板中也能使用了
      }
    }
}
  1. 接下把代码中所有用到订单状态的地方都换成常量就好了,以操作按钮部分为例:
<ElTableColumn label="操作">
	<template v-slot="{row}">
		<ElButton v-if="row.status === status.pendingPayment" type="text">
            去付款
		</ElButton>

		<ElButton v-if="row.status === status.waiting4Delivery" type="text">
            催发货
		</ElButton>

		<ElButton v-if="row.status === status.waiting4Receipt" type="text">
            收货
		</ElButton>

		<ElButton v-if="row.status === status.waiting4Evaluation" type="text">
            去评价
		</ElButton>
	</template>
</ElTableColumn>

Ps:操作按钮这部分代码还可以使用Vue中render函数的方式进一步优化(cn.vuejs.org/v2/guide/re…

初始的筛选条件(默认展示待评价订单):

export default {
    name: 'OrderList',

    data() {
      return {
       // ...
        status, // 在data中定义一下,这样模板中也能使用了
		form: {
          status: status.waiting4Evaluation
        }
      }
    }
}

改为使用常量后上面提到的问题就得到了解决:变量名更能展示出代码的实际意义,即使了解命名的含义,通过跳转到常量的定义,即可查看每个常量的注释;如果需要修改某个状态对应的编码,修改常量常量就好了,一次修改就覆盖到了所有;

还能不能再进一步?

在这个案例中,渲染搜索框的options、列表中的数据状态中都存在这状态码转为中文的需要;而且不管是放到模板中的v-if还是放到js逻辑中做判断,本质上也都一样;更进一步,我们可以再定义一个状态编码为key,文字为值的对象(Ps:js中实现策略模式也是这个思路,只不过值的位置放的是一个个的策略),需要转换的时候通过状态编码取对应的文字就行了;

话不多说,先上代码:

<template>
  <div class="container">
    <div>
      <ElSelect v-model="form.status" style="margin-right: 20px">
        <ElOption
            v-for="(label, code) in status2Text"
            :key="code"
            :value="code"
            :label="label"
        />
      </ElSelect>
      <ElButton type="primary" @click="handleSearch">
        查询
      </ElButton>
    </div>
    <hr>

    <ElTable :data="orderList" type="index">
      <ElTableColumn prop="id" label="订单号"/>
      <ElTableColumn prop="address" label="收货地址"/>
      <ElTableColumn prop="phone" label="手机号"/>

      <template>
        <ElTableColumn label="订单状态">
          <template v-slot="{row}">
            {{ status2Text[row.status] }}
          </template>
        </ElTableColumn>
      </template>

      <ElTableColumn label="操作">
        <template v-slot="{row}">
          <ElButton v-if="row.status === status.pendingPayment" type="text">
            去付款
          </ElButton>

          <ElButton v-if="row.status === status.waiting4Delivery" type="text">
            催发货
          </ElButton>

          <ElButton v-if="row.status === status.waiting4Receipt" type="text">
            收货
          </ElButton>

          <ElButton v-if="row.status === status.waiting4Evaluation" type="text">
            去评价
          </ElButton>
        </template>
      </ElTableColumn>
    </ElTable>
  </div>
</template>

<script>
  // 订单状态编码
  const status = {
    pendingPayment: '0', // 待付款
    waiting4Delivery: '1', // 待发货
    waiting4Receipt: '2', // 待收货
    waiting4Evaluation: '3' // 待评价
  };

  // 订单状态编码映射文字
  const status2Text = {
    // 这里使用了ES6的计算属性命名 (computed Property Name)
    [status.pendingPayment]: '待付款',
    [status.waiting4Delivery]: '待发货',
    [status.waiting4Receipt]: '待收货',
    [status.waiting4Evaluation]: '待评价'
  }

  export default {
    name: 'OrderList',

    data() {
      return {
        status,
        status2Text,
        form: {
          status: status.waiting4Evaluation
        },

        orderList: []
      }
    },

    methods: {
      handleSearch() {
        // 模拟异步查询
        setTimeout(() => {
          this.orderList = [
            {id: 202004000, address: '北京市', phone: 1311112223, status: '0'},
            {id: 202004001, address: '上海市', phone: 1311112223, status: '1'},
            {id: 202004002, address: '天津市', phone: 1311112222, status: '2'},
            {id: 202004003, address: '重庆市', phone: 1311112222, status: '3'}
          ].filter(item => item.status === this.form.status)
        }, 500)
      }
    },

    created() {
      this.handleSearch()
    }
  }
</script>

<style>
  .container {
    margin: 70px;
  }
</style>

上面通过ES6中计算属性命名的方式定义了一个映射关系的对象;

列表中的数据状态直接使用{{ status2Text[row.status] }}这种方式取出对应的文字,优化掉了if判断的分支语句(通过key取对应value的实际上比分支语句的执行效率要高);

搜索框的options通过直接在模板代码中循环这个映射关系的对象直接就可以渲染出各个选项;在js代码中使用for-in循环也是相同的效果。

其他的方式

Vuex文档中也对消灭Mutation名产生的魔数提供了方案

vuex.vuejs.org/zh/guide/mu…](vuex.vuejs.org/zh/guide/mu…)

除了和后台在接口中约定使用数字或字符串,前端业务中也可以通过定义Symbol类型的常量来解决;使用TS的话,可以通过定义枚举来消灭魔数;

结语

通过以上一系列的操作,看起来相比硬编码的方式,增加了工作量;实际上,用常量代替魔数后,提高了项目的可读性以及可维护性,收益更高;项目维护时间越长越有体现;坚持养成好的编码习惯;