当前位置: 首页 > news >正文

(六)重构的艺术:简化复杂条件逻辑的秘诀

代码重构专题文章:

(一)代码匠心:重构之道,化腐朽为神奇
(二)重构的艺术:精进代码的第一组基本功
(三)封装与结构优化:让代码更优雅
(四)优雅重构:洞悉“搬移特性”的艺术与实践
(五)数据重构的艺术:优化你的代码结构与可读性
(六)重构的艺术:简化复杂条件逻辑的秘诀

文章目录

  • 代码重构专题文章:
  • 第 10 章 重构的艺术:简化复杂条件逻辑的秘诀
    • 0 前言
    • 10.1 分解条件表达式(Decompose Conditional)
      • 动机
      • 做法
      • 范例
    • 10.2 合并条件表达式(Consolidate Conditional Expression)
      • 动机
      • 做法
      • 范例
      • 范例:使用逻辑与
    • 10.3 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
      • 动机
      • 做法
      • 范例
      • 范例:将条件反转
    • 10.4 以多态取代条件表达式(Replace Conditional with Polymorphism)
      • 动机
      • 做法
      • 范例:鸟类行为分析
      • 范例:用多态处理变体逻辑——航运评级系统
    • 10.5 引入特例(Introduce Special Case)
      • 动机
      • 做法
      • 范例
        • 原始Java代码示例
        • 1. 修改 `Customer` 接口和创建 `UnknownCustomer` 类
        • 2. 封装特例比较逻辑
        • 3. 修改 `Site` 类,返回特例对象
        • 4. 更新 `isUnknownCustomer` 方法
        • 5. 逐步迁移客户端行为并内联 `isUnknownCustomer`
      • 范例:使用对象字面量(Java中的记录或不可变类)
      • 范例:使用变换(`Stream` API 或辅助方法)
    • 10.6 引入断言(Introduce Assertion)
      • 动机
      • 做法
      • 范例
        • 原始Java代码示例
        • 1. 转换条件表达式 (如果需要)
        • 2. 在 `applyDiscount` 方法中引入断言
        • 3. 将断言移动到设值函数中(更佳实践)
    • 参考

第 10 章 重构的艺术:简化复杂条件逻辑的秘诀

第 10 章 简化条件逻辑

0 前言

程序的大部分威力来自条件逻辑,但很不幸,程序的复杂度也大多来自条件逻辑。我经常借助重构把条件逻辑变得更容易理解。我常用分解条件表达式(260)处理复杂的条件表达式,用合并条件表达式(263)厘清逻辑组合。我会用以卫语句取代嵌套条件表达式(266)清晰表达“在主要处理逻辑之前先做检查”的意图。如果我发现一处 switch 逻辑处理了几种情况,可以考虑拿出以多态取代条件表达式(272)重构手法。

很多条件逻辑是用于处理特殊情况的,例如处理 null 值。如果对某种特殊情况的处理逻辑大多相同,那么可以用引入特例(289)(常被称作引入空对象)消除重复代码。另外,虽然我很喜欢去除条件逻辑,但如果我想明确地表述(以及检查)程序的状态,引入断言(302)是一个不错的补充。

10.1 分解条件表达式(Decompose Conditional)

// 原始代码
public double calculateCharge(Date aDate, Plan plan, int quantity) {double charge;if (!aDate.before(plan.getSummerStart()) && !aDate.after(plan.getSummerEnd())) {charge = quantity * plan.getSummerRate();} else {charge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();}return charge;
}

重构后:

public double calculateCharge(Date aDate, Plan plan, int quantity) {if (isSummer(aDate, plan)) {return summerCharge(quantity, plan);} else {return regularCharge(quantity, plan);}
}private boolean isSummer(Date aDate, Plan plan) {return !aDate.before(plan.getSummerStart()) && !aDate.after(plan.getSummerEnd());
}private double summerCharge(int quantity, Plan plan) {return quantity * plan.getSummerRate();
}private double regularCharge(int quantity, Plan plan) {return quantity * plan.getRegularRate() + plan.getRegularServiceCharge();
}

动机

程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。我必须编写代码来检查不同的条件分支,根据不同的条件做不同的事,然后,我很快就会得到一个相当长的函数。大型函数本身就会使代码的可读性下降,而条件逻辑则会使代码更难阅读。在带有复杂条件逻辑的函数中,代码(包括检查条件分支的代码和真正实现功能的代码)会告诉我发生的事,但常常让我弄不清楚为什么会发生这样的事,这就说明代码的可读性的确大大降低了。

和任何大块头代码一样,我可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。

本重构手法其实只是提炼函数(Extract Method)的一个应用场景。但我要特别强调这个场景,因为我发现它经常会带来很大的价值。

做法

对条件判断和每个条件分支分别运用提炼函数(Extract Method)手法。

范例

假设我要计算购买某样商品的总价(总价=数量 × 单价),而这个商品在冬季和夏季的单价是不同的:

// 原始代码
public double calculateCharge(Date aDate, Plan plan, int quantity) {double charge;if (!aDate.before(plan.getSummerStart()) && !aDate.after(plan.getSummerEnd()))charge = quantity * plan.getSummerRate();elsecharge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();return charge;
}

我把条件判断提炼到一个独立的函数中:

public double calculateCharge(Date aDate, Plan plan, int quantity) {double charge;if (isSummer(aDate, plan))charge = quantity * plan.getSummerRate();elsecharge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();return charge;
}private boolean isSummer(Date aDate, Plan plan) {return !aDate.before(plan.getSummerStart()) && !aDate.after(plan.getSummerEnd());
}

然后提炼条件判断为真的分支:

public double calculateCharge(Date aDate, Plan plan, int quantity) {double charge;if (isSummer(aDate, plan))charge = summerCharge(quantity, plan); // 调用新的 summerCharge 函数elsecharge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();return charge;
}private boolean isSummer(Date aDate, Plan plan) {return !aDate.before(plan.getSummerStart()) && !aDate.after(plan.getSummerEnd());
}private double summerCharge(int quantity, Plan plan) {return quantity * plan.getSummerRate();
}

最后提炼条件判断为假的分支:

public double calculateCharge(Date aDate, Plan plan, int quantity) {double charge;if (isSummer(aDate, plan))charge = summerCharge(quantity, plan);elsecharge = regularCharge(quantity, plan); // 调用新的 regularCharge 函数return charge;
}private boolean isSummer(Date aDate, Plan plan) {return !aDate.before(plan.getSummerStart()) && !aDate.after(plan.getSummerEnd());
}private double summerCharge(int quantity, Plan plan) {return quantity * plan.getSummerRate();
}private double regularCharge(int quantity, Plan plan) {return quantity * plan.getRegularRate() + plan.getRegularServiceCharge();
}

提炼完成后,我喜欢用三元运算符(在Java中,更常见的做法是直接返回条件表达式的结果)重新安排条件语句,让代码更简洁:

public double calculateCharge(Date aDate, Plan plan, int quantity) {return isSummer(aDate, plan) ? summerCharge(quantity, plan) : regularCharge(quantity, plan);
}private boolean isSummer(Date aDate, Plan plan) {return !aDate.before(plan.getSummerStart()) && !aDate.after(plan.getSummerEnd());
}private double summerCharge(int quantity, Plan plan) {return quantity * plan.getSummerRate();
}private double regularCharge(int quantity, Plan plan) {return quantity * plan.getRegularRate() + plan.getRegularServiceCharge();
}

10.2 合并条件表达式(Consolidate Conditional Expression)

// 原始代码
public double disabilityAmount(Employee anEmployee) {if (anEmployee.getSeniority() < 2) return 0;if (anEmployee.getMonthsDisabled() > 12) return 0;if (anEmployee.isPartTime()) return 0;// compute the disability amountreturn calculateActualDisabilityAmount(anEmployee);
}

重构后:

public double disabilityAmount(Employee anEmployee) {if (isNotEligibleForDisability(anEmployee)) {return 0;}// compute the disability amountreturn calculateActualDisabilityAmount(anEmployee);
}private boolean isNotEligibleForDisability(Employee anEmployee) {return anEmployee.getSeniority() < 2 ||anEmployee.getMonthsDisabled() > 12 ||anEmployee.isPartTime();
}

动机

有时我会发现这样一串条件检查:检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用“逻辑或”(||)和“逻辑与”(&&)将它们合并为一个条件表达式。

