重构速查表

重构的相关技能
帅旋
关注
充电
IT宅站长,技术博主,共享单车手,全网id:arthinking。

简化条件表达式

发布于 2019-03-08 | 更新于 2024-02-23

简化条件表达式

如果有一个复杂的条件语句,可以使用 Decompose Conditional(分解条件表达式) 从 if then else 三个段落中分别提炼出独立函数。

1
2
3
4
5
6
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
}
else {
charge = quantity * summerRate;
}
1
2
3
4
5
6
if (isSummer(date)) {
charge = summerCharge(quantity);
}
else {
charge = winterCharge(quantity);
}

好处

  • 通过把条件代码提取为具有明确命名的方法,方便后期的维护;
  • 这种重构技术也适用于条件中的短表达。例如字符串isSalaryDay()比直接比较日期的代码更漂亮,更具描述性。

合并条件表达式

如果有一系列条件测试,都得到相同的结果,那么可以通过 Consolidate Conditional Expression(合并条件表达式) 将这些测试合并为一个条件表达式,并将这个条件表达式提炼为一个独立函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
double disabilityAmount() {
if (seniority < 2) {
return 0;
}
if (monthsDisabled > 12) {
return 0;
}
if (isPartTime) {
return 0;
}
// Compute the disability amount.
// ...
}
1
2
3
4
5
6
7
double disabilityAmount() {
if (isNotEligableForDisability()) {
return 0;
}
// Compute the disability amount.
// ...
}

好处

  • 消除重复的控制流代码。组合具有相同“目标”的多个条件有助于显示您只执行一次复杂检查,从而导致一个操作;
  • 通过合并所有运算符成一个方法,您可以在使用恰当的方法名来解释条件的目的,使代码更具有可读性。

合并重复的条件片段

如果在条件表达式的每个分支上以后相同的一段代码,可以使用 Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 将这段重复代码版一到条件表达式之外。

1
2
3
4
5
6
7
8
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.98;
send();
}
1
2
3
4
5
6
7
if (isSpecialDeal()) {
total = price * 0.95;
}
else {
total = price * 0.98;
}
send();

好处

  • 去除重复代码。

移除控制标记

如果在一系列布尔表达式中,某个变量带有“控制标记”的作用,那么请使用 Remove Control Flag(移除控制标记) ,以break语句或者return语句取代控制标记。

1
2
3
4
5
6
for (const p of people) {
if (! found) {
if ( p === "Don") {
sendAlert();
found = true;
}
1
2
3
4
5
for (const p of people) {
if ( p === "Don") {
sendAlert();
break;
}

重构原因

  • 控制标志代码通常比使用控制流操作符编写的代码更加笨重。

以卫语句取代嵌套条件表达式

如果函数中的嵌套条件逻辑使人难以看清正常的执行路径,那么请尝试使用 Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套函数表达式) ,将所有特殊检查和边缘情况隔离到单独的子句中,并将它们放在主检查之前。理想情况下,你应该有一个接一个的“平行”条件列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public double getPayAmount() {
double result;
if (isDead){
result = deadAmount();
}
else {
if (isSeparated){
result = separatedAmount();
}
else {
if (isRetired){
result = retiredAmount();
}
else{
result = normalPayAmount();
}
}
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
public double getPayAmount() {
if (isDead){
return deadAmount();
}
if (isSeparated){
return separatedAmount();
}
if (isRetired){
return retiredAmount();
}
return normalPayAmount();
}

好处

  • 使条件语句结构平整,易于阅读。

以多态取代条件表达式

如果你手上有个条件表达式,根据对象类型的不同而选择不同的行为,那么可以尝试使用 Replace Conditional with Polymorphism(以多态取代条件表达式),创建与条件的分支匹配的子类,将这个表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。用相关的方法调用替换条件,从而达到以多态取代条件表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Bird {
// ...
double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException("Should be unreachable");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class Bird {
// ...
abstract double getSpeed();
}

class European extends Bird {
double getSpeed() {
return getBaseSpeed();
}
}
class African extends Bird {
double getSpeed() {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
double getSpeed() {
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}

// Somewhere in client code
speed = bird.getSpeed();

重构步骤

  • 准备阶段:对于这种重构技术,您应该有一个包含各个行为的类的层次结构。如果您没有这样的层次结构,请创建一个,有两种选择:
    • 使用 Replace Type Code with Subclasses 将为对象设计条件判断的属性不同值创建不同的子类。这种方法很简单,但不太灵活,因为您无法为对象的其他属性创建子类;
    • 使用 Replace Type Code with State/Strategy ,类将专用于特定的对象属性,并且为不同的属性值创建不同的子类。当前类将包含对此类对象的引用,并将执行委托给它们;
  • 如果条件是在执行其他操作的方法中,请执行“提取方法”;
  • 对于每个层次结构子类,重新定义包含条件的方法,并将相应条件分支的代码复制到该位置;
  • 从条件中删除此分支;
  • 重复替换,直到条件为空。然后删除条件并声明方法abstract;

引入Null对象

很多的null检查会使您的代码变得更长,更丑陋。如果你需要再三检查某个对象是否为null,可以通过 Introduce Null Object(引入Null对象) 将null值替换为null对象。

1
2
3
4
5
6
if (customer == null) {
plan = BillingPlan.basic();
}
else {
plan = customer.getPlan();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class NullCustomer extends Customer {
boolean isNull() {
return true;
}
Plan getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}

// Replace null values with Null-object.
customer = (order.customer != null) ?
order.customer : new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.getPlan();

好处

  • 这也是利用了多态的好处:你不用在询问对象是什么类型,然后在根据答案来调用对象的某个行为了,你只管调用该行为就是了;

引入断言

如果某一段代码需要对程序状态做出某种假设,那么可以通过 Introduce Assertion(引入断言) 以断言明确表现这种假设。

1
2
3
4
5
6
7
double getExpenseLimit() {
// Should have either expense limit or
// a primary project.
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit :
primaryProject.getMemberExpenseLimit();
}
1
2
3
4
5
6
7
double getExpenseLimit() {
Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);

return (expenseLimit != NULL_EXPENSE) ?
expenseLimit:
primaryProject.getMemberExpenseLimit();
}

好处

  • 如果假设不正确并且代码因此给出错误的结果,那么最好在此之前停止执行导致致命后果和数据损坏。这也意味着您可能忽略了编写必要的测试用例。

其他说明

  • 有时,异常比简单的断言更合适。您可以选择必要的异常类,并让其余代码正确处理它;
  • 什么时候异常比断言更好?如果异常可能由用户或系统的操作引起,您可以处理异常。另一方面,普通的未命名和未处理的异常基本上等同于简单的断言,这个时候你不处理它们,它们只是由于一个永远不会发生的程序错误而导致的。

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/refactoring/simplifying-conditional-expressions.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。