重构 - 方法重构

177 阅读15分钟

目录

  1. 提取方法(Extract Method)
  2. 内联方法(Inline Method)
  3. 提取变量(Extract Variable)
  4. 内联临时变量(Inline Temp)
  5. 以查询取代临时变量(Replace Temp with Query)
  6. 拆分临时变量(Split Temporary Variable)
  7. 移除参数赋值(Remove Assignments to Parameters)
  8. 用方法对象替代方法(Replace Method with Method Object)
  9. 替换算法(Substitute Algorithm)
  10. 引入解释性变量(Introduce Explaining Variable)

一、提炼方法(Extract Method)

1.概述

提炼方法(Extract Method) 指的是将某个方法或函数中某一段独立的、具有特定功能的代码块提取出来,封装成一个独立的方法。通过这一操作,可以使代码变得更加简洁、易于理解和维护。

适用场景

  • 方法过长:​当一个方法的代码行数过多,导致阅读困难时,可以考虑将其拆分为多个小方法。
  • 重复代码:​当多个方法中存在相似或相同的代码时,可以将其提取为一个独立的方法,以减少重复。
  • 逻辑复杂:​当方法内部包含复杂的逻辑判断或计算时,可以将其提炼成独立的方法,以提高代码的清晰度。
  • 提高可复用性:​当某段代码在多个地方可能被使用时,将其提炼为独立的方法,有助于代码的复用。

理想状态:每个方法的命名就是对它内部细节的最高层次概括。

2.重构前

public void ProcessOrder(Order order)
{
    Console.WriteLine("Processing order...");

    double total = 0;
    foreach (var item in order.Items)
    {
        total += item.Price * item.Quantity;
    }

    if (order.Customer.IsPremium)
    {
        total *= 0.9;
    }

    total *= 1.2;

    Console.WriteLine($"Order ID: {order.Id}");
    Console.WriteLine($"Customer: {order.Customer.Name}");
    Console.WriteLine($"Total due: {total:C}");

    Console.WriteLine("Order processed.");
}

  • 方法过长:​ProcessOrder 方法包含了多个职责,如计算订单总额、应用折扣、计算税费和打印订单信息,使得方法过于庞大。
  • 职责不单一:​该方法同时处理了多个业务逻辑,违背了单一职责原则。
  • 可读性差:​将所有逻辑放在一个方法中,导致代码难以理解和维护。

3.重构后

public void ProcessOrder(Order order)
{
    Console.WriteLine("Processing order...");

    double total = CalculateOrderTotal(order);
    total = ApplyDiscount(total, order.Customer);
    total = AddTax(total);

    PrintOrderSummary(order, total);

    Console.WriteLine("Order processed.");
}

private double CalculateOrderTotal(Order order)
{
    double total = 0;
    foreach (var item in order.Items)
    {
        total += item.Price * item.Quantity;
    }
    return total;
}

private double ApplyDiscount(double total, Customer customer)
{
    if (customer.IsPremium)
    {
        return total * 0.9;
    }
    return total;
}

private double AddTax(double total)
{
    return total * 1.2;
}

private void PrintOrderSummary(Order order, double total)
{
    Console.WriteLine($"Order ID: {order.Id}");
    Console.WriteLine($"Customer: {order.Customer.Name}");
    Console.WriteLine($"Total due: {total:C}");
}

  • 提高可读性:​每个方法都有明确的职责,代码更易于理解。
  • 增强可维护性:​修改某一业务逻辑时,只需在对应的方法中进行修改,减少了对其他部分的影响。
  • 促进代码复用:​将通用的逻辑提取为独立的方法,便于在其他地方复用。
  • 符合单一职责原则:​每个方法只处理一个特定的任务,符合设计原则。

二、内联方法(Inline Method)

1.概述

内联方法(Inline Method) 旨在将方法调用替换为其方法体的内容,从而消除不必要的间接调用。​这种做法通常适用于方法体非常简单、调用频繁且没有额外逻辑的情况。​通过这种方式,可以减少代码的复杂性,提高可读性和维护性。

适用场景:

  • 方法体简单:​方法体仅包含一行或少量代码,且该代码足够直观。
  • 方法调用频繁:​该方法在代码中被多次调用,且每次调用的上下文相似。
  • 无额外逻辑:​方法没有副作用,如修改全局状态或执行复杂计算。
  • 提高可读性:​内联后,代码的意图更加明确,易于理解。

