当我重构时,我在想些什么

发布于:2024-11-29 ⋅ 阅读:(32) ⋅ 点赞:(0)

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 class AccessStrategyFactory {
    public static AccessStrategy getStrategy(String roleCode) {
        UserRole userRole = UserRole.getByCode(roleCode);
        return userRole.getStrategy();
    }
}

使用Spring依赖注入优化工厂创建过程

//--------------使用spring定义策略接口------------
public interface AccessStrategy {
    String checkAccess();
}

@Component("adminStrategy")
public class AdminAccessStrategy implements AccessStrategy {
    @Override
    public String checkAccess() {
        return "管理员可以访问所有功能。";
    }
}

@Component("userStrategy")
public class UserAccessStrategy implements AccessStrategy {
    @Override
    public String checkAccess() {
        return "普通用户只能访问部分功能。";
    }
}

@Component("guestStrategy")
public class GuestAccessStrategy implements AccessStrategy {
    @Override
    public String checkAccess() {
        return "访客仅能浏览首页。";
    }
}

//--------------优化后的工厂实现------------
@Component
public class AccessStrategyFactory {

    @Autowired
    private Map<String, AccessStrategy> strategies;

    public AccessStrategy getStrategy(String roleCode) {
        return strategies.get(roleCode.toLowerCase()+"Strategy");
    }
}


比较两种优化工厂创建的方式

Spring依赖注入优点:新增实现不用修改工厂代码。

枚举创建优点:可以在一个枚举中直观的看到所有实现和说明。

3.3 散弹式修改,导致维护成本过高

散弹式修改(Shotgun Surgery)是指在软件开发中,对某一个功能的修改需要在多个不同的类或模块中进行小幅度的修改,这种情况通常出现在代码设计不合理或耦合度过高的情况下。
– 《重构 改善既有代码的设计》

3.3.1 诱因

1)重复代码过多

2)耦合度过高

高度耦合的代码模块之间存在紧密的依赖关系。这种依赖关系导致在修改某个模块时,必须同时考虑并修改与之相关的其他模块,以确保整个系统的一致性和正确性。

3)逻辑或使用过于分散

3.3.2 优化方式

针对1)重复代码过多和2)耦合度过高优化后续有说明详见 3.4 过长的函数

下面针对 3)逻辑或使用过于分散场景,进行优化说明。

1)优化方式一:收口内联

面对霰弹式修改,一个常用的策略就是使用与内联相关的重构,比如内联函数或是内联类:把本不该分散的逻辑拽回一处(一个类或一个包或一个模块中)。

或者使用一些设计 比如Spring IOC(统一修改注入对象或参数),状态机等。

2)优化方式二:增加中间层

场景示例

一个简单的通过Jedis操作缓存场景,如果只是简单的操作Jedis来讲的话没有任何问题,但如果这时要求Jedis所有set操作前要检查下value值是不是大key

基于以上代码常规的修改方式实现这个需求将是噩梦。

@Service  
public class AuthServiceImpl implements AuthService {
    //登陆标识
    private String LOGGED_IN="loggedIn";
  
    @Autowired  
    private JedisPool jedisPool;  
  
    @Override  
    public boolean login(String username, String password) {
        //校验用户名密码是否正确
        if (authenticate(username, password)) {  
            try (Jedis jedis = jedisPool.getResource()) {  
                //检查是不是大key
                checkBigKey(LOGGED_IN);  
                // 使用SETEX命令设置键值对,并指定过期时间(以秒为单位)
                jedis.setex(username, 3600, LOGGED_IN); 

                //设置用户信息缓存
                User user=queryUser(username);
                String userJson=JSON.toJSONString(user);
                 //检查是不是大key
                checkBigKey(userJson);
                jedis.setex(username, 7200, userJson); 

                //设置手机号索引缓存
                //检查是不是大key
                checkBigKey(username);
                jedis.setex(user.getPhone(), 7000,username); 

                return true;  
            }  
        }  
        return false;  
    }  
}

优化方式

