重构心法修炼第二层:在对象之间搬移特性
7.1 搬移函数
当前类中的某个方法,却与其它类配合的更加紧密,那么应该把原类的方法迁移到其它类中。如果用到了当前类中少许字段或者方法,可用方法参数来传递当前字段或者对象。在迁移方法成功后,将当前类的旧函数变成一个单纯的委托函数,或是将旧函数完全删除,即内联函数。
下面的例子中,一种租赁包含一部电影与租期。而计算费用的函数,主要是根据电影的类型,所以才会出现多个if语句。因此,caculate()函数其实与电影Movie对象配合的更加紧密,所以,将函数搬移到电影类中。
搬移后Rental类中的计算函数,委托给电影类即可。代码如下
//重构之前的代码
@Getter
@Setter
public class Rental {
Movie movie;
int days;
public double caculate() {
//随着电影的类型变化而出现多个分支,因此当前函数的代码量取决于电影的类型
//所以说,当前函数与电影类配合的更加紧密,而不是函数所在类:租赁类
if (Movie.CHILDERN.equals(movie.getType())) {
return 3 * days;
}
if (Movie.NEW_RELEASE.equals(movie.getType())) {
return 5 * days;
}
return 0;
}
}
@Getter
@Setter
class Movie {
//儿童片
public static final Long CHILDERN = 1L;
//新片
public static final Long NEW_RELEASE = 2L;
Long type = CHILDERN;
public Movie(Long type) {
this.type = type;
}
}
重构后的代码
@Getter
@Setter
public class Rental {
Movie movie;
int days;
public double caculate() {
return this.movie.caculate(this.days);
}
}
@Getter
@Setter
class Movie {
public static final Long CHILDERN = 1L;
public static final Long NEW_RELEASE = 2L;
Long type = CHILDERN;
public Movie(Long type) {
this.type = type;
}
public double caculate(int days) {
if (Movie.CHILDERN.equals(this.getType())) {
return 3 * days;
}
if (Movie.NEW_RELEASE.equals(this.getType())) {
return 5 * days;
}
return 0;
}
}
7.2 搬移字段
搬移字段心法与搬移方法心法一样,原因就是某个字段与其它类配合的更加紧密,那么就可以把此字段转到其它类中。
重构手法:
(1)在当前类中所有的使用到此字段的地方,使用 getField()方法。
(2)在其它类中创建与之相同的字段与SET GET方法。
(3)将当前类的getField()方法,委托为其它类的getField()方法。
(4)内联方法。
7.3 提炼类
如果某个类中有太多的字段,并且这个类做了两个类应该做的事情。
建立一个新类,将相关的字段和函数从旧类中搬移到新类中。并在旧类中创建此新类的引用。
是否公开这个新类?例如从 “Person”类中,抽取出来了“Telephone”对象。
如果将新创建的类设置为public,那么当修改此Telephone对象可以直接调用set方法。
如果不公开这个对象,例如把这个Telephone对象设置为内部类对象或者包访问权限,可以直接将以前的set方法,改为
//telephone对象可以内部类对象,也可以设置为包访问权限的对象
public void setTelephoneNumber(String number){
telephone.number = number;
}
public void getTelephoneNumber(){
return telephone.number;
}
采用内部类的缺点就是,在外部类中,需要建立与内部类一样的set/get方法,外部类需要将get/set委托给内部类对象去做。
所以,最佳的做法就是,在这个类中,建立一个不可修改的新类对象。所以,不允许不通过Person对象就修改Telephone对象。任何希望修改这个人的phone对象的时候,都需要这样做: person.phone.setAreaCode( code );
public class Person{
private String personName;
private String idCode;
public final Telephone phone = new Telephone();
//get and set
}
class Telephone{
private String areaCode;
private String officeNumber;
private String houseNumner;
//get and set
}
7.4 内联类
如果某个类的责任不足,或者说某个类没有做太多的事情,将这个类的所有特性搬到另外一个类中,然后移除原类。
也就是说,没必要分离类,所以将其所有的set / get 搬移到某个类中。
以前 person.getTelephone().setArea( code ); 就变成了 person.setArea( code );
7.5 隐藏委托关系
封装意味着每个对象应该尽量减少了解系统的其它部分。如果某个客户先通过服务对象的字段获取到另外一个对象,再通过调用后者的函数,那么客户就必须知晓这一层委托关系。万一委托关系发生了变化,那么客户也得做出相应的变化。你可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而取出这种依赖。这么一来,即使将来发生委托关系上的变化,变化也将被限制在服务对象中,不会波及客户。
客户端应该尽量减少与其它类的耦合,尽可能的减少了解系统的其它部分。例如,客户端希望获取某个开发人员的开发经理。客户端如果调用了 Department department = person.getDepartment(); 再通过部门对象 department.getManager();获取到开发部经理。这样的话,相当于暴露了获取的逻辑,即 person.getDepartment().getManager();如果将来经理不是从部门对象获取,即所谓的需求变化,那么在改动代码的时候,一定会改动客户端的 person.getDepartment().getManager(); 这句代码。
出现上述问题的原因就是客户端知晓了这一层委托关系。所以需要我们在代码中隐藏这层委托关系。这里的服务对象,即person对象,可以提供一个接口, person.getManager()接口,返回这个开发人员的开发经理。person对象隐藏了通过Department对象去找部门经理的步骤。客户端直接调用 person.getManager()方法,不需要知道开发经理是怎么获取的。即使需求发生变化,那么修改代码的地方也是 在person这个对象里面修改。
用代码体现:
manager = person.getDepartment().getManager();
这样客户就与Department对象耦合了。
修改person对象,在Person对象中建立一个简单的委托关系。
public Person getManager(){
return _department.getManager();
}
客户端代码就可改变为
manager = person.getManager();
7.6 移除中间人
隐藏委托关系的反操作。
隐藏委托关系的优势在于封装,但是它也是要付出代价的。这个代价就是如果要用受委托类(Department)的某个新特性的时候,就必须在服务对象中增加一个委托函数。随着服务对象的功能越来越多,这个过程就会变得笨拙。服务对象已经变成了一个中间人,此时就应该让客户直接调用受委托类。
修改方法就是在服务对象(person)中直接返回一个受委托类(department),然后使用受委托类的(department)的新特性。
如果department中新增加了一个方法,getDepartmentAddress(),那么客户端希望获取这个地址。为了隐藏委托关系,可能就会在服务对象person对象中增加了 person.getDepartmentAdress()方法。这就意味着,department对象每增加一个新特性,那么person中就需要增加一个与之对应的委托函数。等department的特性变得很多的了以后,person代码会做相应的增加。这样的话,这层委托关系就会变的笨拙不堪。所以,不如让person对象直接返回受委托对象,移除这层委托关系。
所以,隐藏委托关系 与 释放委托关系 需要灵活切换。若department类中不断加入新特性,那么就可以考虑移除中间人了。
在服务对象中直接返回一个受委托对象。用代码体现:
class Person ..
public Department getDepartment() {
return _department ;
}
这样的话,就不必在person对象中建立委托函数了。客户端可以如下调用
person.getDepartment(). getNewFunction();
7.7 引入外加函数
当你需要为提供服务的类添加一个函数的时候,但是却无法修改这个类。
我们可以新建一个函数,函数的参数接收这个服务类。通过这个服务类的好用的API,组合成功能更加强大的函数。
这个函数不应该调用客户端类的任何特性,并以服务类实例作为该函数的第一个参数。
最后,在该函数上注释为:外加函数 foreign method ,应该在服务类中实现。
下面的代码希望表达的意思:如果发现Date类的API非常的好用,但是它却没有提供获取明天Date的方法。所以,我们可以手动编写一个外加函数,并加上注释。
Date newStart = nextDay(someday);
private static Date nextDay(Date arg){
// 外加函数,应该将该方法加入到 java.util.Date 类中
return new Date(arg.getYear(),arg.getMonth(),arg.getDate() + 1);
}
7.8 引入本地扩展
如果有太多的外加函数,那么就需要将这些外加函数组织在一起。
通常使用 继承 或 包装 的手段来实现本地扩展。