在开发过程中,程序提供的功能由简单变得复杂,承担功能的主要类也会因此变得庞大臃肿,如果不加以维护,就会散发出浓重的代码味道。下面这篇博文,主要讲述了利用Enum,反射等手段简化重构代码的过程。
代码涉及的工程是一个基于Webhook调用的项目,Webhook可以简单理解为网络上文件和文件夹的创生和监控API,用户可以通过调用API在目标机器上创建文件目录,当它们发生变化时获得提醒。
既然是调用API,以某账户创建webhook为例,返回的可能性就只有三种:目标机器收到指令创建成功、目标机器收到指令但因为webhook已经存在而不做操作、API调用出现异常,再以该用户删除webhook为例,返回的可能性也是三种:收到指令发现要删除的对象存在删除成功,收到指令发现要删除的对象不存在而不做操作,API调用异常...归纳一下,返回的可能性就是目标已改变,目标未改变,调用失败三种情况。这时我们就可用一个Enum对象作为wenhook基本操作函数的返回值:
public enum CmdResultType { CHANGED(1), UNCHANGE(0), FAILED(-1); private static final Logger logger = LoggerFactory.getLogger(CmdResultType.class); private int index; private CmdResultType(int index) { this.index = index; } public static CmdResultType fromIndex(int idx) { for (CmdResultType type : CmdResultType.values()) { if (type.getIndex() == idx) { return type; } } logger.warn("Unexcepted index:{} was set to CmdResultType", idx); return null; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } }
该类一开头就定义三种可能的返回值,并且提供了一个静态函数以方便从int值得到CmdResultType类型,下面就能看到这个类的应用:
public CmdResultType delete(String accountName) { logger.info(FUNCTION_ACCOUNT_NAME, "WebhookService.delete()", accountName); try { int deletedCount = 0; List<String> webhookIdList = getWebhookIdList(this.boxApiConn); for (String webhookId : webhookIdList) { logger.info("Found Webhood(id={})", webhookId); if (deleteWebHook(this.boxApiConn, webhookId)) { deletedCount++; } } logger.info("Deleted webhook count:{}", deletedCount); return CmdResultType.fromIndex(deletedCount); } catch (Exception ex) { logger.warn("Cannot delete webhook due to {}", ex.getMessage()); } return CmdResultType.FAILED; }
上面这个函数中,由删除数量而产生返回的CmdResultType类型,如果删除数量为零,说明目标未改变,自然会返回UNCHANGE;如果删除数量为一,说明目标已改变,就会返回CHANGED;这两种情况之外,自然是返回FAILED调用失败。因为业务约定,一个账户下只允许拥有一个Webhook,因此删除数量最大就是1,不会有大于一的情况。
有了CmdResultType这个类,delete函数和delete函数的调用者之间就有了一个契约,callee和caller相当于在CmdResultType类里做好了约定。
下面我们可以看看某个caller的调用情况:
1 CmdResultType resultType = null; 2 3 // Execute command via reflection 4 try { 5 Class<?> serviceCls = WebhookService.class; 6 Method method = serviceCls.getMethod(cmdBundle.childCmd, String.class); 7 method.setAccessible(true); 8 resultType = (CmdResultType) method.invoke(service, accountName); 9 } catch (Exception ex) { 10 String errMsg = String.format("Can not invoke method:%s via reflection because of %s", 11 cmdBundle.childCmd, ex.getMessage()); 12 logger.warn(errMsg); 13 throw new RtmsWebhookException(errMsg, ex); 14 } 15 16 String text = ""; 17 if (CmdResultType.CHANGED == resultType) { 18 text = cmdBundle.changedWord; 19 retval++; 20 } else if (CmdResultType.UNCHANGE == resultType) { 21 text = cmdBundle.unchangeWord; 22 } else { 23 text = cmdBundle.failedWord; 24 }
上面的第八行就是通过反射调用delete函数,而16到24行就是根据返回值做出相应处理。
使用Enum作为返回值比int好的地方在于int值作为约定是松散和缺乏约束的,caller的书写者不得不查看callee函数的代码才能准确判断返回值代表什么意思;而Enum做返回值只用到Enum类里看就好了,看常量比看代码容易得多,callee的编写者也不可能因为业务变化而弄出一个在Enum类里没有定义过的值来。
好了,调用wenhook的三个基本函数create,delete,get(listall,二者功能等同)写好了,因为业务的扩展,还派生出了三个批量调用函数createall,deleteall,getall, 而用户是通过命令方式调用的,指令是“java -jar xxx.jar create accountname”的方式,程序解析出命令后再调用具体函数。
这样就产生了六个分支,而分支多了一是可读性差,二是会导致类似的重复代码,而利用反射我们可以消除分支,达到简化代码的目的:
1 printCmdBegin(cmd.getText()); 2 3 // Execute command via reflection 4 Class<?> serviceCls = WebhookService.class; 5 Method method = serviceCls.getMethod(cmd.getText(), String.class); 6 method.setAccessible(true); 7 CmdResultType result = (CmdResultType) method.invoke(service, accountName); 8 9 if (CmdResultType.CHANGED == result || CmdResultType.UNCHANGE == result) { 10 runCmdRetval = true; 11 } 12 printCmdResult(cmd.getText(), runCmdRetval);
这段代码代表的是create、delete、get三种函数的调用,一次性可以消除三个分支。
1 CmdType cmd = CmdType.fromText(action); 2 printCmdBegin(cmd.getText()); 3 4 // Execute command via reflection 5 Class<?> batchServiceCls = BatchWebhookService.class; 6 Method method = batchServiceCls.getMethod(cmd.getText()); 7 method.setAccessible(true); 8 int changed = (int) method.invoke(batchService); 9 10 if (changed >= 0) { 11 runCmdRetval = true; 12 } 13 printCmdResult(cmd.getText(), runCmdRetval);
上面这段代码代表的是createall,deleteall,getall三种函数的调用,也消除了三个分支。
以上情况是指令的文本正好与调用函数名吻合的情况,但如果不吻合比如大小写不一致,多了前缀后缀怎么办呢?不用怕,用HashMap做个映射就好了。
至于指令本身和调用函数也得做个约定,于是CmdType类就产生了:
public enum CmdType { CREATEALL("createall"), DELETEALL("deleteall"), CREATE("create"), LISTALL("listall"), DELETE("delete"), GET("get"), GETALL("getall"); private static final Logger logger = LoggerFactory.getLogger(CmdType.class); private String text; private CmdType(String txt) { this.text = txt; } public static CmdType fromText(String txt) { for (CmdType type : CmdType.values()) { if (type.getText().equalsIgnoreCase(txt)) { return type; } } logger.warn("Unexcepted text:{} was set to CmdType", txt); return null; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
有了这个类,程序能接受什么指令一目了然,这比去翻设计文档明了多了。
外界文本型的指令通过校验后,会转化成CmdType型的格式:
CmdType cmd = CmdType.fromText(action);
程序再根据cmd的值进行反射调用,就不会因为输入错误命令而导致反射调用异常的情况发生了。
CmdType相当于在用户输入的命令和实际运行的函数间做了个契约,这边是Enum的用意之一。
好了,关于Enum作为callee和caller之间的约定,反射用来消除分支和重复代码就讲到这里。
--2020年4月18日--