2.重构前

public class Order
{
    private int _numberOfLateReturns;

    public int GetRating()
    {
        return MoreThanThreeLateReturns() ? 0 : 1;
    }

    private bool MoreThanThreeLateReturns()
    {
        return _numberOfLateReturns > 3;
    }
}

  • 方法过于简单:​MoreThanThreeLateReturns 方法仅返回一个条件判断,实际代码中并未增加额外的逻辑或可读性。
  • 增加了代码复杂性:​引入了额外的方法调用,增加了理解和维护的难度。
  • 违反了“不要过度抽象”原则:​在这种情况下,过度抽象反而使代码变得冗长。

3.重构后

public class Order
{
    private int _numberOfLateReturns;

    public int GetRating()
    {
        return _numberOfLateReturns > 3 ? 0 : 1;
    }
}

  • 提高可读性:​直接表达了“如果迟还超过三次,则评级为 0,否则为 1”的逻辑。
  • 减少了不必要的间接调用:​代码更简洁,减少了方法调用的层级。
  • 符合“简单优于复杂”原则:​代码更直观,易于理解。

三、提取变量(Extract Variable)

1.概述

提取变量(Extract Variable) 是一种重构技术,旨在将复杂的表达式或重复使用的代码片段提取为具名的局部变量。 这一过程提高了代码的可读性、可维护性和可测试性。

适用场景

  • 复杂表达式:​当条件判断或计算表达式过于复杂时,提取中间结果为变量可以增强可读性。
  • 重复代码:​当相同的表达式在多个地方出现时,提取为变量可以减少重复代码。
  • 长方法:​当方法体过长时,提取变量可以帮助分解复杂逻辑。
  • 嵌套逻辑:​当存在多层嵌套的条件判断时,提取变量可以简化逻辑结构。

2.重构前

public void ProcessOrder(Order order)
{
    if (order.Quantity * order.Price > 1000 && order.Customer.IsPremium)
    {
        // 执行某些操作
    }
}

  • order.Quantity * order.Price 的计算逻辑不明确,可能导致理解困难。
  • 条件判断过于复杂,增加了出错的概率。
  • 重复计算了 order.Quantity * order.Price,降低了效率。

3.重构后

public void ProcessOrder(Order order)
{
    var totalAmount = order.Quantity * order.Price;
    var isPremiumCustomer = order.Customer.IsPremium;
    
    if (totalAmount > 1000 && isPremiumCustomer)
    {
        // 执行某些操作
    }
}

  • 通过引入 totalAmountisPremiumCustomer,使代码的意图更加明确。
  • 避免了重复计算,提高了代码效率。
  • 简化了条件判断,降低了出错的概率。

四、内联临时变量(Inline Temp)

1.概述

内联临时变量(Inline Temp) 是一种代码重构技术,旨在消除那些只使用一次且赋值后不再修改的临时变量。通过将这些变量直接替换为其赋值表达式,可以提高代码的简洁性和可读性。

适用场景

  • 临时变量只被使用一次:如果一个变量在赋值后仅被使用一次,保留该变量可能会增加代码的复杂性。
  • 变量命名不具备表达性:当变量名无法清晰表达其用途时,内联该变量可以提高代码的可理解性。
  • 赋值表达式简单:赋值给临时变量的表达式较为简单,直接使用该表达式不会影响代码的可读性。

2.重构前

public decimal CalculateDiscount(decimal price)
{
    decimal discountRate = GetDiscountRate();
    return price - (discountRate * price);
}
  • discountRate 变量仅在 return 语句中使用一次,且其赋值表达式 GetDiscountRate() 直接返回一个值。

3.重构后

public decimal CalculateDiscount(decimal price)
{
    return price - (GetDiscountRate() * price);
}
  • 通过将 GetDiscountRate() 的调用直接嵌入到 return 语句中,消除了临时变量 discountRate,使代码更加简洁。

五、以查询取代临时变量(Replace Temp with Query)

1.概述

