重构基础手法(下):分解条件表达式、合并重复代码、移除对参数的赋值
好,咱们接着聊重构的基础手法。上一节讲了提取方法和临时变量,今天这三个手法,说白了就是专门对付「烂代码三兄弟」的——条件逻辑混乱、重复代码满天飞、参数被乱改。
我个人习惯,每次接手遗留系统,第一件事就是扫一遍条件表达式。你想想看,一个方法里嵌套四五层 if-else,读起来跟走迷宫似的。这时候,分解条件表达式就是你的救星。
分解条件表达式(Decompose Conditional)
什么叫分解条件表达式?就是把复杂的条件判断拆成独立的方法。每个方法只做一件事,名字起得清清楚楚。
举个例子,你可能会看到这样的代码:
if (date.Before(SUMMER_START) || date.After(SUMMER_END))
{
charge = quantity * winterRate + winterServiceCharge;
}
else
{
charge = quantity * summerRate;
}
这段代码本身不长,但逻辑混在一起。改写成这样:
if (IsWinter(date))
{
charge = WinterCharge(quantity);
}
else
{
charge = SummerCharge(quantity);
}
然后每个方法单独定义:
private bool IsWinter(DateTime date)
{
return date.Before(SUMMER_START) || date.After(SUMMER_END);
}
private double WinterCharge(double quantity)
{
return quantity * winterRate + winterServiceCharge;
}
private double SummerCharge(double quantity)
{
return quantity * summerRate;
}
核心要点:条件表达式拆开后,每个分支的逻辑都变成了自解释的方法。读代码的人不用再琢磨「这个条件到底是什么意思」,直接看方法名就懂了。
我在项目中遇到过最夸张的一个案例——一个方法里写了 12 层嵌套的条件判断,光括号就对了半天。拆完之后,变成了 15 个小方法,每个方法不超过 5 行。测试也好写了,维护也轻松了。
我的小技巧:拆条件的时候,先看条件本身能不能独立成方法。比如「是否在夏季」「是否VIP客户」「是否超时」这些,都是很好的候选。别急着拆分支逻辑,先把条件判断拎出来。
合并重复代码(Consolidate Duplicate Code)
重复代码,说白了就是「Ctrl+C、Ctrl+V」的后遗症。你想想看,同一个逻辑散落在三四个地方,改需求的时候漏了一个,bug 就来了。
合并重复代码,不是简单地把代码复制到一起。你得先分析:这些重复的片段,到底是完全一样,还是「长得像」?
完全一样的,直接提取成方法。长得像的,用参数化方法处理。
看个例子:
// 文件A
double totalA = priceA * quantityA;
double discountA = totalA * 0.1;
double finalA = totalA - discountA;
// 文件B
double totalB = priceB * quantityB;
double discountB = totalB * 0.1;
double finalB = totalB - discountB;
合并之后:
private double CalculateFinalPrice(double price, double quantity)
{
double total = price * quantity;
double discount = total * 0.1;
return total - discount;
}
避坑指南:我曾经犯过一个错误——看到两段代码「差不多」,就直接合并了。结果发现它们对边界条件的处理不一样。合并之前,一定要确认逻辑完全等价。否则,你合并的不是代码,是 bug。
还有一种情况,重复代码出现在不同的类里。这时候,可以考虑用继承或者组合来消除重复。我个人更倾向于组合,因为继承容易把类层次搞得太深。
移除对参数的赋值(Remove Assignments to Parameters)
这个手法,很多新手容易忽略。什么叫对参数赋值?就是在方法内部,直接修改传入的参数值。
比如:
public int CalculateDiscount(int amount)
{
if (amount > 100)
{
amount = amount - 10; // 直接修改了参数
}
return amount * 0.9;
}
这段代码的问题在哪?参数是方法的输入,不是临时变量。你改了它,调用方如果后续还用这个变量,就会出问题。而且,代码的可读性也变差了——别人读到这里,得停下来想「这个 amount 现在到底是多少?」
正确的做法:引入临时变量。
public int CalculateDiscount(int amount)
{
int discountedAmount = amount;
if (discountedAmount > 100)
{
discountedAmount = discountedAmount - 10;
}
return discountedAmount * 0.9;
}
为什么这么做?因为参数是「输入」,临时变量是「中间结果」。把两者分开,代码的意图就清晰了。你一看就知道:amount 是原始值,discountedAmount 是计算后的值。
我记得有一次 code review,看到同事写了一段代码,参数被改了七八次。我问他为什么要这么写,他说「省一个变量」。嗯,省一个变量,省出来的却是维护的噩梦。
还有一种特殊情况——引用类型的参数。比如传入一个 List,你在方法里往里面添加元素。这算不算对参数赋值?严格来说,不算。因为你没有改变参数本身的引用,只是修改了它指向的对象。但这里有个坑:如果你在方法里 new 了一个新对象赋值给参数,那就改变了引用,调用方是感知不到的。
public void AddItem(List<string> items, string item)
{
items.Add(item); // 没问题,修改的是对象内容
}
public void ResetList(List<string> items)
{
items = new List<string>(); // 有问题,改变了引用
}
我的建议:不管什么类型,只要不是绝对必要,就别改参数。用临时变量,成本几乎为零,收益却很大——代码更清晰,调试更容易,重构更安全。
三个手法的关系
这三个手法,其实是一个递进关系:
- 分解条件表达式:让条件逻辑变得清晰
- 合并重复代码:消除散落的重复逻辑
- 移除对参数的赋值:让方法的行为可预测
它们的目标都一样——让代码更容易理解和修改。你想想看,一个方法如果条件清晰、没有重复、参数不被乱改,读起来是不是舒服多了?
在实际项目中,我通常先做「移除对参数的赋值」,因为这是最基础、最安全的。然后做「合并重复代码」,最后才处理复杂的条件表达式。顺序很重要——先清理小问题,再解决大问题。
好了,这一章就到这里。下一章,咱们聊聊「重新组织函数」——如何把一个大方法拆成多个小方法,让代码真正变得可维护。