你的程序中,有个函数与其所驻类之外的另一个类进行更多的交流,调用后者或被后者调用。
在该函数最常引用的类中建立一个有着类似行为的新函数。将就函数变成一个单纯的委托函数,或是将旧函数完全移除
动机
- 如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,我就会搬移函数。
做法
-
检查源类中被源函数所使用的一切特性(包含字段和函数),考虑他们是否也该被搬移。
如果某个特性制备你打算搬移的那个函数用到,就应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬移。有时候,搬移一组函数比注意搬移简单些。
-
检查源类的子类和超类,看看是否有该函数的其他声明。
如果出现其他声明,你或许无法进行搬移,除非目标类也同样出现多态性。
-
在目标类中声明这个函数。
你可以为此函数选择一个新名词—对目标类更有意义的名称。
-
将原函数的代码复制到目标函数中,调整后者,使其能在新家中正常运行。
如果目标函数使用了源类中的特性,你得决定如何从目标函数引用源对象。如果目标类中没有相应的引用机制,就把源对象的引用当做参数,传给新建立的目标函数。
如果原函数包含异常处理,你得判断逻辑上应该由哪个类来处理这一异常。如果应该由源类来负责,就把异常处理留在原地。
-
编译目标类
-
决定如何从原函数正确引用目标对象
可能会有一个现成的字段或函数帮助你取得目标对象。如果没有,就看能否轻松建立一个这样的函数。如果还是不行,就得在源类中新建一个字段来保存目标对象,这可能是一个永久性修改,但你也可以让他是暂时的。因为后继的其他重构项目可能会把这个新建字段去掉。
-
修改原函数,使之成为一个纯委托函数。
-
编译测试。
-
决定是否删除原函数,或是将它当做一个委托函数保留下来。
如果你经常要在源对象中引用目标函数,那么将原函数作为委托函数保留下来会比较简单。
-
如果要移除原函数,请将源类中对原函数的所有调用,替换成对目标函数的调用。
你可以没修改一个引用点就编译并测试一次,也可以通过一次“查找、替换”改掉所有引用点,这通常会简单一些。
-
编译测试。
范例
class Account...
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;
}
private AccountType _type;
private int _daysOverdrawn;
}
重构后
class Account...{
double bankCharge(){
double result=4.5;
if(_dayasOverdrawn>0){
result +=_type.overdraftCharge(_daysOverdrawn);
}
return result;
}
}
class AccountType...
double overdraftCharge(Account account){
if(isPremium()){
double result=10;
if(account.getDaysOverdrawn()>7){
result+=(account.getDaysOverdrawn()-7)*0.85;
}
return result;
}
else{
return account.getDaysOverdrawn()*1.75;
}
}
你的程序中,某个字段被其所驻类之外的另一个类更多地用到。
在目标类新建一个字段,修改原字段的所有用户,令他们改用新字段
动机
- 在类间移动状态和行为,是重构过程中必不可少的措施。
- 如果对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,我就会考虑搬移这个字段。
做法
-
如果字段的访问级是public,使用Encapsulate Field(206)将它封装起来。
如果你有可能移动哪些频繁访问该字段的函数,或如果有许多函数访问某个字段,先使用Self Encapsulate Field(171)也许会有帮助。
-
编译测试
-
在目标类中建立与源字段相同的字段,并同时建立相应的设置、取值函数
-
编译目标类。
-
决定如何在源对象中引用目标对象。
首先看是否有一个现成的字段或函数可以助你得到目标对象。如果没有,就看能否轻易建立这样的一个函数。如果还不行,就得在源类中新建一个字段来存放目标对象。这可能是个永久性修改,但你也可以让它是暂时的,因为后续重构可能会把这个新建字段除掉。
-
删除源字段。
-
将所有对源字段的引用替换为对某个目标函数的调用
如果需要读取该变量,就把对源字段的引用替换为对目标取值函数的调用。如果要对该变量赋值,就把对源字段的引用替换成对设值函数的调用。
如果源字段不是private,就必须在源类的所有子类中查找源字段的引用点,并进行相应的替换。
-
编译、测试。
范例
class Account...
private AccountType _type;
private double _interestRate;
double interestForAmount_days(double amount,int days){
return _interestRate * amount *days /365;
}
目前已有数个函数引用了_interestRate,可以将其搬移到AccountType类去。可以在AccountType中建立一个_interestRate字段及相应的访问函数
class AccountType...
private double _interestRate;
void setInterestRate(double arg){
_interestRate = arg;
}
double getInterestRate(){
return _interestRate;
}
private double _interestRate;
double interestFromAmount_days(double amount ,int days){
return _type.getInterestRate()*amount*days/365;
}
使用Self-Encapsulation:如果很多函数使用了_interestRate字段,可以使用Self Encapsulate field(171)(自我封装):
double interestForAmount_days(double amount,int days){
return _interestRate * amount *days /365;
}
private void setInterestRate(double arg){
_type.setInterestRate(arg);
}
private double getInterestRate(){
return _type.interestRate();
}
某个类做了应该由两个类做的事,建立一个新类,将相关的字段和函数从旧类搬移到新类。
动机
- 一个类应该是一个清楚的抽象,处理一些明确的责任。
- 另一个往往在开发后期出现的信号是类的子类化方式。如果你发现子类化只影响累的部分特性,或如果你发现某些特性需要以一种方式来子类化,某些特性则需要以另一种方式子类化,这意味着你需要分解原来的类。
做法
-
决定如何分解类所负的责任。
-
建立一个新类,用以表现从旧类中分离出来的责任。
如果旧类剩下的责任与旧类名称不符,为旧类更名。
-
建立“从旧类访问新类”的连接关系。
有可能需要一个双向连接。但是在真正需要它之前,不要建立“从新类通往旧类”的连接。
-
对于你想搬移的每一个字段,运用Move Field(146)搬移之。
-
每次搬移后,编译,测试。
-
使用Move Method(142)将必要函数搬移到新类。先搬移较低层函数(也就是被其他函数调用多余“调用其他函数”者),再搬移较高层函数。
-
每次搬移后,编译、测试、
-
检查,精简每个类的接口。
如果你建立起双向连接,检查是否可以将它改为单向连接。
-
决定是否公开新类。如果你需要公开他,就要决定让他成为引用对象是不可变的值对象。
Extract Class(149)是改善并发程序的一种常用技术,因为他是你可以为提炼后的两个类分别加锁。
某个类没有做太多事情,将这个类的所有特性搬移到另一个类中,然后移除源类。
动机
- Inline Class(154)和Extract Class(149)相反,如果一个类不再承担足够责任,不再有单独存在的理由。
做法
-
在目标类身上声明源类的public协议,并将其中所有函数委托至源类。
如果“以一个独立接口表示源类函数”更合适的话,就应该在内联之前先使用Extract Interface(341).
-
修改所有源类引用点,改而引用目标类。
将源类声明为private,以斩断包之外的所有引用可能。同时修改源类的名称,这便可使编译器帮助你捕捉到所有对于源类的隐藏引用点。
-
编译测试。
-
运用Move Method(142)和Move Field(146),将源类的特性全部搬移到目标类。
-
为源类举行一个简单的“丧礼”。
范例
class People...
public String getName(){
return _name;
}
public String getTelephoneNumber(){
return _officeTelephone;
}
TelephoneNumber getOfficeTelephone(){
return _officeTelephone;
}
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
class TelephoneNumber...
public String getTelephoneNumber(){
return ("("+_areaCode+")"+_number);
}
void setAreaCode(String arg){
_areaCOde = arg;
}
String getAreaCode(){return _areaCode;}
String getNumber(){
return _number;
}
void setNumber(String arg){_number= arg;}
private String _number;
private String _areaCode;
重构后
class Person...
String getAreaCode(){
return _officeTelephone.getAreaCode();
}
void setAreaCode(String arg){
_officeTelephone.setAreaCode(arg);
}
String getNumber(){
return _officeTelephone.getNumber();
}
void setNumber(String arg){
_officeTelephone.setNumber(arg);
}
#####7.5 Hide Delegate(隐藏“委托关系”)
客户通弄个一个委托类来调用另一个对象。在服务类上建立客户所需的所有函数,用以影藏委托关系。
动机
- 封装即使不是对象的最关键特征,也是最关键特征之一。“封装”意味着每个对象都应该尽可能少了解系统的其他部分。
做法
-
对于每一个委托关系中的函数,在服务对象端建立一个简单的委托函数。
-
调整客户,令他值调用服务对象提供的函数。
如果使用者和服务提供者不在同一个包,考虑修改委托函数的访问权限,让客户得以在包之外调用它。
-
每次调整后,编译并测试。
-
如果将来不再有任何客户需要取用委托类,便可移除读物对象中的相关访问函数。
-
变异测试。
范例
class Person{
Department _department;
public Department getDepartment(){
return _department;
}
public void setDepartment(Department arg){
_department =arg;
}
}
class Department{
private String _chargeCode;
private Person _manager;
public Department(Person manager){
_manager = manager;
}
public Person getManager(){
return _manager;
}
}
如果客户希望知道某人的经理是谁,他必须先取得Department对象:
manager = john.getDepartment().getManager();
这时,可以在Person中建立一个简单的委托函数:
public Person getManager(){
return _department.getManager();
}
某个类做了过多的简单委托动作,让客户直接调用受托类
动机
- 封装受托对象的代价就是:每当客户要使用受托类的新特性时,必须在服务端添加一个简单的委托函数。
- 重构的意义在于:你永远不必说对不起,只要把出问题的地方修补好就行。
做法
- 建立一个函数,用以获得受托对象。
- 对于每个委托函数,在服务类中删除该函数,并让需要调用该函数的客户转为调用受托对象。
- 处理每个受托函数后,编译、测试。
你需要为提供服务的类添加一个函数,但你无法修改这个类。
在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。
动机
- 当你使用一个类时,他为你提供了需要的所有服务。但是,当你需要新服务,这个类却无法提供。如果修改源码,自行添加一个新函数即可,但如果不能,你就得在客户端编码,补足你要的函数。
- 进行重构,如果你以外加函数实现这一功能,那这个函数原本应该在提供服务的类中实现。
- 外加函数终归是权宜之计,你仍需要将其搬移到理想家园。
做法
-
在客户类建立一个函数,用来提供你需要的功能。
这个函数不应该调用客户类的任何特性。如果你需要一个值,把该值当做参数传给他。
-
以服务类实例作为该函数的第一个参数。
-
将该函数注释为“外加函数,应在服务类实现”
这样一来,如果将来有机会将其搬移到服务类中,你便可以轻松找出这些外加函数
范例
Date newStart = new Date(previousEnd.getYear(),previousEnd.getMonth(),previousEnd.getDate()+1);
重构后
Date newStart = nextDay(previousEnd);
private static Date nextDay(Date arg){
//foreign method,should be on date
return new Date(arg.getYear(),arg.getMonth(),arg.getDate()+1);
}
你需要为服务类提供一些额外函数,但是你无法修改这个类。
建立一个新类,使他包含这些额外函数。让这个扩展品成为源类的子类或包装类。
做法
-
建立一个扩展类,将它作为原始类的子类或包装类。
-
在扩展类中加入转型构造函数,
所谓“转型构造函数”是指“接受源对象作为参数”的构造函数。如果采用子类化方案,那么转型构造函数应该调用适当的超类构造函数。如果采用包装类方案,那么转型构造函数应该将它得到的传入参数以实例变量的形式保存起来,用作接受委托的源对象。
-
在扩展类中加入新特性。
-
根据需要,将源对象替换为扩展对象。
-
将针对原始类定义的所有外加函数搬移到扩展类中。