消除代码坏味道:及时重构降低过长代码行数

共 28046字,需浏览 57分钟

 ·

2021-04-12 16:29

本文是笔者新书中关于编码与重构技巧的一节,希望文章中的案例与思考能够帮助读者在日常开发中,消除过长代码带来的坏味道,提升代码可读性。

在日常开发过程中,过长的代码行数也是一种坏味道。

笔者认为一个方法行数至少不能超过一屏,换算成行数大约是50行左右。这是一种偏感性的认识,如果超过一屏,就需要不断上下翻动,影响代码的阅读体验。

代码行数过长之所以会出现,主要原因在于对业务逻辑的编写过程是平铺直叙的,或者通俗的讲,代码是“面条式”的,将各种业务逻辑都放在一个方法中。有的程序员会细心的通过分段以及配合注释的方式便于维护者理解代码逻辑,而大多数情况下是几百行的代码既没有注释有没有层次感,让阅读者心力交瘁。

来看一段这样的“面条式”代码,感受一下过长的代码行数带来的坏味道。

    public PlayerRealNameCheckResponse checkReal(String userCode) throws IOException {
        RestTemplate restTemplate = new RestTemplate();
        PlayerRealNameCheckResponse playerRealNameCheckResponse = new PlayerRealNameCheckResponse();
        // 1. 设置请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        // 2. 设置请求参数
        String requestUrl = GlobalConstant.CHECK_URL;
        // 要求10位时间戳,即精确到秒
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String sign = SignUtil.realCheckSign(userCode, timeStamp, GlobalConstant.KEY);
        MultiValueMap<String, String> requestParam= new LinkedMultiValueMap<>();
        requestParam.add("UserCode", userCode);
        requestParam.add("Timestamp", timeStamp);
        requestParam.add("Sign", sign);
        LOGGER.info("请求远程接口进行实名认证,地址:{}, 入参:{}", requestUrl, JSON.toJSONString(requestParam));
        // 3. 请求开始
        HttpEntity<MultiValueMap<String, String>> requestEntity =
                new HttpEntity<>(requestParam, headers);
        ResponseEntity<String> responseEntity = restTemplate.exchange(
                requestUrl, HttpMethod.POST, requestEntity, String.class);
        // 4. 返回参校验
        if (responseEntity == null) {
            LOGGER.error("[checkReal失败]-请求远程接口进行实名认证为空, userCode:{}", userCode);
            return null;
        }
        // 5. 解析返回参
        String checkResponseBody = responseEntity.getBody();
        if (StringUtils.isBlank(checkResponseBody)) {
            LOGGER.error("[checkReal失败]-请求远程接口进行实名认证responseBody为空,userCode:{}", userCode);
            return null;
        }
        // 反序列化为JsonNode
        JsonNode responseNode = OBJECT_MAPPER.readTree(checkResponseBody);
        String status = responseNode.get("Status").asText();
        String msg = responseNode.get("Msg").asText();
        JsonNode dataNode = responseNode.get("Data");
        if (dataNode == null) {
            LOGGER.error("[checkReal失败]-请求远程接口进行实名认证Json格式异常,缺失data节点.userCode:{}, status:{}, msg:{}",
                    userCode, status, msg);
            return null;
        }
        JsonNode userCodeNode = dataNode.get(userCode);
        if (userCodeNode == null) {
            LOGGER.error("[checkReal失败]-请求远程接口进行实名认证Json格式异常,缺失data.userCode节点.userCode:{}, status:{}, msg:{}",
                    userCode, status, msg);
            return null;
        }
        String originUserCode = userCodeNode.get("UserCode").asText();
        String code = userCodeNode.get("Code").asText();
        String isReal = userCodeNode.get("IsReal").asText();
        String isAdult = userCodeNode.get("IsAdult").asText();

        if (GlobalConstant.CODE_REAL_CHECK_STATUS_SUCCESS.equals(status)) {
            LOGGER.info("[checkReal]-请求远程接口进行实名认证, [获取数据成功!], userCode:{}, userCode:{}, status:{}",
                    userCode, status, msg);
            if (!userCode.equals(originUserCode)) {
                LOGGER.error("[checkReal]-请求远程接口进行实名认证, [请求userCode与返回userCode不匹配], userCode:{}, originUserCode:{}, status:{}, msg:{}",
                        userCode, originUserCode, status, msg);
                return null;
            }

            playerRealNameCheckResponse.setUserCode(userCode);
            if (code.equals("0")) {
                // 正常游戏用户
                playerRealNameCheckResponse.setUserStatusCode(PlayerStatusCodeEnum.STATUS_LEGAL.getType());
            } else {
                LOGGER.warn("当前用户为非法用户,请关注! userCode:{}", userCode);
                playerRealNameCheckResponse.setUserStatusCode(PlayerStatusCodeEnum.STATUS_ILLEGAL.getType());
            }

            // isREAL 是 0 才校验isAdult 否则不校验,因为对面默认对未实名的玩家标记为未成年人
            if (isReal.equals("0")) {
                // 已经实名认证
                playerRealNameCheckResponse.setRealCheckStatus(RealCheckStatusEnum.HAS_REAL_CHECK.getType());

                if (isAdult.equals("0")) {
                    playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.ADULT.getType());
                } else if (isAdult.equals("1")) {
                    playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.CHILDREN.getType());
                } else {
                    playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.VISITOR.getType());
                }
                LOGGER.info("[checkReal]-请求远程接口进行实名认证,[实名认证成功], 返回参playerRealNameCheckResponse:[{}]", JSON.toJSONString(playerRealNameCheckResponse));
            } else {
                // 未进行实名验证的用户可以判断为游客模式
                playerRealNameCheckResponse.setRealCheckStatus(RealCheckStatusEnum.NOT_REAL_CHECK.getType());
                playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.VISITOR.getType());
                LOGGER.info("[checkReal]-请求远程接口进行实名认证,[未成功进行实名认证], 返回参playerRealNameCheckResponse:[{}]", JSON.toJSONString(playerRealNameCheckResponse));
            }
            return playerRealNameCheckResponse;
        } else {
            LOGGER.error("[checkReal]-请求远程接口进行实名认证, [获取数据失败或异常!], userCode:{}, status:{}, msg:{}",
                    userCode, status, msg);
            return null;
        }
    }

