《重构 改善既有代码的设计》 读书笔记(二十一)

6.6 分解临时变量(Split Temporaray Variable)

你的程序有个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。

针对每次赋值,创造一个独立、对应的临时变量。

double temp = 2 * (height + width);
System.out.println(temp);
temp = height * width;
System.out.println(temp);

>>>
final double temp1 = 2 * (height + width);
System.out.println(temp);
final double temp2 = height * width;
System.out.println(temp2);

动机

临时变量的用途往往不只是被使用,被多次赋值的事情时有发生。循环是一个很经典的例子,特别是for循环,总会有“i”这么个变量存在。

当临时变量被两次赋值,那就意味着,你定位到程序的某个位置时,并不能确定此时临时变量到底存放的是哪个的值。这种情况下,把临时变量分割成两个更易于看懂代码。

另类的单一职责:每个变量只承担一个责任。同一个变量承担两种不同的责任,会令代码阅读者糊涂。

做法

  1. 在待分解临时变量的声明及其第一次被赋值处,修改其名称。

如果这是个结果收集变量(比如i=i+表达式),那么不要分解它,因为它这个时候的职责还是一致的。

  1. 将新的临时变量声明为final。

  2. 以该临时变量的第二次复制动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。

  3. 在第二次赋值处,重新声明原先那个临时变量。

  4. 编译、测试。

范例

下面范例中我要计算一个苏格兰布丁运动的距离。在起点处,静止的苏格兰布丁会受到一个初始力的作用而开始运动。一段时间后,第二个力作用于布丁,让它再次加速。根据牛顿第二定律,可以这样计算距离:

//注意例子中的acc,它被赋值两次
double primaryForce = 10;
double mass = 5;
int delay = 20;
double secondaryForce = 20;
double getDistanceTravelled(int time) {
	double result;
	// primaryForce:基本力
	// mass:团;块;堆;大量;许多;(常指混乱的)一群,一堆
	double acc = primaryForce / mass;
	int primaryTime = Math.min(time, delay);
	result = 0.5 * acc * primaryTime * primaryTime;
	int secondaryTime = time - delay;
	if(secondaryTime > 0) {
		double primaryVel = acc * delay;
		acc = (primaryForce + secondaryForce) / mass;
		result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
	}
	return result;
}

acc变量有两个责任:

  • 保存第一个力造成的初始加速度

  • 保存两个力共同造成的加速度

我们可以通过修改临时变量名称,增加新变量,同时把两个变量用final修饰来保证一次赋值。

double getDistanceTravelled(int time) {
	double result;
	// primaryForce:基本力
	// mass:团;块;堆;大量;许多;(常指混乱的)一群,一堆
	final double primaryAcc = primaryForce / mass;
	int primaryTime = Math.min(time, delay);
	result = 0.5 * primaryAcc * primaryTime * primaryTime;
	int secondaryTime = time - delay;
	if (secondaryTime > 0) {
		double primaryVel = primaryAcc * delay;
		final double secondaryAcc = (primaryForce + secondaryForce) / mass;
		result += primaryVel * secondaryTime
				+ 0.5 * secondaryAcc * secondaryTime * secondaryTime;
	}
	return result;
}

记住,我在这里一次性替换完成,但是实际应该一个一个地替换——先将第一次赋值的变量改名,然后用final修饰;然后把第二次赋值所在位置先只定义类型,然后确定没有问题时再用final修饰。

大家也许有好奇苏格兰布丁运动是什么,具体我也不清楚,反正就是一群东西混合在一起的,不好吃的东西。

6.7 移除对参数的赋值(Remove Assignments to Parameters)

方法的一个形参在内部重新赋值。

此时要以一个临时变量赋值取代对该参数的赋值。

int discount(int inputVal, int quantity, int yearToDate){
    if (inputVal > 50)
        inputVal -= 2;
}

>>>
int discount(int inputVal, int quantity, int yearToDate){
    int result = inputVal;
    if (inputVal > 50)
        result -= 2;
}

这样重构是为了保证,参数从传递进去开始,一直没有改变,保持着最初的值。

动机

当你把一个变量作为参数传入方法,然后在方法内部重新对参数赋值,这就意味着你把参数的值改变了,转而引用到另一个对象,此时,无论对这个参数进行怎么样的改变,都不会影响本封装方法之外的内容。

void method(Order order){
    //修改了方法之外的对象内的name字段
    order.setName("笔记本");
    //更改了引用对象
    order = new Order();
    //方法之外的对象内的name字段仍旧是"笔记本"没有改变
    order.setName("笔");
}

这里涉及到一个面试题:Java是值传递还是引用传递?老生常谈的一个问题,在此不多说,上网查一大堆。

在Java里,最好不要对参数赋值,这样可能会降低代码可读性,甚至造成误导。

做法

  1. 建立一个临时变量,把待处理的参数值赋予它。

  2. 以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为临时变量。

  3. 把对参数的赋值变成对临时变量的赋值。

  4. 编译、测试。

范例

int discount(int inputVal, int quantity, int yearToDate) {
	if (inputVal > 50)
		inputVal -= 2;
	if (quantity > 100)
		inputVal -= 1;
	if (yearToDate > 10000)
		inputVal -= 4;
	return inputVal;
}

