不要写死特征码

  • 作者:KK

  • 发表日期:2016.4.6


可能有些人不知道特征码是什么,这个名词我是从书上取下来的,确实应该叫特征码

它就是我们经常会在代码里出现的type这种东西,比如有这样的代码:

user = db.query('SELECT * FROM user where id = 99')[0];
if(user.type == 1){
	alert('学生用户');
}else if(user.type == 2){
	alert('家长用户');
}

这里的type就是一个特征码,它标记了一个用户的类型,如果值是"1"那么就是学生,如果是"2"那么就是家长;其实本文并不特别说明type这个名称,而是指类似这种为了标记一个特征的标记符就是了,比如flag呀,xxx_level什么的那些枚举类型的东西

当有很多地方进行这样的判断的时候,就会很多这样的if判断代码嘛,这样写有两个比较明显的问题,分别是可读性差维护难度高的问题


可读性差

未来有新的团队成员加入,或者自己隔好久后回去维护这些代码时,通常都已经不记得 ==1是什么,==2是什么了,这时候就依赖下面的逻辑代码看看怎么工作的,有时候这些代码的工作特征也不能明显地告诉你是什么意思,那你就呆了

有时候type有十来种,那不更加坑人,大家都知道许多人懒得写文档,所以这个type不一定有对照表

  • 不靠普的解决办法

    • 在代码旁边写备注

      这样的话,要知道通常有很多地方都if(type==1),那行呀如果你每处if都能写下备注,不嫌麻烦的话,但实际上你真的能做到这样吗?我看未必吧!

      至少就算你能做到处处注释,其他成员可不一定也处处注释呀,所以项目里总会有许多地方的type判断代码中是没注释说明这个type值的含义对不对?

    • 在数据表的字段里写注释

      这个方法进步了些,可是也不靠谱,第一,现在type有4个值,于是字段注释就写了1到4的含义,某天因为业务变更增加5,可通常有人忘了改数据库的字段注释

      还有,就是阅读代码时,始终都一下子不记得==1,==2,==15这些是什么含义,于是又要打开数据库文档或表注释什么的对照了是不是?这样看读代码是不是读得好辛苦?

      这里还有一个理解误区的问题,其实特征码并不仅仅是指数据库里的那些type,flag,category,system_id之类的类型标记字段,"特征码"就是代表了某种东西的"特征",所以其实非数据的那些,比如以下代码中"1"和"2"也是特征码

      if(request.contentType == 1){
      	return '<html>...</html>';
      }else if(request.contentType == 2){
      	return '{"data":[{"id":1,"name":"xx"},{"id":2,"name":"yy"},{"id":3,"name":"zz"}]}';
      }
      

      所以"1"和"2"就是请求内容格式的特征码,可是这跟数据库有一毛钱关系吗?然而它就是内容格式的特征哟

    • 把特征码写成字符串

      这个办法倒是好了许多,有时候还真凑效,适当用用还可以,但它还是会遇到问题

      你不可能全部特征码都写字符串,因为有的特征码你要存入数据库,数据存字符串占空间,索引成本又高,你可知道设计数据表时要尽量用最小的类型?

      所以最终还是有好多特征码是数字对不对?


维护难度高

为什么又说它维护难度高呢,其实特征码有时候是要被修改的,有时候数据的type为1是A逻辑做事,但未来改成了6才是A逻辑,1弃用,或者1被用于其它逻辑了

修改需求总会有,你也不是没碰过吧,所以如果你要将type == 1这样的代码改成type == 6的话,是不是要改好多好多地方呢?(我想大多数PHP程序员就想起了模板经常这样改,因为老是按照type输出不同的html,突然说type换了,于是到处打开模板来修改,有的还改漏)

没错就是改漏,判断地方都不知道有多少,修改成本高,有的文件是弃用的,改它又改多了,有的又改漏了,所以说难度就在这里


解决办法

解决办法就是不要在代码里写死这些特征码,而是定义对应的常量,然后代码里与这些特征码比较,比如define('USER_TYPE_STUDENT', 1),再下面这样对比:

if(user.type == USER_TYPE_STUDENT){
	alert('学生用户');
}else if(user.type == USER_TYPE_PARENT){
	alert('家长用户');
}

if(request.contentType == CONTENT_TYPE_HTML){
	return '<html>...</html>';
}else if(request.contentType == CONTENT_TYPE_JSON){
	return '{"data":[{"id":1,"name":"xx"},{"id":2,"name":"yy"},{"id":3,"name":"zz"}]}';
}

这样就有可读性了,直接看常量名就知道它的意思,如果要改值,直接修改常量定义的值就行了


那不就是有很多常量?

是呀,这里先以PHP为例子,一会说JAVA的

你看人家PHP官方一些演示代码会不会这样教你追加写入文件:

file_put_contents('test.txt', 8);

那第2个参数"8"其实就是追加写入的特征码,其实人家的演示代码是这样写的:

file_put_contents('test.txt', FILE_APPEND);

