重构 改善既有代码设计(第9章-简化条件表达式)

分解条件式

if (data.before(SUMMER_START) || data.after(SUMMER_END))
	charge = quantity * _winterRate + _winterServiceCharge;
else
	charge = quantity * _summerRate;
========================>
if (notSummer(data))
	charge = winterCharge(quantity);
else
	charge = summerCharge(quantity);
	
private boolean notSummer(Date date){
	return data.before(SUMMER_START) || data.after(SUMMER_END);
}
private double summerCharge(int quantity){
	return quantity * _summerRate;
}
...

动机

程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。必须写代码检查不同的分支,根据不同的分支做不同的事情,然后会得到也给很长的函数,条件逻辑会使代码很难理解。

做法

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

合并条件式

double disabilityAmount(){
	if (_seniority < 2) return 0;
	if (_monthsDisabled > 12) return 0;
	if (_isPartTime) return 0;
...
}
========================>
double disabilityAmount(){
	if (isNotEligableForDisability()) return 0;
...
}
boolean isNotEligibleForDisability(){
	return ((_seniority < 2) || (_monthsDisabled > 12) || (_isPartTime));
}

动机

检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用logical-AND 和 logical-OR 将它们合并为一个条件式。
之所以要合并条件代码:

  1. 实际上只有一次条件检查,只不过有数个并列条件需要检查而已
  2. 将检查条件提炼成一个独立函数对于厘清代码意义非常有用,因为它把描述**【做什么】的语句换成了【为什么这样做】**。
  3. 条件语句的【合并理由】也同时指出了【不要合并】的理由。

做法

  • 确定这些条件语句都没有副作用(连带影响)
  • 如果条件式有副作用,你就不能使用本项重构。
  • 使用适当的逻辑操作符,将一系列相关条件式合并为一个。
  • 编译,测试。
  • 对合并后的条件式实施Extract Method。

范例

代码展示"logical-AND"的用法:

if (onVacation())
	if (lengthOfService()>10)
		return 1;
	return 0.5;
========================>
if (onVacation() && lengthOfService() > 10) return 1;
else return 0.5;

return (onVacation() && lengthOfService() > 10) ? 1: 0.5;

合并重复的条件片段

在条件式的每个分支上有着相同的一段代码。

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 将共通 代码提炼到一个独立函数中, 再以前面所说的办法来处理。

移除控制标记

在一系列布尔表达式( boolean expressions) 中, 某个变量带有「 控制标记」 ( control flag) 的作用。以break 语句或return 的语句取代控制标记。

动机

set done to false
while not done
	if (condition)
		do something
		set done to true
	next step of loop

结构化编程原则告诉他们:
每个子程序( routines) 只能有一个入口( entry) 和一个出口( exit) 。
但是「 单一出口」 原则会让你在代码中加入讨厌的控制标记, 大大降低条件表达式的可读性。
这就是编程语言提供break 语句和continue 语句的原因: 你可以用它们跳出复杂的条件语句。

做法

对控制标记处理,使用break或者continue语句。

  • 找出让你得以跳出这段逻辑的控制标记值
  • 找出将可跳出条件式之值赋予标记变量那个语句, 代以恰当的break 语句或continue 语句
  • 每次替换后, 编译并测试。

在未能提供break 和continue 语句的编程语言中

  • 将整段逻辑提炼到一个独立函数
  • 找出让你得以跳出这段逻辑 的那些控制标记值
  • 找出将可跳出条件式之值赋予标记变量的那个语句, 代以恰当的return 语句
  • 每次替换后, 编译并测试。

范例

下列函数用来检查一系列人名之中是否包含两个可疑人物的名字:以break/continue

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";
			}
		}
	}
}
================step: 1================>
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();
				found = "Don";
			}
			if (people[i].equals("John")){
				sendAlert();
				found = "John";
			}
		}
	}
	return found;
}
================step: 2================>
String foundMiscreant(String[] people){
	String found = "";
	for (int i = 0; i < people.length; i ++){
		// if (found.equals("")){ 删掉控制变量语句
			if (people[i].equals("Don")){
				sendAlert();
				// found = "Don"; 赋予标记变量的那个语句,以return代替
				return "Don"; 
			}
			if (people[i].equals("John")){
				sendAlert();
				// found = "John"; 赋予标记变量的那个语句,以return代替
				return "John";
			}
		// }
	 }
	return found;
}

以卫语句取代嵌套条件式

函数中的条件逻辑( conditional logic) 使人难以看清正常的执行路径。

动机

条件式呈现形式:

  1. 所有分支都属于正常行为
  2. 只有一种是正常行为, 其他都是不常见的情况
    卫语句告诉阅读者: 『 这种情况很罕见, 如果它真的发生了, 请做 一些必要的整理工作, 然后退
    出。 』

作法

  • 对于每个检查, 放进一个卫语句
  • 从函数中返回/抛出一个异常
  • 将「 条件检查」 替换成「 卫语句」 后, 编译并测试
  • 所有卫语句都导致相同结果,使用绝对条件表达

范例

薪资系统

double getPayAmount(){
	double result;
	// 非正常情况的检查掩盖了正常情况的检查
	// 应该使用『卫语句」 来取代这些检查, 以提高程序清晰度
	if (_isDead) result = deadAmount();
	else
		if (_isSeparated) result = separatedAmount();
		else {
			if (_isRetired) result = retiredAmount();
			// 正常情况
			else result = normalPayAmount();
		}
	}
	return reuslt;
}
=============================================>
// 并不遵循每个函数只能有一个出口!!!!
// 嵌套条件代码往往由那些深信「每个函数只能有一个出口」 的程序员写出。
double getPayAmount(){
	if (_isDead) return deadAmount();
	if (_isSearated) return separatedAmount();
	if (_isRetired) return retiredAmount();
	return normalPayAmount();
}

