在对象之间搬移特性
类往往会因为承担过多责任而变得臃肿不堪,可以使用Extract Class将一部分责任分离出去,如果一个类变得太”不负责任”,就使用Inline Class将它融入另一个类,如果一个类使用了另一个类,运用Hide Delegate将这种关系隐藏起来通常是有帮助的,有时候隐藏委托类会导致拥有者的接口经常变化,此时需要使用Remove Middle Man
Move Method(搬移函数)
在该函数最常引用的类中建立一个有着类似行为的新函数,将旧函数变成一个单纯的委托函数,或是将旧函数完全移除
动机
“搬移函数”是重构理论的支柱,如果一个类有太多行为,或如果一个类与另一个类有太多合作而形成高度耦合,就应该搬移函数,通过这种手段,可以使系统中的类更简单,这些类最终也将更干净利落地实现系统交付的任务
做法
- 检测源类中被源函数所使用的一切特性(包括字段和函数),考虑它们是否也该被搬移(如果某个特性只被打算搬移的那个函数用到,就应该将它一并搬移,如果另有其他函数使用了这个特性,可以考虑将使用该特性的所有函数全部一并搬移)
- 检查源类的子类和超类,看看是否有该函数的其他声明
- 在目标类中声明这个函数
- 将源函数的代码复制到目标函数中,调整后者,使其能在新家中正常运行
- 编译目标类
- 决定如何从源函数正确引用目标对象
- 修改源函数,使之成为一个纯委托函数
- 编译、测试
- 决定是否删除源函数,或将它当作一个委托函数保留下来
- 如果要移除源函数,请将源类中对源函数的所有调用,替换为对目标函数的调用
- 编译、测试
范例
class Account...
double overdraftCharge(){
if (_type.isPremium()){
double result = 10;
if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85;
return result;
}
else return _dayOverdrawn * 1.75;
}
double bankCharge(){
double result = 4.5;
if (_dayOverdrawn > 0) result += overdraftCharge();
return result;
}
private AccountType _type;
private int _daysOverdrawn;
=>
class Account...
double bankCharge(){
double result = 4.5;
if (_dayOverdrawn > 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.getDayOverdrawn * 1.75;
}
Move Field(搬移字段)
如果某个字段被其所驻类之外的另一个类更多地用到,就应该在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段
动机
对于一个字段,在其所驻类之外的另一个类中有更多函数使用了它,就应该考虑搬移这个字段
做法
- 如果字段的访问级是public,使用Encapsulate Field将它封装起来
- 编译,测试
- 在目标类中建立与源字段相同的字段,并同时建立相应的设置/取值函数
- 编译目标类
- 决定如何在源对象中引用目标对象
- 删除源字段
- 将所有对源字段的引用替换为对某个目标函数的调用
- 编译,测试
范例
class Account...
private AccountType _type;
private double _interestRate;
double interestForAmountDays(double amount, int days){
return _interestRate * amount * days / 365;
}
=> 将_interestRate搬移到AccountType类中
class AccountType...
private double _interestRate;
void setInterestRate(double arg){
_interestRate = arg;
}
double getInterestRate(){
return _interestRate;
}
class Account...
private AccountType _type;
private double _interestRate;
double interestForAmountDays(double amount, int days){
return _type.getInterestRate() * amount * days / 365;
}
范例:使用Self-Encapsulation
class Account...
private AccountType _type;
private double _interestRate;
double interestForAmountDays(double amount, int days){
return _type.getInterestRate() * amount * days / 365;
}
private void setInterestRate(double arg){
_type.setInterestRate(arg);
}
private double getInterestRate(){
return _type.getInterestRate();
}
Extract Class(提炼类)
如果某个类做了应该由两个类做的事,就应该建立一个新类,将相关的字段和函数从旧类搬移到新类
动机
一个类应该是一个清楚的抽象,处理一些明确的责任
做法
- 决定如何分解类所负的责任
- 建立一个新类,用以表现从旧类中分离出来的责任
- 建立”从旧类访问新类”的连接关系
- 对于想要搬移的每一个字段,运用Move Field搬移
- 每次搬移后,编译、测试
- 使用Move Method将必要函数搬移到新类,先搬移较低层函数(也就是”被其他函数调用”多于”调用其他函数”者),再搬移较高层函数
- 每次搬移之后,编译、测试
- 检查,精简每个类的接口
- 决定是否公开新类,如果的确需要公开,就要决定让它成为引用对象还是不可变的值对象
范例
class Person...
public String getName(){
return _name;
}
public String getTelephoneNumber(){
return ("(" + _officeAreaCode + ") " + _officeNumber);
}
String getOfficeAreaCode(){
return _officeAreaCode;
}
void setOfficeAreaCode(String arg){
_officeAreaCode = arg;
}
String getOfficeNumber(){
return _officeNumber;
}
void setOfficeNumber(String arg){
_officeNumber = arg;
}
private String _name;
private String _officeAreaCode;
private String _officeNumber;
=> 新建一个TelephoneNumber类
class Person...
public String getName(){
return _name;
}
public String getTelephoneNumber(){
return _officeTelephone.getTelephoneNumber();
}
TelephoneNumber getOfficeTelephone(){
return _officeTelephone;
}
private String _name;
private TelephoneNumber _officeTelephone = new TelephoneNumber();
class TelephoneNumber...
public String getTelephoneNumber(){
return ("(" + _areaCode + ") " + _number);
}
String getAreaCode(){
return _areaCode;
}
void setAreaCode(String arg){
_areaCode = arg;
}
String getNumber(){
return _number;
}
void setNumber(String arg){
_number = arg;
}
private String _number;
private String _areaCode;
Inline Class(将类内联化)
如果类没有做太多事情,就应该将这个类的所有特性搬移到另一个类中,然后移除原类
动机
如果一个类不再承担足够责任、不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任),就应该挑选这一”萎缩类”的最频繁用户(也是个类),以Inline Class手法将”萎缩类”塞进另一个类中
做法
- 在目标类身上声明源类的public协议,并将其中所有函数委托至源类
- 修改所有源类引用点,改而引用目标类
- 编译,测试
- 运用Move Method和Move Field,将源类的特性全部搬移到目标类
- 为源类举行一个简单的”丧礼”
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,在Person中增加
public Person getManager(){
return _department.getManager();
}
Remove Middle Man(移除中间人)
如果某个类做了过多的简单委托动作,就让客户直接调用受托类
动机
“封装受托对象”的代价是每当客户要使用受托类的新特性时,就必须在服务端添加一个简单委托函数,随着受托类的特性越来越多,服务类完全变成了一个”中间人”,此时就应该让客户直接调用受托类
做法
- 建立一个函数,用以获得受托对象
- 对于每个委托函数,在服务类中删除该函数,并让需要调用该函数的客户转为调用受托对象
- 处理每个委托函数后,编译、测试
范例
class Person...
Department _department;
public Person getManager(){
return _department.getManager();
}
class Department...
private Person _manager;
public Department(Person manager){
_manager = manager;
}
=>
class Person...
public Department getDepartment(){
return _department;
}
manager = john.getDepartment().getManager();
Introduce Foreign Method(引入外加函数)
当需要为提供服务的类增加一个函数,但无法修改这个类时,应该在客户类中建立一个函数,并以第一参数形式传入一个服务类实例
Date newStart = new Date(previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
=>
Date newStart = nextDay(previousEnd);
private static Date nextDay(Date arg){
return New Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
动机
当需要服务类提供一个新功能时,又不能修改服务类的源码,就得在客户端编码,如果需要为服务类建立大量外加函数或有许多类都需要同样的外加函数,就不应该再使用本项重构,而应该使用Introduce Local Extension
做法
- 在客户类中建立一个函数,用来提供需要的功能,该函数不应该调用客户类的任何特性,如果需要一个值,把该值当作参数传递给它
- 以服务类实例作为该函数的第一个参数
- 将该函数注释为:”外加函数,应在服务类实现”
Introduce Local Extension(引入本地扩展)
当需要为服务类提供一些额外函数,但无法修改这个类,应该建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或包装类
动机
本地扩展是一个独立的类,但也是被扩展类的子类型:它提供源类的一切特性,同时额外添加新特性。在任何使用源类的地方,都可以使用本地扩展取而代之
在子类和包装类之间做选择时,通常首选子类,因为这样的工作量比较少。制作子类的最大障碍在于,它必须在对象创建期实施,此外,子类化方案还必须产生一个子类对象,这种情况下, 如果有其他对象引用了旧对象,就同时有两个对象保存了原数据,如果原数据允许被修改,就必须改用包装类,使用包装类时,对本地扩展的修改会波及原对象,反之亦然
做法
- 建立一个扩展类,将它作为原始类的子类或包装类
- 在扩展类中加入转型构造函数(指”接受原对象作为参数”的构造函数),如果采用子类化方案,那么转型构造函数应该调用适当的超类构造函数;如果采用包装类方案,那么转型构造函数应该将它得到的传入参数以实例变量的形式保存起来,用作接受委托的原对象
- 在扩展类中加入新特性
- 根据需要,将原对象替换为扩展对象
- 将针对原始类定义的所有外加函数搬移到扩展类中
范例:使用子类
client class...
private static Date nextDay(Date arg){
// foreign method,should be on date
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
=>
// 用MfDateSub包装Date类
class MfDateSub...
Date nextDay(){
return new Date(getYear(), getMonth(), getDate() + 1);
}
范例:使用包装类
client class...
private static Date nextDay(Date arg){
// foreign method,should be on date
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
=>
class MfDateWrap...
Date nextDay(){
return new Date(getYear(), getMonth(), getDate() + 1);
}
public int getYear(){
return _original.getYear();
}
public boolean equals(Object arg){
if (this == arg)
return true;
if (!(arg instanceof MfDateWrap))
return false;
MfDateWrap other = ((MfDateWrap) arg);
return (_original.equals(other._original));
}
重新组织数据
Self Encapsulate Field(自封装字段)
如果直接访问一个字段,与字段之间的耦合关系会逐渐变得笨拙,应该为这个字段建立取值/设置函数,并且只以这些函数来访问字段
private int _low, _high;
boolean includes(int arg){
return arg >= _low && arg <= _high;
}
=>
private int _low, _high;
boolean includes(int arg){
return arg >= getLow() && arg <= getHigh();
}
int getLow() {return _low;}
int getHigh() {return _high;}
动机
直接访问变量的好处是:代码比较容易阅读
间接访问变量的好处是:子类可以通过覆写一个函数而改变获取数据的途径,它还支持更灵活的数据管理方式,例如延迟初始化
做法
- 为待封装字段建立取值/设值函数
- 找出该字段的所有引用点,将它们全部改为调用取值/设值函数
- 将该字段声明为private
- 复查,确保找出所有引用点
- 编译,测试
Replace Data Value with Object(以对象取代数据值)
当有一个数据项,需要与其他数据和行为一起使用才用意义时,将数据项变成对象
做法
- 为待替换数值新建一个类,在其中声明一个final字段,其类型和源类中的待替换数值类型一样,然后在新类中加入这个字段的取值函数,再加上一个接受此字段为参数的构造函数
- 编译
- 将源类中的待替换数值字段的类型改为前面新建的类
- 修改源类中该字段的取值函数,令它调用新类的取值函数
- 如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令它改用新类的构造函数来对字段进行赋值动作
- 修改源类中待替换字段的设置函数,令它为新类创建一个实例
- 编译,测试
- 现在,你有可能需要对新类使用Change Value to Reference
范例
class Order...
public Order(String customer){
_customer = customer;
}
public String getCustomer(){
return _customer;
}
public void setCustomer(String arg){
_customer = arg;
}
private String _customer;
// 使用Order类的代码如下
private static int numberOfOrdersFor(Collection orders, String customer){
int result = 0;
Iterator iter = orders.iterator();
while(iter.hasNext()){
Order each = (Order) iter.next();
if (each.getCustomer().equals(customer)) result++;
}
return result;
}
=>
class Order...
public Order(String customerName){
_customer = new Customer(customerName);
}
public String getCustomerName(){
return _customer.getName();
}
public void setCustomer(String customerName){
_customer = new Customer(customerName);
}
private Customer _customer;
class Customer{
public Customer(String name){
_name = name;
}
public String getName(){
return _name;
}
private final String _name;
}
Change Value to Reference(将值对象改为引用对象)
从一个类衍生出许多彼此相等的实例,将这个值对象变成引用对象
动机
在许多系统中,可以对对象做一个有用的分类:引用对象和值对象,前者就像”客户”、”账户”这样的东西,每个对象都代表真实世界中的一个实物;后者则是像”日期”、”钱”这样的东西,它们完全由其所含的数据值来定义
做法
- 使用Replace Constructor with Factory Method
- 编译,测试
- 决定由什么对象负责提供访问新对象的途径
- 决定这些引用对象应该预先创建好,或是应该动态创建
- 修改工厂函数,令它返回引用对象
- 编译,测试
范例
class Customer{
public Customer(String name){
_name = name;
}
public String getName(){
return _name;
}
private final String _name;
}
class Order...
public Order(String customerName){
_customer = new Customer(customerName);
}
public void setCustomer(String customerName){
_customer = new Customer(customerName);
}
public String getCustomerName(){
return _customer.getName();
}
private Customer _customer;
private static int numberOfOrdersFor(Collection orders, String customer){
int result = 0;
Interator iter = orders.iterator();
while(iter.hasNext()){
Order each = (Order) iter.next();
if (each.getCustomerName().equals(customer))
result++;
}
return result;
}
=>
class Customer{
static void loadCustomers(){
new Customer("Lemon Car Hire").store();
new Customer("Associated Coffee Machines").store();
new Customer("Bilston Gasworks").store();
}
private void store(){
_instances.put(this.getName(), this);
}
public static Customer getNamed(String name){
return (Customer) _instances.get(name);
}
private Customer(String name){
_name = name;
}
}
class Order{
public Order(String customer){
_customer = Customer.create(customer);
}
}
Change Reference to Value(将引用对象改为值对象)
当有一个引用对象,很小且不可变,而且不易管理,应该将它变成一个值对象
动机
引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象,它们可能造成内存区域之间错综复杂的关联,在分布系统和并发系统中,不可变的值对象特别有用,因为无需考虑它们的同步问题
值对象有一个非常重要的特性:它们应该是不可变的
做法
- 检查重构目标是否为不可变对象,或是否修改为不可变对象,如果该对象目前还不是不可变的,就使用Remove Setting Method,到它成为不可变的为止,如果无法将该对象修改为不可变的,就放弃使用本项重构
- 建立equals()和hashCode()
- 编译,测试
- 考虑是否可以删除工厂函数,并将构造函数声明为public
范例
class Currency...
private String _code;
public String getCode(){
return _code;
}
private Currency(String code){
_code = code;
}
// Currency类维护一个包含所有Currency实例的链表
new Currency("USD").equals(new Currency("USD")) // return false
=>
// 定义equals()
public boolean equals(Object arg){
if(!(arg instanceof Currency)) return false;
Currency other = (Currency)arg;
return (_code.equals(other._code));
}
// 定义hashCode()
public int hashCode(){
return _code.hashCode();
}
new Currency("USD").equals(new Currency("USD")) // now return true
Replace Array with Object(以对象取代数组)
以对象取代数组,对于数组中的每个元素,以一个字段来表示
String[] row = new String[3];
row[0] = "Liverpool";
row[1] = "15";
=>
Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");
动机
一个数组容纳了多种不同对象,会给用户带来麻烦,而使用对象可以将信息封装起来,并使用Move Method为它加上相关行为
做法
- 新建一个类表示数组所拥有的信息,并在其中以一个public字段保存原先的数组
- 修改数组的所有用户,让它们改用新类的实例
- 编译,测试
- 逐一为数组元素添加取值/设值函数,根据元素的用途,为这些访问函数命名,修改客户端代码,让它们通过访问函数取用数组内的元素,每次修改后,编译并测试
- 当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字段声明为private
- 编译
- 对于数组内的每个元素,在新类中创建一个类型相当的字段,修改该元素的访问函数,令它改用上述的新建字段
- 每修改一个元素,编译并测试
- 数组的所有元素都有了相应字段之后,删除该数组
Duplicate Observed Data(复制”被监视数据”)
如果有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据,应该将该数据复制到一个领域对象中,建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据
动机
一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开,对于内嵌于用户界面之中的数据必须将它们复制到新的对象中,并提供相应的同步机制
做法
- 修改展现类,使其成为领域类的Observer[GoF],如果尚未有领域类,就建立一个,如果没有”从展现类到领域类”的关联,就将领域类保存于展现类的一个字段中
- 针对GUI类中的领域数据,使用Self Encapsulate Field
- 编译,测试
- 在事件处理函数中调用设值函数,直接更新GUI组件
- 编译,测试
- 在领域类中定义数据及其相关访问函数
- 修改展现类中的访问函数,将它们的操作对象改为领域对象(而非GUI组件)
- 修改Observer的update(),使其从相应的领域对象中将所需数据复制给GUI组件
- 编译,测试
Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
两个类都需要使用对方特性,但其间只有一条单向连接,应该添加一个反向指针,并使修改函数能够同时更新两条连接
动机
被引用类需要得到其引用者以便进行某些处理
做法
- 在被引用类中增加一个字段,用以保存反向指针
- 决定由哪个类——引用端还是被引用端——控制关联关系
- 在被控端建立一个辅助函数,其命名应该清楚指出它的有限用途
- 如果既有的修改函数在控制端,让它负责更新反向指针
- 如果既有的修改函数在被控端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数
范例
class Order...
Customer getCustomer(){
return _customer;
}
void setCustomer(Customer arg){
_customer = arg;
}
Customer _customer;
// 首先为Customer添加一个字段
class Customer{
private Set _orders = new HashSet();
// 决定哪个类负责控制关联关系
1.如果两者都是引用对象,而其间的关联是"一对多"关系,那么就由"拥有单一引用"的那一方承担"控制者"角色,以本例而言,如果一个客户可拥有多分定单,那么就由Order类(定单)来控制关联关系
2.如果某个对象是组成另一对象的部件,那么由后者负责控制关联关系
3.如果两者都是引用对象,而其间的关联是"多对多"关系,那么随便其中哪个对象来控制关联关系
class Customer...
Set friendOrders(){
/** should only be used by Order when modifying the association */
return _orders;
}
void addOrder(Order arg){
arg.setCustomer(this);
}
class Order...
void setCustomer(Customer arg){
if (_customer != null)
_customer.friendOrders().remove(this);
_customer = arg;
if (_customer != null)
_customer.friendOrders().add(this);
}
// 如果是多对多的情况,一份定单也对应多个客户,重构后的函数可能如下
class Order...
void addCustomer(Customer arg){
arg.friendOrders().add(this);
_customers.add(arg);
}
void removeCustomer(Customer arg){
arg.friendOrders().remove(this);
_customers.remove(arg);
}
class Customer...
void addOrder(Order arg){
arg.addCustomer(this);
}
void removeOrder(Order arg){
arg.removeCustomer(this);
}
Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性,就应该去除不必要的关联
动机
维护双向连接、确保对象被正确创建和删除会增加复杂度;大量的双向连接也容易造成”僵尸对象”:某个对象本来已经该死亡了,却仍然保留在系统中,因为对它的引用还没有完全清除
此外,双向关联也迫使两个类之间有了依赖:对其中任一个类的任何修改,都可能会引发另一个类的变化,所以只有在真正需要双向关联的时候,才应该使用它
做法
- 找出保存”你想要去除的指针”的字段,检查它的每一个用户,判断是否可以去除该指针
- 如果客户使用了取值函数,先运用Self Encapsulate Field将待删除字段自我封装起来,然后使用Substitute Algorithm对付取值函数,令它不再使用该字段,编译测试
- 如果客户并未使用取值函数,那就直接修改待删除字段的所有被引用点:改以其他途径获得该字段所保存的对象,每次修改后,编译并测试
- 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后移除该字段
- 编译,测试
范例
class Order...
Customer getCustomer(){
return _customer;
}
void setCustomer(Customer arg){
if (_customer != null)
_customer.friendOrders().remove(this);
_customer = arg;
if (_customer != null)
_customer.friendOrders().add(this);
}
private Customer _customer;
class Customer...
void addOrder(Order arg){
arg.setCustomer(this);
}
private Set _orders = new HashSet();
Set friendOrders(){
return _orders;
}
Replace Magic Number withSymbolic Constant(以字面常量取代魔法数)
创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量
double potentialEnergy(double mass, double height){
return mass * 9.81 * height;
}
=>
double potentialEnergy(double mass, double height){
return mass * GRAVITATIONAL_CONSTANT * height;
}
static final double GRAVITATIONAL_CONSTANT = 9.81;
动机
魔法数是指拥有特殊意义,却又不能明确表现出这种意义的数字,应该用常量代替,大大提高代码的可读性。如果这个魔法数是个类型码,应考虑使用Replace Type Code with Class
做法
- 声明一个常量,令其值为原本的魔法数值
- 找出这个魔法数的所有引用点
- 检查是否可以使用这个新声明的常量替换该魔法数,如果可以,便以此常量替换之
- 编译
- 所有魔法数都被替换完毕后,编译并测试
Encapsulate Field(封装字段)
如果在类中存在一个public字段,应该将它声明为private,并提供相应的访问函数
public String _name;
=>
private String _name;
public String getName() {return _name;}
public void setName(String arg) {_name = arg;}
动机
数据声明为public被看做是一种不好的做法,因为这样会降低程序的模块化程度
做法
- 为public字段提供取值/设值函数
- 找到这个类以外使用该字段的所有地点,如果客户只是读取该字段,就把引用替换为对取值函数的调用;如果客户修改了该字段值,就将此引用点替换为对设值函数的调用
- 每次修改之后,编译并测试
- 将字段的所有用户修改完毕后,把字段声明为private
- 编译,测试
Encapsulate Collection(封装集合)
有个函数返回一个集合,让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数
动机
取值函数不应该返回集合自身,不应该为整个集合提供一个设置函数,而应该提供用以为集合添加/移除元素的函数,这样,集合就被很好地封装起来了,可以降低集合拥有者和用户之间的耦合度
做法
- 加入为集合添加/移除元素的函数
- 将保存集合的字段初始化为一个空集合
- 编译
- 找出集合设置函数的所有调用者,修改
- 编译,测试
- 找出所有”通过取值函数获得集合并修改其内容”的函数,逐一修改这些函数,让它们改用添加/移除函数,每次修改后,编译并测试
- 修改完上述后,修改取值函数自身,使它返回该集合的一个只读副本
- 编译,测试
- 找出取值函数的所有用户,从中找出应该存在于集合所属对象内的代码,运用Extract Method和Move Method将这些代码移到宿主对象去
- 修改现有取值函数的名字,然后添加一个新取值函数,使其返回一个枚举,找出旧取值函数的所有被使用点,将它们都改为使用新取值函数
- 如果这一步跨度太大,可以先使用Rename Method修改原取值函数名称;再建立一个新取值函数用以返回枚举;最后再修改所有调用者,使其调用新取值函数
- 编译,测试
范例
class Course...
public Course(String name, boolean isAdvanced){...}
public boolean isAdvanced(){....}
class Person...
public Set getCourses(){
return _courses;
}
public void setCourses(Set arg){
_courses = arg;
}
private Set _courses;
Person kent = new Person();
Set s = new HashSet();
s.add(new Course("Smalltalk", false));
s.add(new Course("Appreciating", true));
kent.setCourses(s);
Course refact = new Course("Refactoring", true);
kent.getCourses().add(refact);
kent.getCourses().add(new Course("Brutal Sarcasm", false));
=>
class Person...
public void addCourse(Course arg){
_courses.add(tag);
}
public void removeCourse(Course arg){
_courses.remove(arg);
}
private Set _courses = new HashSet();
public void initializeCourses(Set arg){
Assert.isTrue(_courses.isEmpty());
_courses.addAll(arg);
}
private void addAll(Set arg){
Iterator iter = arg.iterator();
while(iter.hasNext()){
addCourse((Course) iter.next());
}
}
Person kent = new Person();
kent.addCourse(new Course("Smalltalk", false));
kent.addCourse(new Course("Appreciating", true));
Replace Record with Data Class(以数据类取代记录)
当需要面对传统编程环境中的记录结构时,为该记录创建一个” 哑”数据对象
动机
建立一个看起来类似外部记录的类,以便日后将某些字段和函数搬移到这个类中
做法
- 新建一个类,表示这个记录
- 对于记录中的每一项数据,在新建的类中建立对应的一个private字段,并提供相应的取值/设值函数
这个对象现在还没有任何有用的行为,但是更进一步的重构会解决这个问题
Replace Type Code with Class(以类取代类型码)
类之中有一个数值类型码,但它并不是影响类的行为,以一个新的类替换该数值类型码
动机
把类型码换成类,编译器就可以对这个类进行类型检验,只要为这个类提供工厂函数,就可以始终保证只有合法的实例才会被创建出来,而且它们都会被传递给正确的宿主对象
做法
- 为类型码建立一个类
- 修改源类实现,让它使用上述新建的类
- 编译,测试
- 对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类
- 逐一修改源类用户,让它们使用新接口
- 每修改一个用户,编译并测试
- 删除使用类型码的旧接口,并删除保存就类型码的静态变量
- 编译,测试
范例
class Person{
public static final int O = 0;
public static final int A = 1;
public static final int B = 2;
public static final int AB = 3;
private int _bloodGroup();
public Person(int bloodGroup){
_bloodGroup = bloodGroup;
}
public void setBloodGroup(int arg){
_bloodGroup = arg;
}
public int getBloodGroup(){
return _bloodGroup;
}
}
=>
class BloodGroup{
public static final BloodGroup O = new BloodGroup(0);
public static final BloodGroup A = new BloodGroup(1);
public static final BloodGroup B = new BloodGroup(2);
public static final BloodGroup AB = new BloodGroup(3);
private final int _code;
private BloodGroup(int code){
_code = code;
}
private int getCode(){
return _code;
}
private static BloodGroup code(int arg){
return _values[arg];
}
}
class Person{
public static final int O = BloodGroup.O.getCode();
public static final int A = BloodGroup.A.getCode();
public static final int B = BloodGroup.B.getCode();
public static final int AB = BloodGroup.AB.getCode();
private BloodGroup _bloodGroup;
public Person(int bloodGroup){
_bloodGroup = BloodGroup.code(bloodGroup);
}
public Person(BloodGroup bloodGroup){
_bloodGroup = bloodGroup;
}
public void setBloodGroup(int arg){
_bloodGroup = BloodGroup.code(arg);
}
public void setBloodGroup(BloodGroup arg){
_bloodGroup = arg;
}
public int getBloodGroupCode(){
return _bloodGroup.getCode();
}
public BloodGroup getBloodGroup(){
return _bloodGroup;
}
}
Replace Type Code with Subclasses(以子类取代类型码)
一个不可变的类型码会影响类的行为,应该以子类取代这个类型码
动机
如果面对的类型码不会影响宿主类的行为,可以使用Replace Type Code with Class来处理它们,如果类型码会影响宿主类的行为,就最好借助多态来处理变化行为
一般来说,这种情况的标志就是像switch这样的条件表达式,这时应该以Replace Conditional with Polymorphism进行重构,但为了能够顺利进行那样的重构,首先应该将类型码替换为可拥有多态行为的继承体系,这样的一个继承体系应该以类型码的宿主类为基类,并针对每一种类型码各建立一个子类
但是以下两种情况不能那么做:(1)类型码值在对象创建之后发生了改变;(2)由于某些原因,类型码宿主类已经有了子类。如果面临这两种情况之一,就需要使用Replace Type Code with State/Strategy
Replace Type Code with Subclasses的好处在于:它把”对不同行为的了解”从类用户那儿转移到了类自身,如果需要再加入新的行为变化,只需添加一个子类就行,
做法
- 使用Self Encapsulate Field将类型码自我封装起来
- 为类型码的每一个数值建立一个相应的子类,在每个子类中覆写类型码的取值函数,使其返回相应的类型码值
- 每建立一个新的子类,编译并测试
- 从超类中删掉保存类型码的字段,将类型码访问函数声明为抽象函数
- 编译,测试
范例
class Employee...
private int _type;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
Employee(int type){
_type = type;
}
=>
class Employee...
private int _type;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
static Employee create(int type){
switch(type){
case ENGINEER:
return New Engineer();
case SALESMAN:
return new Salesman();
case MANAGER:
return new Manager();
default:
throw new IllegalArgumentException("Incorrect type code value");
}
}
private Employee(int type){
_type = type;
}
abstract int getType();
class Engineer extends Exployee{
int getType(){
return Employee.ENGINEER;
}
}
Replace Type Code with State/Strategy(以State/Strategy取代类型码)
如果有一个类型码,它会影响类的行为,但无法通过继承手法消除它, 可以以状态对象取代类型码
动机
如果打算在完成本项重构之后再以Replace Conditional with Polymorphism简化一个算法,那么选择Strategy模式比较合适;如果打算搬移与状态相关的数据,而且把新建对象视为一种变迁状态,就应该选择State模式
做法
- 使用Self Encapsulate Field将类型码自我封装起来
- 新建一个类,根据类型码的用途为它命名,这就是一个状态对象
- 为这个新类添加子类,每个子类对应一种类型码
- 在超类中建立一个抽象的查询函数,用以返回类型码,在每个子类中覆写该函数,返回确切的类型码
- 编译
- 在源类中建立一个字段,用以保存新建的状态对象
- 调整源类中负责查询类型码的函数,将查询动作转发给状态对象
- 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给”保存状态对象”的那个字段
- 编译,测试
范例
class Employee...
private int _type;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
Employee(int type){
_type = type;
}
int payAmount(){
switch(_type){
case ENGINEER:
return _monthlySalary;
case SALESMAN:
return _monthlySalary + _commission;
case MANAGER:
return _monthlySalary + _bonus;
default:
throw new RuntimeException("Incorrect Employee");
}
}
// 假设对象的类型码是可变的,所以不能使用继承的方式来处理类型码
=>
class Employee...
private EmployeeType _type;
Employee(int type){
setType(type);
}
int getType(){
return _type.getTypeCode();
}
void setType(int arg){
_type = EmployeeType.newType(arg);
}
int payAmount(){
switch(getType()){
case EmployeeType.ENGINEER:
return _monthlySalary;
case EmployeeType.SALESMAN:
return _monthlySalary + _commission;
case EmployeeType.MANAGER:
return _monthlySalarty + _hours;
default:
throw new RuntimeException("Incorrect Employee");
}
}
abstract class EmployeeType{
abstract int getTypeCode();
static EmployeeType newType(int code){
switch(code){
case ENGINEER:
_type = new Engineer();
break;
case SALESMAN:
_type = new Salesman();
break;
default:
throw new IllegalArgumentException("Incorrect Employee Code");
}
}
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
}
class Engineer extends EmployeeType{
int getTypeCode(){
return Employee.ENGINEER;
}
}
Replace Subclass with Fields(以字段取代子类)
如果各个子类的唯一差别只在”返回常量数据”的函数身上,就应该修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类
动机
建立子类的目的是为了增加新特性或变化其行为,如果子类中只有常量函数,就没有足够的存在价值,可以在超类中设计一个与常量函数返回值相应的字段,从而完全去除这样的子类
做法
- 对所有子类使用Replace Constructor with Factory Method
- 如果有任何代码直接引用子类,令它改而引用超类
- 针对每个常量函数,在超类中声明一个final字段
- 为超类声明一个protected构造函数,用以初始化这些新增字段
- 新建或修改子类构造函数,使它调用超类的新增构造函数
- 编译,测试
- 在超类中实现所有常量函数,令它们返回相应字段值,然后将函数从子类中删除
- 每删除一个常量函数,编译并测试
- 子类中所有的常量函数都被删除后,使用Inline Method将子类构造函数内联到超类的工厂函数中
- 编译,测试
- 将子类删掉
- 编译,测试
- 重复”内联构造函数、删除子类”过程,直到所有子类都被删除
范例
abstract class Person{
abstract boolean isMale();
abstract char getCode();
...
Class Male extends Person{
boolean isMale(){
return true;
}
char getCode(){
return 'M';
}
}
Class Female extends Person{
boolean isMale(){
return false;
}
char getCode(){
return 'F';
}
}
=>
class Person...
private final boolean _isMale;
private final char _code;
protected Person(boolean isMale, char code){
_isMale = isMale;
_code = code;
}
static Person createMale(){
return new Person(true, 'M');
}
static Person createFemale(){
return new new Person(false, 'F');
}
boolean isMale(){
return _isMale;
}