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

7.1 搬移函数(Move Method)

范例

用一个表示“账户”的Account类来说明:

class Account {
	private AccountType type;
    //超支天数
	private int daysOverdrawn;
	double overdraftCharge() {
        //是否是保险
		if (type.isPremium()) {
			double result = 10;
			if(daysOverdrawn > 7)
				result += (daysOverdrawn - 7) * 0.85;
			return result;
		} else
			return daysOverdrawn * 1.75;
	}
	
	double bankCharge() {
		double result = 4.5;
		if(daysOverdrawn>0)
			result += overdraftCharge();
		return result;
	}
}

假设有几种新账户,每一种都有自己的“透支金额计费规则”。所以我希望将overdraftCharge()搬移到AccountType类去。

第一步:观察被overdraftCharge()使用的每一项特性,考虑是否值得将它们与overdraftCharge()一起移动。

由于超支天数daysOverdrawn会随着账户不同而不同,所以需要把它留在Account类中。然后,将overdraftCharge()复制到AccountType中,做一些调整。

class AccountType {
	boolean isPremium() {
		return true;
	}
	double overdraftCharge(int daysOverdrawn) {
		if (this.isPremium()) {
			double result = 10;
			if(daysOverdrawn > 7)
				result += (daysOverdrawn - 7) * 0.85;
			return result;
		} else
			return daysOverdrawn * 1.75;
	}
}

这里,由于我们将这个方法移到了AccountType中,所以原先

type.isPremium()就被this.isPremium()替代了,其中,this是可以省略的。

而在原先方法中,daysOverdrawn作为Account类的字段,并且不适合移动到AccountType中,所以我们就要以参数的形式来传递此值。

