本周有一个开发任务涉及到了枚举类型的修改,需要对枚举类型新增一项。在新增的时候我没有加在已有项的最后面,而是在中间随便找了个位置(其实也不是很随便,我是根据语义关联性觉得放在某一项后面比较合适)。没想到的是,我的无心之举经造成了大”混乱“,相关联的业务在使用该枚举类型时几乎都错位了。查了查,原来是ordinal()在作怪。写Java代码时间也不算短了,但用到枚举类型的次数不多,ordinal()方法就更少用了。趁着这次机会对枚举类型及ordinal()方法稍稍做了下研究,于是就有了这篇文章。
枚举就是要让某个类型的变量的取值只能为若干个固定值中的一个,否则,编译器就会报错。枚举可以让编译器在编译时就可以控制源程序中填写的非法值,普通变量的方式在开发阶段无法实现这一目标。 —— [ Java的枚举类型 ]
参考了java枚举类型enum的使用这篇文章
public enum Light {
/**
* 红灯
*/
RED ,
/**
* 绿灯
*/
GREEN ,
/**
* 黄灯
*/
YELLOW ;
}
public static void main(String[] args) {
System.out.println(Light.GREEN.ordinal());
}
输出结果是:1
我想,ordinal()方法在设计之初,仅仅只是像每一个javabean的get方法一样,返回枚举项在枚举类中出现的序号而已(从0开始的,所以这里的绿灯序号是1)。但是,聪明的我们,却发掘出了它的其他作用。聪明反被聪明误,说的就是我们吧。下面就将项目中的使用方式列出来,作为一个不成功的反例展示给大家。
服务端对图片枚举类型的定义:
public enum ImageEnum {
other("其它"),
logo("_游戏logo"),
split("_游戏截图"),
banner("游戏大图"),
advert("广告"),
information("资讯"),
column("栏目"),
imgs("图集"),
videopreview("视频预览");
private final String value;
/**
* @param value
*/
private ImageEnum(String value) {
this.value = value;
}
/**
* 定义获取游戏图片alt值
* @param enumValue
* @return
*/
public static String getImageEnumValue(ImageEnum enumValue) {
String val;
switch (enumValue) {
case other:
val = ImageEnum.other.value;
break;
case logo:
val = ImageEnum.logo.value;
break;
……
……
case imgs:
val = ImageEnum.imgs.value;
break;
default:
val = "";
}
return val;
}
}
action中声明一个枚举类:
//设置图标类型
ImageEnum banner = ImageEnum.banner;
前端页面拿到这个枚举类:
gameImgBannerType = ${banner.ordinal()}
js中使用了枚举类的序号并作为参数传给服务端处理:
jQuery('#file_upload').uploadify({
'swf' : '/public/include/uploadify/uploadify.swf',
'uploader' : '/Games/uploadImg?Id='+gameImgBannerType+'&gameId='+tmpGameId,
'fileObjName' : 'file',
'method' : 'POST',
'fileTypeExts' : '*.*',
'formData' : {},
……
});
回到action层,看看action层拿到这个枚举类的序号干嘛用:
public static void uploadImg(@Required @Valid File file, Boolean img2jpgs) throws IOException {
response.setContentTypeIfNotSet("text/html");
String imgType = RequestHelper.getParam("Id", 0);//拿到前端传过来的枚举类的序号
String gameId = RequestHelper.getParam("gameId", 0);
……
//图标类型,写入保存时值要加1(Note:注意这句话,为什么要加1?)
int enumValue = Integer.valueOf(imgType);
//获取图标类型
ImageEnum imageEnum = ImageEnum.values()[enumValue];
//游戏图片alt值
String typeStr = ImageEnum.getImageEnumValue(imageEnum);
……
//省去中间部分的处理逻辑
try {
if (imgImage.getId() == null) {
imgImage.setCreateTime(new Date());
imgImage.setModifyTime(new Date());
//设置上传文件类型(Note:果然在这里加了1,但是为什么要这么做呢?)
imgImage.setTypeId(enumValue + 1);
……
……
ImgImageDao.insert(imgImage, true);
……
} else {
……
}
……
} catch (DuplicateNamesException e) {
……
}
}
OK,让我们串起来看看到底是怎么一回事
最后,让我们来揭开这神秘的面纱,为毛存的时候id要加1,且看这张表img_type:
id | name | create_time | modify_time |
---|---|---|---|
1 | 其他 | 2000-01-01 | 2013-09-25 |
2 | 图标 | 2000-01-01 | 2013-09-25 |
3 | 截图 | 2000-01-01 | 2013-09-25 |
4 | 网游banner | 2000-01-01 | 2013-09-25 |
5 | 广告 | 2000-01-01 | 2013-09-25 |
6 | 资讯 | 2000-01-01 | 2013-09-25 |
7 | 栏目 | 2000-01-01 | 2013-09-25 |
8 | 图集 | 2000-01-01 | 2013-09-25 |
9 | 视频预览 | 2000-01-01 | 2013-09-25 |
还记得ImageEnum类的各个项的顺序吗?对比一下,我想你跟我一样,露出了会心的微笑,终于知道这神秘的加1是怎么回事了。兜了好大的圈子,穿越了整个时空,却发现,“其实就是二锅头,兑的那个白开水”。
来,让我们试着用最简单的语言总结一下吧。前端有各种图片要上传,后端有一个通用的上传方式,后端存储的图片类型必须是img_type表里的9个数字之一。前后端于是约定了以ImageEnum中各个枚举项的序号作为标记。ImageEnum中枚举项的顺序和img_type表中各项的顺序一致,而序号是从0开始,表id却是从1开始的,因此服务端存储的时候要加1再存。Over。
首先必须肯定的是,整个代码是完成了预设的功能的(废话,不完成能让上线么)。如果图片类型就这么多,以后再也不会新增,那么这样的代码其实也没啥好吐槽的。但问题是,图片类型真的会新增。甚至后台都开放了新增入口给运营同学,运营同学可以随意在img_type表里面加数据。试想一下吧,新业务来了,图片类型新增了,你在ImageEnum这个类中添加了一个新的枚举项,写了sql往img_type表里插数据。你小心翼翼地维持着ImageEnum枚举项和img_type表数据之间的对应关系,但运营后台开放的新增入口,却极有可能打破这一平衡。又或者像我一样,新接手这些代码,并不清楚枚举类和数据表之间的关系,在ImageEnum类中随意找了位置添加了一个枚举项,那么,旧有的数据就都乱套了。再或者,即使本地环境和测试环境都OK,可是线上环境多了一些图片类型,上线后才发现有问题。ordinal()只是个序号,我们却把它当做一个枚举项的唯一标识,让它承担了本不该承担的责任,那么,增加出错的可能性就不可避免了。
改造ImageEnum
public enum ImageEnum {
other(1, "其它"),
……
videopreview(9, "视频预览");
private final Integer id;
private final String value;
/**
* @param id
* @param value
*/
private ImageEnum(Integer id, String value) {
this.id = id;
this.value = value;
}
/**
* 定义获取游戏图片alt值
* @param enumValue
* @return 图片alt值
*/
public static String getImageEnumValue(ImageEnum enumValue) {
String val;
switch (enumValue) {
case other:
val = ImageEnum.other.value;
break;
……
case imgs:
val = ImageEnum.imgs.value;
break;
}
return val;
}
/**
* 获取图片类型对应的id
* @param enumValue
* @return id
*/
public static Integer getImageEnumId(ImageEnum enumValue) {
Integer id;
switch (enumValue) {
case other:
id = ImageEnum.other.id;
break;
……
case imgs:
id = ImageEnum.imgs.id;
break;
}
return id;
}
/**
* 根据id获取图片类型
* @param id
* @return
*/
public static ImageEnum getImageEnum(Integer id) {
ImageEnum val = null;
switch (id) {
case 1:
val = ImageEnum.other;
break;
……
case 9:
val = ImageEnum.imgs;
break;
}
return val;
}
}
action中声明枚举类的代码不变,前端页面拿到这个枚举类之后不再拿ordinal()而是id:
gameImgBannerType = ${banner.id}
js部分也不改变,回到action层,这里也有变化:
public static void uploadImg(@Required @Valid File file, Boolean img2jpgs) throws IOException {
response.setContentTypeIfNotSet("text/html");
String imgTypeId = RequestHelper.getParam("Id", 0);//拿到前端传过来是枚举类型的id而不是序号
String gameId = RequestHelper.getParam("gameId", 0);
……
//获取图标类型
ImageEnum imageEnum = ImageEnum.getImageEnum(imgTypeId);
//游戏图片alt值
String typeStr = ImageEnum.getImageEnumValue(imageEnum);
……
//省去中间部分的处理逻辑
try {
if (imgImage.getId() == null) {
imgImage.setCreateTime(new Date());
imgImage.setModifyTime(new Date());
//设置上传文件类型,这里不用再加1了
imgImage.setTypeId(imgTypeId);
……
……
ImgImageDao.insert(imgImage, true);
……
} else {
……
}
……
} catch (DuplicateNamesException e) {
……
}
}
经过改造之后,至少我们已经摆脱了ordinal()这个我们不太熟悉的方法,另外一个大大的好处就是,ImageEnum类里的枚举项,可以不用限定顺序了,新增类型的时候,我爱在哪行加都行。
但是,还不够好。因为ImageEnum里的id还是要和img_type中的id保持一致,代码耦合依然严重。因此,继续优化!
为什么一定要用枚举类呢?不用不可以吗?答案是,可以。接下来我们尝试抛弃枚举类型来实现业务功能。
首先我们回头看一下Java枚举类的定义,枚举类是对取值范围的一个限定,比如说交通灯的颜色,一定是红黄绿三种,那我们创建一个交通灯的枚举类是合乎情景合乎常理的。接下来再看我们的业务场景。(与游戏相关的)图片类型明显不是像交通灯颜色那样是基本固定的,尤其是后台给出了新增入口,就更加肯定地告诉我们,图片类型是可以新增而且有经常新增的可能性。既然如此,我们为何要维护两套图片类型呢?假设说我们从头开始做这个功能,这样设计是不是会好一些?
先设计表结构,其中id是primary key:
id | name | alt | create_time | modify_time |
---|---|---|---|---|
other | 其他 | 其他 | 2000-01-01 | 2013-09-25 |
logo | 图标 | _游戏logo | 2000-01-01 | 2013-09-25 |
split | 截图 | _游戏截图 | 2000-01-01 | 2013-09-25 |
banner | 网游banner | 游戏大图 | 2000-01-01 | 2013-09-25 |
advert | 广告 | 广告 | 2000-01-01 | 2013-09-25 |
information | 资讯 | 资讯 | 2000-01-01 | 2013-09-25 |
column | 栏目 | 栏目 | 2000-01-01 | 2013-09-25 |
imgs | 图集 | 图集 | 2000-01-01 | 2013-09-25 |
videopreview | 视频预览 | 视频预览 | 2000-01-01 | 2013-09-25 |
然后在ImgType(即与数据表对应的实体)中添加一些静态变量(之所以是一些而不是全部,是因为并不是所有的图片类型都在后台有用到,也许后台只需要用到banner和videopreiview的上传,因此只声明需要用到的)
public class ImgType {
public static final String IMG_TYPE_OTHER = "other";
……
public static final String IMG_TYPE_VIDEOPREVIEW = "videopreview";
//其他逻辑省略
……
}
ImgTypeDao提供根据主键查询的功能:
public class ImgTypeDao {
……
/**
* 根据主键获取数据对象
* @param name
* @return 图片类型
*/
public static ImgType getByPrimaryKey(String id){
//省略具体逻辑
}
……
}
action中直接声明banner图片类型:
//设置图标类型
String gameImgBannerType= ImgType.IMG_TYPE_BANNER;
前端页面拿到banner图片类型:
gameImgBannerType = ${gameImgBannerType}
js部分不变,回到action层,这里变化如下:
public static void uploadImg(@Required @Valid File file, Boolean img2jpgs) throws IOException {
response.setContentTypeIfNotSet("text/html");
String imgTypeId = RequestHelper.getParam("Id", 0);//拿到前端传过来是图片类型的id
String gameId = RequestHelper.getParam("gameId", 0);
……
//获取图标类型
ImgType imgType = ImgTypeDao.getByPrimaryKey(imgTypeName);
//游戏图片alt值
String typeStr = imgType.getAlt();
……
//省去中间部分的处理逻辑
try {
if (imgImage.getId() == null) {
imgImage.setCreateTime(new Date());
imgImage.setModifyTime(new Date());
//设置上传文件类型,这里的id是字符串类型
imgImage.setTypeId(imgTypeId);
……
……
ImgImageDao.insert(imgImage, true);
……
} else {
……
}
……
} catch (DuplicateNamesException e) {
……
}
}
调整完的代码有以下优点:
本文从枚举类型的ordinal()方法说起,最后却弃枚举类型而去,是枚举类型不好吗?肯定不是。事实上,java 枚举类型enum的使用这篇文档刚好和我走了一条相反的道路,他的观点是枚举类强于静态常量,具体理由有兴趣可以看原文。虽然路是相反的,但我想观点并不冲突。在我看来,枚举类型和静态常量都有自己更适合的场景。在决定使用枚举类型还是静态常量的时候,多思考一下代码的扩展性和可维护性,就知道到底应该选择哪一个了。这里不再赘述,文章到这里就结束吧。