这段代码主要含义为:请求远程的实名认证接口判断当前用户角色是否为实名用户,如图所示。

这段代码的主要流程为:

  1. 首先初始化RestTemplate与返回体PlayerRealNameCheckResponse,设置HTTP请求头、并将请求参数设置到请求体HttpEntity中;
  2. 向远程接口发起HTTP调用,获取返回结果,判断返回结果是否为空,如果不为空则获取返回结果的ResponseBody,否则请求结束;
  3. 解析ResponseBody中的状态码、业务参数Data,判断Data是否为空,如果非空则获取具体业务参数,否则请求结束;
  4. 获取用户状态code,判断code是否为0,如果为0则为正常用户,否则为非法用户请求结束;
  5. 获取用户实名认证标识isReal,对isReal具体值进行判断,如果为0则用户角色为成年人,如果为1则用户角色为未成年人,否则为游客;
  6. 组装实名认证返回体,结束请求。

回过头看代码,会发现这段代码比较长,笔者的开发环境中显示有124行,这已经属于过长方法的范畴了.

因此需要考虑对其进行重构。

(1)首先将请求头、请求参数的设置以及发送HTTP请求相关的代码进行抽取,方法名为sendHttpRequest(),代码如下:

private ResponseEntity<String> sendHttpRequest(String userCode, RestTemplate restTemplate) {
        // 1. 设置请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        // 2. 设置请求参数
        String requestUrl = GlobalConstant.CHECK_URL;
        // 要求10位时间戳,即精确到秒
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String sign = SignUtil.realCheckSign(userCode, timeStamp, GlobalConstant.KEY);
        MultiValueMap<String, String> requestParam= new LinkedMultiValueMap<>();
        requestParam.add("UserCode", userCode);
        requestParam.add("Timestamp", timeStamp);
        requestParam.add("Sign", sign);
        LOGGER.info("请求远程接口进行实名认证,地址:{}, 入参:{}", requestUrl, JSON.toJSONString(requestParam));
        // 3. 请求开始
        HttpEntity<MultiValueMap<String, String>> requestEntity =
                new HttpEntity<>(requestParam, headers);
        return restTemplate.exchange(
                requestUrl, HttpMethod.POST, requestEntity, String.class);
    }

