重构手法——对象结构优化类 | 类关系解耦 | 提取方法

242 阅读5分钟

简介

“提取函数”(Extract Function)是一种常用的代码重构手法,它可以将一段具有特定功能的代码块从原函数中提取出来,形成一个新的独立函数。通过这种方式,可以提高代码的可读性、可维护性和复用性,使代码结构更加清晰,逻辑更加模块化。

针对的症状(代码坏味道)

  • 过长函数:函数包含过多的代码逻辑,导致函数难以理解和维护。
  • 重复代码:在多个地方出现相同或相似的代码片段,违反了代码复用原则。
  • 逻辑复杂:代码块中的逻辑复杂,难以一眼看出其功能和意图。

提取函数(Extract Function)的详细步骤

  1. 识别需要提取的代码块
    • 寻找重复代码:在代码中找到多次出现的相同或相似的代码片段。
    • 识别复杂逻辑:找到逻辑复杂、难以理解的代码块,这些代码块通常实现了一个相对独立的功能。
    • 评估代码块的独立性:确保提取的代码块具有一定的独立性,能够作为一个独立的功能单元存在。
  2. 创建新的函数
    • 选择有意义的函数名:为新函数选择一个能够清晰描述其功能的名称,遵循命名规范。
    • 提取代码块:将选定的代码块复制到新函数中。
    • 确定参数:分析代码块中使用的外部变量,将这些变量作为参数传递给新函数。
    • 确定返回值:如果代码块有计算结果,将该结果作为新函数的返回值。
  3. 替换原始代码
    • 调用新函数:在原函数中,用调用新函数的语句替换提取的代码块。
    • 清理未使用的变量:删除原函数中因提取代码块而不再使用的局部变量。
  4. 测试
    • 编译代码:确保代码编译通过,没有任何语法错误。
    • 运行测试:运行所有相关的单元测试,确保重构操作没有引入新的错误。
    • 手动测试:如果有必要,进行手动测试以验证功能的正确性。
  5. 代码审查
    • 同行评审:让同事或其他团队成员审查你的更改,确保代码质量和可维护性没有下降。
    • 文档更新:如果项目有维护文档的习惯,记得更新相关文档,说明提取函数的影响。

示例

假设有一个函数 printUserInfo,其中包含一段计算用户年龄的复杂逻辑,我们希望对其进行“提取函数”的重构。

原始代码

import java.time.LocalDate;
import java.time.Period;

public class User {
    private String name;
    private LocalDate birthDate;

    public User(String name, LocalDate birthDate) {
        this.name = name;
        this.birthDate = birthDate;
    }

    public void printUserInfo() {
        LocalDate currentDate = LocalDate.now();
        Period period = Period.between(birthDate, currentDate);
        int age = period.getYears();

        System.out.println("User Name: " + name);
        System.out.println("User Age: " + age);
    }
}

重构步骤

  1. 识别需要提取的代码块:计算用户年龄的逻辑 LocalDate currentDate = LocalDate.now(); Period period = Period.between(birthDate, currentDate); int age = period.getYears(); 是一个相对独立的功能,需要提取出来。

  2. 创建新的函数

    • 选择有意义的函数名:calculateAge
    • 提取代码块并传递必要的参数:
      private int calculateAge(LocalDate birthDate) {
         LocalDate currentDate = LocalDate.now();
         Period period = Period.between(birthDate, currentDate);
         return period.getYears();
      }
    
  3. 替换原始代码

    • printUserInfo 函数中,用调用新函数的语句替换提取的代码块:
    import java.time.LocalDate;
    import java.time.Period;
    
    public class User {
       private String name;
       private LocalDate birthDate;
    
       public User(String name, LocalDate birthDate) {
          this.name = name;
          this.birthDate = birthDate;
       }
    
       public void printUserInfo() {
          int age = calculateAge(birthDate);
    
          System.out.println("User Name: " + name);
          System.out.println("User Age: " + age);
       }
    
       private int calculateAge(LocalDate birthDate) {
          LocalDate currentDate = LocalDate.now();
          Period period = Period.between(birthDate, currentDate);
          return period.getYears();
       }
    }
    
  4. 测试

    • 编译代码:确保代码编译通过,没有任何语法错误。
    • 运行测试:运行所有相关的单元测试,确保重构操作没有引入新的错误。
  5. 代码审查

    • 让同事审查代码,确保没有引入新的问题。

练习

