文章目录
- 1 讨论
- 1.1 什么是重构
- 1.2 重构与性能优化是一件事吗
- 2 重构的目的、时机、难点
- 2.1 重构的目的
- 2.2 何时重构
- 2.2.1 添加新功能时对周边历史代码进行小型重构
- 2.2.2 code review时
- 2.2.3 有计划有目的的重构
- 2.2.4 出现线上问题
- 2.2.5 何时不该重构
- 2.3 重构的难点
- 2.3.1 如何说服产品
- 2.3.2 重构阶段一些新功能可能需要实现两次
- 2.3.3 重构不彻底或烂尾导致新老逻辑使代码理解成本更高
- 2.3.4 控制重构的风险
- 2.3.5 包含库表结构的重构
- 3 常见重构场景与方式
- 3.1 过长的参数
- 3.1.1 提取参数获取逻辑
- 3.1.2 过长方法参数,有可选或可设置默认值参数
- 3.2 简化条件逻辑
- 3.2.1 提炼函数分解条件表达式
- 3.2.2 合并条件表达式
- 3.2.3 卫语句取代嵌套表达式
- 3.2.4 策略模式取代条件表达式
- 3.3 散弹式修改,导致维护成本过高
- 3.3.1 诱因
- 3.3.2 优化方式
- 3.4 过长的函数
- 3.4.1 提炼子函数
- 3.4.2 减少重复代码,增加代码复用
- 3.4.3 划分边界,降低耦合性
- 3.5 死代码
- 4 重构工具
- 4.1 idea原生支持功能
- 4.1.1 Reactor模块功能
- 4.1.2 项目内死代码扫描
- 4.2 其它idea插件
- 5 总结
1 讨论
在全文开始之前,我们先来讨论几个小问题。
1.1 什么是重构
书中是这么定义的:
在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减小整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。 – 《重构 改善既有代码的设计》
我们按范围划分的话可以方便理解(本文主要讨论中小型重构)
类型 | 修改范围 | 示例 |
小型重构 | 对单个类内部的重构优化 | 重命名、提取变量、提取函数等 |
中型重构 | 对多个类间的重构优化 | 提取接口、超类、委托等 |
大型重构 | 针对系统组件架构重构优化 | 服务的拆分合并、组件化等 |
1.2 重构与性能优化是一件事吗
在我看来重构是将代码变成人喜欢的样子,而性能优化是将代码优化成计算机更喜欢的模样。这两种行为可能会有交集,但并不一样,甚至有的重构还会对性能造成一些影响。
尽管我认同代码应当清晰易懂,但这并不意味着我们可以完全不顾性能,还是要根据场景来进行平衡选择。在那些对性能要求不是特别严格的场景下,编写出易于优化的软件是更为明智的选择。首先确保代码的可读性和可维护性,然后再逐步优化其性能,这样通常能够达到既易于管理又具有一定性能水平的结果。
2 重构的目的、时机、难点
2.1 重构的目的
- 优化代码结构、提高可读性。
- 提高扩展效率。
- 降低修改代码的风险。
2.2 何时重构
第一次做某件事时只管去做,第二次做类似的事会产生反感,但无论如何还是可以去做,第三次再做类似的事,你就应该重构。
正如老话说的:事不过三,三则重构。 – Don Roberts
2.2.1 添加新功能时对周边历史代码进行小型重构
- 当差不多的代码复制粘贴了3~5遍的时候。
- 比如方法提炼、变量提炼、优化方法参数、消除重复逻辑等。
- 当然也要取舍,对于简单影响小的可以立即重构,如果比较复杂有风险的可以先做记录完成当前任务后或者另找时间重构。
2.2.2 code review时
让有经验的同学把知识传递给编写代码的同学,从而给予改进的灵感。(老同学对业务更加熟悉,也更了解业务的变化点有助于做出合理的设计)
2.2.3 有计划有目的的重构
对于中小型重构通常在需求中见缝插针进行重构就可以了。但对于大型重构难度和影响相对要大一些,所以就要做好设计,确定影响范围,这通常需要安排整块的时间。
2.2.4 出现线上问题
发生线上问题可以暴露出一些问题,这也是改进的好时机。比如上下游系统出现故障影响到你的系统,就可以思考是不是耦合性太强了能不能解耦。
2.2.5 何时不该重构
- 重写比重构还容易。(到这种程度重构的风险也非常高)
- 隐藏在某个接口下运行稳定且极少修改的丑陋代码。(难以看到收益)
2.3 重构的难点
2.3.1 如何说服产品
对于困难的重构,可能会需要较长整块的时间甚至还会影响正常需求的进度。所以还需要业务或产品同学的理解与支持。
为此,我们需要在他人的视角上说明重构能够带来的好处,比如能够提升某类需求的开发效率缩短排期,再或者是系统存在什么隐患会对业务带来什么影响等。
2.3.2 重构阶段一些新功能可能需要实现两次
- 需要评估新功能是否可以等待重构后完成。
- 重构分为多个阶段小步快跑的方式,尽量不影响需求。
2.3.3 重构不彻底或烂尾导致新老逻辑使代码理解成本更高
- 重构如果要创建新服务还是要谨慎评估。
- 提前想好兼容新老模式的设计,线上问题应对方案。
- 如果遇到烂尾思考是兼容并行还是将新逻辑下线。
2.3.4 控制重构的风险
1)保障重构前后行为一致
- 使用
IDEA
重构功能进行安全重构 - 单元测试
- 功能测试
- 回归测试后
- 流量会回放测试
2)减少出现问题带来的影响
- 灰度 & 开关
- 监控报警快速发现问题
2.3.5 包含库表结构的重构
提前设计好数据迁移初始化方案,以及回滚方案。
3 常见重构场景与方式
3.1 过长的参数
3.1.1 提取参数获取逻辑
方法有23个参数,根本无法复用,而且这些参数跟随着子方法还会不断被传递。
public List<WorkAuditDetail> workCal(Long groupID, Long orgID, Long startDate, Long endDate, Long month, Map<Long, WorkEmpDto> empMap,List<Long> festivalDates, Map<String, String> holidayItemMap,
Multimap<Long, WorkOrderDto> works, List<Long> employeeIdList, Multimap<Long, WorkEmpDto> employeeMultimap,
Map<Long, Map<String, Integer>> empHolidayRemainNumMap, Multimap<Long, HolidayInfoDto> holidayInfoMultimap,
Multimap<Long, CheckTimeDto> checkTimeMultimap, List<WorkAuditDetail> workAudits, List<WorkAuditDetail> workAuditsMonth,
List<DataValue> checkInRuleIds, List<DataValue> restItemRuleIds, Map<Long, EmpLendRestDto> empLendRestDtoMap
, Map<Long, List<HolidayItemDto>> empHolidayRuleMap, List<HolidayInfoDto> holidayInfoYearList, Map<Long, BigDecimal> empRemainStoreDateMap) {
//...
}
优化方式一:以函数取代参数
public List<WorkAuditDetail> workCal(Long groupID, Long orgID, Long startDate, Long endDate, Long month, List<Long> employeeIdList) {
List<Long> festivalDates=workerDataService.getFestivalDates(groupID);
Map<String, String> holidayItemMap=workerDataService.getHolidayItemMap(startDate,endDate,employeeIdList);
//....
}
优点:参数少且复杂变量获取逻辑已经被封装到内部当中便于方法复用。
缺点:变量不进行传递每次都通过函数获取对性能十分不友好。
适用场景: 适合参数逻辑十分简单且不需要外部调用。
优化方式二:封装参数对象
public List<WorkAuditDetail> workCal(EmployeeDataContext employeeDataContext) {
List<Long> festivalDates=employeeDataContext.getFestivalDates();
Map<String, String> holidayItemMap=employeeDataContext.getHolidayItemMap();
//....
}
//----------封装的参数对象--------
@Data
public class EmployeeDataContext{
private Long groupID;
private Long orgID;
private Long startDate;
private Long endDate;
private Long month;
private String operator;
private List<Long> festivalDates;
private Map<String, String> holidayItemMap;
//...
}
优点:增加参数,不用修改所有调用方法的地方了。
缺点:时间久了对象的参数也可能比较多,每次复用方法的时还需要看下对象中哪些参数是会被使用到(需要赋值),不方便复用。
适用场景:适合参数不是非常多或不考虑过多复用的场景。
优化方式三:充血模型延迟加载(结合1+2方式)
将获取参数的逻辑放入到参数对象中,并提供缓存与延迟加载的功能。
//-----调用前先构造参数对象--------
public List<WorkAuditDetail> useWorkCal(Long groupID, Long orgID, Long startDate, Long endDate, Long month, List<Long> employeeIdList) {
EmployeeDataContext employeeDataContext=new EmployeeDataContext(groupID,employeeIdList,endDate,endDate);
List<WorkAuditDetail> result= workCal(employeeDataContext);
//...
}
//------------执行业务方法---------
public List<WorkAuditDetail> workCal(EmployeeDataContext employeeDataContext) {
List<Long> festivalDates=employeeDataContext.queryFestivalDates();
Map<String, String> holidayItemMap=employeeDataContext.queryHolidayItemMap();
//....
}
//------------充血模型参数对象--------
public class EmployeeDataContext{
private Long groupID;
private Long orgID;
private Long startDate;
private Long endDate;
private Long month;
private List<Long> festivalDates;
private Map<String, String> holidayItemMap;
public WorkAuditDetail(Long groupID, Long employeeId, Long startDate, Long endDate) {
this.groupID = groupID;
this.employeeId = employeeId;
//...
}
public List<Long> queryFestivalDates(){
if(this.festivalDates !=null){
return this.festivalDates;
}
//初始化参数变量
this.festivalDates=workerDataService.getFestivalDates(this.groupID);
return festivalDates;
}
//...
}
优点:方便复用。
缺点:可能会有大类产生,延迟查询属性不要用get方法(一些序列化方法会造成所有属性都会被初始化)。
使用场景:适合参数获取逻辑复杂需要多复用的场景。
3.1.2 过长方法参数,有可选或可设置默认值参数
//业务逻辑
public void pushGoods(){
//获取商品来源 会参考标签与商品明细但不是必须的
Integer goosSource=goodsService.getGoosSource(goods,null,null);
//...
}
//商品Service
public class GoodsService(){
public int getGoosSource(Goods goods,Set<Long> labelIdSet,PriceConfig priceConfig){
//...
}
}
优化方式:
方法重载并依赖同一逻辑(让方法提供者判断哪些参数非必须并提供新的方法)。
//业务逻辑
public void pushGoods(){
//获取商品来源 会参考标签与商品明细但不是必须的
Integer goosSource=getGoosSource(goods);
//...
}
//商品Service
public class GoodsService(){
//只使用商品对象获取来源
public int getGoosSource(Goods goods){
return getGoosSource(goods,Collections.emptySet(),getDefaultPriceConfig());
}
public int getGoosSource(Goods goods,Set<Long> labelIdSet,PriceConfig priceConfig){
//...
}
}
3.2 简化条件逻辑
3.2.1 提炼函数分解条件表达式
从if、else三个段落中分别提炼出独立函数。
//修改前
if(data.before(SUMMER_START) || date.after(SUMMER_END)){
charge =quantity * _winterRate + _avinterserviceCharge;
}
else{
charge = quantity * _summerRate;
}
//修改后
if (notSummer(date)){
charge = winterCharge(quantity);
}
else{
charge = summerCharge(quantity);
}
3.2.2 合并条件表达式
条件各不相同,最终行为却一致。
//修改前
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
//修改后
if (isNotEligibleForDisability()) return 0;
private boolean isNotEligibleForDisability() {
return ((anEmployee.seniority < 2)
|| (anEmployee.monthsDisabled > 12)
|| (anEmployee.isPartTime));
}
3.2.3 卫语句取代嵌套表达式
对于一些异常情况的判断可以单独检查并返回的方式通常被称为“卫语句
”(Guard Clauses)。
多用于异常情况返回,循环或方法跳出(对于异常情况处理也可以采用直接抛异常,然后上层统一处理异常来简化上层的判断)。
//优化前
public static String checkEntry(boolean hasTicket, int age, boolean hasID, boolean isEvening) {
// 如果没有票,则直接返回提示信息
if (!hasTicket) {
return "您需要先买票。";
} else { // 这里开始嵌套
// 如果年龄不足18岁,则返回提示信息
if (age < 18) {
return "您必须年满18岁才能参加此活动。";
} else { // 继续嵌套
// 如果是晚间活动并且没有携带身份证,则返回提示信息
if (isEvening && !hasID) {
return "请携带身份证参加晚间活动。";
} else { // 最后一层嵌套
// 所有条件都满足后,返回欢迎信息
return "欢迎参加本次活动!";
}
}
}
}
//优化后
public static String checkEntryOptimized(boolean hasTicket, int age, boolean hasID, boolean isEvening) {
// 如果没有票,则直接返回提示信息
if (!hasTicket) {
//也可以直接抛异常让上层统一处理 trow new BusinessException("您需要先买票");
return "您需要先买票。";
}
// 如果年龄不足18岁,则返回提示信息
if (age < 18) {
return "您必须年满18岁才能参加此活动。";
}
// 如果是晚间活动并且没有携带身份证,则返回提示信息
if (isEvening && !hasID) {
return "请携带身份证参加晚间活动。";
}
// 所有条件都满足后,返回欢迎信息
return "欢迎参加本次活动!";
}
3.2.4 策略模式取代条件表达式
增加策略类,用于一些更复杂的场景。
public class AccessChecker {
public static String checkAccess(String role) {
// 使用多个 if 语句来检查用户角色
if ("admin".equals(role)) {
return "管理员可以访问所有功能。";
} else if ("user".equals(role)) {
return "普通用户只能访问部分功能。";
} else if ("guest".equals(role)) {
return "访客仅能浏览首页。";
} else {
return "未知角色,请联系管理员。";
}
}
}
使用策略模式+工厂模式优化
//--------------定义策略接口------------
public interface AccessStrategy {
String checkAccess();
}
//--------------实现策略类------------
public class AdminAccessStrategy implements AccessStrategy {
@Override
public String checkAccess() {
return "管理员可以访问所有功能。";
}
}
public class UserAccessStrategy implements AccessStrategy {
@Override
public String checkAccess() {
return "普通用户只能访问部分功能。";
}
}
public class GuestAccessStrategy implements AccessStrategy {
@Override
public String checkAccess() {
return "访客仅能浏览首页。";
}
}
//--------------创建工厂类------------
public class AccessStrategyFactory {
public static AccessStrategy getStrategy(String role) {
switch (role) {
case "admin":
return new AdminAccessStrategy();
case "user":
return new UserAccessStrategy();
case "guest":
return new GuestAccessStrategy();
default:
throw new IllegalArgumentException("Unknown role: " + role);
}
}
}
//--------------应用策略------------
public class AccessManager {
public String getAccessLevel(String role) {
return AccessStrategyFactory.getStrategy(role).checkAccess();
}
}
进一步使用枚举优化工厂创建过程
//--------------角色枚举------------
public enum UserRole {
ADMIN("admin", new AdminAccessStrategy()),
USER("user", new UserAccessStrategy()),
GUEST("guest", new GuestAccessStrategy());
private final String code;
private final AccessStrategy strategy;
UserRole(String code, AccessStrategy strategy) {
this.code = code;
this.strategy = strategy;
}
public String getCode() {
return code;
}
public AccessStrategy getStrategy() {
return strategy;
}
// 静态方法,根据 code 获取枚举对象
public static UserRole getByCode(String code) {
for (UserRole role : values()) {
if (role.getCode().equalsIgnoreCase(code)) {
return role;
}
}
throw new IllegalArgumentException("Unknown role code: " + code);
}
}
//--------------优化后的工厂实现------------
public clas