2.1)基于原代码因为我们已经大量的使用了Jedis,所以需要使用继承JedisPool(虽然不建议继承),再通过@Primary方式修改统一修改依赖注入的实现,代码见下方。


@Service 
@Primary
public class RedisService extends  JedisPool  {  
  @Override
  public void setValue(String key, String value) {
      //检查是不是大key
      checkBigKey(value);
        
      try (Jedis jedis = jedisPool.getResource()) {  
          jedis.set(key, value);  
      }  
  }  

  //.... 其它操作方法
}


@Service  
public class AuthServiceImpl implements AuthService {
  //登陆标识
  private String LOGGED_IN="loggedIn";

  @Autowired  
  private JedisPool jedisPool;  

  @Override  
  public boolean login(String username, String password) {
      //校验用户名密码是否正确
      if (authenticate(username, password)) {  
          try (Jedis jedis = jedisPool.getResource()) {  
              // 使用SETEX命令设置键值对,并指定过期时间(以秒为单位)
              jedis.setex(username, 3600,LOGGED_IN); 

              //设置用户信息缓存
              User user=queryUser(username);
              String userJson=JSON.toJSONString(user);
              jedis.setex(username, 7200, userJson); 

              //设置手机号索引缓存
              jedis.setex(user.getPhone(), 7000,username); 

              return true;  
          }  
      }  
      return false;  
  }  
}

2.2)当然我们通常会在一开始就这样做,再初期就使用组合的方式将Jedis包装一层,后续再有扩展的话也会方便许多,这也是很多公司在使用开源中间键喜欢“包”一层的原因之一,代码见下方。

@Service  
public class RedisService {  
  
    @Autowired  
    private JedisPool jedisPool;  
  
    public void setValue(String key, String value) {
        //检查是不是大key
        checkBigKey(value);
          
        try (Jedis jedis = jedisPool.getResource()) {  
            jedis.set(key, value);  
        }  
    }  
  
    //.... 其它操作方法
}

@Service  
public class AuthServiceImpl implements AuthService {
    //登陆标识
    private String LOGGED_IN="loggedIn";
  
    @Autowired  
    private RedisService redisService;  
  
  
    @Override  
    public boolean login(String username, String password) {
        //校验用户名密码是否正确  
        if (authenticate(username, password)) {  
            // 使用SETEX命令设置键值对,并指定过期时间(以秒为单位)
            redisService.setex(username, 3600, LOGGED_IN); 

            //设置用户信息缓存
            User user=queryUser(username);
            String userJson=JSON.toJSONString(user);
            redisService.setex(username, 7200, userJson); 

            //设置手机号索引缓存
            redisService.setex(user.getPhone(), 7000,username); 

            return true;  
            
        }  
        return false;  
    }  
}

3)优化方式三:用配置封装行为特征

场景示例

某支付系统代码中有大量对渠道id的判断,导致代码可读性不好,而且最重要的是添加新渠道改动量和风险非常高。

    if (dto.getPayChannelId() == 29 || dto.getPayChannelId() == 161 || 162 == dto.getPayChannelId()
                || dto.getPayChannelId() == 16100 || 16200 == dto.getPayChannelId() || 167 == dto.getPayChannelId()
                || 16700 == dto.getPayChannelId()) {
          //....
        }
        else if(168 == dto.getPayChannelId() || 169 == dto.getPayChannelId() || 260 == dto.getPayChannelId() ||
                163 == dto.getPayChannelId() || 164 == dto.getPayChannelId()
                || 16300 == dto.getPayChannelId() || 16400 == dto.getPayChannelId() || 165 == dto.getPayChannelId()
                || 166 == dto.getPayChannelId()){
           //....
        }

优化方式

将判断内容抽象成行为并且配置化。

配置化的方式可以是数据库,枚举,配置文件,配置中心等等。