基础练习题

  1. 过长函数的提取

    • 给定以下 Java 代码,processOrder 方法过长且逻辑复杂。请将计算订单总价的部分提取成一个独立的方法。

      import java.util.ArrayList;
      import java.util.List;
      
      public class OrderProcessor {
         private List<Double> itemPrices;
         private double taxRate;
      
         public OrderProcessor(List<Double> itemPrices, double taxRate) {
            this.itemPrices = itemPrices;
            this.taxRate = taxRate;
         }
      
         public void processOrder() {
            double total = 0;
            for (double price : itemPrices) {
               total += price;
            }
            double tax = total * taxRate;
            double finalTotal = total + tax;
      
            System.out.println("Order Subtotal: " + total);
            System.out.println("Tax: " + tax);
            System.out.println("Order Total: " + finalTotal);
         }
      }
      
  2. 重复代码的提取

    • 下面的 Java 代码中有两个方法 printEvenNumbersprintOddNumbers,都包含遍历数组的重复代码。请将遍历数组的代码提取成一个独立的方法。
          public class NumberPrinter {
       public void printEvenNumbers(int[] numbers) {
          for (int num : numbers) {
             if (num % 2 == 0) {
                System.out.println(num);
             }
          }
       }
    
       public void printOddNumbers(int[] numbers) {
          for (int num : numbers) {
             if (num % 2 != 0) {
                System.out.println(num);
             }
          }
       }
    }
    

进阶练习题

  1. 复杂逻辑块提取与参数传递

    • 在这段 Java 代码中,calculateFinalScore 方法内的逻辑较为复杂。请将计算加权分数的逻辑提取成一个独立方法,并正确处理参数传递。
    public class ScoreCalculator {
       private double[] scores;
       private double[] weights;
    
       public ScoreCalculator(double[] scores, double[] weights) {
          this.scores = scores;
          this.weights = weights;
       }
    
       public double calculateFinalScore() {
          double weightedTotal = 0;
          double weightSum = 0;
          for (int i = 0; i < scores.length; i++) {
             weightedTotal += scores[i] * weights[i];
             weightSum += weights[i];
          }
          return weightedTotal / weightSum;
       }
    }
    
  2. 提取函数与返回值处理

    • 给定以下 Java 代码,processData 方法包含复杂逻辑。请将数据筛选和转换的部分提取成一个独立方法,并确保返回值正确处理。
    import java.util.ArrayList;
    import java.util.List;
    
    public class DataProcessor {
       public List<Integer> processData(int[] data) {
          List<Integer> result = new ArrayList<>();
          for (int num : data) {
             if (num > 0) {
                result.add(num * 2);
             }
          }
          return result;
       }
    }
    

综合拓展练习题

  1. 多模块提取与代码审查模拟
    • 考虑一个简单的 Java 电商系统,有 Product 类、Cart 类和 Order 类。Cart 类中的 calculateCartTotal 方法和 Order 类中的 calculateOrderTotal 方法都有重复且复杂的计算总价逻辑,同时 Cart 类中的 applyDiscount 方法逻辑也较为复杂。

    • 请对这些方法进行 “提取函数” 重构,将重复和复杂逻辑提取成独立方法。

    • 假设你完成了重构,请模拟一份简单的代码审查报告,指出重构后的优点和可能存在的潜在问题。

      class Product {
         private double price;
      
         public Product(double price) {
            this.price = price;
         }
      
         public double getPrice() {
            return price;
         }
      }
      
      class Cart {
         private List<Product> products;
         private double discountRate;
      
         public Cart(List<Product> products, double discountRate) {
            this.products = products;
            this.discountRate = discountRate;
         }
      
         public double calculateCartTotal() {
            double total = 0;
            for (Product product : products) {
               total += product.getPrice();
            }
            double discountedTotal = total * (1 - discountRate);
            return discountedTotal;
         }
      
         public void applyDiscount() {
            if (products.size() > 3) {
               discountRate += 0.1;
            }
            if (calculateCartTotal() > 100) {
               discountRate += 0.05;
            }
         }
      }
      
      class Order {
         private List<Product> products;
         private double shippingCost;
      
         public Order(List<Product> products, double shippingCost) {
            this.products = products;
            this.shippingCost = shippingCost;
         }
      
         public double calculateOrderTotal() {
            double total = 0;
            for (Product product : products) {
               total += product.getPrice();
            }
            return total + shippingCost;
         }
      }