重构心法修炼第二层:在对象之间搬移特性

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  引入本地扩展

        如果有太多的外加函数,那么就需要将这些外加函数组织在一起。

        通常使用 继承 或 包装 的手段来实现本地扩展。

posted @ 2022-07-17 12:15  小大宇  阅读(37)  评论(0编辑  收藏  举报