>>> 用临时变量result替换inputVal进行赋值
int discount(int inputVal, int quantity, int yearToDate) {
	int result = inputVal;
	if (inputVal > 50)
		result -= 2;
	if (quantity > 100)
		result -= 1;
	if (yearToDate > 10000)
		result -= 4;
	return result;
}

>>> 倘若需要强制要求形参不能赋值,那么请给形参加上final修饰
int discount(final int inputVal, final int quantity, final int yearToDate) {
	int result = inputVal;
	if (inputVal > 50)
		result -= 2;
	if (quantity > 100)
		result -= 1;
	if (yearToDate > 10000)
		result -= 4;
	return result;
}

6.8 以函数对象取代函数(Replace Method with Method Object)

你有一个大型函数,其中对局部变量的使用使得无法采用提炼函数6.1 Extract Method)。

将这个函数放进一个单独对象中,把局部变量作为对象内的字段,在这个对象中,对函数进行提炼。这往往会以引入解释性变量6.5 Introduce Explaining Variable)为前提。

class Order{
    double price(){
    	double primaryBasePrice;
    	double secondaryBasePrice;
    	double tertiaryBasePrice;
    	// long computation;
    	...
	}
}

>>>
class PriceCalculator{
    double primaryBasePrice;
    double secondaryBasePrice;
    double tertiaryBasePrice;
    double compute(){
        // long computation;
        //此时局部变量变成了字段,可以在类的内部任何位置使用,方便对其进行提炼函数操作
        ...
    }
}
class Order{
    double price(){
        return new PriceCalculator().compute();
    }
}

动机

程序中总会出现一大段代码无法提炼的情况——往往是因为局部变量的存在导致函数成块聚集,难以分解。

以查询取代临时变量6.4 Replace Temp with Query)是一种方法,但别忘了它可能会影响性能的风险(所以不能滥用),并且有些时候它并不能完成对代码块的分割。

此时,就应该考虑把这一部分赖在一起的代码直接提成一个类,这里有种提炼类(7.3 Extract Class)的感觉。

这种重构方法会将所有局部变量变成函数对象的字段,然后就可以对这个新对象使用提炼函数6.1 Extract Method),将大型函数拆解开来。

做法

  1. 新建一个新类,起一个合适的名字。

  2. 在新类中建立一个final字段,用以保存原先大型函数所在的对象——我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数,都在新类中建立一个对应的字段保存之。

说到这里我有个想法,事实上如果合适的话,可以把一个方法中的部分变量作为此方法所在类的字段。

  1. 在新类中建立一个构造函数,接收源对象及原函数的所有参数。

  2. 在新类中建立一个compute()函数。

  3. 将原函数的代码批量复制到compute()中。如果要调用源对象的任何函数,请通过源对象字段调用——这也正是为什么我们要建立一个final修饰的字段,用于存放原先大型函数所在的对象。

  4. 编译。

  5. 将旧函数的函数本体替换为,实例化新类对象且调用新对象中的compute()函数。

此时,由于所有局部变量成了字段,所以无论你在类中怎么分解大型函数,都不必传递任何参数。

范例

class Account {
	int gamma(int inputVal, int quantity,int yearToDate) {
		int importantValue1 = (inputVal * quantity) + delta();
		int importantValue2 = (inputVal * yearToDate) + 100;
		if((yearToDate-importantValue1) > 100)
			importantValue2 -= 20;
		int importantValue3 = importantValue2 * 7;
		return importantValue3 - 2 * importantValue1;
	}
}

假设我们是一个正在重构别人代码的程序员,这段代码功能含义不明……

为了把这个函数变成一个函数对象,我首先需要声明一个新类。在此新类中我应该提供一个final字段用以保存源对象;对于函数的每一个参数和每一个临时变量,也以一个字段逐一保存。

// 不晓得用途,暂时就以方法名作为类名即可
class Gamma{
    private final Account account;
    private int inputVal;
    private int quantity;
    private int yearToDate;
    private int importantValue1;
    private int importantValue2;
    private int importantValue3;
}

接下来加入构造函数:

Gamma(Account source, int inputValArg, int quantityArg, int yearToDateArg){
    account = source;
    inputVal = inputValArg;
    yearToDate = yearToDateArg;
}

此时就可以把原先函数的所有内容移到compute()中了:

int compute() {
	importantValue1 = (inputVal * quantity) + delta();
	importantValue2 = (inputVal * yearToDate) + 100;
	if((yearToDate-importantValue1) > 100)
		importantValue2 -= 20;
	importantValue3 = importantValue2 * 7;
	return importantValue3 - 2 * importantValue1;
}

最后,修改旧函数,让它把它的工作委托给刚完成的这个函数对象:

int gamma(int inputVal, int quantity,int yearToDate) {
	return new Gamma(
    	this,inputVal,quantity,yearToDate)
        .compute();
}

本项重构的好处是,我可以在新类中直接进行提炼函数,没有后顾之忧,唯一的缺点就是,多加了一个类。

本节最后,对这个方法进行一次提炼:

int compute() {
	importantValue1 = (inputVal * quantity) + delta();
	importantValue2 = (inputVal * yearToDate) + 100;
	importantThing();
	importantValue3 = importantValue2 * 7;
	return importantValue3 - 2 * importantValue1;
}

void importantThing(){
    if((yearToDate-importantValue1) > 100)
		importantValue2 -= 20;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NewReErWen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值