使用枚举类Enum作为callee和caller的约定,运用反射消除分支和重复代码在命令式程序中的应用

在开发过程中,程序提供的功能由简单变得复杂,承担功能的主要类也会因此变得庞大臃肿,如果不加以维护,就会散发出浓重的代码味道。下面这篇博文,主要讲述了利用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日--