渠道id 品牌 服务商 支付方式 是否允许代付 单笔限额 优先级
29 银联 XX银行 app支付 10000 1
161 微信 原生 扫码 3000 1
162 支付宝 XX支付公司 h5 1000 1
//比如需要支持代付并且是h5支付方式的渠道
if(channel.getAllowProxyPay && channel.getPayType == PayTypeEnum.H5.getCode(){
//...
)

修改后如果要新增渠道,只需要增加相应的配置即可。

3.4 过长的函数

据我们的经验,活得最长、最好的程序,其中的函数都比较短。初次接触到这种代码库的程序员常常会觉得“计算都没有发生”——程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,你就会明白这些小函数的价值所在。间接性带来的好处——更好的阐释力、更易于分享、更多的选择——都是由小函数来支持的。
– 《重构 改善既有代码的设计》

3.4.1 提炼子函数

可以使用idea自带工具安全提取子函数(详见 4 重构工具)。

3.4.2 减少重复代码,增加代码复用

有复用价值的逻辑可以优先考虑提取。并且理论上来说逻辑拆分的更细更利于复用。

如果逻辑不利于拆分也可以考虑以下一些方法:

1)使用aop来实现通用逻辑

2)使用代码块 (@FunctionalInterface接口帮助我们可以更简化的传递代码块,在此之前需要new 匿名类实现)

代码块参数可以对一些环绕的逻辑进行一些复用。

    //包含代码块参数方法
    private void upload(File file,Runnable serviceFunction){
        //1.上传通用逻辑。。。。
        serviceFunction.run();
        //2.上传通用逻辑。。。。
    }
    //推荐集中一个地方维护此逻辑
    Runnable serviceFunction = ()->{
        //执行业务逻辑。。。
    };
    
    //使用包含代码块的上传
    private void useUpload(File file){
        //1.上传通用逻辑。。。。
        upload(file,serviceFunction);
        //2.上传通用逻辑。。。。
    }

除了接口Runnable jdk还为我们使用@FunctionalInterface提供了ConsumerSupplyingFunction等。详情可以查阅java.util.function包下的接口。

使用建议:传入代码块时不建议使用匿名对象会增加维护者的理解成本,建议将代码块逻辑集中封装到某个类或枚举中并且做好说明。

3)自定义 FunctionalInterface

如果JDK提供的接口无法满足可以自定义接口。

4)模版方法

如果业务逻辑比较复杂,流程固定并且想提高复用与可读性可以考虑使用模版方法设计模式。

3.4.3 划分边界,降低耦合性

1)使用java.util.Observer实现观察者模式

优点:依赖少

缺点:需要自己维护订阅关系,并且需要继承,逻辑上也存在加锁。易用性与灵活性不如Spring事件的方式,故不推荐使用。

2)Spring 事件

优点:实现简单,依赖少。

缺点:服务重启会导致事件丢失。

3)使用MQ消息,同一个应用自己发送自己消费

优点:可以借助MQ天然实现失败重试与重试过多通知。

缺点:需要MQ组件,并且自己发送消息自己消费比较怪异,浪费消息传递的性能开销,MQ消息堆积可能会有延迟。

4)任务数据库持久化XXL-Job定时调度(或者其它定时调度机制)

优点:持久化好,可以支持重试,任务可以关闭,基于数据库数据所以任务执行情况可视化比MQ的方式更好。

缺点:工具需要一部分开发量,历史任务数据需要定期清理,流量过高可能会有延迟。

3.5 死代码

我们部署到生产环境甚至是用户设备上的代码,从来未因代码量太大而产生额外费用。就算有几行用不上的代码,似乎也不会因此拖慢系统速度,或者占用过多的内存,大多数现代的编译器还会自动将无用的代码移除。但当你尝试阅读代码、理解软件的运作原理时,无用代码确实会带来很多额外的思维负担。它们周围没有任何警示或标记能告诉程序员,让他们能够放心忽略这段函数,因为已经没有任何地方使用它了。当程序员花费了许多时间,尝试理解它的工作原理时,却发现无论怎么修改这段代码都无法得到期望的输出。
– 《重构 改善既有代码的设计》

优化方式

1)应用内接口可以使用IDEA自带功能扫描(详见 4重构工具)。

2)跨应用接口可以查看一些RPC调用监控工具查看接口调用量,或者先在目标方法做调用报警通知,一段时间内都无报警再进行接口下线。

