“如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。”
本周开始阅读和学习《重构》,我想上面这句引用自书中的话,足以简单概括了我们为需要重构代码的原因。
一、重构的第一步
重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检测能力。
重构,就是为保持代码易读,易修改,在不改变代码外在的前提下,对代码做出修改,以改进程序内部结构,提高其可理解性,降低修改成本。而代码测试则可以保证重构时不会因为代码错误而受到影响。
每当要进行重构的时候,第一个步骤就是确保即将修改的代码拥有一组可靠的测试。尽管在过往的开发经历中单元测试都没有显得很重要,但是最近的读书阅读中,有经验的牛人都在强调测试对代码规范的重要性。程序越大,修改不小心破坏其他代码的可能性就越大,所以对代码的测试必不可少。
二、重构的原则
为何重构
1.改进软件设计,如果没有重构,程序的内部设计会逐渐腐败变质。
2.使软件更容易理解,在重构上花点时间就可以让代码更好的表达自己的意图。
3.帮助找到bug,代码组织结构更清晰了找到bug也会更容易。
4.提高编程速度,内部质量良好的入那件让人可以很容易找到在哪里、如何修改。
何时重构
1.预备性重构,重构的最佳时机就是在添加新功能之前。
2.帮助理解的重构,看到一段结构糟糕的条件逻辑,也可能希望复用一个函数,或者函数命名不清晰,这都是重构的机会。
3.捡垃圾式重构,我已经理解代码在做什么,但发现它做得不好,例如逻辑不必要地迂回复杂,或者两个函数几乎完全相同,可以用一个参数化的函数取而代之。
4.有计划的重构,上述例子都是见机行事的重构,而有计划的重构则是在项目计划上专门留给重构的时间。
5.复审代码时重构,代码复审有助于在开发团队中传播知识,也有助于让较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统中的更多部分。重构还可以帮助代码复审工作得到更具体的结果。
三、代码的坏味道
知道了什么何时重构,还要明白那些代码需要重构,没有一个必须重构的精确衡量标准,所以我们必须培养自己的判断力。书里提供了一些“坏味道”的代码类型可以用于参考。
1.神秘命名,即含义不明确的命名,改名可能是最常用的重构手法,包括改变函数声明、变量改名、字段改名等。
2.重复代码,在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
3.过长函数,函数越长,越难理解。
4.过长参数列表,如果有几项参数总是同时出现,可以用引入参数对象将其合并成一个对象。如果某个参数被用作区分函数行为的标记,可以使用移除标记参数。
5.全局数据,有少量的全局数据或许无妨,但数量越多,处理的难度就会指数上升。
6.可变数据,可以用封装变量来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进。
7.发散式变化,每当要对某个上下文做修改时,我们只需要理解这个上下文,而不必操心另一个。
8.霰弹是修改,如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。
9.依恋情结,一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。
10.数据泥团,在多个地方看到相同的三四项数据,两个类中相同的字段、许多函数签名中相同的参数。
11.基本类型偏执,一些带有逻辑处理的变量使用了基本类型而不是使用对象去定义,比如把钱当作普通数字来计算。
12.重复的switch,重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有的switch,并逐一更新。
。。。
书中列举了包括以上在内总共24条坏代码味道,这里不全部列举。通过阅读了解坏代码味道,让我们知道那些代码需要更好的完善。
四、代码实例
public List<PaymentOptionConfView> paymentTrialCalculation(List<PaymentOptionConfView> channels, ChannelConsultDTO paymentChannelConsultDTO) {
//初始化参数
paymentChannelConsultDTO.setConsultFromChannel(paymentChannelConsultDTO.getConsultFromChannel() != null ? paymentChannelConsultDTO.getConsultFromChannel() : false);
if (CollectionUtils.isEmpty(channels) || Boolean.FALSE.equals(paymentChannelConsultDTO.getConsultFromChannel())
|| paymentChannelConsultDTO.getChannelParamsMap().isEmpty()) {
return channels;
}
Set<String> paymentOptions = paymentChannelConsultDTO.getChannelParamsMap().keySet();
List<PaymentOptionConfView> views = Lists.newArrayList();
RpcContext_inner ctx = EagleEye.getRpcContext();
List<Future<PaymentOptionConfView>> futureList = new ArrayList<>();
for (PaymentOptionConfView channel : channels) {
PaymentOptionConfView view = BeanCopierUtil.copy(channel, PaymentOptionConfView.class);
if (!channel.getActive()
|| !paymentOptions.contains(channel.getPaymentOption())
|| !SupportConsultEnum.SUPPORT.equals(channel.getSupportConsult())
|| !checkValidParams(view, paymentChannelConsultDTO)
//降级开关
|| !needConsultWithChannelSwitch(view)) {
views.add(view);
continue;
}
//进入支付试算先初始化渠道不可用,加入结果列表集,等pci明确返回可用再修改渠道active可用状态
view.setActiveChannel(false, FinResultCode.TRANSACTION_TIMED_OUT.getCode(), FinResultCode.TRANSACTION_TIMED_OUT.getMessage());
views.add(view);
PaymentChannelConsultThread task = new PaymentChannelConsultThread(view, paymentChannelConsultDTO, paymentOptions);
task.setRpcContext(ctx);
futureList.add(ThreadPoolUtils.submitConsultJob(task));
}
//获取线程执行结果
long startTime = System.currentTimeMillis();
for (Future future : futureList) {
long expTime = consultTaskTimeout - (System.currentTimeMillis() - startTime);
expTime = expTime > 0 ? expTime : 0;
try {
future.get(expTime, TimeUnit.MILLISECONDS);
} catch (Exception e) {
LogUtil.warn(log, "paymentTrialCalculation thread --> pci paymentChannelConsultDTO={}",
paymentChannelConsultDTO);
} finally {
future.cancel(true);
}
}
return views;
}
最近看到这么一段代码,首先它的问题是函数过长了,而且if的条件也很长,于是有了以下重构
public List<PaymentOptionConfView> paymentTrialCalculation(List<PaymentOptionConfView> channels, ChannelConsultDTO paymentChannelConsultDTO) {
//初始化参数
paymentChannelConsultDTO.setConsultFromChannel(paymentChannelConsultDTO.getConsultFromChannel() != null ? paymentChannelConsultDTO.getConsultFromChannel() : false);
if (retrunChannels(channels,paymentChannelConsultDTO)) {
return channels;
}
Set<String> paymentOptions = paymentChannelConsultDTO.getChannelParamsMap().keySet();
List<PaymentOptionConfView> views = Lists.newArrayList();
RpcContext_inner ctx = EagleEye.getRpcContext();
List<Future<PaymentOptionConfView>> futureList = new ArrayList<>();
for (PaymentOptionConfView channel : channels) {
PaymentOptionConfView view = BeanCopierUtil.copy(channel, PaymentOptionConfView.class);
if (isAddViews(paymentOptions,channel,view, paymentChannelConsultDTO)) {
views.add(view);
continue;
}
//进入支付试算先初始化渠道不可用,加入结果列表集,等pci明确返回可用再修改渠道active可用状态
view.setActiveChannel(false, FinResultCode.TRANSACTION_TIMED_OUT.getCode(), FinResultCode.TRANSACTION_TIMED_OUT.getMessage());
views.add(view);
PaymentChannelConsultThread task = new PaymentChannelConsultThread(view, paymentChannelConsultDTO, paymentOptions);
task.setRpcContext(ctx);
futureList.add(ThreadPoolUtils.submitConsultJob(task));
}
//获取线程执行结果
long startTime = System.currentTimeMillis();
for (Future future : futureList) {
long expTime = consultTaskTimeout - (System.currentTimeMillis() - startTime);
expTime = expTime > 0 ? expTime : 0;
processFuture(future,expTime);
}
return views;
}
public boolean retrunChannels(List<PaymentOptionConfView> channels, ChannelConsultDTO paymentChannelConsultDTO){
if (CollectionUtils.isEmpty(channels) || Boolean.FALSE.equals(paymentChannelConsultDTO.getConsultFromChannel())
|| paymentChannelConsultDTO.getChannelParamsMap().isEmpty()) {
return true;
}
return false;
}
public boolean isAddViews(Set<String> paymentOptions,PaymentOptionConfView channel,PaymentOptionConfView view, ChannelConsultDTO paymentChannelConsultDTO){
if(channel.getActive()
&& paymentOptions.contains(channel.getPaymentOption())
&& SupportConsultEnum.SUPPORT.equals(channel.getSupportConsult())
&& checkValidParams(view, paymentChannelConsultDTO)
&& needConsultWithChannelSwitch(view)){
return false;
}
return true;
}
public void processFuture(Future future,long expTime){
try {
future.get(expTime, TimeUnit.MILLISECONDS);
} catch (Exception e) {
LogUtil.warn(log, "paymentTrialCalculation thread --> pci paymentChannelConsultDTO={}",
paymentChannelConsultDTO);
} finally {
future.cancel(true);
}
}
paymentTrialCalculation方法里的for循环中包含好几个循环外的变量,如果循环抽出方法的话,方法参数列表会比较长,所以没有抽方法。只是将if条件判断和try代码块进行了重构。
其中,isAddViews方法原来是由5个非用" || "组成的判断条件,逻辑就是只要其中一个条件为false然后加上其前面的“!”运算最终的结果就是true。这样阅读起来不是很方便,所以重构之后还是五个判断条件但是改成了用“&&”连接,最终结果和改造之前是相同的。
五、总结
重构代码是一种持续改进的过程,它可以帮助开发人员不断提高代码的质量和性能,从而更好地满足业务需求。我们通过阅读学习《重构》,去提升对代码鉴别的品味,知道那些代码应该重构,在后续添加功能、修补错误和复审代码时尽量运用重构的原则,提升对代码的把握能力。