作者:尹梦雨(惜时)
前言
很久之前团队师兄向我引荐了《重构:改善既有代码的设计》这本书,粗略翻阅看到许多重构的细节技巧,但当时还处于未触摸过工程代码,只注重代码功用,不太考虑后期保护的阶段,读起来觉得枯燥无味,几乎没有共识,一直没有细细阅览。在作业一年后,终于在师兄的催促下,利用一个月左右的早起韶光读完了这本书,收成许多,感谢师兄的催促,感谢这本书陪伴我找回了阅览习惯。把这本书引荐给现已触摸了工程代码、作业一年左右的新同学,信任有了一定的经历积累,再结合日常项目实践中遇到的问题,对这本书的内容会有许多自己的考虑感悟。
重构的界说
书中给出了重构的界说:对软件内部结构的一种调整,目的是在不改动软件可观察前提下,进步其可了解性,下降其修正本钱。
每个人对重构有自己的了解,我了解的重构:重构是一种在不改动代码自身履行效果的前提下,让代码变得愈加整齐易懂的办法。代码不仅要让机器能够完成预期的处理逻辑,更要能够面向开发人员简洁易懂,便于后期保护晋级。
为什么要重构
我对书中的一句话形象很深刻,“削减重复代码,确保一种行为表述的逻辑只在一个当地呈现,即使程序自身的运转时刻、逻辑不会有任何改动,但削减重复代码能够进步可读性,下降日后修正的难度,是优异设计的底子”。回想在刚结业作业不久时,我也曾对同组师兄的代码重构定见有所疑惑,重构自身或许不会改动代码实践的履行逻辑,也纷歧定会对功用发生优化,为什么一定要对代码的整齐度、可复用性如此执着?结合书中的答案以及自己作业中的领会,主要有以下几点:
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 单一责任,避免过长的办法
看到书中提到避免过长的办法这样的观点时,我也有这样的疑问,多少行的办法算过长的办法?关于函数多少行算长这个问题,行数自身不重要,重要的是函数称号与语义的距离。将完成每个功用的过程提炼出独立办法,尽管提炼后的函数代码量纷歧定大,但却是如何做与做什么之间的语义转变,提炼后的函数经过恰当直观命名,可明显提高可读性。 以上总结了一些关于日常研制过程中应该坚持代码整齐准则的考虑,虽小但只需保持,信任代码整齐度会有很大的进步,共勉。