关于if-else的重构

248 阅读4分钟

因项目需要,确实遇到了需要重构的场景。
项目A、B、C通过消息队列通信,为了减少配置,也方便对接,一个项目一个队列。比如项目A对应队列a,且项目A只监听队列a,项目B与C做队列a的生产者,反之亦然。
现在还是把目光聚焦在项目A上,因为监听的队列只有一个,那么就只能定义一个类型字段来区分各种各样的消息。
以json格式消息举例
{
    "messageType": "TYPE_1",
    ……,
    ……
}

然后在监听的时候,靠if-else去识别

public class ListenerA implements SessionAwareMessageListener<Message> {
    @Override
    public void onMessage(Message message, Session session) throws JMSException {
    	if (!(message instanceof TextMessage)) {
            return;
    	}
    	TextMessage textMessage = (TextMessage) message;
    	String messageStr = textMessage.getText();
    	if (StringUtils.isBlank(messageStr)) {
    	    return;
    	}
    	JSONObject messageJSONObject = JSON.parseObject(messageStr);
    	String messageType = messageJSONObject.getString("messageType");
    
    	if (StringUtils.equals(messageType, "TYPE_1")) {
            ……;
    	} else if (StringUtils.equals(messageType, "TYPE_2")) {
    	    ……;
    	} else if (StringUtils.equals(messageType, "TYPE_3")) {
    	    ……;
    	} else if (StringUtils.equals(messageType, "TYPE_4")) {
            ……;
    	} else if ……
    }
}

由于if块内的业务代码被我用省略号给代替了,所以上面那个伪码看着不是很复杂,但实际编码过程中,相信我,会疯的!!

是否必要

这里刚好插句题外话,就我个人而言也不是if-else写多了就必须要去重构,比方说下面这个方法
/**
 * windows环境下,某些特殊字符无法用来做文件名,必须过滤掉,不然会抛异常
 *
 * @param fileName
 * @return
 */
public static String filterInvalidCharacter(String fileName) {
    if (StringUtils.contains(fileName, "\\")) {
        fileName = fileName.replaceAll("\\", " ");
    }
    if (StringUtils.contains(fileName, "/")) {
        fileName = fileName.replaceAll("/", " ");
    }
    if (StringUtils.contains(fileName, ":")) {
        fileName = fileName.replaceAll(":", " ");
    }
    if (StringUtils.contains(fileName, "*")) {
        fileName = fileName.replaceAll("*", " ");
    }
    if (StringUtils.contains(fileName, "?")) {
        fileName = fileName.replaceAll("?", " ");
    }
    if (StringUtils.contains(fileName, "\"")) {
        fileName = fileName.replaceAll("\"", " ");
    }
    if (StringUtils.contains(fileName, "<")) {
        fileName = fileName.replaceAll("<", " ");
    }
    if (StringUtils.contains(fileName, ">")) {
        fileName = fileName.replaceAll(">", " ");
    }
    if (StringUtils.contains(fileName, "|")) {
        fileName = fileName.replaceAll("|", " ");
    }
    return fileName;
}

这个方法看着是笨了点,但首先它只是一个工具方法,不包含业务操作,写完后也只会去调,基本不会再去看里面的代码。其次,就算看也看着很清楚,因为if块内的代码很少,也不会让人犯迷糊。当然非要重构的话,也可以,就是将特殊字符抽成数组,然后用for循环的方式进行替换。
但说实在的,重构这种方法对项目而言没什么意义。

public static String filterInvalidCharacter2(String fileName) {
    String[] invalidCharacterArr = {……,……};
    for (int i = 0; i < invalidCharacterArr.length; i++) {
        fileName = fileName.replaceAll(invalidCharacterArr[i], " ");
    }
    return fileName;
}

好现在换回来
文章开头提到的,消息相关的if-else,就很有必要进行重构,因为每一个if块内,都存在不同的业务代码。在这种场景下,if块的数量与代码的复杂度是成正比的。将这些复杂的代码分离,提升可读性、可维护性,降低耦合度,便很有必要了。

重构

敲黑板
  • 提取接口,定义不同的实现类
  • 按场景获取不同的实现类
虽然每一种消息,都对应不同的业务代码,但对于每一种消息而言,它们都需要被处理。(你看了要是觉得这是句废话,那一定是我表达能力不足引起的)
“被处理”作为一种共性,就可以被提取成接口。
public interface MessageHandler {
	void handle(String message);
}

而后,一种消息,就是一个实现类,顺带一提加了@Service注解,并且使用handler类内的静态常量为此handler命名,因为接下来有用。

@Service(Type1MessageHandler.HANDLER_NAME)
public class Type1MessageHandler implements MessageHandler{
    public static final String HANDLER_NAME = "Type1MessageHandler";
    @Override
    public void handle(String message) {
        ……;
    }
}

此时再看listener类,只要能写成下面这样,是不是就相当的清晰了

public class ListenerA implements SessionAwareMessageListener<Message> {
    @Override
    public void onMessage(Message message, Session session) throws JMSException {
        ……;
        ……;
        
        // handle
        getMessageHandler().handle(messageStr);
    }
}

how?

上面讲过了,每一个实现类都加了@Service注解,那也就是每一个实现类都保存在spring的容器当中。所以这个问题就变成了,如何从spring容器里取出对应的handler。

我这里的解决方案是依靠枚举,其中一个变量是消息的messageType,另一个变量是对应handler的Bean name。
当然,枚举是有缺点的,当枚举的个数多了,一样不好维护。但目前我遇到的消息类型一共才十几个,所以用枚举刚刚好,写起来省力。

public enum MessageTypeEnum {
    TYPE_1("TYPE_1", Type1MessageHandler.HANDLER_NAME),
    ……,
    ;
    
    private String messageType;
    private String handlerServiceName;
    private AllotMessageTypeEnum(String messageType, String handlerServiceName) {
        this.messageType = messageType;
        this.handlerServiceName = handlerServiceName;
    }
    
    public String getMessageType() {
        return messageType;
    }
    
    public String getHandlerServiceName() {
        return handlerServiceName;
    }
    
    public static String getHandlerNameByType(String messageType) {
        for (AllotMessageTypeEnum typeEnum : MessageTypeEnum.values()) {
            if (StringUtils.equals(messageType, typeEnum.getMessageType())) {
                return typeEnum.getHandlerServiceName();
            }
        }
        return null;
    }
}

最后,Listener类变成了这样

public class ListenerA implements SessionAwareMessageListener<Message> {
    @Override
    public void onMessage(Message message, Session session) throws JMSException {
        JSONObject jsonObject = JSON.parseObject(message);
        String messageType = jsonObject.getString("messageType");
        
        String handlerName = MessageTypeEnum.getHandlerNameByType(messageType);
        if (StringUtils.isBlank(handlerName)) {
        	logger.error("未根据消息类型找到对应的handler,放弃处理");
        	return;
        }
        
        MessageHandler messageHandler = SpringContextUtil.getBean(handlerName);
        messageHandler.handle(message);
    }
}