重构之简化条件表达式

@(常识)
[toc]

那有什么天生如此,只是我们天天坚持。

本篇文章主要讲解 《重构—-改善既有代码的设计》 这本书中的 第九章简化条件表达式中 的知识点,

Decompose Conditional(分解条件表达式)

问题:你有一个复杂的条件(if、then、else) 语句
解决:从if、then、else三个段落中分别提炼出独立函数

  1. //重构前
  2. if (date.before(SUMMER_START) || date.after(SUMMER_END))
  3. charge = quantity * _winterRate + _winterServiceCharge;
  4. else charge = quantity * _summerRate;
  1. //重构后
  2. if (notSummer(date))
  3. charge = winterCharge(quantity);
  4. else charge = summerCharge(quantity);

动机

将条件分支的代码分解成多个独立函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新建函数,可以更清楚地表达自己的意图。

做法

  • 将if段落提炼出来,构成一个独立函数
  • 将then段落和else段落都提炼出来,各自构成一个独立函数

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

    问题:如果有一系列条件测试,都得到相同结果
    解决:将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数

    1. //重构前
    2. double disabilityAmount(){
    3. if(_seniority < 2) return 0;
    4. if(_monthsDisabled > 12) return 0;
    5. if(_isPartTime) return 0;
    6. }
    1. //重构后
    2. double disabilityAmount(){
    3. if(isNotEligableForDisability()) return 0;
    4. }

    动机

    如果一串条件检查:检查条件各不相同,最终行为却一致,就应该将它们合并为一个条件表达式,之所以要合并条件代码,有两个重要原因,
    首先,合并后的条件代码用意更清晰,其次,这项重构往往可以为使用Extract Method(提炼函数)做好准备

    做法

  • 确定这些条件语句都没有副作用

  • 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个
  • 编译,测试
  • 对合并后的条件表达式实施Extract Method

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

    问题:在条件表达式的每个分支上有着相同的一段代码
    解决:将这段重复代码搬移到条件表达式之外

    1. //重构前
    2. if(isSpecialDeal()){
    3. total = price * 0.95;
    4. send();
    5. }
    6. else{
    7. total = price * 0.98;
    8. send();
    9. }
    1. //重构后
    2. if(isSpecialDeal())
    3. total = price * 0.95;
    4. else
    5. total = price * 0.98;
    6. send();

    动机

    有助于清楚地表明哪些东西随条件的变化而变化、哪些东西保持###

    做法

  • 鉴别出”执行方式不随条件变化而变化”的代码‘

  • 如果这些共通代码位于条件表达式起始处,就将它移到条件表达式之前
  • 如果这些共通代码位于条件表达式尾端,就将它移到条件表达式之后
  • 如果这些共通代码位于条件表达式中段,就需要观察共通代码之前或之后的代码是否改变了什么东西,如果的确有所改变,应该首先将共通代码向前或向后移动,移至条件表达式的起始处或尾端,再以前面所受的办法来处理
  • 如果共通代码不止一条语句,应该先使用Extract Method(提炼函数)将共通代码提炼到一个独立函数中,再以前面所说的办法###

    范例

    1. //重构前
    2. if (isSpecialDeal()) {
    3. total = price * 0.95;
    4. send();
    5. }
    6. else {
    7. total = price * 0.98;
    8. send();
    9. }

    由于条件式的两个分支都执行了 send() 函数,所以我应该将send() 移到条件式的外围:

    1. if (isSpecialDeal())
    2. total = price * 0.95;
    3. else
    4. total = price * 0.98;
    5. send();

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

    Remove Control Flag(移除控制标记)

    问题:在一系列布尔表达式中,某个变量带有”控制标记”的作用
    解决:以break语句或return语句### 制标记

    动机

    在一系列条件表达式中,你常常会用到[用以何时停止条件检查]的控制标记。

    1. set done to false
    2. while not done
    3. if (condition)
    4. //do something
    5. //set done to true
    6. next step of loop

    用break语句和continue语句跳出复### 件语句

    做法

  • 找出让你跳出这段逻辑的控制标记值

  • 找出对标记变量赋值的语句,代以恰当的break语句或continue语句
  • 每次替换后,编译并测试

    范例 :以break取代简单的控制标记

    1. //重构前
    2. void checkSecurity(String[] people){
    3. boolean found = false;
    4. for(int i = 0; i < people.length; i++){
    5. if(!found){
    6. if(people[i].equals("Don")){
    7. sendAlert();
    8. found = true;
    9. }
    10. if(people[i].equals("John")){
    11. sendAlert();
    12. found = true;
    13. }
    14. }
    15. }
    16. }

    =>

    1. //重构后
    2. void checkSecurity(String[] people){
    3. for(int i = 0; i < people.length; i++){
    4. if(people[i].equals("Don")){
    5. sendAlert();
    6. break;
    7. }
    8. if(people[i].equals("John")){
    9. sendAlert();
    10. break;
    11. }
    12. }
    13. }

    范例 :以return返回控制标记

    1. //重构前
    2. void checkSecurity(String[] people){
    3. String found = "";
    4. for(int i = 0; i < people.length; i++){
    5. if(found.equals("")){
    6. if(people[i].equals("Don")){
    7. sendAlert();
    8. found = "Don";
    9. }
    10. if(people[i].equals("John")){
    11. sendAlert();
    12. found = "John";
    13. }
    14. }
    15. }
    16. someLaterCode(found);
    17. }
    1. //重构后
    2. void checkSecurity(String[] people){
    3. String found = foundMiscreant(people);
    4. someLaterCode(found);
    5. }
    6. String foundMiscreant(String[] people){
    7. String found = "";
    8. for(int i = 0; i < people.length; i++){
    9. if(found.equals("")){
    10. if(people[i].equals("Don")){
    11. sendAlert();
    12. return "Don";
    13. }
    14. if(people[i].equals("John")){
    15. sendAlert();
    16. return "John";
    17. }
    18. }
    19. }
    20. return "";
    21. }

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

    问题:函数中的条件逻辑使人难以看清正常的执行路径
    解决:使用卫语句表现所有特殊情况(启哥备注:可以减少嵌套)

    1. //重构前
    2. double getPayAmount(){
    3. double result;
    4. if(_isDead) result = deadAmount;
    5. else{
    6. if(_isSeparated) result = separatedAmount();
    7. else{
    8. if(_isRetired) result = retiredAmount();
    9. else result = normalPayAmount();
    10. }
    11. }
    12. return result;
    13. }
    1. //重构后
    2. double getPayAmount(){
    3. if(_isDead) return deadAmount();
    4. if(_isSeparated) return separatedAmount();
    5. if(_isRetired) return retiredAmount;
    6. return normalPayAmount();
    7. }

    动机

    条件表达式通常有两种表现形式:
    第一种是:所有分支都属于正常行为
    第二种是:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况
    如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为”卫语句”

    做法

  • 对于每个检查,放进一个卫语句,卫语句要么从函数中返回,要么就抛出一个异常

  • 每次将条件检查替换成卫语句后,编译并测试

    范例:将条件反转

    1. //重构前
    2. public double getAdjustedCapital(){
    3. double result = 0.0;
    4. if(_capital > 0.0){
    5. if(_intRate > 0.0 && _duration > 0.0){
    6. result = (_income / _duration) * ADJ_FACTOR;
    7. }
    8. }
    9. return result;
    10. }
    1. //重构后
    2. public double getAdjustedCapital(){
    3. double result = 0.0;
    4. if(_capital <= 0.0) return 0.0;
    5. if(_intRate <= 0.0 || _duration <= 0.0) return 0.0;
    6. return (_income / _duration) * ADJ_FACTOR;
    7. }

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

    问题:你手上有个条件表达式,他根据对象类型的不同而选择不同的行为
    解决:将条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数

    1. //重构前
    2. double getSpeed(){
    3. switch(_type){
    4. case EUROPEAN:
    5. return getBaseSpeed();
    6. case AFRICAN:
    7. return getBaseSpeed() - getLoadFactory() * _numberOfCoconuts;
    8. case NORWEGIAN_BLUE:
    9. return (_isNailed) ? 0 : getBaseSpeed(_voltage);
    10. }
    11. throw new RuntimeException("Should be unreachable");
    12. }

    ==>
    //重构后

    动机

    多态最根本的好处就是:如果需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式

    做法

  • 如果要处理的条件表达式是一个更大函数中的一部分,首先对条件表达式进行分析,然后使用Extract Method将它提炼到一个独立函数中

  • 如果有必要,使用Move Method(搬移函数)将条件表达式放置到继承结构的顶端
  • 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数,将与该子类相关的条件表达式分支复制到新建函数中,并对它进行适当调整
  • 编译,测试
  • 在超类中删除条件表达式内被复制了的分支
  • 编译,测试
  • 针对条件表达式的每个分支,重复上述过程,直到所有分支都被移动子类内函数为止
  • 将超类之中容乃条件表达式的函数声明为抽象函数

    范例

    请允许我继续使用「员工与薪资」这个简单而又乏味的例子。
    继承构:

    1. //重构前
    2. class Employee...
    3. int payAmount(int type){
    4. switch(type){
    5. case Employee.ENGINEER:
    6. return _monthlySalary;
    7. case Employee.SALESMAN:
    8. return _monthlySalary + _commission;
    9. case Employee.MANAGER:
    10. return _monthlySalary + _bonus;
    11. default:
    12. throw new IllegalArgumentException("Incorrect type code value");
    13. }
    14. }
    15. private Employee _type;
    16. int getType(){
    17. return _type.getTypeCode()
    18. }
    19. abstract class EmployeeType...
    20. abstract int getTypeCode();
    21. class Engineer extends EmployeeType...
    22. int getTypeCode(){
    23. return Employee.ENGINEER:
    24. }
    25. ...and other subclasses

    switch 语句已经被很好地提炼出来,因此我不必费劲再做一遍。不过我需要将它移至EmployeeType class,因为EmployeeType 才是被subclassing 的class 。

    1. class EmployeeType...
    2. int payAmount(Employee emp) {
    3. switch (getTypeCode()) {
    4. case ENGINEER:
    5. return emp.getMonthlySalary();
    6. case SALESMAN:
    7. return emp.getMonthlySalary() + emp.getCommission();
    8. case MANAGER:
    9. return emp.getMonthlySalary() + emp.getBonus();
    10. default:
    11. throw new RuntimeException("Incorrect Employee");
    12. }
    13. }

    由于我需要EmployeeType class 的数据,所以我需要将Employee 对象作为参数传递给payAmount()。这些数据中的一部分也许可以移到EmployeeType class 来,但那是另一项重构需要关心的问题了。
    调整代码,使之通过编译,然后我修改Employee 中的payAmount() 函数,令它委托(delegate,转调用)EmployeeType :

    1. class Employee...
    2. int payAmount() {
    3. return _type.payAmount(this);
    4. }

    现在,我可以处理switch 语句了。这个过程有点像淘气小男孩折磨一只昆虫——每次掰掉它一条腿(意思就是「去掉一个分支」)。首先我把switch 语句中的”Engineer”这一分支拷贝到Engineer class:

    1. class Engineer...
    2. int payAmount(Employee emp) {
    3. return emp.getMonthlySalary();
    4. }

    这个新函数覆写了superclass 中的switch 语句之内那个专门处理”Engineer”的分支。我是个徧执狂,有时我会故意在case 子句中放一个陷阱,检查Engineer class 是否正常工作(是否被调用):

    1. class EmployeeType...
    2. int payAmount(Employee emp) {
    3. switch (getTypeCode()) {
    4. case ENGINEER:
    5. throw new RuntimeException ("Should be being overridden");
    6. case SALESMAN:
    7. return emp.getMonthlySalary() + emp.getCommission();
    8. case MANAGER:
    9. return emp.getMonthlySalary() + emp.getBonus();
    10. default:
    11. throw new RuntimeException("Incorrect Employee");
    12. }
    13. }

    接下来,我重复上述过程,直到所有分支都被去除为止:

    1. class Salesman...
    2. int payAmount(Employee emp) {
    3. return emp.getMonthlySalary() + emp.getCommission();
    4. }
    5. class Manager...
    6. int payAmount(Employee emp) {
    7. return emp.getMonthlySalary() + emp.getBonus();
    8. }

    然后,将superclass 的payAmount() 函数声明为抽象函数:
    class EmployeeType…
    abstract int payAmount(Employee emp);
    a

    Introduce Null Object(引入Null 对象)

    问题:你需要再三检查某物是否为null value
    解决:将null值替换为null对象

    1. if (customer == null) plan = BillingPlan.basic();
    2. else plan = customer.getPlan();

    //重构后
    启哥说: 和提供一个默认的不做任何处理的空实现是一个意思

    动机(Motivation)

    当实例变量的某个字段内容允许为null时,在进行操作时往往要进行非空判断,这个工作是非常繁杂的,
    所以不让实例变量被设为null,而是插入各式各样的空对象——它们都知道如何正确地显示自己,这样就可以摆脱大量过程化的代码
    空对象一定是常量,它们的任何成分都不会发生变化,因此可以使用Singleton模式来实现它们

    做法

  • 为源类建立一个子类,使其行为就像是源类的null版本,在源类和null子类中都加上isNull()函数,前者的应该返回false,后者的应该返回true,或者建立一个nullable接口,将isNull()函数放入其中,让源类实现这个接口

  • 编译
  • 找出所有”索求源对象却获得一个null”的地方,修改这些地方,使它们改而获得一个空对象
  • 找出所有”将源对象与null做比较”的地方,修改这些地方,使它们调用isNull()函数
  • 编译,测试
  • 找出这样的程序点:如果对象不是null,做A动作,否则做B动作
  • 对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同
  • 使用上述被覆写的动作,然后删除”对象是否等于null”的条件测试,编译并测试

    范例

    —家公用事业公司的系统以Site 表示地点(场所)。庭院宅等和集合公寓(apartment)都使用该公司的服务。任何时候每个地点都拥有(或说都对应于)一个顾客,顾客信息以Customer 表示:

    1. //重构前
    2. class Site...
    3. Customer getCustomer(){
    4. return _customer;
    5. }
    6. Customer _customer;
    7. class Customer...
    8. public String getName(){...}
    9. public BillingPlan getPlan(){...}
    10. public PaymentHistory getHistory(){...}
    11. public class PaymentHistory...
    12. int getWeesDelingquentInLastYear()
    13. Customer customer = site.getCustomer();
    14. BillingPlan plan;
    15. if(customer == null) plan = BillingPlan.basic();
    16. else plan = customer.getPlan();
    17. ...
    1. //重构后
    2. class NullCustomer extens Customer{
    3. public boolean isNull(){
    4. return true;
    5. }
    6. }
    7. class Customer...
    8. public boolean isNull(){
    9. return false;
    10. }
    11. static Customer new Null(){
    12. return new NullCustomer();
    13. }
    14. class Site...
    15. Customer getCustomer(){
    16. return (_customer == null) ? Customer.newNull() : _customer;
    17. }
    18. Customer customer = site.getCustomer();
    19. BillingPlan plan;
    20. if(customer.isNull()) plan = BillingPlan.basic();
    21. else plan = customer.getPlan();

    Introduce Assertion(引入断言)

    问题:如果某一段代码需要对程序状态做出某种假设
    解决:以断言明确表现这种假设

    1. //重构前
    2. double getExpenseLimit(){
    3. return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit();
    4. }
    1. //重构后
    2. double getExpenseLimit(){
    3. Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null);
    4. return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit();
    5. }

    动机

    常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行,这时应该使用断言,把不符合条件的假设标明出来
    断言可以作为交流与调试的辅助,在交流的角度上,断言可以帮助程序阅读者理解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们
    在一段逻辑中加入断言是有好处的,因为它迫使你重新考虑这段代码的约束条件,如果不满足这些约束条件,程序也可以正常运行,断言就不会带给你任何帮助,只会把代码变得混乱,并且有可能妨碍以后的修改

    做法

  • 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况

    范例

    下面是一个简单例子:开支(经费)限制。后勤部门的员工每个月有固定的开支限额;业务部门的员工则按照项目的开支限额来控制自己的开支。一个员工可能没有开支额度可用,也可能没有参与项目,但两者总得要有一个(否则就没有经费可用 了)。在开支限额相关程序中,上述假设总是成立的,因此:

    1. //重构前
    2. class Employee...
    3. private static final double NULL_EXPENSE = -1.0;
    4. private double _expenseLimit = NULL_EXPENSE;
    5. private Project _primaryProject;
    6. double getExpenseLimit() {
    7. return (_expenseLimit != NULL_EXPENSE) ?
    8. _expenseLimit:
    9. _primaryProject.getMemberExpenseLimit();
    10. }
    11. boolean withinLimit (double expenseAmount) {
    12. return (expenseAmount <= getExpenseLimit());
    13. }

    这段代码包含了一个明显假设:任何员工要不就参与某个项目,要不就有个人开支限额。我们可以使用assertion 在代码中更明确地指出这一点:

    1. double getExpenseLimit() {
    2. Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null);
    3. return (_expenseLimit != NULL_EXPENSE) ?
    4. _expenseLimit:
    5. _primaryProject.getMemberExpenseLimit();
    6. }

    这条assertion 不会改变程序的任何行为。另一方面,如果assertion中的条件不为真,我就会收到一个运行期异常:也许是在withinLimit() 函数中抛出一个空指针(null pointer)异常,也许是在Assert.isTrue() 函数中抛出一个运行期异常。有时assertion 可以帮助程序员找到臭虫,因为它离出错地点很近。但是,更多时候,assertion 的价值在于:帮助程序员理解代码正确运行的必要条件。
    我常对assertion 中的条件式使用Extract Method ,也许是为了将若干地方的重复码提炼到同一个函数中,也许只是为了更清楚说明条件式的用途。

    1. //重构后
    2. double getExpenseLimit() {
    3. Assert.isTrue (Assert.ON &&
    4. (_expenseLimit != NULL_EXPENSE || _primaryProject != null));
    5. return (_expenseLimit != NULL_EXPENSE) ?
    6. _expenseLimit:
    7. _primaryProject.getMemberExpenseLimit();
    8. }

    或者是这种手法

    1. //重构后
    2. double getExpenseLimit() {
    3. Assert.isTrue (Assert.ON &&
    4. (_expenseLimit != NULL_EXPENSE || _primaryProject != null));
    5. return (_expenseLimit != NULL_EXPENSE) ?
    6. _expenseLimit:
    7. _primaryProject.getMemberExpenseLimit();
    8. }

    如果Assert.ON 是个常量,编译器(译注:而非运行期间)就会对它进行检查; 如果它等于false ,就不再执行条件式后半段代码。但是,加上这条语句实在有点丑陋,所以很多程序员宁可仅仅使用Assert.isTrue() 函数,然后在项目结束前以过滤程序滤掉使用assertions 的每一行代码(可以使用Perl 之类的语言来编写这样 的过滤程序)。
    Assert class应该有多个函数,函数名称应该帮助程序员理解其功用。除了isTrue() 之外,你还可以为它加上equals() 和shouldNeverReachHere() 等函数。