“以查询取代临时变量”(Replace Temp with Query) 是指当你在方法中使用临时变量存储某个表达式的计算结果时,可以考虑将该表达式提炼为一个独立的查询方法。​这样做不仅可以提高代码的可读性,还能为后续的重构和维护提供便利。

适用场景

  • 临时变量用于存储复杂表达式的结果:​如果临时变量存储了一个复杂的表达式结果,并且该表达式在方法中多次使用,可以考虑将其提取为一个查询方法。​
  • 临时变量的计算逻辑具有独立性:​如果临时变量的计算逻辑可以独立于当前方法存在,并且具有一定的业务意义,提取为查询方法可以提高代码的模块化程度。​
  • 提高代码的可读性和可维护性:​通过将复杂的表达式提取为具有描述性名称的查询方法,可以使代码的意图更加明确,减少理解上的障碍。

2.重构前

public class Order
{
    private int _numberOfLateReturns;

    public int GetRating()
    {
        double basePrice = _numberOfLateReturns * 100; // 假设每次迟还扣除100元
        if (basePrice > 1000)
        {
            return 0; // 评级为0
        }
        else
        {
            return 1; // 评级为1
        }
    }
}
  • 临时变量的计算逻辑与业务逻辑混杂:​basePrice 的计算逻辑直接嵌入在 GetRating 方法中,导致方法的职责不单一。​
  • 难以维护和扩展:​如果未来需要调整 basePrice 的计算逻辑,可能需要修改多个方法,增加了维护的难度。​
  • 降低代码的可读性:​basePrice 的计算逻辑可能对其他开发人员不直观,增加了理解的成本。

3.重构后

public class Order
{
    private int _numberOfLateReturns;

    public int GetRating()
    {
        if (BasePrice() > 1000)
        {
            return 0; // 评级为0
        }
        else
        {
            return 1; // 评级为1
        }
    }

    private double BasePrice()
    {
        return _numberOfLateReturns * 100; // 假设每次迟还扣除100元
    }
}
  • 提高代码的可读性:​BasePrice 方法的名称清晰地表达了其计算的内容,使代码的意图更加明确。​
  • 增强代码的可维护性:​如果未来需要调整 basePrice 的计算逻辑,只需修改 BasePrice 方法,避免了在多个方法中重复修改。​
  • 符合单一职责原则:​GetRating 方法专注于获取评级,BasePrice 方法专注于计算基本价格,职责更加清晰。​

六、拆分临时变量

1.概述

“拆分临时变量”(Split Temporary Variable) 是指将一个临时变量的多次赋值拆分成多个独立的临时变量,以提高代码的可读性和可维护性。​

在某些情况下,临时变量可能会被赋值多次,每次赋值代表不同的逻辑目的。​这种做法会导致临时变量承担多个职责,增加代码的复杂性。​通过将其拆分为多个独立的临时变量,可以使代码更加清晰,职责更加明确。

适用场景

  • 临时变量被赋值多次:​如果一个临时变量在同一方法中被赋值多次,每次赋值代表不同的逻辑目的。​
  • 提高代码的可读性和可维护性:​通过将一个临时变量拆分为多个独立的临时变量,可以使代码的意图更加明确,减少理解上的障碍。​
  • 遵循单一职责原则:​每个临时变量只承担一个职责,符合单一职责原则

2.重构前

public class Rectangle
{
    private int _width;
    private int _height;

    public void PrintAreaAndPerimeter()
    {
        int temp = 2 * (_width + _height);
        Console.WriteLine($"Perimeter: {temp}");
        temp = _width * _height;
        Console.WriteLine($"Area: {temp}");
    }
}
  • 临时变量的多次赋值:​temp 变量被赋值两次,分别用于存储周长和面积,导致其承担了多个职责。
  • 降低代码的可读性:​temp 的值在两次赋值之间发生变化,增加了理解的难度。
  • 违反了单一职责原则:​temp 变量同时承担了计算周长和计算面积的职责。

3.重构后

public class Rectangle
{
    private int _width;
    private int _height;

    public void PrintAreaAndPerimeter()
    {
        int perimeter = 2 * (_width + _height);
        Console.WriteLine($"Perimeter: {perimeter}");
        int area = _width * _height;
        Console.WriteLine($"Area: {area}");
    }
}
  • 提高代码的可读性:​perimeterarea 变量的名称清晰地表达了其存储的内容,使代码的意图更加明确。​
  • 增强代码的可维护性:​如果未来需要调整周长或面积的计算逻辑,只需修改对应的变量,避免了在多个地方修改。​
  • 符合单一职责原则:​每个临时变量只承担一个职责,职责更加清晰。