4 重构工具

4.1 idea原生支持功能

4.1.1 Reactor模块功能

这些重构操作大多可以通过快捷键直接触发,也可以在右键菜单或通过菜单栏的“Refactor”选项访问。


1)重命名(Rename)

可以安全地重命名变量、方法、类、包等,并自动更新所有相关引用。快捷键通常为Shift + F6(或Alt + Shift + R)。

2)提取方法(Extract Method)

将选中的代码块提取到一个新的方法中,有助于减少代码重复和提高代码的可读性。快捷键通常为Ctrl + Alt + M(或Alt + Shift + M)。

3)内联变量/方法(Inline Variable/Method)

将变量的使用替换为它的值,或将方法的调用替换为方法的主体内容,以减少间接性并简化代码。快捷键分别为Ctrl + Alt + N(对于变量)和操作界面中的选项(对于方法),或者Alt + Shift + I

4)修改签名(Change Signature)

改变方法的参数列表或返回类型,IDEA会自动处理调用该方法的所有地方。快捷键为Ctrl + F6

5)提取接口/类(Extract Interface/Class)

从现有类中提取接口或创建抽象类,以实现更好的面向接口编程和设计模式的应用。可通过右键菜单找到这些选项。

6)移动重构(Move Refactoring)

将类、方法、文件等移动到不同的包或目录中,IDEA会自动调整相关的导入语句和包声明。

7)上移/下移成员(Pull Up/Push Down)

将成员(字段或方法)从子类移到父类(上移),或者从父类移到子类(下移),有助于遵循面向对象的继承原则。操作可通过右键菜单完成。

8)安全删除(Safe Delete)

安全地删除代码元素,IDEA会检查该元素是否被其他地方引用,并提供相应的处理选项。

9)引入变量/常量/字段(Introduce Variable/Constant/Field)

将复杂的表达式结果存储在新变量、常量中,或将其提升为类字段。快捷键为Ctrl + Alt + V(变量)、Ctrl + Alt + C(常量)、Ctrl + Alt + F(成员变量)。

10)优化导入(Optimize Imports)

自动移除未使用的导入语句,整理和排序现有的导入。快捷键为Ctrl + Alt + O

11)结构化搜索与替换(Structure Search & Replace)

根据代码结构而非文本进行搜索,并执行结构敏感的替换,非常适合批量修改代码模式。

4.1.2 项目内死代码扫描

输入Unused declaration

选择扫描范围

查看结果

处理

4.2 其它idea插件

1)Lombok

虽然Lombok不是专门的重构插件,但它通过注解的方式极大地简化了Java对象的编写,如自动生成getsettoString等方法。这在一定程度上减少了开发人员需要手动编写的冗余代码,从而间接支持了重构工作。

2)Alibaba Java Coding Guidelines

这款插件基于阿里巴巴的Java开发手册,提供了一系列的代码检查和自动修复功能。它能够帮助开发人员遵循阿里巴巴的Java编码规范,从而提高代码的质量和一致性。在重构过程中,这款插件也可以作为代码检查工具,确保重构后的代码仍然符合规范。

3)CodeGlance

这款插件在编辑器左侧添加了代码文件的缩略图视图,方便开发人员快速浏览和定位代码结构。在重构大型代码库时,这款插件可以大大提高导航速度,使开发人员能够更快地找到需要重构的代码部分。

4)SequenceDiagram

这款插件它能够根据源代码自动生成时序图(Sequence Diagram)。时序图是一种UML(统一建模语言)交互图,用于描述对象之间发送消息的时间顺序,从而展示多个对象之间的动态协作。

5 总结

在重构开始前还是需要好好思考重构的目的与边界。技术没有银弹重构也是如此,有时会因为代码冗长做类或方法的拆分,有时也会因为逻辑过于分散将代码内联回来,还是需要根据具体的场景选择合适的方式。
当然以上的内容也存在一定主观性,大家可以选择性食用。

最后的最后祝愿大家能够远离屎山代码,快乐coding!


关于作者
季伟 采货侠Java开发工程师