那有什么天生如此,只是我们天天坚持。
本篇文章主要讲解 《重构---改善既有代码的设计》 这本书中的 第九章简化条件表达式中 的知识点,
Decompose Conditional(分解条件表达式)问题:你有一个复杂的条件(if、then、else) 语句
解决:从if、then、else三个段落中分别提炼出独立函数
//重构前 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);动机
将条件分支的代码分解成多个独立函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新建函数,可以更清楚地表达自己的意图。
做法将if段落提炼出来,构成一个独立函数
将then段落和else段落都提炼出来,各自构成一个独立函数
Consolidate Conditional Expression(合并条件表达式)问题:如果有一系列条件测试,都得到相同结果。
解决:将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数
//重构前 double disabilityAmount(){ if(_seniority < 2) return 0; if(_monthsDisabled > 12) return 0; if(_isPartTime) return 0; }
//重构后 double disabilityAmount(){ if(isNotEligableForDisability()) return 0; }动机
如果一串条件检查:检查条件各不相同,最终行为却一致,就应该将它们合并为一个条件表达式,之所以要合并条件代码,有两个重要原因,
首先,合并后的条件代码用意更清晰,其次,这项重构往往可以为使用Extract Method(提炼函数)做好准备
确定这些条件语句都没有副作用
使用适当的逻辑操作符,将一系列相关条件表达式合并为一个
编译,测试
对合并后的条件表达式实施Extract Method
Consolidate Duplicate Conditional Fragments(合并重复的条件片段)问题:在条件表达式的每个分支上有着相同的一段代码
解决:将这段重复代码搬移到条件表达式之外
//重构前 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();动机
有助于清楚地表明哪些东西随条件的变化而变化、哪些东西保持不变
做法鉴别出”执行方式不随条件变化而变化”的代码‘
如果这些共通代码位于条件表达式起始处,就将它移到条件表达式之前
如果这些共通代码位于条件表达式尾端,就将它移到条件表达式之后
如果这些共通代码位于条件表达式中段,就需要观察共通代码之前或之后的代码是否改变了什么东西,如果的确有所改变,应该首先将共通代码向前或向后移动,移至条件表达式的起始处或尾端,再以前面所受的办法来处理
如果共通代码不止一条语句,应该先使用Extract Method(提炼函数)将共通代码提炼到一个独立函数中,再以前面所说的办法来处理
范例//重构前 if (isSpecialDeal()) { total = price * 0.95; send(); } else { total = price * 0.98; send(); }
由于条件式的两个分支都执行了 send() 函数,所以我应该将send() 移到条件式的外围:
if (isSpecialDeal()) total = price * 0.95; else total = price * 0.98; send();
我们也可以使用同样的手法来对待异常(exceptions)。如果在try 区段内「可能引发异常」的语句之后,以及所有catch 区段之内,都重复执行了同一段代码,我就 可以将这段重复代码移到final 区段。
Remove Control Flag(移除控制标记)问题:在一系列布尔表达式中,某个变量带有”控制标记”的作用
解决:以break语句或return语句取代控制标记
动机在一系列条件表达式中,你常常会用到[用以何时停止条件检查]的控制标记。
set done to false while not done if (condition) //do something //set done to true next step of loop
用break语句和continue语句跳出复杂的条件语句
做法找出让你跳出这段逻辑的控制标记值
找出对标记变量赋值的语句,代以恰当的break语句或continue语句
每次替换后,编译并测试
范例 :以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; } } } }
=>
//重构后 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; } } }范例 :以return返回控制标记
//重构前 void checkSecurity(String[] people){ String found = ""; for(int i = 0; i < people.length; i++){ if(found.equals("")){ if(people[i].equals("Don")){ sendAlert(); found = "Don"; } if(people[i].equals("John")){ sendAlert(); found = "John"; } } } someLaterCode(found); }
//重构后 void checkSecurity(String[] people){ String found = foundMiscreant(people); someLaterCode(found); } String foundMiscreant(String[] people){ String found = ""; for(int i = 0; i < people.length; i++){ if(found.equals("")){ if(people[i].equals("Don")){ sendAlert(); return "Don"; } if(people[i].equals("John")){ sendAlert(); return "John"; } } } return ""; }Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)
问题:函数中的条件逻辑使人难以看清正常的执行路径
解决:使用卫语句表现所有特殊情况(启哥备注:可以减少嵌套)
//重构前 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(); }动机
条件表达式通常有两种表现形式:
第一种是:所有分支都属于正常行为
第二种是:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况
如果某个条件极其罕见,就应该多带带检查该条件,并在该条件为真时立刻从函数中返回,这样的多带带检查常常被称为”卫语句”
做法对于每个检查,放进一个卫语句,卫语句要么从函数中返回,要么就抛出一个异常
每次将条件检查替换成卫语句后,编译并测试
范例:将条件反转//重构前 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(){ double result = 0.0; if(_capital <= 0.0) return 0.0; if(_intRate <= 0.0 || _duration <= 0.0) return 0.0; return (_income / _duration) * ADJ_FACTOR; }Replace Conditional with Polymorphism(以多态取代条件表达式)
问题:你手上有个条件表达式,他根据对象类型的不同而选择不同的行为
解决:将条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数
//重构前 double getSpeed(){ switch(_type){ case EUROPEAN: return getBaseSpeed(); case AFRICAN: return getBaseSpeed() - getLoadFactory() * _numberOfCoconuts; case NORWEGIAN_BLUE: return (_isNailed) ? 0 : getBaseSpeed(_voltage); } throw new RuntimeException("Should be unreachable"); }
==>
//重构后
多态最根本的好处就是:如果需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式
做法如果要处理的条件表达式是一个更大函数中的一部分,首先对条件表达式进行分析,然后使用Extract Method将它提炼到一个独立函数中
如果有必要,使用Move Method(搬移函数)将条件表达式放置到继承结构的顶端
任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数,将与该子类相关的条件表达式分支复制到新建函数中,并对它进行适当调整
编译,测试
在超类中删除条件表达式内被复制了的分支
编译,测试
针对条件表达式的每个分支,重复上述过程,直到所有分支都被移动子类内函数为止
将超类之中容乃条件表达式的函数声明为抽象函数
范例请允许我继续使用「员工与薪资」这个简单而又乏味的例子。
继承构:
//重构前 class Employee... int payAmount(int type){ switch(type){ case Employee.ENGINEER: return _monthlySalary; case Employee.SALESMAN: return _monthlySalary + _commission; case Employee.MANAGER: return _monthlySalary + _bonus; default: throw new IllegalArgumentException("Incorrect type code value"); } } private Employee _type; int getType(){ return _type.getTypeCode() } abstract class EmployeeType... abstract int getTypeCode(); class Engineer extends EmployeeType... int getTypeCode(){ return Employee.ENGINEER: } ...and other subclasses
switch 语句已经被很好地提炼出来,因此我不必费劲再做一遍。不过我需要将它移至EmployeeType class,因为EmployeeType 才是被subclassing 的class 。
class EmployeeType... int payAmount(Employee emp) { switch (getTypeCode()) { case ENGINEER: return emp.getMonthlySalary(); case SALESMAN: return emp.getMonthlySalary() + emp.getCommission(); case MANAGER: return emp.getMonthlySalary() + emp.getBonus(); default: throw new RuntimeException("Incorrect Employee"); } }
由于我需要EmployeeType class 的数据,所以我需要将Employee 对象作为参数传递给payAmount()。这些数据中的一部分也许可以移到EmployeeType class 来,但那是另一项重构需要关心的问题了。
调整代码,使之通过编译,然后我修改Employee 中的payAmount() 函数,令它委托(delegate,转调用)EmployeeType :
class Employee... int payAmount() { return _type.payAmount(this); }
现在,我可以处理switch 语句了。这个过程有点像淘气小男孩折磨一只昆虫——每次掰掉它一条腿(意思就是「去掉一个分支」)。首先我把switch 语句中的"Engineer"这一分支拷贝到Engineer class:
class Engineer... int payAmount(Employee emp) { return emp.getMonthlySalary(); }
这个新函数覆写了superclass 中的switch 语句之内那个专门处理"Engineer"的分支。我是个徧执狂,有时我会故意在case 子句中放一个陷阱,检查Engineer class 是否正常工作(是否被调用):
class EmployeeType... int payAmount(Employee emp) { switch (getTypeCode()) { case ENGINEER: throw new RuntimeException ("Should be being overridden"); case SALESMAN: return emp.getMonthlySalary() + emp.getCommission(); case MANAGER: return emp.getMonthlySalary() + emp.getBonus(); default: throw new RuntimeException("Incorrect Employee"); } }
接下来,我重复上述过程,直到所有分支都被去除为止:
class Salesman... int payAmount(Employee emp) { return emp.getMonthlySalary() + emp.getCommission(); } class Manager... int payAmount(Employee emp) { return emp.getMonthlySalary() + emp.getBonus(); }
然后,将superclass 的payAmount() 函数声明为抽象函数:
class EmployeeType... abstract int payAmount(Employee emp);
a
Introduce Null Object(引入Null 对象)问题:你需要再三检查某物是否为null value
解决:将null值替换为null对象
if (customer == null) plan = BillingPlan.basic(); 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 表示:
//重构前 class Site... Customer getCustomer(){ return _customer; } Customer _customer; class Customer... public String getName(){...} public BillingPlan getPlan(){...} public PaymentHistory getHistory(){...} public class PaymentHistory... int getWeesDelingquentInLastYear() Customer customer = site.getCustomer(); BillingPlan plan; if(customer == null) plan = BillingPlan.basic(); else plan = customer.getPlan(); ...
//重构后 class NullCustomer extens Customer{ public boolean isNull(){ return true; } } class Customer... public boolean isNull(){ return false; } static Customer new Null(){ return new NullCustomer(); } class Site... Customer getCustomer(){ return (_customer == null) ? Customer.newNull() : _customer; } Customer customer = site.getCustomer(); BillingPlan plan; if(customer.isNull()) plan = BillingPlan.basic(); else plan = customer.getPlan();Introduce Assertion(引入断言)
问题:如果某一段代码需要对程序状态做出某种假设
解决:以断言明确表现这种假设
//重构前 double getExpenseLimit(){ return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit(); }
//重构后 double getExpenseLimit(){ Assert.isTrue(_expenseLimit != NULL_EXPENSE || _primaryProject != null); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit:_primaryProject.getMemberExpenseLimit(); }动机
常常会有这样一段代码:只有当某个条件为真时,该段代码才能正常运行,这时应该使用断言,把不符合条件的假设标明出来
断言可以作为交流与调试的辅助,在交流的角度上,断言可以帮助程序阅读者理解代码所做的假设;在调试的角度上,断言可以在距离bug最近的地方抓住它们
在一段逻辑中加入断言是有好处的,因为它迫使你重新考虑这段代码的约束条件,如果不满足这些约束条件,程序也可以正常运行,断言就不会带给你任何帮助,只会把代码变得混乱,并且有可能妨碍以后的修改
做法如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况
范例下面是一个简单例子:开支(经费)限制。后勤部门的员工每个月有固定的开支限额;业务部门的员工则按照项目的开支限额来控制自己的开支。一个员工可能没有开支额度可用,也可能没有参与项目,但两者总得要有一个(否则就没有经费可用 了)。在开支限额相关程序中,上述假设总是成立的,因此:
//重构前 class Employee... private static final double NULL_EXPENSE = -1.0; private double _expenseLimit = NULL_EXPENSE; private Project _primaryProject; double getExpenseLimit() { return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); } boolean withinLimit (double expenseAmount) { return (expenseAmount <= getExpenseLimit()); }
这段代码包含了一个明显假设:任何员工要不就参与某个项目,要不就有个人开支限额。我们可以使用assertion 在代码中更明确地指出这一点:
double getExpenseLimit() { Assert.isTrue (_expenseLimit != NULL_EXPENSE || _primaryProject != null); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); }
这条assertion 不会改变程序的任何行为。另一方面,如果assertion中的条件不为真,我就会收到一个运行期异常:也许是在withinLimit() 函数中抛出一个空指针(null pointer)异常,也许是在Assert.isTrue() 函数中抛出一个运行期异常。有时assertion 可以帮助程序员找到臭虫,因为它离出错地点很近。但是,更多时候,assertion 的价值在于:帮助程序员理解代码正确运行的必要条件。
我常对assertion 中的条件式使用Extract Method ,也许是为了将若干地方的重复码提炼到同一个函数中,也许只是为了更清楚说明条件式的用途。
//重构后 double getExpenseLimit() { Assert.isTrue (Assert.ON && (_expenseLimit != NULL_EXPENSE || _primaryProject != null)); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); }
或者是这种手法
//重构后 double getExpenseLimit() { Assert.isTrue (Assert.ON && (_expenseLimit != NULL_EXPENSE || _primaryProject != null)); return (_expenseLimit != NULL_EXPENSE) ? _expenseLimit: _primaryProject.getMemberExpenseLimit(); }
如果Assert.ON 是个常量,编译器(译注:而非运行期间)就会对它进行检查; 如果它等于false ,就不再执行条件式后半段代码。但是,加上这条语句实在有点丑陋,所以很多程序员宁可仅仅使用Assert.isTrue() 函数,然后在项目结束前以过滤程序滤掉使用assertions 的每一行代码(可以使用Perl 之类的语言来编写这样 的过滤程序)。
Assert class应该有多个函数,函数名称应该帮助程序员理解其功用。除了isTrue() 之外,你还可以为它加上equals() 和shouldNeverReachHere() 等函数。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/67989.html
摘要:但条件逻辑也是不能忽视的分解条件表达式问题有一个复杂的条件语句。没什么说的动机重构代码就是错移除控制标志问题在一系列布尔表达式中,某个变量带有控制标记的作用方法以语句或语句取代控制标记动机控制标记大大降低了代码可读性。 前言 前面已经对类,方法,字段都进行了重构。貌似看起来很完整了。但条件逻辑也是不能忽视的 分解条件表达式 问题 有一个复杂的条件(if-then-else)语句。(判断...
摘要:函数改名问题函数的名称未能揭示函数的用途。这些人甚至会在构造函数中使用设值函数。方法将构造函数替换为工厂函数。以上所说的情况,常会在返回迭代器或集合的函数身上发生。以异常取代错误码问题某个函数返回一个特定的代码,用以表示某种错误情况。 Rename Method 函数改名 问题 函数的名称未能揭示函数的用途。 方法 修改函数名称。 动机 好的函数需要有一个清晰的函数名。保证一看就懂 A...
摘要:重构在不改变代码的外在的行为的前提下对代码进行修改最大限度的减少错误的几率本质上,就是代码写好之后修改它的设计。重构可以深入理解代码并且帮助找到。同时重构可以减少引入的机率,方便日后扩展。平行继承目的在于消除类之间的重复代码。 重构 (refactoring) 在不改变代码的外在的行为的前提下 对代码进行修改最大限度的减少错误的几率 本质上, 就是代码写好之后 修改它的设计。 1,书中...
摘要:为何重构重构有四大好处重构改进软件设计如果没有重构,程序的设计会逐渐腐败变质。经常性的重构可以帮助维持自己该有的形态。你有一个大型函数,其中对局部变量的使用使你无法采用。将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。 哪有什么天生如此,只是我们天天坚持。 -Zhiyuan 国庆抽出时间来阅读这本从师傅那里借来的书,听说还是程序员的必读书籍。 关于书的高清下载连...
阅读 901·2021-11-22 12:09
阅读 3685·2021-09-27 13:36
阅读 1374·2021-08-20 09:37
阅读 3913·2019-12-27 12:22
阅读 2327·2019-08-30 15:55
阅读 2295·2019-08-30 13:16
阅读 2797·2019-08-26 17:06
阅读 3417·2019-08-23 18:32