定义

如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象, 然后再请求另一个对象……这就是消息链。在实际代码中你看到的可能是一长串取值函数或一长串临时变量。

影响

  • 客户端代码将与查找过程中的调用结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。

改进目标

  • 针对过长消息链,可以用这时候应该使用隐藏委托关系,把调用链这种耦合关系放在中间人中。

方法

  • 观察清楚消息链的调用业务逻辑;
  • 通过提炼函数把所有相同调用放在一个函数中;
  • 搬移函数到对应的中间人类中(新增或者使用以前的类);
  • 替换这个函数到之前调用链,隐藏委托关系。

案例:问题代码

代码背景

  • 一个客户端期望能访问一个人的部门地址的门牌号和具体地址 。普通实现如果采用消息链调用的方式,调用方式为 : person.getDepartment().getAddress().getStreet() .getStreetName()

症状/问题

  • InformationClient会知道整个Person依赖关系和组织实现,形成耦合关系。
  • 消息链太长导致代码不易重构,不易修改。如果Person内部依赖关系或数据结构进行调整 、重构,那么外部调用链同时被修改。

重构目标

  • InformationClient直接访问Person就能得出Street对象(这里是部门对象)
  • 人、部门、地址、街道整个依赖关系对client透明
  • 后面如果发生变化和重构,外部无感知
/**
 * 信息服务类,作为代理解决
 */
public class InformationClient {
  /**
    * 获取员工所在的部门地址街道名称
    *
    * @param person 当前员工
    * @return 街道名称
    */
  public String getServerStreetName(Person person) {
    return person.getDepartment().getAddress().getStreet().getStreetName();
  }

  /**
    * 获取员工所在的部门地址街道编号
    *
    * @param person 当前员工
    * @return 街道编号
    */
  public Integer getServerStreetNo(Person person) {
    return person.getDepartment().getAddress().getStreet().getStreetNo();
  }
}

/**
 * 一个员工个人信息, 包含部门{@link Department}信息。
 */
public class Person {
    /** 部门信息 */
    private Department department;

    public Person(Department department) {
        this.department = department;
    }

    public Department getDepartment() {
        return department;
    }
}

改进手法:提炼/搬移函数、隐藏委托关系

改进前:Client感知到了整个内部逻辑,形成依赖 改进后:隐藏了内部数据结构依赖关系,使用Person作为“中间人”,为Client屏蔽委托关系

/**
 * 信息服务类,作为代理解决
 */
public class InformationClient {
    /**
     * 获取员工所在的部门地址街道名称
     *
     * @param person 当前员工
     * @return 街道名称
     */
    public String getServerStreetName(Person person) {
        return person.getDepartmentStreet().getStreetName();
    }

    /**
     * 获取员工所在的部门地址街道编号
     *
     * @param person 当前员工
     * @return 街道编号
     */
    public Integer getServerStreetNo(Person person) {
        return person.getDepartmentStreet().getStreetNo();
    }
}

/**
 * 一个员工个人信息, 包含部门{@link Department}信息。
 */
public class Person {
    /** 部门信息 */
    private Department department;

    public Person(Department department) {
        this.department = department;
    }

    public Department getDepartment() {
        return department;
    }

    public Street getDepartmentStreet() {
        return getDepartment().getAddress().getStreet();
    }
}

操作手法

操作快捷键(推荐)Ctrl+Alt+Shift+T(或:鼠标右键“Refactor”)
提炼函数Ctrl + Alt + MExtract Method
移动函数F6Move Method
重命名Shift + F6Rename

引申

过长的消息链到底是在说什么?到底有什么问题,为什么需要修改?深模块 VS 浅模块

深模块: 最好的模块提供了强大的功能,又有着简单的接口。术语“深”可以用于描述这种模块。 可以从成本与收益的角度思考模块深度。模块提供的收益是它的功能。 模块的成本(从系统复杂度的角度考虑)是它的接口。接口代表了模块施加给系统其余部分的复杂度。接口越小而简单,它引入的复杂度就越少。 好的模块就是那些成本低收益高的模块。

浅模块: 就是模块内部暴露给外部调用或者使用,内部没有隐藏太多的逻辑,外部都能感知到内部的具体逻辑或者结构组织,或调用关系。 上述修改前的代码就是一个典型的浅模块的暴露和外部调用,外部是可以感知到内部实现的,是一种典型的不好的代码设计。

最后:Classitis 当今,深模块的价值并没有被广为接受。一般常识是类需要小,而不是深。学生被告知:类设计中最重要的事情是把大类拆分成更小的类。 相似建议还包括:“要把方法行数大于N的方法分成多个方法”,有时候N甚至只有10这么小。这会导致大量的浅模块,增加系统的总复杂度。 极端的“类应该小”的做法是一种综合症的表现,这种症状可以被称为Classitis。 它源于一种错误思维:“类是好的,所以越多类越好”。 这种思想最终会导致系统层面积累了巨大的复杂度,程序风格也会变得啰嗦。