(2)将从responseBody中获取Data参数的代码抽取为独立的方法,命名为parseDataFromResponse(),代码如下:

    private JsonNode parseDataFromResponse(String userCode, JsonNode responseNode) {
        JsonNode dataNode = responseNode.get("Data");
        if (dataNode == null) {
            LOGGER.error("[checkReal失败]-请求远程接口进行实名认证Json格式异常,缺失data节点.userCode:{}", userCode);
            return null;
        }
        return dataNode;
    }

(3)将业务逻辑中对用户角色的判断单独抽取为一个方法,方法名为selectUserRole,代码如下:

    private PlayerRealNameCheckResponse selectUserRole(String userCode, PlayerRealNameCheckResponse playerRealNameCheckResponse, String checkResponseBody, JsonNode responseNode, JsonNode dataNode) {
        String msg = responseNode.get("Msg").asText();
        JsonNode userCodeNode = dataNode.get(userCode);
        if (userCodeNode == null) {
            LOGGER.error("[checkReal失败]-请求远程接口进行实名认证Json格式异常,缺失data.userCode节点.userCode:{}, msg:{}",
                    userCode, msg);
            return null;
        }

        String status = responseNode.get("Status").asText();
        String originUserCode = userCodeNode.get("UserCode").asText();
        String code = userCodeNode.get("Code").asText();
        String isReal = userCodeNode.get("IsReal").asText();
        String isAdult = userCodeNode.get("IsAdult").asText();

        if (GlobalConstant.CODE_REAL_CHECK_STATUS_SUCCESS.equals(status)) {
            LOGGER.info("[checkReal]-请求远程接口进行实名认证, [获取数据成功!], userCode:{}", userCode);
            if (!userCode.equals(originUserCode)) {
                LOGGER.error("[checkReal]-请求远程接口进行实名认证, [请求userCode与返回userCode不匹配], userCode:{}, originUserCode:{}",
                        userCode, originUserCode);
                return null;
            }

            playerRealNameCheckResponse.setUserCode(userCode);
            if (code.equals("0")) {
                // 正常游戏用户
                playerRealNameCheckResponse.setUserStatusCode(PlayerStatusCodeEnum.STATUS_LEGAL.getType());
            } else {
                LOGGER.warn("当前用户为非法用户,请关注! userCode:{}", userCode);
                playerRealNameCheckResponse.setUserStatusCode(PlayerStatusCodeEnum.STATUS_ILLEGAL.getType());
            }

            // isREAL 是 0 才校验isAdult 否则不校验,因为对面默认对未实名的玩家标记为未成年人
            if (isReal.equals("0")) {
                // 已经实名认证
                playerRealNameCheckResponse.setRealCheckStatus(RealCheckStatusEnum.HAS_REAL_CHECK.getType());

                if (isAdult.equals("0")) {
                    playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.ADULT.getType());
                } else if (isAdult.equals("1")) {
                    playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.CHILDREN.getType());
                } else {
                    playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.VISITOR.getType());
                }
                LOGGER.info("[checkReal]-请求远程接口进行实名认证,[实名认证成功], 返回参playerRealNameCheckResponse:[{}]", JSON.toJSONString(playerRealNameCheckResponse));
            } else {
                // 未进行实名验证的用户可以判断为游客模式
                playerRealNameCheckResponse.setRealCheckStatus(RealCheckStatusEnum.NOT_REAL_CHECK.getType());
                playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.VISITOR.getType());
                LOGGER.info("[checkReal]-请求远程接口进行实名认证,[未成功进行实名认证], 返回参playerRealNameCheckResponse:[{}]", JSON.toJSONString(playerRealNameCheckResponse));
            }
            return playerRealNameCheckResponse;
        } else {
            LOGGER.error("[checkReal]-请求远程接口进行实名认证, [获取数据失败或异常!], checkResponseBody:{}", checkResponseBody);
            return null;
        }
    }