FILE_APPEND是PHP文件扩展的预定义常量,这个常量的值是8,所以你传8和传FILE_APPEND是一模一样的效果,然而演示代码都叫你用FILE_APPEND而不是用8

所以你可以发现,PHP也自带了非常多的常量,你执行print_r(get_defined_constants(true))看看有多少常量,几乎都是特征码呀


再看看安卓的

Toast.makeText(context, "请输入账号和密码", Toast.LENGTH_LONG);

这第3个参数传递了Toast.LENGTH_LONG也就是Toast类的LENGTH_LONG常量,表示Toast提示的显示时间是长的

而不是下面这样写

Toast.makeText(context, "请输入账号和密码", 1);

说白了,你看外面的任项目那些他们的代码都是通过常量来传递特征码,而不是直接在参数里写死一个特征码的


在PHP的面向对象方面,其实我们也应该用类的常量来作为特征码,比如

class User{
	/**
	 * 学生类型
	 */
	const TYPE_STUDENT = 1;
	
	/**
	 * 家长类型
	 */
	const TYPE_PARENT = 2;
}

$user = User::findOne(99); //这里比如从db取出
if($user->type == User::TYPE_STUDENT){
	echo '学生用户';
}else if($user->type == User::TYPE_PARENT){
	echo '家长用户';
}

上面的User类定义了两个特征码常量,而特征码的含义就写在常量上面的备注中(phpdoc备注格式)

这样在逻辑代码上看着常量名又有可读性,如果还不能理解的话一般IDE都能按着Ctrl+单击常量值跳转到相关的类里面看注释,另外还要有代码规范要求写代码的成员必须添加常量时都为常量添加注释,说明这个常量代表了什么含义

  • 其实这也符合面向对象的设计,用户对象有2种性别,那自然就应该是以User::SEX_MALEUser::SEX_FEMALE两个常量来表达,这两个常量都是属于"用户"这个业务对象的

    而常量常量为何叫做常量呢?它就是某个对象上客观存在了,不能再修改的枚举属性(因项目需求调整这个对象的定义不算修改),比如用户的性别有多少种,用户的类型有多少种,登陆的方式有多少种(比如用Login::BY_ACCOUNT表示本站账号登陆,用Login::BY_QQ表示QQ账号登陆等)

    我觉得这些例子举得不是非常好,也许有部分人会难以理解,等我有更好的材料了会回来补充的。理解这个了,你就进一步掌握了面向对象的理念了


探讨性价比问题

唉这么下来确实会好多常量啊,有时候我们用一个控制器接口处理一种业务数据的多种不同的处理请求时(大概因为总体代码少而合成一个吧,个人比较少这么做了),代码会这样:

if($type === 'add'){
	//执行添加操作
}elseif($type === 'update'){
	//执行更新操作
}elseif($type === 'delete'){
	//执行删除操作
}

按照我之前的说法,这个操作类型应该属于控制器的操作类型,所以应该由XXController::TYPE_ADD这样的常量来代表,自然也有XXController::TYPE_UPDATEXXController::TYPE_DELETE

有些人第一时间就会思考这样值得不值得,他总感觉不值得定义常量,为什么呢?我猜大概是想着这个只是控制器用的吧(范围小?),只是一个与前端交操作互数据的特征码,又不是什么业务数据的特征码

首先不能因为范围小而不定义,是否要定义,应该从可读性和可维护性上考虑

  • 先说可读性,如果单用字符串表示,那本身if($type === 'add')已经能够满足,但是如果传递的不是字符串而是数字if($type === 1)那就要看代码实际情况是否具有可读性,你觉得可以接受那就不用常量呗,只是有的地方用有的地方又不用就有一个不统一的问题在这里

  • 再说可维护性,作为一个控制器与前端交互的特征码,这个作用范围仅仅是某几个页面和某一个控制器的关系,所以它的代码书写范围并不广,如果说要修改这个type的话那其实很快可以修改掉并且几乎不会漏(实际上也有少数人改漏或改错,添加变成了更新有木有~),更多的情况是不用改的,因为这个交互特征码在产品人员那边几乎是透明,一般的产品需求变动不至于要修改这个,只是程序员自己想优化代码时就会修改。产品能导致变动的更多是业务对象里的特征码(特别是模型),本来8是删除状态的,改成了9,然后8是删除待审核状态这样(某些业务数据的特征码有连续性要求)

能解决以上问题的时候你自然知道是否要定义常量,但千万别考虑它写的地方有几处那么少而不定义,因为某些个别的业务数据也很少在代码里调用到,但为了可读性也定义了,这么一来为什么人家特征代码少就定义,控制的特征代码少调用就不定义了呢?慢慢地就会延伸出统一性的问题了


如果你否认这种做法,你只会见到外面越来越多的项目在使用常量特征码,慢慢地你就会反思自己是不是错了,还有,当你的项目在长时间成长后,项目成员们慢慢对写死的特征码无法进行阅读理解的时候,你就会作出改变了,所以,如果你今天不变,以后总会改变,但那时候已经造成了一些不良的后果,浪费了不少代码沟通和查询时间了,及早醒悟吧