重构-改善既有代码的设计(九):简化条件表达式

    xiaoxiao2025-03-28  17

    1、分解条件表达式(Decompose Condition)

    (1)症状:你有一个复杂的条件(if - then - else )语句

    (2)解决:从if、then、else三个段落中分别提炼出独立函数

    (3)将任何大块头代码分解成多个独立函数,根据每个小块代码的用途为分解而得的新函数命名

    (4)将每个分支条件分解成新函数可以突出条件逻辑,更清楚地表明每个分支的作用,突出每个分支的原因

    if (date.before (SUMMER_START) || date.after(SUMMER_END)) charge = quantity * _winterRate + _winterServiceCharge; else charge = quantity * _summerRate; ----------------------------------------------------------------- 重构(分解条件表达式)之后的代码 if (notSummer(date)) charge = winterCharge(quantity); else charge = summerCharge(quantity); private boolean notSummer(Date date) { return date.before (SUMMER_START) || date.after(SUMMER_END); } private double summerCharge(int quantity) { return quantity * _summerRate; } private double winterCharge(int quantity) { return quantity * _winterRate + _winterServiceCharge; }

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

    (1)症状:有一系列条件测试,都得到相同的结果

    (2)解决:将这些测试合并为一个条件表达式,并将这个条件表达式提炼为一个独立函数

    (3)一连串条件检查,检查条件各不相同,但最终行为却一致

    (4)合并后的条件代码会更清晰的表明只有一次条件检查,只不过有多个并列条件需要检查而已

    (5)如果有些检查彼此独立,的确不应该被视为同一次检查,就不要使用本项重构

    if (seniorty < 2) { return 0; } if (monthDisabled > 12) { return 0; } if (isPartTime) { return 0; } ---------------------------------------------------------------------------------- 重构(合并条件表达式)后的代码 if (isNotEligableForDisability()) { return 0; } private Boolean isNotEligableForDisability() { return seniorty < 2 || monthDisabled > 12 || isPartTime; }

    3、合并重复的条件片段(Consolidate Duplicate Conditional Fragments)

    (1)症状:在条件表达式的每个分支上有着相同的一段代码

    (2)解决:将这段重复代码搬移到条件表达式之外

    (3)合并重复代码有利于清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变

    if (isSpecialDeal()) { total = price * 0.95; send(); }else { total = price * 0.98; send(); } ---------------------------------------------------------------- 重构(合并重复的条件片段)后的代码 if (isSpecialDeal()){ total = price * 0.95; }else{ total = price * 0.98; } send();

    (4)也可以使用同样的手法来对待异常(exceptions),如果在try 区段内「可能引发异常」的语句之后,以及所有catch 区段之内,都重复执行了同一段代码,就可以将这段重复代码移到finally区段。

     

    4、移除控制标记(Remove Control Flag)

    (1)症状:在一系列布尔表达式中,某个变量带有“控制标记(control flag)”的作用

    (2)解决:以break语句或return语句取代控制标记

    (3)在未能提供break和Continue语句的编程语言中,可以使用Extract Method方法,将整段逻辑提炼到一个独立函数中

    以break 取代简单的控制标记 下列函数用来检查一系列人名之中是否包含两个可疑人物的名字(这两个人的名字硬编码于代码中〕: void checkSecurity(String[] people) { boolean found = false; for (int i = 0; i < people.length; i++) { if (! found) { if (people[i].equals ("Don")){ sendAlert(); found = true; } if (people[i].equals ("John")){ sendAlert(); found = true; } } } } 这种情况下很容易找出控制标记:当变量found 被赋予true 时,搜索就结束,可以逐一引入break 语句: void checkSecurity(String[] people) { for (int i = 0; i < people.length; i++) { if (people[i].equals ("Don")){ sendAlert(); break; } if (people[i].equals ("John")){ sendAlert(); break; } } }

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

    (1)症状:函数中的条件逻辑使人难以看清正常的执行路径

    (2)解决:使用卫语句表现所有特殊情况

    (3)卫语句:如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回

    (4)本项重构的精髓:给某条分支以特别的重视,告诉读者如果真的发生了,请做一些必要的整理工作,然后退出

    (5)卫语句要不就从函数中返回,要不就抛出一个异常

    范例1:想像一个薪资系统,其中以特殊规则处理死亡员工、驻外员工、退休员工的薪资。这些情况不常有,但的确偶而会出现。 double getPayAmount() { double result; if (_isDead) result = deadAmount(); else { if (_isSeparated) result = separatedAmount(); else { if (_isRetired) result = retiredAmount(); else result = normalPayAmount(); }; } return result; } 在这段代码中,非正常情况的检查掩盖了正常情况的检查,所以我应该使用『卫语句」来取代这些检查,以提高程序清晰度。我可以逐一引入卫语句。让我们从最上面的条件检查动作开始: ------------------------------------------------------------------------------------------------ 重构(以卫语句取代嵌套条件表达式)后的代码 double getPayAmount() { if (_isDead) return deadAmount(); if (_isSeparated) return separatedAmount(); if (_isRetired) return retiredAmount(); return normalPayAmount(); }; 范例2:将条件逆反(Reversing the Conditions) public double getAdjustedCapital() { double result = 0.0; if (_capital > 0.0) { if (_intRate > 0.0 && _duration > 0.0) { result = (_income / _duration) * ADJ_FACTOR; } } return result; } ------------------------------------------------------------------------------------------------ 重构后的代码 public double getAdjustedCapital() { if (_capital <= 0.0) return 0.0; if (_intRate <= 0.0 || _duration <= 0.0) return 0.0; return (_income / _duration) * ADJ_FACTOR; }

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

    (1)症状:你手上有个条件表达式,它根据对象类型的不同而选择不同的行为

    (2)解决:将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数

    (3)多态使你不必编写明显的条件表达式,可以根据对象的不同类型而采取不同的行为

    (4)多态在一定程度上消除“类型码的switch语句”以及“基于类型名称的if-then-else语句”

    如果要处理的条件式是一个更大函数中的一部分,首先对条件式进行分析,然后使用Extract Method 将它提炼到一个独立函数去。如果有必要,使用Move Method 将条件式放置到继承结构的顶端。任选一个subclass ,在其中建立一个函数,使之覆写superclass 中容纳条件式的那个函数。将「与subclass 相关的条件式分支」拷贝到新建函数中,并对它进行适当调整。为了顺利进行这一步骤,你可能需要将superclass 中的某些private 值域声明为protected 。编译,测试。在superclass 中删掉条件式内被拷贝出去的分支。编译,测试。针对条件式的每个分支,重复上述过程,直到所有分支都被移到subclass 内的函数为止。将superclass 之中容纳条件式的函数声明为抽象函数(abstract method)。 class Engineer extends EmployeeType{ int getTypeCode(){ return Employee.ENGINEER; } } class Manager extends EmployeeType{ int getTypeCode(){ return Employee.MANAGER; } } class Salesman extends EmployeeType{ int getTypeCode(){ return Employee.SALEMAN; } } class Employee... private employeeType _type; int payAmount(){ switch(getType()){ case EmployeeType.ENGINEER: return _monthlySalary; case EmployeeType.SALESMAN: return _monthlySalary + _commission; case EmployeeType.MANAGER: return _monthlySalary + bonus; default; throw new RuntimeException(); } } int getType(){ return _type.getTypeCode(); } void setType(int arg){ _type = EmployeeType.newType(arg); } class employeeType... static EmployeeType newType(int code){ switch(code){ case ENGINEER: return new Engineer(); case SALESMAN: return new Salesman(); case MANAGER: return new Manager(); default: throw new IllegalArgumentException(); } } static final int ENGINEER = 0; static final int SALESMAN = 1; static final int MANAGER = 2; ----------------------------------------------------------------------------------------------- 重构(以多态取代条件表达式)后的代码 class Employee... private Engineer _type; int payAmount(){ return _type.payAmount(this); } class Engineer... int payAmount(Employee emp){ return emp.getMonthlySalary(); } class Salesman... int payAmount(Employee emp){ return emp.getMonthlySalary() + emp.getCommission(); } class Manager... int payAmount(Employee emp){ return emp.getMonthlySalary() + emp.getBonus(); } class EmployeeType... abstract int payAmount(Employee emp);

    7、引入Null对象(Introduce Null Object)

    (1)症状:你需要再三检查某对象是否为null

    (2)解决:将null值替换成null对象

    (3)空对象一定是常量,它们的任何成分都不会发生变化

    为源类建立一个子类,使其行为就像是源类的null版本。在源类和null子类中都加上IsNull()函数,前者的IsNull()应该返回false,后者的IsNull()返回true。编译。找出所有“索求源对象却获得一个null”的地方。修改这些地方,使它们改而获得一个空对象。找出“将源对象与null做比较”的地方。修改这些地方,使它们调用IsNull()函数。编译、测试。找出这样的程序点:如果对象不是null,做A动作,否则做B动作。对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同。使用上述被覆写的动作,然后删除“对象是否等于null”的条件测试。编译并测试。

    if (customer == null) { plan = BillingPlan.basic(); } else { plan = customer.getPlan(); } ---------------------------------------------------------------------------------- 重构之后的代码 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();

    8、引入断言(Introduce Assertion)

    (1)症状:某一段代码需要对程序状态作出某种假设

    (2)解决:以断言明确表现这种假设

    (3)通常这样的假设并没有在代码中明确的表现出来,有时程序员会以注释写出这样的假设

    (4)断言是一种更好的解决方案,断言是一个条件表达式,如果它失败了就表示程序员犯了错误

    (5)不要滥用断言。不要使用它来检查“你认为应该为真”的条件,只使用它来检查“一定必须为真”的条件,滥用断言可能会造成难以维护的重复逻辑

    (6)如果断言所指示的约束条件不能满足,代码是否仍能正常运行?如果可以,就把断言拿掉

    (7)注意断言中的重复代码,你可以大胆使用Extract Method(提炼函数)去掉那些重复代码

    (8)在项目结束前以过滤程序滤掉使用断言的每一行代码(可以使用Perl之类的语言来编写这样的过滤程序)

    double getExpenseLimit() { // should have either expense limit or a primary project return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); } ---------------------------------------------------------------------------------- 重构(引入断言)之后的代码 double getExpenseLimit() { Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); }

     

    尾注

    上述的总结与思考是基于对《重构—改善既有代码的设计》这本书的精读与演绎更多及时干货,请关注微信公众号:JAVA万维猿圈

    最新回复(0)