乐趣区

关于云计算:工作一年我重新理解了重构

前言

很久之前团队师兄向我举荐了《重构:改善既有代码的设计》这本书,粗略翻阅看到很多重构的细节技巧,但过后还处于未接触过工程代码,只关注代码性能,不太思考前期保护的阶段,读起来感觉枯燥无味,简直没有共鸣,始终没有细细浏览。在工作一年后,终于在师兄的督促下,利用一个月左右的早起时光读完了这本书,播种很多,感激师兄的督促,感激这本书陪伴我找回了浏览习惯。把这本书举荐给曾经接触了工程代码、工作一年左右的新同学,置信有了肯定的教训积攒,再联合日常我的项目实际中遇到的问题,对这本书的内容会有很多本人的思考感悟。

重构的定义

书中给出了重构的定义:对软件内部结构的一种调整,目标是在不扭转软件可察看前提下,进步其可了解性,升高其批改老本。

每个人对重构有本人的了解,我了解的重构:重构是一种在不扭转代码自身执行成果的前提下,让代码变得更加整洁易懂的形式。代码不仅要让机器可能实现预期的解决逻辑,更要可能面向开发人员简洁易懂,便于前期保护降级。

为什么要重构

我对书中的一句话印象很粗浅,“缩小反复代码,保障一种行为表述的逻辑只在一个中央呈现,即便程序自身的运行工夫、逻辑不会有任何扭转,但缩小反复代码能够进步可读性,升高日后批改的难度,是优良设计的基本”。回忆在刚毕业工作不久时,我也曾对同组师兄的代码重构意见有所纳闷,重构自身可能不会扭转代码理论的执行逻辑,也不肯定会对性能产生优化,为什么肯定要对代码的整洁度、可复用性如此执着?联合书中的答案以及本人工作中的领会,次要有以下几点:

2.1 晋升开发效率

在日常研发过程中,首先须要了解已有代码,再在已有代码根底上进行性能迭代降级。在开发过程中,大部分工夫用于浏览已有代码,代码的可读性必然会影响开发效率。而在我的项目进度缓和的状况下,为保障性能失常上线,常常会呈现过程中的代码,可读性不强。如果没有后续重构优化,在我的项目实现一段时间后,当初的开发同学都很难在短时间内从代码看出当初设计时次要的出发点和以及须要留神的点,后续保护老本高。因而,通过重构加强代码的可读性,更便于后续保护降级,也有助于大部分问题通过 CR 阶段得以发现、解决。

2.2 升高批改危险

代码的简洁水平越高、可读性越强,批改危险越低。在理论我的项目开发过程中,因为工夫紧、工期赶,优先保障性能失常,往往衡量之下决定先上线后续再重构,但随着工夫的推移理论后续再进行批改的可能性很低,暂且不谈后续重构自身的 ROI,对于蚂蚁这种极器重稳定性的公司,后续的批改无疑会带来可能的危险,秉持着“上线稳固运行了那么久的代码,能不动尽量不要动”的思维,当初的长期版本很有可能就是最终版本,长此以往,零碎累积的长期代码、反复代码越来越多,升高了可读性,导致后续的保护老本极高。因而,必要的重构短期看可能会减少额定老本投入,但长期来看重构能够升高批改危险。

重构实际

3.1 缩小反复代码

思前想后,重构例子的第一条,也是集体认为最重要的一条,就是缩小反复代码。如果零碎中反复代码意味着减少批改危险:当须要批改反复代码中的某些性能,本来只应须要批改一个函数,但因为存在反复代码,批改点就会由 1 处减少为多处,漏改、改错的危险大大增加。缩小反复代码次要有两种办法,一是及时删除代码迁徙等操作造成的无流量的反复文件、反复代码;二是缩小代码耦合水平,尽可能应用繁多性能、可复用的办法,保持复用准则。