想要使用源类的特性时,有四种选择:

  • 将这个特性移动到目标类(下一节所讲的搬移字段
  • 建立或使用一个从目标类到源类的引用关系(可能会使两个类相互依赖)
  • 将源对象当作参数传给目标函数(出现过长参数列(3.4 Long Parameter List)时可以考虑这么做)
  • 如果所需特性是个变量,将它作为参数传给目标函数。

现在,经过编译可以发现此时目标函数已能够使用,这个时候就能将原先它所在的位置替换成调用目标函数,即委托目标函数。

class Account {
	private AccountType type;
	private int daysOverdrawn;
	double overdraftCharge() {
		return type.overdraftCharge(daysOverdrawn);
	}
	
	double bankCharge() {
		double result = 4.5;
		if(daysOverdrawn>0)
			result += overdraftCharge();
		return result;
	}
}

此时,就应该测试一下是否正常运行。

写到这里,其实已经可以结束了,但还有一步可能是画蛇添足的点——去掉源函数,将所有引用点都使用目标函数。

class Account {
	private AccountType type;
	private int daysOverdrawn;
	
	double bankCharge() {
		double result = 4.5;
		if(daysOverdrawn>0)
			result += type.overdraftCharge(daysOverdrawn);
		return result;
	}
}

倘若在想要搬移的方法内部调用了Account的另一个函数,此时就必须将源对象作为参数传给目标函数。

class AccountType {
	boolean isPremium() {
		return true;
	}
	double overdraftCharge(Account account) {
		if (this.isPremium()) {
			double result = 10;
			if (account.getDaysOverdrawn() > 7)
				result += (account.getDaysOverdrawn() - 7) * 0.85;
			return result;
		} else
			return account.getDaysOverdrawn() * 1.75;
	}
}

如果需要源类的多个特性,那么也需要把源对象传递给目标函数。但如果目标函数需要的源类特性过多,那么就该考虑,是不是此处的搬移函数不合适,或者说还有进一步改进的空间,需要把其中一部分再放回源类。

7.2 搬移字段(Move Field)

你的程序中,某个字段被其所驻类之外的另一个类更多地用到。

在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。

class A{
	int bField;
}
class B{}

>>>
class A{}
class B{
	int bField;
}

动机

在类之间移动状态和行为,是重构过程中不可缺少的措施。

搬移字段也和搬移函数一样,被广泛用在重构过程中。

对于类中的一个字段,如果另一个类中使用这个字段的次数更频繁,那么就该考虑搬移字段。

需要注意的一点是,搬移字段是相对的,如果另一个类的方法更适合搬到这边,我们要使用的就该是搬移函数了。

使用提炼类(7.3 Extract Class)时,可能需要搬移字段+搬移函数。

做法

  1. 如果字段的访问级别是public,就先封装字段(8.10 Encapsulate Field)。

封装字段(8.10 Encapsulate Field):将字段设置为private,并写getter和setter方法,这样更安全。

如果你有可能移动那些频繁访问该字段的函数,或如果有很多函数访问某个字段,先使用自封装字段(8.1 Self Encapsulate Field)也许会有帮助。

自封装字段(8.1 Self Encapsulate Field):在当前类下,使用getter和setter方法去访问和修改,这样更能保证灵活性。

  1. 编译、测试。
  2. 在目标类中建立与源字段相同的字段,并同时建立相应的getter和setter方法。
  3. 编译目标类。
  4. 决定如何在源函数中引用目标对象。

首先看是否有现成的字段或方法可以助你得到目标对象。如果没有,就尝试自己新建一个这样的函数去获取目标对象。如果还不行,就得在源类中新建一个字段来存放目标对象。

  1. 删除源字段。
  2. 将所有对源字段的引用替换为对某个目标函数的调用。

如果需要读取该变量,就把对源字段的引用替换为对目标取值函数的调用;如果要对该变量赋值,就把对源字段的引用替换成对设值函数的调用。

如果源字段不是private修饰,那么它可能出现在任何使用这个类的地方,需要确认。

  1. 编译、测试。

范例

class Account {
	private AccountType type;
    // 利率
	private double interestRate;
	
	double interestForAmountDays(double amount, int days) {
		return interestRate * amount * days / 365;
	}
}

我想把表示利率的interestRate搬移到AccountType类中。

第一步,将字段搬移到AccountType中,并且封装字段(8.10 Self Encapsulate Field)。

class AccountType {
	private double interestRate;

	public double getInterestRate() {
		return interestRate;
	}

	public void setInterestRate(double interestRate) {
		this.interestRate = interestRate;
	}
}

然后,在Account中,访问源字段的地方都改成通过对象去取目标字段。

class Account {
	private AccountType type;
    // 利率
	private double interestRate;
	
	double interestForAmountDays(double amount, int days) {
		return type.getInterestRate() * amount * days / 365;
	}
}

现在我们简单地扩展一下业务,假设在Account中存在有十个函数,都需要使用interestRate。(这只是个例子,如果现实真的如此,可能这里使用搬移字段不太合适)

那么就该用一下自封装字段(8.1 Self Encapsulate Field)。

class Account {
	private AccountType type;
    
    public double getInterestRate() {
		return type.getInterestRate();
	}

	public void setInterestRate(double interestRate) {
		type.setInterestRate(interestRate);
	}
}

结果显而易见,我在Account中的十个函数不必都写成type.getInterestRate(),而是统一写成getInterestRate()即可,这样也保证了,当未来即便出现最坏的情况(type没了,或者它里面的interestRate改动了),我们只需要改动Account类中的getter和setter方法即可,这样更灵活。

如果我需要对类做许多处理,那么封装字段很有必要,它能使得重构可以小步前进。

先使用自封装字段能够让搬移函数变得更为方便。

7.3 提炼类(Extract Class)

某个类做了应该由两个类做的事。

新建一个新类,将相关的字段和函数从旧类搬移到新类。

/'在线作图(UML)网址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的话,打开网址后,直接复制上图片链接(或者粘贴下方代码)修改即可'/
@startuml
Title "巨大类"
class Person{
- private String name
- private String officeAreaCode
- private String officeNumber
+ public String getTelePhoneNumber()
}
@enduml

在这里插入图片描述

/'在线作图(UML)网址:
http://www.plantuml.com/plantuml/uml/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000
如果要修改的的话,打开网址后,直接复制上图片链接(或者粘贴下方代码)修改即可'/
@startuml
Title "提炼类"
class Person{
- private String name
- private TelephoneNumber telephoneNumber
+ public String getTelePhoneNumber()
}
class TelephoneNumber{
- private String areaCode
- private String number
+ public String getTelePhoneNumber()
}
Person --> TelephoneNumber
@enduml

在这里插入图片描述

动机

一个类应该是一个清楚的抽象,处理一些明确的责任。但在实际工作中,类会不断成长扩展,渐渐变得复杂。

当一个类被添加一项新的职责时,也许你会认为这并不值得为了这个新职责专门分出一个类。当责任不断增加,你会被渐渐麻痹,突然有天发现,这个类已经一团乱麻。

这样的类往往含有大量函数和数据,它浑身上下散发着过大的类(3.3 Large Class)这样的坏味道,我们应该去清理——也算是弥补过去增加责任时没有及时修改的‘过错’。

如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表明你该将其提炼出来。

当你决定要提炼的东西后,要问一下你自己,如果把这些东西拿走,这个类中的其他部分是否会受到极大影响?其他字段会不会因此变得毫无意义?

除了成对出现的情况,还有一种开发后期出现的信号——类的子类化方式。如果你发现子类化只影响类的部分特性,或如果你发现某些特性需要以一种方式来子类化,而另外一些特性需要另一种方式子类化,此时出现了分歧,就意味着该把原来的类进行分解提炼。

做法

  1. 决定如果分解类所负的责任。
  2. 建立一个新类,用以存放从旧类中分离出的责任。(如果旧类搬走这个责任后,类名不太合适,就得改)
  3. 建立“从旧类访问新类”的连接关系。

除非真正需要,否则不要在新类中引入旧类的连接。(尽可能单向连接,而非双向连接)

  1. 对于你想搬移的每一个字段,运用搬移字段(7.2 Move Field)。
  2. 每次搬移字段后,都要编译、测试。
  3. 使用搬移函数(7.1 Move Method)将必要函数搬移到新类。

先搬移较低层的函数,再搬移高层函数。

最低层函数:即便搬到新类中也不会编译报错。(相当于它在旧类里起到的是被调用的作用,而不会调用旧类中的其他方法)

  1. 每次搬移方法后,编译、测试。
  2. 检查,精简每个类的接口。
  3. 决定是否公开新类。如果你的确需要公开它,就要决定让它成为引用对象还是不可变的值对象。

范例

先从简单的Person类开始:

class Person {
	private String name;
	private String officeAreaCode;
	private String officeNumber;
	
	// 省略getter和setter
	
	public String getTelephoneNumber() {
		return ("(" + officeAreaCode + ") " + officeNumber);
	}
}

除去了基本的getter和setter方法,还剩一个getTelephoneNumber方法,在这个方法内部用到了两个属性。

此时,我们可以将电话号码相关的行为分离到一个独立类中。

首先,定义一个新类TelephoneNumber来表示电话号码这个概念:

class TelephoneNumber{}

然后,在旧类中建立对新类的连接:

class Person{
	...
	private TelephoneNumber officeTelephone = new TelephoneNumber();
}

之后就开始搬移字段,这里我为了节省空间,一次性搬移完成,事实上应该一个一个搬移,以防错误。

class TelephoneNumber{
    private String number;
    private String areaCode;
    
    // 省略getter和setter
}

然后,搬移函数:

class TelephoneNumber{
    ...
    
    public String getTelephoneNumber(){
        return ("(" + areaCode + ") " + number);
    }
}

这个时候旧类就变成了这副摸样:

class Person {
	private String name;
	private TelephoneNumber officeTelephone = new TelephoneNumber();
	
	// 省略getter和setter
	
	public String getTelephoneNumber() {
		return officeTelephone.getTelephoneNumber();
	}
}

下一步要做个决定:要不要对用户公开这个新类?

如果想要隐藏这个新类,那么可以将Person中与电话号码相关的函数委托至TelephoneNumber,也就是说我不需要通过新类调用,而是从Person这里调用。

如果选择公开新类,就需要考虑别名带来的危险。如果公开了TelephoneNumber,而有个用户修改了对象中的areaCode字段值,我又怎么能知道呢?而且,做出修改的可能不是直接用户,而是用户的用户的用户。

面对以上问题,有以下几种选择:

  • 允许任何对象修改TelephoneNumber对象的任何部分。这种情况下,考虑将值对象改为引用对象(8.3 Change Value to Reference)。Person仅仅需要在使用它的时候才会调用,而不起到中间人的作用。(也就是说,别的类如果想用TelephoneNumber,那么不需要经过Person)

将值对象改为引用对象(8.3 Change Value to Reference):这里并没有看明白,似乎是改变了组合结构,从合成关系变成了关联关系

  • 不许任何人直接修改TelephoneNumber对象——只能够通过Person对象间接读取。此时,可以将TelephoneNumber设为不可修改的,或为它提供一个不可修改的接口。

  • 另一个办法是:先复制一个TelephoneNumber对象,然后将复制得到的新对象传递给用户。但这可能会造成一定程度的迷惑,因为人们会认为他们可以修改TelephoneNumber对象值。(这里我并没有理解他的意思是什么)此外,如果同一个TelephoneNumber对象被传递给多个用户,也可能在用户之间造成别名问题。

提炼类是改善并发程序的一种常用技术,因为它可以让不同职责分离,然后给提炼后的类分别加锁。

这里也存在危险性——如果需要确保两个对象同时被锁定,就面临事务问题,需要使用其他类型的共享锁。

事务具有实用性,但编写事务管理程序超出了大多数程序员的职责范围。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NewReErWen

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

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

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

打赏作者

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

抵扣说明:

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

余额充值