之所以要合并条件代码,有两个重要原因。首先,合并后的条件代码会表述“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。当然,合并前和合并后的代码有着相同的效果,但原先代码传达出的信息却是“这里有一些各自独立的条件测试,它们只是恰好同时发生”。其次,这项重构往往可以为使用提炼函数(Extract Method)做好准备。将检查条件提炼成一个独立的函数对于厘清代码意义非常有用,因为它把描述“做什么”的语句换成了“为什么这样做”。

条件语句的合并理由也同时指出了不要合并的理由:如果我认为这些检查的确彼此独立,的确不应该被视为同一次检查,我就不会使用本项重构。

做法

确定这些条件表达式都没有副作用。

如果某个条件表达式有副作用,可以先用将查询函数和修改函数分离(Separate Query from Modifier)处理。

使用适当的逻辑运算符,将两个相关条件表达式合并为一个。

顺序执行的条件表达式用逻辑或来合并,嵌套的 if 语句用逻辑与来合并。

测试。

重复前面的合并过程,直到所有相关的条件表达式都合并到一起。

可以考虑对合并后的条件表达式实施提炼函数(Extract Method)。

范例

在走读代码的过程中,我看到了下面的代码片段:

public double disabilityAmount(Employee anEmployee) {if (anEmployee.getSeniority() < 2) return 0;if (anEmployee.getMonthsDisabled() > 12) return 0;if (anEmployee.isPartTime()) return 0;// compute the disability amountreturn calculateActualDisabilityAmount(anEmployee); // 假设有一个实际计算方法
}

这里有一连串的条件检查,都指向同样的结果。既然结果是相同的,就应该把这些条件检查合并成一条表达式。对于这样顺序执行的条件检查,可以用逻辑或运算符来合并。

public double disabilityAmount(Employee anEmployee) {if (anEmployee.getSeniority() < 2 || anEmployee.getMonthsDisabled() > 12) return 0;if (anEmployee.isPartTime()) return 0;// compute the disability amountreturn calculateActualDisabilityAmount(anEmployee);
}

测试,然后把下一个条件检查也合并进来:

public double disabilityAmount(Employee anEmployee) {if (anEmployee.getSeniority() < 2 || anEmployee.getMonthsDisabled() > 12 || anEmployee.isPartTime()) return 0;// compute the disability amountreturn calculateActualDisabilityAmount(anEmployee);
}

合并完成后,再对这句条件表达式使用提炼函数(Extract Method)。

public double disabilityAmount(Employee anEmployee) {if (isNotEligibleForDisability(anEmployee)) {return 0;}// compute the disability amountreturn calculateActualDisabilityAmount(anEmployee);
}private boolean isNotEligibleForDisability(Employee anEmployee) {return anEmployee.getSeniority() < 2 ||anEmployee.getMonthsDisabled() > 12 ||anEmployee.isPartTime();
}

范例:使用逻辑与

上面的例子展示了用逻辑或合并条件表达式的做法。不过,我有可能遇到需要逻辑与的情况。例如,嵌套 if 语句的情况:

// 原始代码
public double calculateBonus(Employee anEmployee) {if (anEmployee.isOnVacation()) {if (anEmployee.getSeniority() > 10) {return 1;}}return 0.5;
}

可以用逻辑与运算符将其合并。

public double calculateBonus(Employee anEmployee) {if (anEmployee.isOnVacation() && anEmployee.getSeniority() > 10) {return 1;}return 0.5;
}

如果原来的条件逻辑混杂了这两种情况,我也会根据需要组合使用逻辑与和逻辑或运算符。在这种时候,代码很可能变得混乱,所以我会频繁使用提炼函数(Extract Method),把代码变得可读。

10.3 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)

// 原始代码
public double getPayAmount(Employee employee) {double result;if (employee.isDead()) {result = deadAmount();} else {if (employee.isSeparated()) {result = separatedAmount();} else {if (employee.isRetired()) {result = retiredAmount();} else {result = normalPayAmount();}}}return result;
}

重构后:

public double getPayAmount(Employee employee) {if (employee.isDead()) {return deadAmount();}if (employee.isSeparated()) {return separatedAmount();}if (employee.isRetired()) {return retiredAmount();}return normalPayAmount();
}

动机

根据我的经验,条件表达式通常有两种风格。第一种风格是:两个条件分支都属于正常行为。第二种风格则是:只有一个条件分支是正常行为,另一个分支则是异常的情况。

这两类条件表达式有不同的用途,这一点应该通过代码表现出来。如果两条分支都是正常行为,就应该使用形如 if...else... 的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”(guard clauses)。

以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。如果使用 if-then-else 结构,你对 if 分支和 else 分支的重视是同等的。这样的代码结构传递给阅读者的消息就是:各个分支有同样的重要性。卫语句就不同了,它告诉阅读者:“这种情况不是本函数的核心逻辑所关心的,如果它真发生了,请做一些必要的整理工作,然后退出。”

“每个函数只能有一个入口和一个出口”的观念,根深蒂固于某些程序员的脑海里。我发现,当我处理他们编写的代码时,经常需要使用以卫语句取代嵌套条件表达式。现今的编程语言都会强制保证每个函数只有一个入口,至于“单一出口”规则,其实不是那么有用。在我看来,保持代码清晰才是最关键的:如果单一出口能使这个函数更清楚易读,那么就使用单一出口;否则就不必这么做。

做法

选中最外层需要被替换的条件逻辑,将其替换为卫语句。

测试。

有需要的话,重复上述步骤。

如果所有卫语句都引发同样的结果,可以使用合并条件表达式(Consolidate Conditional Expression)合并之。

范例

下面的代码用于计算要支付给员工(employee)的工资。只有还在公司上班的员工才需要支付工资,所以这个函数需要检查两种“员工已经不在公司上班”的情况。