将条件逆反

double getAdjustedCapital(){
	double result = 0.0;
	if (_capital > 0.0){
		if (_intRate > 0.0 && _duration > 0.0){
			result = (_income / _duration) * ADJ_FACTOR;
		}
	}
	return result;
}
====================================================>
// 这次在插入卫语句(guard clauses) 时, 我需要将相应的条件逆反过来:
double getAdjustedCapital(){
	double result = 0.0;
	// if (_capital > 0.0){ 逆反条件
	if (_capital <= 0.0) return result;
	// 分两步逆反:1. 首先加入一个"logical-NOT"操作
		// if (_intRate > 0.0 && _duration > 0.0){
		// if (!(_intRate > 0.0 && _duration > 0.0)) return result;
		if (_intRate <= 0.0 || _duration <= 0.0) return result;
		// 剩下的情况就是原来满足所有条件的表达式
		result = (_income / _duration) * ADJ_FACTOR;
		}
	// }
	return result;
}
// 2. 将临时变量移除
double getAdjustedCapital(){
	if (_capital <= 0.0) return 0.0;
	if (_intRate <= 0.0 || _duration <= 0.0) return 0.0
	return (_income / _duration) * ADJ_FACTOR;
}

以多态取代条件式

动机

多态( polymorphism) 最根本的好处就是:
如果你需要根据对象的不同型别而采取不同的行为, 多态使你不必编写明显的条件式( explicit conditional ) 。「 针对type code( 型别码) 而写的switch 语句」 以及「 针对type string ( 型别名称字符串) 而写的if-then-else 语句」 在面向对象程序中很少出现。

做法

  • 如果要处理的条件式是一个更大函数中的一部分, 首先对条件式进行分析, 然后使用Extract Method 将它提炼到一个独立函数去。
  • 如果有必要, 使用Move Method 将条件式放置到继承结构的顶端。
  • 任选一个subclass , 在其中建立一个函数, 使之覆写superclass 中容纳条件式的那个函数。 将「与subclass 相关的条件式分支」 拷贝到新建函数中, 并对它进行适当调整。
  • 为了顺利进行这一步骤, 你可能需要将superclass 中的某些private 值域声明为protected 。
  • 编译, 测试。
  • 在superclass 中删掉条件式内被拷贝出去的分支。
  • 编译, 测试。
  • 针对条件式的每个分支, 重复上述过程, 直到所有分支都被移到subclass 内的函数为止。
  • 将superclass 之中容纳条件式的函数声明为抽象函数(abstract method) 。

范例

员工与薪资
在这里插入图片描述

class Employee...
	/* 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("Incorrect Employee");
		}
	} 
	*/
	int getType() {
		return _type.getTypeCode();
	} 
	private EmployeeType _type;
// EmployeeType 才是被subclassing 的class 。
abstract class EmployeeType...
	abstract int payAmount(Employee emp);
	stract int getTypeCode();
class Engineer extends EmployeeType...
		int payAmount(Employee emp) {
			return emp.getMonthlySalary();
		}
		int getTypeCode() {
			return Employee.ENGINEER;
		}
	... and other subclasses

引入Null对象

动机

系统在对对象发送一个消息之前, 总要检査对象是否存在,这样的检査出现很多次。造成的重复代码让我们很烦心。编写了一个MissingPerson class, 让它返回 ‘0’ 薪资等级( 我们把null objects 称为missing object( 虚构对象)。请记住: null objects 一定是常量, 它们的任何成分都不会发生变化,可以使用Singleton 模式[Gang of Four]来实现它们。

作法

  • 为source class 建立一个subclass , 使其行为像source class 的null 版本。在source class 和null class 中都加上isNull() 函数, 前者的isNull() 应该返回false, 后者的isNull() 应该返回true。
  • 建立一个nullable 接口, 将isNull() 函数放在其中, 让source class 实现这个接口。
  • 创建一个testing 接口, 专门用来检查对象是否为null
  • 编译
  • 找出所有「 索求source object 却获得一个null 」 的地方。 修改这些地方, 使它们改而获得一个null object
  • 找出所有「 将source object 与null 做比较」 的地方。 修改这些地方, 使它们调用isNull() 函数。
  • 每次只处理一个source object 及其客户程序, 编译并测试后, 再处理另一个source object 。
  • 在「 不该再出现null value」 的地方放上一些assertions( 断言) , 确保null 的确不再出现。
  • 编译, 测试
  • 如果对象不是null , 做A动作, 否则做B 动作。
  • 对于每一个上述地点, 在null class 中覆写A动作, 使其行为和B 动作相同
  • 使用上述的被覆写动作( A) , 然后删除「 对象是否等于null」 的条件测试。 编译并测试。

引入断言

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();
}

动机

有时程序员会以注释写出这样的假设。 而我要介绍的是一种更好的技术: 使用assertion( 断言) 明确标明这些假设。assertion 是一个条件式, 应该总是为真。 如果它失败, 表示程序员犯了错误。

作法

如果程序员不犯错, assertions 就应该不会对系统运行造成任何影响, 所以加入assertions 永远不会影响程序的行为。

  • 如果你发现代码「 假设某个条件始终( 必须) 为真], 就加入一个assertion 明确说明这种情况
  • 你可以新建一个Assert class, 用于处理各种情况下的assertions 。
    你应该常常问自己: 如果assertions 所指示的约束条件不能满足, 代码是否仍能正常运行? 如果可以, 就把assertions 拿掉。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值