问题背景:在开发过程中,未对之前的代码进行提炼复用,存在反复代码。在开发时对于刚刚接触这部分代码的同学减少了浏览老本,在批改反复的那局部代码时,存在漏改、多处改变不统一的危险。

public PhotoHomeInitRes photoHomeInit() {if (!photoDrm.inUserPhotoWhitelist(SessionUtil.getUserId())) {LoggerUtil.info(LOGGER, "[PhotoFacade] 用户暂无应用权限,userId=", SessionUtil.getUserId());
        throw new BizException(ResultEnum.NO_ACCESS_AUTH);
    }
    PhotoHomeInitRes res = new PhotoHomeInitRes();
    InnerRes innerRes = photoAppService.renderHomePage();
    res.setSuccess(true);
    res.setTemplateInfoList(innerRes.getTemplateInfoList());
    return res;
}

public CheckStorageRes checkStorage() {if (!photoDrm.inUserPhotoWhitelist(SessionUtil.getUserId())) {LoggerUtil.info(LOGGER, "[PhotoFacade] 用户暂无应用权限,userId=", SessionUtil.getUserId());
        throw new BizException(ResultEnum.NO_ACCESS_AUTH);
    }
    CheckStorageRes checkStorageRes = new CheckStorageRes();
    checkStorageRes.setCanSave(photoAppService.checkPhotoStorage(SessionUtil.getUserId()));
    checkStorageRes.setSuccess(true);
    return checkStorageRes;
}

重构办法:及时清理无用代码、缩小反复代码。

public PhotoHomeInitRes photoHomeInit() {photoAppService.checkUserPhotoWhitelist(SessionUtil.getUserId());
    PhotoHomeInitRes res = new PhotoHomeInitRes();
    InnerRes innerRes = photoAppService.renderHomePage();
    res.setSuccess(true);
    res.setTemplateInfoList(innerRes.getTemplateInfoList());
    return res;
}

public CheckStorageRes checkStorage() {photoAppService.checkUserPhotoWhitelist(SessionUtil.getUserId());
    CheckStorageRes checkStorageRes = new CheckStorageRes();
    checkStorageRes.setCanSave(photoAppService.checkPhotoStorage(SessionUtil.getUserId()));
    checkStorageRes.setSuccess(true);
    return checkStorageRes;
}

public boolean checkUserPhotoWhitelist(String userId) {if (!photoDrm.openMainSwitchOn(userId) && !photoDrm.inUserPhotoWhitelist(userId)) {LoggerUtil.info(LOGGER, "[PhotoFacade] 用户暂无应用权限, userId=", userId);
        throw new BizException(ResultEnum.NO_ACCESS_AUTH);
     }
    return true;
}

咱们在零碎中或多或少都看到过未复用已有代码产生的反复代码或者曾经无流量的代码,但对造成背景不理解,出于稳定性思考,不敢贸然清理,工夫久了沉积越来越多。因而,咱们在日常开发过程中,对我的项目产生的无用代码、反复代码要及时清理,避免造成前面同学在看代码时的困惑,以及不够相熟背景的同学改变相干代码时漏改、错改的危险。

3.2 晋升可读性

3.2.1 无效的正文

问题背景:业务代码不足无效正文,须要浏览代码细节能力理解业务流程,排查问题时效率较低。