// 原始代码
public PaymentInfo payAmount(Employee employee) {PaymentInfo result;if (employee.isSeparated()) {result = new PaymentInfo(0, "SEP");} else {if (employee.isRetired()) {result = new PaymentInfo(0, "RET");} else {// logic to compute amount// A placeholder for complex computationperformComplexCalculationStep1();performComplexCalculationStep2();performComplexCalculationStep3();result = someFinalComputation();}}return result;
}// 假设 PaymentInfo 是一个简单的类
class PaymentInfo {private double amount;private String reasonCode;public PaymentInfo(double amount, String reasonCode) {this.amount = amount;this.reasonCode = reasonCode;}// Getters for amount and reasonCodepublic double getAmount() { return amount; }public String getReasonCode() { return reasonCode; }
}

嵌套的条件逻辑让我们看不清代码真实的含义。只有当前两个条件表达式都不为真的时候,这段代码才真正开始它的主要工作。所以,卫语句能让代码更清晰地阐述自己的意图。

一如既往地,我喜欢小步前进,所以我先处理最顶上的条件逻辑。

public PaymentInfo payAmount(Employee employee) {if (employee.isSeparated()) {return new PaymentInfo(0, "SEP"); // 替换为卫语句}// else 块现在是主逻辑的开始if (employee.isRetired()) {return new PaymentInfo(0, "RET");} else {// logic to compute amountperformComplexCalculationStep1();performComplexCalculationStep2();performComplexCalculationStep3();return someFinalComputation(); // 直接返回}
}

做完这步修改,我执行测试,然后继续下一步。

public PaymentInfo payAmount(Employee employee) {if (employee.isSeparated()) {return new PaymentInfo(0, "SEP");}if (employee.isRetired()) { // 替换为卫语句return new PaymentInfo(0, "RET");}// 现在,主要的计算逻辑是函数中剩余的部分performComplexCalculationStep1();performComplexCalculationStep2();performComplexCalculationStep3();return someFinalComputation();
}

此时,result 变量已经没有用处了,因为它在每个分支中都被直接返回了。在Java中,我们通常不会先声明 result 变量再赋值,而是直接返回。

public PaymentInfo payAmount(Employee employee) {if (employee.isSeparated()) {return new PaymentInfo(0, "SEP");}if (employee.isRetired()) {return new PaymentInfo(0, "RET");}// logic to compute amountperformComplexCalculationStep1();performComplexCalculationStep2();performComplexCalculationStep3();return someFinalComputation();
}

能减少一个可变变量总是好的。

范例:将条件反转

审阅本书第 1 版的初稿时,Joshua Kerievsky 指出:我们常常可以将条件表达式反转,从而实现以卫语句取代嵌套条件表达式。为了拯救我可怜的想象力,他还好心帮我想了一个例子:

// 原始代码
public double adjustedCapital(Instrument anInstrument) {double result = 0;if (anInstrument.getCapital() > 0) {if (anInstrument.getInterestRate() > 0 && anInstrument.getDuration() > 0) {result = (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor();}}return result;
}// 假设 Instrument 类
class Instrument {private double capital;private double interestRate;private double duration;private double income;private double adjustmentFactor;// Getters for all fieldspublic double getCapital() { return capital; }public double getInterestRate() { return interestRate; }public double getDuration() { return duration; }public double getIncome() { return income; }public double getAdjustmentFactor() { return adjustmentFactor; }
}

同样地,我逐一进行替换。不过这次在插入卫语句时,我需要将相应的条件反转过来:

public double adjustedCapital(Instrument anInstrument) {// 条件反转:如果 capital 不大于 0,则直接返回 0if (anInstrument.getCapital() <= 0) {return 0; // 或者 result}// 原始代码中的 result 变量现在可以去掉了,直接使用返回值if (anInstrument.getInterestRate() > 0 && anInstrument.getDuration() > 0) {return (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor();}return 0; // 如果第二个条件不满足,同样返回 0
}

下一个条件稍微复杂一点,所以我分两步进行反转。首先加入一个逻辑非操作:

public double adjustedCapital(Instrument anInstrument) {if (anInstrument.getCapital() <= 0) {return 0;}// 逻辑非操作if (!(anInstrument.getInterestRate() > 0 && anInstrument.getDuration() > 0)) {return 0;}return (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor();
}

但是在这样的条件表达式中留下一个逻辑非,会把我的脑袋拧成一团乱麻,所以我把它简化成下面这样(使用德摩根定律):

public double adjustedCapital(Instrument anInstrument) {if (anInstrument.getCapital() <= 0) {return 0;}// 简化逻辑非条件:(A && B) 的非是 (!A || !B)if (anInstrument.getInterestRate() <= 0 || anInstrument.getDuration() <= 0) {return 0;}return (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor();
}

这两行逻辑语句引发的结果一样,所以我可以用合并条件表达式(Consolidate Conditional Expression)将其合并。

public double adjustedCapital(Instrument anInstrument) {if (anInstrument.getCapital() <= 0 ||anInstrument.getInterestRate() <= 0 ||anInstrument.getDuration() <= 0) {return 0;}return (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor();
}

此时 result 变量做了两件事:一开始我把它设为 0,代表卫语句被触发时的返回值;然后又用最终计算的结果给它赋值。我可以彻底移除这个变量,避免用一个变量承担两重责任,而且又减少了一个可变变量。

public double adjustedCapital(Instrument anInstrument) {if (anInstrument.getCapital() <= 0 ||anInstrument.getInterestRate() <= 0 ||anInstrument.getDuration() <= 0) {return 0;}return (anInstrument.getIncome() / anInstrument.getDuration()) * anInstrument.getAdjustmentFactor();
}

10.4 以多态取代条件表达式(Replace Conditional with Polymorphism)

动机

复杂的条件逻辑是编程中最难理解的东西之一,因此我一直在寻求给条件逻辑添加结构。很多时候,我发现可以将条件逻辑拆分到不同的场景(或者叫高阶用例),从而拆解复杂的条件逻辑。这种拆分有时用条件逻辑本身的结构就足以表达,但使用类和多态能把逻辑的拆分表述得更清晰。

一个常见的场景是:我可以构造一组类型,每个类型处理各自的一种条件逻辑。例如,我会注意到,图书、音乐、食品的处理方式不同,这是因为它们分属不同类型的商品。最明显的征兆就是有好几个函数都有基于类型代码的 switch 语句。若果真如此,我就可以针对 switch 语句中的每种分支逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。

另一种情况是:有一个基础逻辑,在其上又有一些变体。基础逻辑可能是最常用的,也可能是最简单的。我可以把基础逻辑放进超类,这样我可以首先理解这部分逻辑,暂时不管各种变体,然后我可以把每种变体逻辑单独放进一个子类,其中的代码着重强调与基础逻辑的差异。

多态是面向对象编程的关键特性之一。跟其他一切有用的特性一样,它也很容易被滥用。我曾经遇到有人争论说所有条件逻辑都应该用多态取代。我不赞同这种观点。我的大部分条件逻辑只用到了基本的条件语句——if/else 和 switch/case,并不需要劳师动众地引入多态。但如果发现如前所述的复杂条件逻辑,多态是改善这种情况的有力工具。

做法

  1. 如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。
  2. 在调用方代码中使用工厂函数获得对象实例。
  3. 将带有条件逻辑的函数移到超类中。
  4. 如果条件逻辑还未提炼至独立的函数,首先对其使用提炼函数(Extract Method)。
  5. 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。
  6. 重复上述过程,处理其他条件分支。
  7. 在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函数声明为 abstract,或在其中直接抛出异常,表明计算责任都在子类中。

范例:鸟类行为分析

我的朋友有一群鸟儿,他想知道这些鸟飞得有多快,以及它们的羽毛是什么样的。所以我们写了一小段程序来判断这些信息。

原始 JavaScript 代码:

function plumages(birds) {return new Map(birds.map(b => [b.name, plumage(b)]));
}
function speeds(birds) {return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}function plumage(bird) {switch (bird.type) {case 'EuropeanSwallow':return "average";case 'AfricanSwallow':return (bird.numberOfCoconuts > 2) ? "tired" : "average";case 'NorwegianBlueParrot':return (bird.voltage > 100) ? "scorched" : "beautiful";default:return "unknown";}
}function airSpeedVelocity(bird) {switch (bird.type) {case 'EuropeanSwallow':return 35;case 'AfricanSwallow':return 40 - 2 * bird.numberOfCoconuts;case 'NorwegianBlueParrot':return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;default:return null;}
}

有两个不同的操作,其行为都随着“鸟的类型”发生变化,因此可以创建出对应的类,用多态来处理各类型特有的行为。

我先对 airSpeedVelocityplumage 两个函数使用函数组合成类(Encapsulate Function into Class)

Java 代码 (重构第一步:引入 Bird 类封装行为):

import java.util.HashMap;
import java.util.List;
import java.util.Map;// 假设 BirdData 是一个简单的POJO,包含 type, name, numberOfCoconuts, voltage, isNailed
class BirdData {public String type;public String name;public int numberOfCoconuts;public int voltage;public boolean isNailed;public BirdData(String type, String name, int numberOfCoconuts, int voltage, boolean isNailed) {this.type = type;this.name = name;this.numberOfCoconuts = numberOfCoconuts;this.voltage = voltage;this.isNailed = isNailed;}
}class Bird {protected BirdData birdData; // 使用 protected 以便子类访问public Bird(BirdData birdData) {this.birdData = birdData;}public String getPlumage() {switch (birdData.type) {case "EuropeanSwallow":return "average";case "AfricanSwallow":return (birdData.numberOfCoconuts > 2) ? "tired" : "average";case "NorwegianBlueParrot":return (birdData.voltage > 100) ? "scorched" : "beautiful";default:return "unknown";}}public Integer getAirSpeedVelocity() {switch (birdData.type) {case "EuropeanSwallow":return 35;case "AfricanSwallow":return 40 - 2 * birdData.numberOfCoconuts;case "NorwegianBlueParrot":return (birdData.isNailed) ? 0 : 10 + birdData.voltage / 10;default:return null;}}public String getName() {return birdData.name;}
}class BirdAnalyzer {public Map<String, String> plumages(List<BirdData> birdsData) {Map<String, String> result = new HashMap<>();for (BirdData b : birdsData) {result.put(b.name, new Bird(b).getPlumage());}return result;}public Map<String, Integer> speeds(List<BirdData> birdsData) {Map<String, Integer> result = new HashMap<>();for (BirdData b : birdsData) {result.put(b.name, new Bird(b).getAirSpeedVelocity());}return result;}
}

现在,针对每种鸟创建一个子类,用一个工厂函数来实例化合适的子类对象。

Java 代码 (重构第二步:引入子类和工厂方法)

// ... (BirdData, Bird 类定义不变)class EuropeanSwallow extends Bird {public EuropeanSwallow(BirdData birdData) {super(birdData);}@Overridepublic String getPlumage() {return "average";}@Overridepublic Integer getAirSpeedVelocity() {return 35;}
}class AfricanSwallow extends Bird {public AfricanSwallow(BirdData birdData) {super(birdData);}@Overridepublic String getPlumage() {return (birdData.numberOfCoconuts > 2) ? "tired" : "average";}@Overridepublic Integer getAirSpeedVelocity() {return 40 - 2 * birdData.numberOfCoconuts;}
}class NorwegianBlueParrot extends Bird {public NorwegianBlueParrot(BirdData birdData) {super(birdData);}@Overridepublic String getPlumage() {return (birdData.voltage > 100) ? "scorched" : "beautiful";}@Overridepublic Integer getAirSpeedVelocity() {return (birdData.isNailed) ? 0 : 10 + birdData.voltage / 10;}
}class BirdFactory {public static Bird createBird(BirdData birdData) {switch (birdData.type) {case "EuropeanSwallow":return new EuropeanSwallow(birdData);case "AfricanSwallow":return new AfricanSwallow(birdData);case "NorwegianBlueParrot":return new NorwegianBlueParrot(birdData);default:// 默认情况下返回基础 Bird 对象,或抛出异常表示未知类型return new Bird(birdData);}}
}class BirdAnalyzerRefactored {public Map<String, String> plumages(List<BirdData> birdsData) {Map<String, String> result = new HashMap<>();for (BirdData b : birdsData) {Bird bird = BirdFactory.createBird(b);result.put(bird.getName(), bird.getPlumage());}return result;}public Map<String, Integer> speeds(List<BirdData> birdsData) {Map<String, Integer> result = new HashMap<>();for (BirdData b : birdsData) {Bird bird = BirdFactory.createBird(b);result.put(bird.getName(), bird.getAirSpeedVelocity());}return result;}
}

说明:

  • BirdData:一个简单的数据类,用来承载原始的鸟类信息,避免将原始数据与行为混淆。
  • Bird:作为超类,包含了所有鸟类共享的属性(通过 BirdData)和默认行为。在最终的重构中,getPlumage()getAirSpeedVelocity()Bird 类中可以变为抽象方法,强制子类实现。如果希望保留默认行为,则可以像示例中那样提供默认实现,或者在工厂方法中确保只返回具体子类,并在 Bird 类中抛出异常。
  • EuropeanSwallow, AfricanSwallow, NorwegianBlueParrot:具体子类,各自覆写了 getPlumage()getAirSpeedVelocity() 方法,实现了特定类型的行为。
  • BirdFactory:一个静态工厂方法,根据 birdData.type 返回正确的 Bird 子类实例。这取代了客户端代码中的 switch 语句。
  • BirdAnalyzerRefactored:调用方代码现在使用工厂方法获取 Bird 对象,并通过多态调用正确的方法,无需再进行条件判断。

通过这种方式,我们成功地用多态取代了复杂的 switch 条件表达式,使得代码更具扩展性、更易于理解和维护。如果将来要增加新的鸟类,只需添加一个新的子类和修改工厂方法即可,而无需修改现有的逻辑。

范例:用多态处理变体逻辑——航运评级系统

在前面的例子中,“鸟”的类型体系是一个清晰的泛化体系:超类是抽象的“鸟”,子类是各种具体的鸟。这是教科书(包括我写的书)中经常讨论的继承和多态,但并不是实践中使用继承的唯一方式。实际上,这种方式很可能不是最常用或最好的方式。另一种使用继承的情况是:我想表达某个对象与另一个对象大体类似,但又有一些不同之处。

下面有一个这样的例子:有一家评级机构,要对远洋航船的航行进行投资评级。这家评级机构会给出“A”或者“B”两种评级,取决于多种风险和盈利潜力的因素。在评估风险时,既要考虑航程本身的特征,也要考虑船长过往航行的历史。

原始 JavaScript 代码:

function rating(voyage, history) {const vpf = voyageProfitFactor(voyage, history);const vr = voyageRisk(voyage);const chr = captainHistoryRisk(voyage, history);if (vpf * 3 > (vr + chr * 2)) return "A";else return "B";
}
function voyageRisk(voyage) {let result = 1;if (voyage.length > 4) result += 2;if (voyage.length > 8) result += voyage.length - 8;if (["china", "east-indies"].includes(voyage.zone)) result += 4;return Math.max(result, 0);
}
function captainHistoryRisk(voyage, history) {let result = 1;if (history.length < 5) result += 4;result += history.filter(v => v.profit < 0).length;if (voyage.zone === "china" && hasChina(history)) result -= 2;return Math.max(result, 0);
}
function hasChina(history) {return history.some(v => "china" === v.zone);
}
function voyageProfitFactor(voyage, history) {let result = 2;if (voyage.zone === "china") result += 1;if (voyage.zone === "east-indies") result += 1;if (voyage.zone === "china" && hasChina(history)) {result += 3;if (history.length > 10) result += 1;if (voyage.length > 12) result += 1;if (voyage.length > 18) result -= 1;}else {if (history.length > 8) result += 1;if (voyage.length > 14) result -= 1;}return result;
}

voyageRiskcaptainHistoryRisk 两个函数负责打出风险分数,voyageProfitFactor 负责打出盈利潜力分数,rating 函数将 3 个分数组合到一起,给出一次航行的综合评级。

调用方的代码大概是这样:

const voyage = { zone: "west-indies", length: 10 };
const history = [{ zone: "east-indies", profit: 5 },{ zone: "west-indies", profit: 15 },{ zone: "china", profit: -2 },{ zone: "west-africa", profit: 7 },
];const myRating = rating(voyage, history);

代码中有两处同样的条件逻辑,都在询问“是否有到中国的航程”以及“船长是否曾去过中国”。

我会用继承和多态将处理“中国因素”的逻辑从基础逻辑中分离出来。如果还要引入更多的特殊逻辑,这个重构就很有用——这些重复的“中国因素”会混淆视听,让基础逻辑难以理解。

起初代码里只有一堆函数,如果要引入多态的话,我需要先建立一个类结构,因此我首先使用函数组合成类(Encapsulate Function into Class)

Java 代码 (重构第一步:封装成 Rating 类)

import java.util.List;
import java.util.Arrays;
import java.util.Objects;class Voyage {public String zone;public int length;public Voyage(String zone, int length) {this.zone = zone;this.length = length;}
}class HistoryEntry {public String zone;public int profit;public HistoryEntry(String zone, int profit) {this.zone = zone;this.profit = profit;}
}class Rating {protected Voyage voyage;protected List<HistoryEntry> history;public Rating(Voyage voyage, List<HistoryEntry> history) {this.voyage = voyage;this.history = history;}public String getValue() {int vpf = getVoyageProfitFactor();int vr = getVoyageRisk();int chr = getCaptainHistoryRisk();if (vpf * 3 > (vr + chr * 2)) return "A";else return "B";}protected int getVoyageRisk() {int result = 1;if (voyage.length > 4) result += 2;if (voyage.length > 8) result += voyage.length - 8;if (Arrays.asList("china", "east-indies").contains(voyage.zone)) result += 4;return Math.max(result, 0);}protected int getCaptainHistoryRisk() {int result = 1;if (history.size() < 5) result += 4;result += history.stream().filter(v -> v.profit < 0).count();if (voyage.zone.equals("china") && hasChinaHistory()) result -= 2;return Math.max(result, 0);}protected boolean hasChinaHistory() {return history.stream().anyMatch(v -> "china".equals(v.zone));}protected int getVoyageProfitFactor() {int result = 2;if (voyage.zone.equals("china")) result += 1;if (voyage.zone.equals("east-indies")) result += 1;if (voyage.zone.equals("china") && hasChinaHistory()) {result += 3;if (history.size() > 10) result += 1;if (voyage.length > 12) result += 1;if (voyage.length > 18) result -= 1;} else {if (history.size() > 8) result += 1;if (voyage.length > 14) result -= 1;}return result;}
}class RatingCalculator {public String calculateRating(Voyage voyage, List<HistoryEntry> history) {return new Rating(voyage, history).getValue();}
}

于是我就有了一个类,用来安放基础逻辑。现在我需要另建一个空的子类,用来安放与超类不同的行为。

Java 代码 (重构第二步:引入子类和工厂方法)

// ... (Voyage, HistoryEntry 类定义不变)class ExperiencedChinaRating extends Rating {public ExperiencedChinaRating(Voyage voyage, List<HistoryEntry> history) {super(voyage, history);}@Overrideprotected int getCaptainHistoryRisk() {// 在基础风险上减2,并确保不小于0int result = super.getCaptainHistoryRisk() - 2;return Math.max(result, 0);}@Overrideprotected int getVoyageProfitFactor() {// 覆盖整个利润因子计算逻辑int result = 2;result += 1; // "china" zone always adds 1result += 3; // Experienced China factorif (history.size() > 10) result += 1;if (voyage.length > 12) result += 1;if (voyage.length > 18) result -= 1;return result;}
}class RatingFactory {public static Rating createRating(Voyage voyage, List<HistoryEntry> history) {if (voyage.zone.equals("china") && history.stream().anyMatch(h -> "china".equals(h.zone))) {return new ExperiencedChinaRating(voyage, history);} else {return new Rating(voyage, history);}}
}class RatingCalculatorRefactored {public String calculateRating(Voyage voyage, List<HistoryEntry> history) {return RatingFactory.createRating(voyage, history).getValue();}
}

说明:

  1. 数据封装VoyageHistoryEntry 类用来封装航行和历史记录的数据。
  2. Rating 超类:包含了所有航行评级的通用逻辑。所有计算风险和利润因子的方法都定义在这里,并且被声明为 protected 以便子类访问和重写。
  3. ExperiencedChinaRating 子类:继承自 Rating,并重写了 getCaptainHistoryRisk()getVoyageProfitFactor() 方法,以实现针对“中国经验”的特殊逻辑。
    • getCaptainHistoryRisk() 中,我们调用 super.getCaptainHistoryRisk() 获取基础风险,然后在此基础上进行调整。
    • getVoyageProfitFactor() 中,由于“中国因素”的影响较大,我们选择完全重写这个方法,而不是在父方法的基础上修修补补。
  4. RatingFactory 工厂类:负责根据 VoyageHistory 的特定条件(是否涉及中国航线且有中国经验)返回 RatingExperiencedChinaRating 的实例。这消除了客户端代码中的条件判断。
  5. RatingCalculatorRefactored:客户端代码现在只需要调用工厂方法,然后通过多态机制,自动调用正确类的 getValue() 方法。

进一步优化 getVoyageProfitFactor

Rating 超类中,getVoyageProfitFactor 方法内部的条件逻辑比较复杂,尤其是在 if (this.voyage.zone === "china" && this.hasChinaHistory) 分支中。这表明这个方法本身还可以进一步重构,例如使用提炼函数(Extract Method)

我们先将 voyageProfitFactor 中的复杂条件逻辑提炼为更小的方法,例如 getVoyageZoneFactor()getHistoryLengthFactor()getVoyageLengthFactor()

Java 代码 (重构第三步:提炼因子方法)

// ... (Voyage, HistoryEntry 类定义不变)class Rating {// ... (构造函数和getValue方法不变)// ... (getVoyageRisk, getCaptainHistoryRisk, hasChinaHistory 方法不变)protected int getVoyageProfitFactor() {int result = 2;result += getVoyageZoneFactor();result += getHistoryAndVoyageLengthFactor(); // 暂时合并,稍后拆分return result;}protected int getVoyageZoneFactor() {int factor = 0;if (voyage.zone.equals("china")) factor += 1;if (voyage.zone.equals("east-indies")) factor += 1;return factor;}// 这是一个中间步骤,方法名包含“And”是一个坏味道protected int getHistoryAndVoyageLengthFactor() {int factor = 0;if (voyage.zone.equals("china") && hasChinaHistory()) {factor += 3;if (history.size() > 10) factor += 1;if (voyage.length > 12) factor += 1;if (voyage.length > 18) factor -= 1;} else {if (history.size() > 8) factor += 1;if (voyage.length > 14) factor -= 1;}return factor;}
}class ExperiencedChinaRating extends Rating {// ... (构造函数和getCaptainHistoryRisk方法不变)@Overrideprotected int getHistoryAndVoyageLengthFactor() {int factor = 0;factor += 3; // 经验中国航线额外加3分if (history.size() > 10) factor += 1;if (voyage.length > 12) factor += 1;if (voyage.length > 18) factor -= 1;return factor;}
}// ... (RatingFactory 和 RatingCalculatorRefactored 不变)

现在,getHistoryAndVoyageLengthFactor 这个方法名很糟糕,且其中仍包含两种不同的逻辑。我们可以将其进一步拆分,在 Rating 类中提供更通用的 getHistoryLengthFactorgetVoyageLengthFactor 方法,并在 ExperiencedChinaRating 中覆写它们。

Java 代码 (重构第四步:进一步拆分因子方法)

// ... (Voyage, HistoryEntry 类定义不变)class Rating {// ... (构造函数和getValue方法不变)// ... (getVoyageRisk, getCaptainHistoryRisk, hasChinaHistory, getVoyageZoneFactor 方法不变)protected int getVoyageProfitFactor() {int result = 2;result += getVoyageZoneFactor();result += getHistoryLengthFactor(); // 拆分历史长度因子result += getVoyageLengthFactor();  // 拆分航程长度因子return result;}protected int getHistoryLengthFactor() {return (history.size() > 8) ? 1 : 0;}protected int getVoyageLengthFactor() {return (voyage.length > 14) ? -1 : 0;}
}class ExperiencedChinaRating extends Rating {// ... (构造函数和getCaptainHistoryRisk方法不变)@Overrideprotected int getHistoryLengthFactor() {return (history.size() > 10) ? 1 : 0; // 经验中国航线,历史长度要求更高}@Overrideprotected int getVoyageLengthFactor() {int factor = 0;if (voyage.length > 12) factor += 1;if (voyage.length > 18) factor -= 1;return factor;}@Overrideprotected int getVoyageProfitFactor() {return super.getVoyageProfitFactor() + 3; // 经验中国航线,总利润因子额外加3}
}// ... (RatingFactory 和 RatingCalculatorRefactored 不变)

最终,我们得到了更清晰、更易于扩展的代码结构。Rating 类包含了所有通用的评级逻辑,而 ExperiencedChinaRating 类则精确地表达了与“中国经验”相关的差异。每个方法都只做一件事,方法名也更具描述性。

10.5 引入特例(Introduce Special Case)

曾用名: 引入 Null 对象(Introduce Null Object)

当你的代码库中充斥着对某个特定值(例如 null 或一个表示“未知”的魔术字符串)的重复检查,并且这些检查之后通常伴随着相同的处理逻辑时,你就应该考虑使用“引入特例”重构手法了。这种模式通过创建一个专门的“特例对象”来封装这些通用行为,从而将分散的条件逻辑收敛到一处,使代码更加清晰和易于维护。

动机

想象一下,如果一个数据结构的使用者在多个地方都检测一个特定的值,并且每次检测到这个特殊值时都执行相同的操作。这种重复的条件检查不仅冗余,而且一旦特殊值的处理逻辑需要修改,你就不得不在多个地方进行改动,增加了出错的风险。

引入特例模式的目的是提供一个统一的接口来处理这些特殊情况。通过用一个“特例对象”替换原始的特殊值,我们可以将处理特殊情况的逻辑从客户端代码中移除,转而由特例对象自身来提供。这样,客户端代码就可以统一地对待普通对象和特例对象,无需进行显式的条件判断,从而大大简化了代码。

特例对象可以是简单的字面量(在Java中通常通过静态工厂方法返回的不可变实例),也可以是一个具有特定行为的完整类。对于 null 值的处理,特例模式被称为“Null 对象模式”,它是特例模式的一个具体应用。

做法

  1. 识别目标: 找到一个包含属性的数据结构或类,其属性值经常被客户端代码与某个“特例值”(如 null 或魔术字符串)进行比较。
  2. 添加特例检查属性: 在原始类(非特例情况)中添加一个名为 isUnknown()isSpecialCase() 的布尔属性或方法,使其返回 false
  3. 创建特例类/对象: 创建一个新的类或对象,用于表示这种特殊情况。在这个特例类中,实现相同的特例检查方法,使其返回 true。这个特例类可以实现与原始类相同的接口或继承自原始类(在Java中推荐实现接口或继承)。
  4. 封装特例值比较: 对所有客户端代码中直接与特例值进行比较的逻辑,使用提炼函数 (Extract Method) 将其封装成一个独立的方法,确保所有客户端都调用这个新方法,而不是直接进行比较。
  5. 引入特例对象: 修改原始数据结构或类的创建/获取逻辑,使其在遇到特例值时,不再返回特例值本身,而是返回新创建的特例对象。
  6. 更新特例比较函数: 修改第4步创建的特例比较方法,使其现在调用特例对象的 isUnknown()isSpecialCase() 方法。
  7. 逐步迁移行为: 遍历所有处理特例情况的客户端代码。将那些通用且重复的特例处理逻辑(例如返回默认值、执行空操作等)逐步从客户端代码中移除,并将其搬移到特例对象中。特例对象会覆写或实现相应的方法,提供特殊情况下的行为。
  8. (可选)字面量记录: 如果特例对象只包含固定的值且是只读的,可以将其实现为不可变的字面量记录(在Java中,可以使用 record 类型或包含 final 字段的简单类)。
  9. 内联特例比较函数: 当所有客户端都已改为直接调用特例对象的方法后,如果之前封装的特例比较方法变得多余,可以使用内联函数 (Inline Method) 将其移除。

范例

我们以一家公共事业服务公司为例,其系统中有表示“场所”(Site)和“顾客”(Customer)的类。某些场所可能没有对应的顾客,此时 customer 字段被设为一个特殊的字符串 "unknown"

原始Java代码示例
// Customer.java
public class Customer {private String name;private String billingPlan;private PaymentHistory paymentHistory;public Customer(String name, String billingPlan, PaymentHistory paymentHistory) {this.name = name;this.billingPlan = billingPlan;this.paymentHistory = paymentHistory;}public String getName() {return name;}public String getBillingPlan() {return billingPlan;}public void setBillingPlan(String billingPlan) {this.billingPlan = billingPlan;}public PaymentHistory getPaymentHistory() {return paymentHistory;}
}// PaymentHistory.java
public class PaymentHistory {private int weeksDelinquentInLastYear;public PaymentHistory(int weeksDelinquentInLastYear) {this.weeksDelinquentInLastYear = weeksDelinquentInLastYear;}public int getWeeksDelinquentInLastYear() {return weeksDelinquentInLastYear;}
}// Site.java
public class Site {private Object customer; // Could be Customer object or "unknown" stringpublic Site(Object customer) {this.customer = customer;}public Object getCustomer() {return customer;}
}// Client Code Example 1
public class Client1 {public static void processSite(Site site) {Object aCustomer = site.getCustomer();String customerName;if (aCustomer.equals("unknown")) {customerName = "occupant";} else {customerName = ((Customer) aCustomer).getName();}System.out.println("Customer Name: " + customerName);}
}// Client Code Example 2
public class Client2 {public static void processSite(Site site) {Object aCustomer = site.getCustomer();String plan;if (aCustomer.equals("unknown")) {plan = "basic"; // Assuming registry.billingPlans.basic is "basic"} else {plan = ((Customer) aCustomer).getBillingPlan();}System.out.println("Billing Plan: " + plan);}
}// Client Code Example 3
public class Client3 {public static void processSite(Site site, String newPlan) {Object aCustomer = site.getCustomer();if (!aCustomer.equals("unknown")) {((Customer) aCustomer).setBillingPlan(newPlan);System.out.println("Billing plan updated.");} else {System.out.println("Cannot update billing plan for unknown customer.");}}
}// Client Code Example 4
public class Client4 {public static void processSite(Site site) {Object aCustomer = site.getCustomer();int weeksDelinquent;if (aCustomer.equals("unknown")) {weeksDelinquent = 0;} else {weeksDelinquent = ((Customer) aCustomer).getPaymentHistory().getWeeksDelinquentInLastYear();}System.out.println("Weeks Delinquent: " + weeksDelinquent);}
}

我们的目标是消除客户端代码中对 aCustomer.equals("unknown") 的重复检查。

1. 修改 Customer 接口和创建 UnknownCustomer

首先,我们定义一个 Customer 接口(或抽象类),让 Customer 类和 UnknownCustomer 类都实现它。在接口中添加 isUnknown() 方法。

// ICustomer.java (Interface)
public interface ICustomer {String getName();String getBillingPlan();void setBillingPlan(String billingPlan);PaymentHistory getPaymentHistory();boolean isUnknown(); // New method
}// Customer.java (Modified to implement ICustomer)
public class Customer implements ICustomer {private String name;private String billingPlan;private PaymentHistory paymentHistory;public Customer(String name, String billingPlan, PaymentHistory paymentHistory) {this.name = name;this.billingPlan = billingPlan;this.paymentHistory = paymentHistory;}@Overridepublic String getName() {return name;}@Overridepublic String getBillingPlan() {return billingPlan;}@Overridepublic void setBillingPlan(String billingPlan) {this.billingPlan = billingPlan;}@Overridepublic PaymentHistory getPaymentHistory() {return paymentHistory;}@Overridepublic boolean isUnknown() {return false;}
}// UnknownCustomer.java (New Special Case Class)
public class UnknownCustomer implements ICustomer {@Overridepublic String getName() {return "occupant"; // Default name for unknown customer}@Overridepublic String getBillingPlan() {return "basic"; // Default billing plan for unknown customer}@Overridepublic void setBillingPlan(String billingPlan) {// Ignore, unknown customer's billing plan cannot be set}@Overridepublic PaymentHistory getPaymentHistory() {return new NullPaymentHistory(); // Return a special NullPaymentHistory}@Overridepublic boolean isUnknown() {return true;}
}// NullPaymentHistory.java (New Special Case Class for PaymentHistory)
public class NullPaymentHistory extends PaymentHistory {public NullPaymentHistory() {super(0); // Default weeksDelinquentInLastYear for null history}@Overridepublic int getWeeksDelinquentInLastYear() {return 0; // Always 0 for null payment history}
}
2. 封装特例比较逻辑

创建一个辅助方法 isUnknownCustomer() 来集中处理“是否未知顾客”的判断。

// Utility class or within a relevant context
public class CustomerUtils {public static boolean isUnknownCustomer(Object customer) {// This initial implementation still checks the "unknown" string// We'll update it in a later stepif (!(customer instanceof Customer || customer instanceof String)) {throw new IllegalArgumentException("Investigate bad customer value: <" + customer + ">");}return customer.equals("unknown");}
}

现在,所有客户端代码都可以改为使用 CustomerUtils.isUnknownCustomer()

// Client Code Example 1 (Modified)
public class Client1 {public static void processSite(Site site) {Object aCustomer = site.getCustomer();String customerName;if (CustomerUtils.isUnknownCustomer(aCustomer)) {customerName = "occupant";} else {customerName = ((ICustomer) aCustomer).getName();}System.out.println("Customer Name: " + customerName);}
}// Client Code Example 2 (Modified)
public class Client2 {public static void processSite(Site site) {Object aCustomer = site.getCustomer();String plan;if (CustomerUtils.isUnknownCustomer(aCustomer)) {plan = "basic";} else {plan = ((ICustomer) aCustomer).getBillingPlan();}System.out.println("Billing Plan: " + plan);}
}// Client Code Example 3 (Modified)
public class Client3 {public static void processSite(Site site, String newPlan) {Object aCustomer = site.getCustomer();if (!CustomerUtils.isUnknownCustomer(aCustomer)) {((ICustomer) aCustomer).setBillingPlan(newPlan);System.out.println("Billing plan updated.");} else {System.out.println("Cannot update billing plan for unknown customer.");}}
}// Client Code Example 4 (Modified)
public class Client4 {public static void processSite(Site site) {Object aCustomer = site.getCustomer();int weeksDelinquent;if (CustomerUtils.isUnknownCustomer(aCustomer)) {weeksDelinquent = 0;} else {weeksDelinquent = ((ICustomer) aCustomer).getPaymentHistory().getWeeksDelinquentInLastYear();}System.out.println("Weeks Delinquent: " + weeksDelinquent);}
}
3. 修改 Site 类,返回特例对象

现在,让 Site 类在顾客未知时返回 UnknownCustomer 实例。

// Site.java (Modified)
public class Site {private Object customer; // Now it will hold ICustomer or "unknown" initiallypublic Site(Object customer) {this.customer = customer;}public ICustomer getCustomer() {if (this.customer instanceof String && "unknown".equals(this.customer)) {return new UnknownCustomer();} else if (this.customer instanceof ICustomer) {return (ICustomer) this.customer;} else {// Handle unexpected types, maybe throw an exception or return a default UnknownCustomerthrow new IllegalStateException("Unexpected customer type: " + this.customer.getClass().getName());}}
}
4. 更新 isUnknownCustomer 方法

现在,isUnknownCustomer 方法可以直接依赖 ICustomer.isUnknown() 方法。

// CustomerUtils.java (Modified)
public class CustomerUtils {public static boolean isUnknownCustomer(ICustomer customer) { // Now takes ICustomerreturn customer.isUnknown();}
}

注意: 此时,所有客户端代码中 Object aCustomer = site.getCustomer(); 应该改为 ICustomer aCustomer = site.getCustomer();,并且直接调用 aCustomer 的方法。

5. 逐步迁移客户端行为并内联 isUnknownCustomer

现在我们可以开始简化客户端代码,因为它不再需要进行条件检查。特例对象会提供正确的默认行为。

// Client Code Example 1 (Final)
public class Client1 {public static void processSite(Site site) {ICustomer aCustomer = site.getCustomer(); // Now ICustomerString customerName = aCustomer.getName(); // Direct callSystem.out.println("Customer Name: " + customerName);}
}// Client Code Example 2 (Final)
public class Client2 {public static void processSite(Site site) {ICustomer aCustomer = site.getCustomer();String plan = aCustomer.getBillingPlan(); // Direct callSystem.out.println("Billing Plan: " + plan);}
}// Client Code Example 3 (Final)
public class Client3 {public static void processSite(Site site, String newPlan) {ICustomer aCustomer = site.getCustomer();// If setBillingPlan on UnknownCustomer does nothing, this works// If we still need to prevent calls, the check could remain but would use aCustomer.isUnknown()if (!aCustomer.isUnknown()) { // Keeping this check for explicit prevention if neededaCustomer.setBillingPlan(newPlan);System.out.println("Billing plan updated.");} else {System.out.println("Cannot update billing plan for unknown customer.");}}
}// Client Code Example 4 (Final)
public class Client4 {public static void processSite(Site site) {ICustomer aCustomer = site.getCustomer();int weeksDelinquent = aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear(); // Direct callSystem.out.println("Weeks Delinquent: " + weeksDelinquent);}
}

现在,CustomerUtils.isUnknownCustomer() 方法可能已经变得多余,可以考虑将其内联或移除。

范例:使用对象字面量(Java中的记录或不可变类)

在Java中,如果特例对象只是一个只读的数据结构,我们可以使用 record 类型(Java 16+)或者一个带有 final 字段的不可变类来作为特例对象。

// Instead of a full UnknownCustomer class, we can use a record if it's purely data-centric and immutable
public record UnknownCustomerRecord(String name,String billingPlan,PaymentHistory paymentHistory,boolean isUnknown
) implements ICustomer { // ICustomer would need to be adapted for records (e.g., default methods or separate interface)public UnknownCustomerRecord() {this("occupant", "basic", new NullPaymentHistory(), true);}// Records automatically generate getters and constructor// For setBillingPlan, we'd still need a strategy if ICustomer defines it.// Here, we adapt ICustomer methods to be compatible with record (e.g., throw UnsupportedOperationException for setters)@Overridepublic void setBillingPlan(String billingPlan) {throw new UnsupportedOperationException("UnknownCustomerRecord is immutable.");}
}// Site.java (Using the record)
public class Site {private Object customer;public Site(Object customer) {this.customer = customer;}public ICustomer getCustomer() {if (this.customer instanceof String && "unknown".equals(this.customer)) {return new UnknownCustomerRecord(); // Returns the immutable record} else if (this.customer instanceof ICustomer) {return (ICustomer) this.customer;} else {throw new IllegalStateException("Unexpected customer type: " + this.customer.getClass().getName());}}
}

范例:使用变换(Stream API 或辅助方法)

当输入是一个简单的记录结构(例如JSON或Map),并且需要转换为更复杂的对象图时,可以使用变换步骤来引入特例。

假设我们从外部API接收到一个 Map<String, Object> 来表示站点数据。

import java.util.HashMap;
import java.util.Map;// Initial raw data structure
Map<String, Object> rawSiteData = new HashMap<>();
rawSiteData.put("name", "Acme Boston");
rawSiteData.put("location", "Malden MA");
Map<String, Object> customerData = new HashMap<>();
customerData.put("name", "Acme Industries");
customerData.put("billingPlan", "plan-451");
Map<String, Object> paymentHistoryData = new HashMap<>();
paymentHistoryData.put("weeksDelinquentInLastYear", 7);
customerData.put("paymentHistory", paymentHistoryData);
rawSiteData.put("customer", customerData);// Or for an unknown customer:
Map<String, Object> unknownSiteData = new HashMap<>();
unknownSiteData.put("name", "Warehouse Unit 15");
unknownSiteData.put("location", "Malden MA");
unknownSiteData.put("customer", "unknown");// A utility to convert raw map to ICustomer
public class SiteEnricher {public static ICustomer createCustomerFromMap(Object customerRawData) {if (customerRawData instanceof String && "unknown".equals(customerRawData)) {return new UnknownCustomer();} else if (customerRawData instanceof Map) {Map<String, Object> customerMap = (Map<String, Object>) customerRawData;String name = (String) customerMap.get("name");String billingPlan = (String) customerMap.get("billingPlan");PaymentHistory paymentHistory = null;Object paymentHistoryRaw = customerMap.get("paymentHistory");if (paymentHistoryRaw instanceof Map) {Map<String, Object> paymentHistoryMap = (Map<String, Object>) paymentHistoryRaw;int weeksDelinquent = (int) paymentHistoryMap.getOrDefault("weeksDelinquentInLastYear", 0);paymentHistory = new PaymentHistory(weeksDelinquent);} else {paymentHistory = new NullPaymentHistory(); // Default if history is missing/malformed}return new Customer(name, billingPlan, paymentHistory);}throw new IllegalArgumentException("Unknown customer data format: " + customerRawData.getClass().getName());}public static Site enrichSite(Map<String, Object> rawSiteMap) {// Deep copy not strictly necessary for simple map processing if original map is not modified// For more complex scenarios, consider using a library like Apache Commons BeanUtils.cloneBean() or custom deep copy.String siteName = (String) rawSiteMap.get("name");String siteLocation = (String) rawSiteMap.get("location");ICustomer customer = createCustomerFromMap(rawSiteMap.get("customer"));return new Site(customer); // Site constructor now takes ICustomer directly}
}// Client usage
public class ClientWithTransformation {public static void main(String[] args) {Map<String, Object> rawSiteMap = new HashMap<>();rawSiteMap.put("name", "Acme Boston");rawSiteMap.put("location", "Malden MA");Map<String, Object> customerData = new HashMap<>();customerData.put("name", "Acme Industries");customerData.put("billingPlan", "plan-451");Map<String, Object> paymentHistoryData = new HashMap<>();paymentHistoryData.put("weeksDelinquentInLastYear", 7);customerData.put("paymentHistory", paymentHistoryData);rawSiteMap.put("customer", customerData);Site enrichedSite = SiteEnricher.enrichSite(rawSiteMap);ICustomer customer = enrichedSite.getCustomer();System.out.println("Customer Name: " + customer.getName());System.out.println("Billing Plan: " + customer.getBillingPlan());System.out.println("Weeks Delinquent: " + customer.getPaymentHistory().getWeeksDelinquentInLastYear());System.out.println("---");Map<String, Object> unknownSiteData = new HashMap<>();unknownSiteData.put("name", "Warehouse Unit 15");unknownSiteData.put("location", "Malden MA");unknownSiteData.put("customer", "unknown");Site unknownEnrichedSite = SiteEnricher.enrichSite(unknownSiteData);ICustomer unknownCustomer = unknownEnrichedSite.getCustomer();System.out.println("Unknown Customer Name: " + unknownCustomer.getName());System.out.println("Unknown Billing Plan: " + unknownCustomer.getBillingPlan());System.out.println("Unknown Weeks Delinquent: " + unknownCustomer.getPaymentHistory().getWeeksDelinquentInLastYear());}
}

通过引入 SiteEnricher 类,我们将原始的 Map 数据转换为强类型的对象图,并在转换过程中自动处理了“未知顾客”的特例,返回一个 UnknownCustomer 实例,从而简化了后续的客户端代码。


10.6 引入断言(Introduce Assertion)

断言是一种强大的工具,用于在程序中明确地表达和检查那些“应该始终为真”的条件。它们是防御性编程的一部分,旨在帮助开发人员在早期发现逻辑错误,而不是处理由不符合预期状态引起的运行时问题。

动机

在软件的生命周期中,程序员经常会对代码的状态或输入做出假设。例如,一个计算平方根的函数可能假设输入永远是非负数;一个处理订单的函数可能假设订单中的商品列表永远不为 null 且至少包含一项。这些假设构成了代码正常运行的契约。

然而,这些关键假设往往没有在代码中明确地记录下来,或者仅仅通过注释说明。当这些假设被违反时,程序可能会进入一个不确定的状态,导致难以追踪的错误。

引入断言的目的就是将这些隐式的假设转化为显式的检查。一个断言是一个布尔表达式,它表达了在程序执行到特定点时,某个条件必须为真。如果断言失败,则表明存在一个程序错误(bug),而不是一个需要正常处理的运行时异常。断言通常在开发和测试阶段启用,而在生产环境中可以禁用以避免性能开销。

断言的价值不仅仅在于发现错误。它们也是一种重要的交流形式。通过断言,你可以清晰地告诉阅读你代码的开发者:“嘿,在这个地方,我假设这个条件是真的。”这有助于其他开发者更好地理解代码的意图和预期行为,从而在修改代码时避免引入新的错误。

做法

  1. 识别核心假设: 仔细审查你的代码,寻找那些你认为在特定执行点“始终为真”的条件。这些条件往往是代码逻辑的基石,如果它们被违反,则说明程序存在根本性错误。
  2. 选择断言位置: 将断言放置在条件必须为真的逻辑点之前。
  3. 插入断言: 使用你所选编程语言提供的断言机制(例如Java的 assert 关键字)来表达这个条件。
  4. 确保行为不变: 断言不应该对程序的正常行为产生任何影响。如果断言失败,程序应该立即终止或抛出未捕获的错误,而不是尝试恢复或优雅降级。断言的成功执行也不应改变程序的任何可观测状态。
  5. 测试: 在引入断言后,执行测试以确保其不会错误地触发,并且在预期条件被违反时能够正确地捕获错误。

范例

考虑一个 Customer 类,其中包含一个 discountRate(折扣率)属性。在 applyDiscount 方法中,我们可能假设折扣率永远是一个非负数。

原始Java代码示例
public class Customer {private double discountRate; // Represents a discount rate, e.g., 0.1 for 10% discountpublic Customer(double discountRate) {this.discountRate = discountRate;}public double getDiscountRate() {return discountRate;}public void setDiscountRate(double discountRate) {this.discountRate = discountRate;}public double applyDiscount(double amount) {if (this.discountRate > 0) { // Only apply if there's a positive discount ratereturn amount - (this.discountRate * amount);} else {return amount;}}
}

在这个 applyDiscount 方法中,我们隐含地假设 discountRate 不会是一个负数。如果 discountRate 是负数,那么 amount - (this.discountRate * amount) 将会增加金额,这显然不是“折扣”的本意。

为了明确这个假设,我们可以引入断言。

1. 转换条件表达式 (如果需要)

applyDiscount 方法中,条件逻辑已经是一个 if-else 结构,所以可以直接插入断言。

2. 在 applyDiscount 方法中引入断言

我们可以在计算折扣之前断言 discountRate 必须是非负数。

public class Customer {private double discountRate;public Customer(double discountRate) {this.discountRate = discountRate;}public double getDiscountRate() {return discountRate;}public void setDiscountRate(double discountRate) {this.discountRate = discountRate;}public double applyDiscount(double amount) {// Assert that discountRate is non-negative before applying it// Note: For 'assert' to work, Java must be run with -ea (enable assertions) flag.assert this.discountRate >= 0 : "Discount rate cannot be negative: " + this.discountRate;if (this.discountRate > 0) {return amount - (this.discountRate * amount);} else {return amount;}}
}

现在,如果 discountRate 被设置为一个负值,并且在启用了断言的情况下运行程序,那么当 applyDiscount 方法被调用时,断言将会失败,并立即指示程序员存在一个不合法的状态。

3. 将断言移动到设值函数中(更佳实践)

虽然在 applyDiscount 方法中放置断言可以立即捕获错误,但更好的做法通常是在数据被设置时就验证其有效性,而不是等到使用时才发现。这样可以更快地定位错误的源头。

public class Customer {private double discountRate;public Customer(double discountRate) {// Assert during construction as wellassert discountRate >= 0 : "Initial discount rate cannot be negative: " + discountRate;this.discountRate = discountRate;}public double getDiscountRate() {return discountRate;}public void setDiscountRate(double discountRate) {// Assert when the discount rate is setassert discountRate >= 0 : "Discount rate cannot be negative: " + discountRate;this.discountRate = discountRate;}public double applyDiscount(double amount) {// No need for a redundant assert here if it's already asserted in setter/constructorif (this.discountRate > 0) {return amount - (this.discountRate * amount);} else {return amount;}}
}

通过在 setDiscountRate 方法和构造函数中引入断言,我们确保了 discountRate 属性在任何时候都不会被赋予一个负值。如果发生这种情况,断言会立即失败,指出错误的根源。

重要提示:

  • 断言不是输入验证: 断言用于检查程序员的错误或程序内部状态的不一致性,而不是验证来自外部(如用户输入、数据库查询)的数据。对于外部输入,应该使用正常的条件逻辑或异常处理。
  • 性能考量: Java的 assert 关键字在默认情况下是禁用的。要在运行时启用它们,需要使用 -ea (enable assertions) JVM 选项。在生产环境中,通常会禁用断言以避免潜在的性能开销。
  • 不要滥用: 只在那些“必须为真”的关键假设上使用断言。过度使用断言可能会导致代码冗余或分散注意力。

引入断言能显著提高代码的健壮性和可维护性,因为它使得隐藏的假设变得显式,并为调试提供了宝贵的信息。通过将断言视为一种交流工具,你可以帮助团队中的每个人更好地理解和维护代码。

参考

《重构:改善既有代码的设计(第二版)》

http://www.dtcms.com/a/406853.html

相关文章:

  • 雏光 网络推广 网站建设ps模板素材网站
  • 高可用MySQL的整体解决方案、体系化原理和指导思路
  • yoda_formatting_func函数解析(105)
  • Vue 3 中 routes 与 route 的详解
  • 哪有做网站推广wordpress 在线编辑器
  • leetcode_138 随机链表的复制
  • Kendo UI for jQuery 2025 Q3新版亮点 - AI 智能网格与全新表单体验
  • 职业规划之软件测试工作五年后,做技术还是做管理?
  • 【一文了解】C#的StringSplitOptions枚举
  • 大连仟亿科技网站建设公司 概况网站搜索 代码
  • 高端网站设计中的微交互:细节如何决定用户体验
  • 香港科技大学提出融合神经网络框架,高效预测蛋白质序列的多金属结合位点
  • 9.9奶茶项目:matlab+FPGA的cordic算法计算±π之间的sin和cos值
  • 越野组(遇到的问题)
  • 29.9元汉堡项目:FPGA多普勒频移解调功能设计开发
  • MyBatis 大于等于、小于等于
  • 南通自助模板建站php做网站好吗
  • [Windows] PDF 专业压缩工具 v3.6
  • 从 0 到 1Flink DataStream API 入门与上手实战
  • 做网站设计电脑买什么高端本好营销企业有哪些
  • 系统架构设计师备考第34天——软件架构风格
  • postman使用总结
  • 做网站 怎么连到数据库怎么做存储网站
  • Java 后端面试技术文档(参考)
  • 分享智能跳绳解决方案
  • 毕业设计的网站app开发公司介绍
  • WebSocket实时通信不卡顿:cpolar内网穿透实验室第503个成功挑战
  • PyTorch 数据处理工具箱
  • C++项目:仿muduo库高并发服务器-------时间轮定时器
  • 边玩边学,13个Python小游戏(含源码)