七、移除参数赋值(Remove Assignments to Parameters)

1.概述

移除参数赋值(Remove Assignments to Parameters) 是指消除在方法内部对参数的赋值操作。​这种做法有助于提高代码的可读性、可维护性和可预测性。

通常做法

  • 建立一个临时变量,将待处理的参数值赋予它。
  • 以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
  • 修改赋值语句,使其改为对新建的临时变量赋值。

2.重构前

public class Order
{
    private int _numberOfLateReturns;

    public int GetRating(int inputVal)
    {
        if (inputVal > 50)
        {
            inputVal -= 2;
        }
        return inputVal;
    }
}
  • 参数被修改:​inputVal 参数在方法内部被修改,可能导致调用者误以为传入的参数值会被改变。
  • 降低代码的可预测性:​修改参数的值可能导致方法的行为不明确,增加了代码的复杂性。
  • 违反单一职责原则:​方法同时承担了修改参数和执行业务逻辑的职责,导致职责不清晰。

3.重构后

public class Order
{
    private int _numberOfLateReturns;

    public int GetRating(int inputVal)
    {
        int result = inputVal;
        if (result > 50)
        {
            result -= 2;
        }
        return result;
    }
}
  • 提高代码的可读性:​result 变量的名称清晰地表达了其存储的内容,使代码的意图更加明确。
  • 增强代码的可维护性:​如果未来需要调整折扣逻辑,只需修改 GetRating 方法,避免了在多个地方修改。
  • 符合单一职责原则:​GetRating 方法专注于获取评级,职责更加清晰。

八、用方法对象替代方法(Replace Method with Method Object)

1.概述

“用方法对象替代方法”(Replace Method with Method Object) 是指将一个复杂的、包含多个局部变量和逻辑的长方法提取到一个新的类中,使其成为该类的一个方法对象。​这种做法有助于解决方法过长、局部变量交织复杂、难以提取子方法等问题。​通过将方法提取到独立的类中,可以将局部变量转化为该类的字段,从而更容易地拆分复杂的逻辑。

适用场景

  • 方法过长:​方法包含大量的逻辑和局部变量,导致代码难以理解和维护。
  • 局部变量交织复杂:​方法中的局部变量之间相互依赖,难以独立提取子方法。
  • 需要拆分复杂逻辑:​希望将复杂的逻辑拆分成多个小的、易于理解和维护的部分。
  • 方法内部逻辑独立:​方法内部的逻辑可以独立于原始类存在,适合提取到新的类中。

2.重构前

public class Order
{
    private int _numberOfLateReturns;
    private double _basePrice;
    private double _discount;

    public double CalculateTotal()
    {
        double primaryBasePrice = _basePrice;
        double secondaryBasePrice = primaryBasePrice * 0.1;
        double tertiaryBasePrice = secondaryBasePrice * 0.05;
        double discount = _discount;
        double total = primaryBasePrice + secondaryBasePrice + tertiaryBasePrice - discount;
        return total;
    }
}
  • 方法过长:​CalculateTotal 方法包含了多个计算逻辑,导致方法过长。
  • 局部变量交织复杂:​primaryBasePricesecondaryBasePricetertiaryBasePricediscount 等局部变量之间相互依赖,难以独立提取子方法。
  • 难以扩展:​如果未来需要调整计算逻辑,可能需要修改多个地方,增加了维护的难度。

3.重构后

public class Order
{
    private int _numberOfLateReturns;
    private double _basePrice;
    private double _discount;

    public double CalculateTotal()
    {
        return new PriceCalculator(this).Compute();
    }
}

public class PriceCalculator
{
    private double _primaryBasePrice;
    private double _secondaryBasePrice;
    private double _tertiaryBasePrice;
    private double _discount;

    public PriceCalculator(Order order)
    {
        _primaryBasePrice = order._basePrice;
        _secondaryBasePrice = _primaryBasePrice * 0.1;
        _tertiaryBasePrice = _secondaryBasePrice * 0.05;
        _discount = order._discount;
    }

