目录
一、魔法值概念
常量在代码中具有穿透性,使用甚广。如果没有一个恰当的命名,就会给代码阅读带来沉重的负担,甚至影响对主干逻辑的理解。首当其冲的问题就是到处使用魔法值。
魔法值即"共识层面"上的常量,直接以具体的数值或者字符出现在代码中。这些不知所云的魔法值极大地影响了代码的可读性和可维护性
1.警示案例1
public void getOnlinePackageCourse(Long packageId,Long userId){
if(packageId == 3){
logger.error("线下课程,无法在线观看");
return;
}
}
//其他逻辑处理
PackageCourse online = packageService.getByTeacherId(userId);
if(online.getPackageId() == 2){
logger.error("未审核课程");
return;
}
以上代码中,信手拈来的2和3分别表示未审核课程和线下课程,仅仅是两个数字,似乎很容易记忆。但事实上,除了2和3两种状态外,还有1、4、5分别代表新建、审核未通过、审核通过。在团队规模较小时,口口相传,倒也勉强能够记住这五个数字的含义,早期还有零星的注释,驾轻就熟的情况下,连注释也省了。现实是残酷的,团队迅速扩大后,课程状态个数也在逐步增加,新来的开发工程师在上线新功能模块时,把"审核通过"和"未审核课程"对应的数字搞反了,使得课程展示错误,导致用户大量投诉。
2.警示案例2
String key = "Id#taobao_" + tradeId;
cache.put(key,value);
上述代码是保存信息到缓存中的方法,即使用魔法值组装Key。这就导致各个调用方导出复制和粘贴字符串Id#taobao_,这样似乎很合理。但某一天,某个粗心的程序员把Id#taobao_复制成了Id#taobao,少了下划线。这个错误在测试过程中,并不容易发现,因为没有命中缓存,会自动访问数据库。但在大促时,数据库压力急剧上升,进而发现缓存全部失效,导致连接占满,查询变慢。"小洞不补,大洞吃亏",再次说明魔法值害人害已。
随着应用变得越来越复杂,这些魔法值几乎成了整个后台服务代码中的梦魇。团队架构师终于下定决心进行系统重构。
二、重构方案
1.静态常量(不要使用)
一是,静态全局常量不安全,开发者传入任意类型的对应类型值。二是,没有命名空间,对常量命名有所要求。详情见另一篇文章Java之枚举类
2.枚举类
下述代码把课程类型分成了三种:录播课程,直播课程,线下课程。枚举类型几乎是固定不变的全局变量,使用频率高、范围广,所以枚举常量都需要添加清晰的注释,比如业务相关信息或注意事项等。
public enum CourseTypeEnum{
/**
* 允许官方和讲师创建和运营
*/
VIDEO_COURSE(1,"录播课程"),
/**
* 只允许官方和讲师创建和运营,初始化必须设置合理的报名人数上线
*/
LIVE_COURSE(2,"直播课程"),
/**
* 只允许官方和讲师创建和运营
*/
OFFLINE_COURSE(3,"线下课程");
private int seq;
private String desc;
CourseTypeEnum(int seq,String desc){
this.seq = seq;
this.desc = desc;
}
public int getSeq(){
return seq;
}
public String getDesc(){
return desc;
}
}
再把课程状态分为新课程、未审核课程、审核通过、审核未通过、已删除五种状态。考虑到后续课程状态还会再追加,并且状态没有扩展信息,所以不能用实例化的抽象类的全局常量(枚举类)来表示课程状态(枚举类适合所有常量已经确定,后期几乎固定不变,这样才符合枚举类的宗旨)
public abstract class BaseCourseState{
public static final int NEW_COURSE = 1;
public static final int UNAUTHED_COURSE = 2;
public static final int PASSED_COURSE = 3;
public static final int NO_PASSED_COURSE = 4;
public static final int DELETED_COURSE = 5;
}
使用重构后的常量修改原有魔法值,对比下代码的可读性
public void getOnlinePackageCourse(Long packageId,Long userId){
if(packageId == CourseTypeEnum.OFFLINE_COURSE.getSeq()){
logger.error("线下课程,无法在线观看");
return;
}
}
//其他逻辑处理
PackageCourse online = packageService.getByTeacherId(userId);
if(online.getPackageId() == BaseCourseState.UNAUTHED_COURSE){
logger.error("未审核课程");
return;
}
三、总结
即使类内常量和局部常量当前只使用一次,也需要赋予一个有意义的名称,目的有两个:第一,望文知义,方便理解;第二,后期多次使用时能保证值出同源。因此,无论如何都不允许任何魔法值直接出现在代码中,避免魔法值随意使用导致取值不一致,特别是对于字符串常量来说,应该避免没有预先定义,就直接使用魔法值。
某些公认的字面常量是不需要预先定义的,如for(int i=;..)这里的0是可以直接使用的。true和false也可以直接使用,但是如果具备了特殊的含义,就必须定义出有意义的常量名称,比如在TreeMap源码中,表示红黑树节点颜色的true和false就被定义成为类内常量,以方便理解
public static final boolean RED = false;
public static final boolean BLACK= true;