(4)对第(3)步中的代码继续进行重构,将设置用户状态码StatusCode的业务逻辑进行抽取,根据code设置用户为正常用户还是非法用户,代码如下:

    private void setUserStatusCode(String userCode, PlayerRealNameCheckResponse playerRealNameCheckResponse, String code) {
        if (code.equals("0")) {
            // 正常游戏用户
            playerRealNameCheckResponse.setUserStatusCode(
                    PlayerStatusCodeEnum.STATUS_LEGAL.getType());
        } else {
            LOGGER.warn("当前用户为非法用户,请关注! userCode:{}", userCode);
            playerRealNameCheckResponse.setUserStatusCode(
                    PlayerStatusCodeEnum.STATUS_ILLEGAL.getType());
        }
    }

(5)将选择用户角色的代码进行抽取,根据isReal具体值返回用户角色,代码如下:

    private void setUserRole(PlayerRealNameCheckResponse playerRealNameCheckResponse, String isReal, String isAdult) {
        // isREAL 是 0 才校验isAdult 否则不校验,因为对面默认对未实名的玩家标记为未成年人
        if (isReal.equals("0")) {
            // 已经实名认证
            playerRealNameCheckResponse.setRealCheckStatus(RealCheckStatusEnum.HAS_REAL_CHECK.getType());

            if (isAdult.equals("0")) {
                playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.ADULT.getType());
            } else if (isAdult.equals("1")) {
                playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.CHILDREN.getType());
            } else {
                playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.VISITOR.getType());
            }
        } else {
            // 未进行实名验证的用户可以判断为游客模式
            playerRealNameCheckResponse.setRealCheckStatus(RealCheckStatusEnum.NOT_REAL_CHECK.getType());
            playerRealNameCheckResponse.setUserRole(PlayerRoleEnum.VISITOR.getType());
        }
    }

(6)主流程checkReal方法重构后的代码如下:

    public PlayerRealNameCheckResponse checkReal(String userCode) throws IOException {
        RestTemplate restTemplate = new RestTemplate();
        PlayerRealNameCheckResponse playerRealNameCheckResponse = new PlayerRealNameCheckResponse();

        // 发送HTTP请求
        ResponseEntity<String> responseEntity = sendHttpRequest(userCode, restTemplate);

        // 返回参校验
        if (responseEntity == null) {
            return null;
        }

        // 解析返回参
        String checkResponseBody = responseEntity.getBody();
        if (StringUtils.isBlank(checkResponseBody)) {
            return null;
        }

        // 反序列化为JsonNode
        JsonNode responseNode = OBJECT_MAPPER.readTree(checkResponseBody);

        // 解析Data
        JsonNode dataNode = parseDataFromResponse(userCode, responseNode);
        if (dataNode == null) {
            return null;
        }

        // 选择用户角色
        return selectUserRole(userCode, playerRealNameCheckResponse, checkResponseBody, responseNode, dataNode);
    }

小结与思考

可以看到,将不同的操作都封装为单独的方法,在主流程中只保留重要的操作步骤,不仅提升了阅读者的体验,而且使得代码的逻辑更加有条理。对于主流程而言,代码行数得到了明显减少,相比重构前代码行数减少了一半以上。

通过对方法进行重构,是降低代码行数的一种行之有效的方法。在开发阶段就应当不断地对代码进行重构,使代码逻辑更加具备层次感,避免过长的代码行所带来的坏味道。


浏览 73
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报