    public double Compute()
    {
        return _primaryBasePrice + _secondaryBasePrice + _tertiaryBasePrice - _discount;
    }
}
  • 提高代码的可读性:​PriceCalculator 类的名称清晰地表达了其功能,使代码的意图更加明确。
  • 增强代码的可维护性:​如果未来需要调整价格计算逻辑,只需修改 PriceCalculator 类,避免了在多个地方修改。
  • 符合单一职责原则:​Order 类专注于订单的管理,PriceCalculator 类专注于价格的计算,职责更加清晰。

九、替换算法(Substitute Algorithm)

1.概述

替换算法(Substitute Algorithm) 是指将现有方法中的复杂或低效算法替换为更简单、清晰或高效的算法。​这种做法有助于提高代码的可读性、可维护性和性能。

适用场景

  • 现有算法过于复杂:​实现逻辑冗长、嵌套深,影响代码可读性。​
  • 性能问题:​当前算法效率低,影响系统运行性能。​
  • 更好的替代方案:​有更直观或更高效的算法能够完成相同功能。

2.重构前

假设我们有一个计算两个整数最大公约数(GCD)的方法,使用了暴力法:

public int CalculateGCD(int a, int b)
{
    List<int> divisorsA = GetDivisors(a);
    List<int> divisorsB = GetDivisors(b);

    int gcd = 1;
    foreach (var divisor in divisorsA)
    {
        if (divisorsB.Contains(divisor))
        {
            gcd = Math.Max(gcd, divisor);
        }
    }

    return gcd;
}

private List<int> GetDivisors(int number)
{
    List<int> divisors = new List<int>();
    for (int i = 1; i <= number; i++)
    {
        if (number % i == 0)
        {
            divisors.Add(i);
        }
    }
    return divisors;
}
  • 性能低下:​获取所有约数的方法效率低,尤其是输入较大时。​
  • 算法复杂:​算法使用了多个循环和集合操作,显得冗长复杂。

3.重构后

使用更高效的欧几里得算法代替:

public int CalculateGCD(int a, int b)
{
    while (b != 0)
    {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}
  • 性能提升:​复杂度从 O(n) 降至 O(log(min(a, b)))。​
  • 代码简洁:​算法逻辑清晰,易于理解和维护。​
  • 符合数学原理:​欧几里得算法是计算 GCD 的经典方法,广泛应用于数学和计算机科学中。

十、引入解释性变量(Introduce Explaining Variable)

1.概述

引入解释性变量(Introduce Explaining Variable) 是指通过将复杂表达式的部分结果赋值给具有描述性的临时变量,从而提高代码的可读性和可维护性。​这种做法有助于使代码的意图更加明确,减少理解上的障碍。

适用场景

  • 复杂的布尔表达式:​条件判断中包含多个逻辑操作,导致表达式难以理解。​
  • 嵌套的数学计算:​数学表达式中包含多个嵌套的运算,影响代码的可读性。​
  • 重复使用的表达式:​相同的表达式在代码中多次出现,容易引发错误。​
  • 需要提高代码的可读性和可维护性:​通过引入解释性变量,使代码的意图更加明确,便于后续的维护和扩展。

2.重构前

public bool CanAccess(User user)
{
    return (user.Role == "Admin" && user.IsActive) || (user.Role == "User" && user.IsActive && user.HasPaid);
}
  • 表达式复杂:​条件判断中包含多个逻辑操作,导致表达式难以理解。​
  • 可读性差:​代码的意图不明确,需要花费时间去理解每个条件的含义。

3.重构后

public bool CanAccess(User user)
{
    bool isAdminAndActive = user.Role == "Admin" && user.IsActive;
    bool isUserAndEligible = user.Role == "User" && user.IsActive && user.HasPaid;

    return isAdminAndActive || isUserAndEligible;
}
  • 提高代码的可读性:​解释性变量的名称清晰地表达了其含义,使代码的意图更加明确。​
  • 增强代码的可维护性:​如果未来需要调整访问权限的逻辑,只需修改对应的解释性变量,避免了在多个地方修改。​
  • 便于调试和测试:​可以单独测试每个解释性变量的逻辑,便于发现和修复问题。