List<String> voucherMarkList = CommonUtil.batchfetchVoucherMark(voucherList);
 if (CollectionUtil.isEmpty(voucherMarkList)) {return StringUtil.EMPTY_STRING;}
 BatchRecReasonRequest request = new BatchRecReasonRequest();
 request.setBizItemIds(voucherMarkList);
 Map<String, List<RecReasonDetailDTO>> recReasonDetailDTOMap = relationRecReasonFacadeClient.batchGetRecReason(request);
 if (CollectionUtil.isEmpty(recReasonDetailDTOMap)) {return StringUtil.EMPTY_STRING;}
 
 for (String voucherMark : recReasonDetailDTOMap.keySet()) {List<RecReasonDetailDTO> reasonDetailDTOS = recReasonDetailDTOMap.get(voucherMark);
      for (RecReasonDetailDTO recReasonDetailDTO : reasonDetailDTOS) {if (needUpdateRecMaxCount(recReasonDetailDTO, RecReasonTypeEnum.FRIEND, recTypeList, friendRecMaxCount)) {friendRecText = recReasonDetailDTO.getRecommendText();
          friendRecMaxCount = recReasonDetailDTO.getCount();
          friendRecMaxCountDetailDTOS = reasonDetailDTOS;
          continue;
      }

      if (needUpdateRecMaxCount(recReasonDetailDTO, RecReasonTypeEnum.LBS, recTypeList, lbsRecMaxCount)) {lbsRecText = recReasonDetailDTO.getRecommendText();
          lbsRecMaxCount = recReasonDetailDTO.getCount();}
 }
 return bulidRecText(friendRecMaxCountDetailDTOS, friendRecText, lbsRecText);

重构办法:补充相应的业务正文,阐明办法的核心思想和业务解决背景。

  //1. 生成对应的券标识,查举荐信息
 List<String> voucherMarkList = CommonUtil.batchfetchVoucherMark(voucherList);
 if (CollectionUtil.isEmpty(voucherMarkList)) {return StringUtil.EMPTY_STRING;}

 BatchRecReasonRequest request = new BatchRecReasonRequest();
 request.setBizItemIds(voucherMarkList);
 Map<String, List<RecReasonDetailDTO>> recReasonDetailDTOMap = relationRecReasonFacadeClient.batchGetRecReason(request);
 if (CollectionUtil.isEmpty(recReasonDetailDTOMap)) {return StringUtil.EMPTY_STRING;}
 //2. 解析对应的举荐文案,取使用量最大的举荐信息,且好友举荐信息优先级更高
 for (String voucherMark : recReasonDetailDTOMap.keySet()) {List<RecReasonDetailDTO> reasonDetailDTOS = recReasonDetailDTOMap.get(voucherMark);
      for (RecReasonDetailDTO recReasonDetailDTO : reasonDetailDTOS) {
      //2.1 获取好友举荐信息
      if (needUpdateRecMaxCount(recReasonDetailDTO, RecReasonTypeEnum.FRIEND, recTypeList, friendRecMaxCount)) {friendRecText = recReasonDetailDTO.getRecommendText();
          friendRecMaxCount = recReasonDetailDTO.getCount();
          friendRecMaxCountDetailDTOS = reasonDetailDTOS;
          continue;
      }
      //2.2 获取地理位置举荐信息
      if (needUpdateRecMaxCount(recReasonDetailDTO, RecReasonTypeEnum.LBS, recTypeList, lbsRecMaxCount)) {lbsRecText = recReasonDetailDTO.getRecommendText();
          lbsRecMaxCount = recReasonDetailDTO.getCount();}
 }
 //3. 组装后果并返回,若好友举荐量最大的券举荐信息中蕴含地理位置信息,则返回组合文案 (好友举荐信息与地理位置举荐信息均来自同一张券)
 return bulidRecText(friendRecMaxCountDetailDTOS, friendRecText, lbsRecText);

重构这本书中表白了对正文的观点,作者认为代码中不应有过多正文,代码性能应该通过失当的办法命名体现,但相比于国内大多数工程师,书中作者对英文的了解和使用更加善于,所以书中有此观点。但每个人的命名格调和对英文的了解不同,仅通过命名不肯定能疾速理解背地的业务逻辑。集体认为,业务正文而非代码性能正文,清晰直观的业务正文可能在短时间内大抵理解代码对应的业务逻辑,能够帮忙阅读者疾速了解为什么这样做,而不是做什么,因而,简洁的业务正文依然是有必要的。

3.2.2 简化简单的条件判断

问题背景:if 语句中的判断条件过于简单,难以了解业务语义

for (RecReasonDetailDTO recReasonDetailDTO : reasonDetailDTOS) {
    //2.1 获取好友举荐信息
    if (StringUtil.equals(recReasonDetailDTO.getRecReasonType(), RecReasonTypeEnum.FRIEND.name())
            && recTypeList.contains(RecReasonTypeEnum.FRIEND.name()) && StringUtil.isNotBlank(recReasonDetailDTO.getRecommendText())
            && recReasonDetailDTO.getCount() != 0 && Long.valueOf(recReasonDetailDTO.getCount()) > friendRecMaxCount) {friendRecText = recReasonDetailDTO.getRecommendText();
        friendRecMaxCount = recReasonDetailDTO.getCount();
        friendRecMaxCountDetailDTOS = reasonDetailDTOS;
        continue;
      }
//2.2 获取地理位置举荐信息
    if (StringUtil.equals(recReasonDetailDTO.getRecReasonType(), RecReasonTypeEnum.LBS.name())
            && recTypeList.contains(RecReasonTypeEnum.LBS.name()) && StringUtil.isNotBlank(recReasonDetailDTO.getRecommendText())
            && recReasonDetailDTO.getCount() != 0 && Long.valueOf(recReasonDetailDTO.getCount()) > lbsRecMaxCount) {lbsRecText = recReasonDetailDTO.getRecommendText();
        lbsRecMaxCount = recReasonDetailDTO.getCount();}
}

重构办法:将判断条件独自放在独立办法中并失当命名,晋升可读性

 for (RecReasonDetailDTO recReasonDetailDTO : reasonDetailDTOS) {
     //2.1 获取好友举荐信息
     if (needUpdateRecMaxCount(recReasonDetailDTO, RecReasonTypeEnum.FRIEND, recTypeList, friendRecMaxCount)) {friendRecText = recReasonDetailDTO.getRecommendText();
         friendRecMaxCount = recReasonDetailDTO.getCount();
         friendRecMaxCountDetailDTOS = reasonDetailDTOS;
         continue;
      }
      //2.2 获取地理位置举荐信息
      if (needUpdateRecMaxCount(recReasonDetailDTO, RecReasonTypeEnum.LBS, recTypeList, lbsRecMaxCount)) {lbsRecText = recReasonDetailDTO.getRecommendText();
          lbsRecMaxCount = recReasonDetailDTO.getCount();}
 }
 private boolean needUpdateRecMaxCount(RecReasonDetailDTO recReasonDetailDTO, RecReasonTypeEnum reasonTypeEnum,
                                       List<String> recTypeList, long recMaxCount) {if (StringUtil.equals(recReasonDetailDTO.getRecReasonType(), reasonTypeEnum.name())
            && recTypeList.contains(reasonTypeEnum.name()) && StringUtil.isNotBlank(recReasonDetailDTO.getRecommendText())
            && recReasonDetailDTO.getCount() != 0 && Long.valueOf(recReasonDetailDTO.getCount()) > recMaxCount) {return true;}
    return false;
 }

将简单的判断条件提炼到独立的办法中,并通过失当命名来帮忙晋升可读性。在浏览含有条件语句的代码时,如果判断条件过于简单,容易将浏览注意力放在了解判断条件中,而对办法整体的业务逻辑了解可能更艰难,耗时更久。因而,简化判断条件并将其语义化更利于疾速专一了解整体业务逻辑。

3.2.3 重构多层嵌套条件语句

问题背景:if 条件多层嵌套,影响可读性。在写代码的过程中,保障性能正确的前提下依照思维逻辑写了多层条件嵌套,失常的业务逻辑暗藏较深。开发者自身对业务流程足够相熟,能够一口气读残缺段办法,但对于其他同学来说,在浏览此类型代码时,读到失常逻辑时,很容易曾经遗记后面判断条件的内容,对于后面的校验拦挡印象不深。

if (Objects.nonNull(cardSaveNotifyDTO) && !noNeedSendOpenCardMsg(cardSaveNotifyDTO)) {CardDO cardDO = CardDAO.queryCardInfoById(cardSaveNotifyDTO.getCardId(),
            cardSaveNotifyDTO.getUserId());
    if (Objects.isNull(cardDO)) {LoggerUtil.warn(LOGGER, "[CardSaveMessage] cardDO is null");
        return;
    }
    openCardServiceManager.sendOpenCardMessage(cardDO);
    LoggerUtil.info(LOGGER, "[CardSaveMessage] send open card message, cardSaveNotifyDTO=" + cardSaveNotifyDTO);
}

重构办法:对于多层 if 嵌套的代码,能够将不满足校验条件的状况疾速返回,加强可读性。

 if (Objects.isNull(cardSaveNotifyDTO)) {LoggerUtil.warn(LOGGER, "[CardSaveMessage] cardSaveNotifyDTO is null");
     return;
 }
 LoggerUtil.info(LOGGER, "[CardSaveMessage] receive card save message, cardSaveNotifyDTO=" + cardSaveNotifyDTO);
 if (noNeedSendOpenCardMsg(cardSaveNotifyDTO)) {
     LoggerUtil.info(LOGGER,
                  "[CardSaveMessage] not need send open card message, cardSaveNotifyDTO=" + cardSaveNotifyDTO);
     return;
 }
 CardDO cardDO = CardDAO.queryCardInfoById(cardSaveNotifyDTO.getCardId(),
            cardSaveNotifyDTO.getUserId());
 if (Objects.isNull(cardDO)) {LoggerUtil.warn(LOGGER, "[CardSaveMessage] cardDO is null");
     return;
 }
 openCardServiceManager.sendOpenCardMessage(cardDO);
 LoggerUtil.info(LOGGER, "[CardSaveMessage] send open card message, cardSaveNotifyDTO=" + cardSaveNotifyDTO);

如果是程序自身多种状况的返回值,能够缩小进口,晋升可读性。对于业务代码的前置校验,更适宜通过疾速返回代替 if 嵌套的形式简化条件语句。尽管实际上实现性能雷同,但可读性及表白含意不同。用多分支(if else)表明多种状况呈现的可能性是等同的,而判断非凡状况后疾速返回的写法,表明只有很少局部呈现其余状况,所以呈现后疾速返回。简化判断条件更易让人了解业务场景。

3.2.4 固定规定语义化

问题背景:在开发过程中,代码中存在蕴含多个枚举的组合或固定业务规定,在浏览代码时不分明背景,容易产生困惑。例如,图中所示代码在满足切换条件下,将办法中的变量 scene 以默认的字符串拼接生成新的 scene,但这种隐含的默认规定须要浏览代码细节能力理解,在排查问题时,依据理论日志中的具体 scene 值来搜寻也无奈定位到具体代码,了解老本高。

if (isMrchCardRemind(appId, appUrl)) {args.put(MessageConstant.MSG_REMIND_APP_ID, appId);
    args.put(MessageConstant.MSG_REMIND_APP_URL, appUrl);
    if (StringUtil.isNotBlank(memberCenterUrl)) {args.put(MessageConstant.MEMBER_CENTER_URL, memberCenterUrl);
        scene = scene + "_WITH_MEMBER_CENTER";
    }
    scene = scene + "_MERCH";
}

重构办法:能够将其语义形象为字段放入枚举中,升高批改时的危险,加强可读性

/**
  * 积分变动
  */
CARD_POINT_UPDATE("CARD_POINT_UPDATE", "CARD_POINT_UPDATE_MERCH", "CARD_POINT_UPDATE_WITH_MEMBER_CENTER", "CARD_POINT_UPDATE_MERCH_WITH_MEMBER_CENTER"),

/**
  * 余额变动
  */
CARD_BALANCE_UPDATE("CARD_BALANCE_UPDATE", "CARD_BALANCE_UPDATE_MERCH", "CARD_BALANCE_UPDATE_WITH_MEMBER_CENTER", "CARD_BALANCE_UPDATE_MERCH_WITH_MEMBER_CENTER"),

/**
  * 等级变动
  */
CARD_LEVEL_UPDATE("CARD_LEVEL_UPDATE", "CARD_LEVEL_UPDATE_MERCH", "CARD_LEVEL_UPDATE_WITH_MEMBER_CENTER", "CARD_LEVEL_UPDATE_MERCH_WITH_MEMBER_CENTER"),
if (isMrchCardRemind(appId, appUrl)) {args.put(MessageConstant.MSG_REMIND_APP_ID, appId);
    args.put(MessageConstant.MSG_REMIND_APP_URL, appUrl);
    if (StringUtil.isNotBlank(memberCenterUrl)) {args.put(MessageConstant.MEMBER_CENTER_URL, memberCenterUrl);
        return remindSceneEnum.getMerchRemindWithMemberScene();}
return remindSceneEnum.getMerchRemindScene();}

在浏览代码理解业务细节时,代码中的固定规定会额定减少浏览老本。在评估相干改变对现有业务影响时,代码中蕴含固定规定须要特地留神。将固定规定语义化,更有助于对已有代码了解和剖析。如上例中,将自定义的固定字符串拼接规定替换为枚举中的具体值,尽管在重构后减少了代码行数,但在晋升可读性的同时也更便于依据具体值搜寻定位具体代码,其中枚举值的含意和关联关系更加清晰,高深莫测。

总结思考

代码的整洁度与代码品质成正比,整洁的代码品质更高,也更利于前期保护。重构自身不是目标,目标是让代码更整洁、可读性更高、易于保护,晋升开发效率。因而,比起如何进行后续重构,在开发过程中意识到什么样的代码是好代码,在不额定减少太多研发老本的前提下,无意识地放弃代码整洁更加重要。即便是在日常开发过程中小的优化,哪怕只有很少的代码改变,只有能让代码更整洁,依然值得去做。

4.1 去除反复代码

反复代码蕴含代码迁徙产生的过程代码、代码文件中反复的代码、相近的逻辑以及类似的业务流程。对于代码迁徙产生的反复代码,在迁徙实现后要及时去除,防止减少后续浏览复杂度。对于类似的性能函数以及类似的业务流程,咱们能够通过提炼办法、继承、模板办法等形式重构,但与其后续通过重构伎俩打消代码,更应在日常写代码的时候保持合成复用准则,缩小反复代码。

4.2 失当直观的命名

怎么的命名算是好的命名?书中给出了对于命名的倡议:好的命名不须要用正文来补充阐明,直观明了,通过命名就能够判断出函数的性能和用法,晋升可读性的同时便于依据常量的语义搜寻查找。同理,代码中有含意的数字、字符串要用常量替换的准则,目标是雷同的。在日常编码中,要用直观的命名来形容函数性能。例如用联合业务场景的用动词短语来命名,在辨别出利用场景的同时,也便于依据业务场景来搜寻相干性能函数。

4.3 繁多职责,防止过长的办法

看到书中提到防止过长的办法这样的观点时,我也有这样的疑难,多少行的办法算过长的办法?对于函数多少行算长这个问题,行数自身不重要,重要的是函数名称与语义的间隔。将实现每个性能的步骤提炼出独立办法,尽管提炼后的函数代码量不肯定大,但却是如何做与做什么之间的语义转变,提炼后的函数通过失当直观命名,可显著晋升可读性。以上总结了一些对于日常研发过程中应该保持代码整洁准则的思考,虽小但只有放弃,置信代码整洁度会有很大的进步,共勉。

作者:尹梦雨 (惜时)

原文链接

本文为阿里云原创内容,未经容许不得转载。

退出移动版