一、上下文
1.1 我为什么要修改我的代码
这是一个我正在做的项目中遇到的问题:在应用中每个模块会有一个用于处理网络请求的类,类中会有多个请求方法,如“我的”模块中就会有消息列表、消息状态修改为已读、全部消息置为已读、清空消息列表、上传头像、获取分享信息等请求。这时就要在类中对不同的请求进行分发,之前的代码是这样的:
public class MineBusiness extends BaseBusiness {
public static final String MINE_MESSAGE = "mine_message";
public static final String MINE_MESSAGE_READ = "mine_message_read";
...
public void setParams(Object[] params) {
this.params = params;
}
public void doBusiness() {
String type = params[0];
switch(type) {
case MINE_MESSAGE:
mineMessage();
break;
case MIME_MESSAGE_READ:
mineMessageRead();
break;
...
default:break;
}
}
private void mineMessage() {
// 消息列表的请求
}
private void mineMessageRead() {
// 消息置为已读的请求
}
}
这样的处理对于业务来说并没有任何问题,但是,维护却变得更为艰难,逻辑不够清晰,更重要的是,这并不符合开闭原则:如果在“我的”模块中需要加一个请求,比如消息收藏,除了直接去修改代码以为没有任何办法。
Martin Fowler在《重构 改善既有代码的设计》中说:
面向对象程序的一个最明显特征就是:少用switch(或case)语句。从本质上说,switch语句的问题在于重复。你常会发现同样的switch语句散布于不同地点。如果要为它添加一个新的case语句,就必须找到所有switch语句并修改它们。面向对象中的多态概念可为此带来优雅的解决办法。
1.2 我为什么要写这段文字
要替换掉switch-case或if-else语句,首先想到的是表驱动法。但是查找了许多资料,说明中的表驱动法都仅仅是“点到为止”,介绍一个不同月份的天数的demo或者打印不同的几个String。想要真正移植到项目中还是困难度多。
恰好这个版本任务不重,于是我尝试着实现了一个表驱动的逻辑处理。一则为了自己做下笔记,二则也为有同样需求的兄弟们提供一个参考,开始动手写这个文章。
二、关于表
最初一个版本的实现,是使用一个map来存储整个表:
private static final HashMap<String, BaseBusiness> map = new HashMap<>();
static {
map.put(MINE_MESSAGE, new MineMessage());
map.put(MINE_MESSAGE_READ, new MineMessageRead());
}
public void doBusiness(String type, Object[] params) {
if (map.containsKey(type)) {
map.get(type).doBusiness(params);
}
}
直接在map中存进要执行的请求的对象,调用时传递type来指定具体的业务类并传递相关参数就可以了。但是这样就使得每一种请求都需要一个单独的类,而且,可能登陆、注册这样的方法用户可能基本用不到,也加载到内存中了。如果我想要扩展一种方法,还是要到这一个分发的类中来给map添加一个键值对。
有没有一种能够存储键值对的资源呢?我想到了Java中的Properties资源。
#系统相关
system=SystemBusiness
system_register=register
system_login=login
#我的相关
mine=MineBusiness
mine_message=mineMessages
mine_message_read=mineMessageRead
Properties继承自HashTable<Object, Object>,调用了load(InputStream)后就能得到一个键值对的资源。
三、通过表实现不同业务
这里使用一个枚举来与Properties相关联:
public enum Mode {
SYSTEM("system"), SYSTEM_REGISTER("system_register"), SYSTEM_LOGIN("system_login"),
MINE("mine"), MINE_MESSAGE("mine_message"), MINE_MESSAGE_READ("mine_message_read");
private String value;
Mode(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
使用时,就通过枚举的值来指定表中的key,从而获取到相对应的业务类名和需要调用的方法名:
public void setParameters(Mode moduleMode, Mode methodMode, Object params) {
this.moduleMode = moduleMode;
this.methodMode = methodMode;
this.params = params;
}
最后通过反射获取到具体的Method对象,调用对应的业务方法。
public void dispense() {
if (moduleMode != null && methodMode != null) {
if (properties != null && properties.containsKey(methodMode.getValue())) {
try {
BaseBusiness business;
Method method;
//1.若缓存中不存在对应的key,则需要反射出指定对象,然后存入缓存中
if (!moduleClasses.containsKey(moduleMode.getValue())) {
//2. 从表中获取到指定的类名
String className = properties.getProperty(moduleMode.getValue());
//3. 拼接上包名
Class<?> businessClazz = null;
for (String prefix : sClassPrefixList) {
try {
businessClazz = Class.forName(prefix + className);
break;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//4. 获取指定对象
if (businessClazz != null) {
Constructor<?> constructor = businessClazz.getConstructor();
business = (BaseBusiness) constructor.newInstance();
moduleClasses.put(moduleMode.getValue(), business);
}
}
business = moduleClasses.get(moduleMode.getValue());
business.initializeParameters();
//5. 若method缓存中不存在指定Method,则需要反射出指定Method对象,然后存入缓存
if (!methodClasses.containsKey(methodMode.getValue())) {
Class<? extends BaseBusiness> methodClazz = business.getClass();
method = methodClazz.getDeclaredMethod(properties.getProperty(methodMode.getValue()), Object[].class);
methodClasses.put(methodMode.getValue(), method);
}
method = methodClasses.get(methodMode.getValue());
method.invoke(business, params);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
使用时,只要获取到用于分发发Dispense类对象,设置参数后调用dispense()方法即可:
//调用注册方法
Object[] params = {Mode.SYSTEM_REGISTER};//具体请求参数
dispense.setParameters(Mode.SYSTEM, Mode.SYSTEM_REGISTER, params);
dispense.dispense();
//调用登录方法
params = new Object[]{Mode.SYSTEM_LOGIN};//具体请求参数
dispense.setParameters(Mode.SYSTEM, Mode.SYSTEM_LOGIN, params);
dispense.dispense();
//调用消息列表方法
params = new Object[]{Mode.MINE_MESSAGE};//具体请求参数
dispense.setParameters(Mode.MINE, Mode.MINE_MESSAGE, params);
dispense.dispense();
//调用消息置为已读方法
params = new Object[]{Mode.MINE_MESSAGE_READ};//具体请求参数
dispense.setParameters(Mode.MINE, Mode.MINE_MESSAGE_READ, params);
dispense.dispense();
需要注意的是:
1.枚举中的值必须与properties文件中定义的key相同。
2.properties的value必须和业务类的类名/方法名相同。
具体代码可访问github/table-driven