7.1. 直通方法

当相邻的层具有相似的抽象时,问题通常以直通方法的形式表现出来。直通方法是一种很少执行的方法,除了调用另一个方法(其签名与调用方法的签名相似或相同)之外。例如,一个实施 GUI 文本编辑器的学生项目包含一个几乎完全由直通方法组成的类。这是该类的摘录:

public class TextDocument ... {
    private TextArea textArea;
    private TextDocumentListener listener;
    ...
    public Character getLastTypedCharacter() {
        return textArea.getLastTypedCharacter();
    }
    public int getCursorOffset() {
        return textArea.getCursorOffset();
    }
    public void insertString(String textToInsert, int offset) {
        textArea.insertString(textToInsert, offset);
    }
    public void willInsertString(String stringToInsert, int offset) {
        if (listener != null) {
            listener.willInsertString(this, stringToInsert, offset);
        }
    }
    ...
}

该类别中 15 个公共方法中的 13 个是直通方法。

直通方法是一种不执行任何操作的方法,只是将其参数传递给另一个方法,通常使用与直通方法相同的 API。这通常表示各类之间没有明确的职责划分。

直通方法使类变浅:它们增加了类的接口复杂性,从而增加了复杂性,但是并没有增加系统的整体功能。在上述四个方法中,只有最后一个具有任何功能,甚至没有什么功能:该方法检查一个变量的有效性。直通方法还会在类之间创建依赖关系:如果针对 TextArea 中的 insertString 方法更改了签名,则必须更改 TextDocument 中的 insertString 方法以进行匹配。

直通方法表明类之间的责任划分存在混淆。在上面的示例中,TextDocument 类提供了 insertString 方法,但是用于插入文本的功能完全在 TextArea 中实现。这通常是一个坏主意:某个功能的接口应该在实现该功能的同一类中。当您看到从一个类到另一个类的直通方法时,请考虑这两个类,并问自己“这些类分别负责哪些功能和抽象?” 您可能会注意到,各类之间的职责重叠。

解决方案是重构类,以使每个类都有各自不同且连贯的职责。图 7.1 说明了几种方法。一种方法,如图 7.1(b)所示,是将较低级别的类直接暴露给较高级别的类的调用者,而从较高级别的类中删除对该功能的所有责任。另一种方法是在类之间重新分配功能,如图 7.1(c)所示。最后,如果无法解开这些类,最好的解决方案可能是如图 7.1(d)所示合并它们。

在上面的示例中,职责交织的三个类为:TextDocument,TextArea 和 TextDocumentListener。学生通过在类之间移动方法并将三个类缩减为两个类来消除直通方法,这两个类的职责更加明确。

When adjacent layers have similar abstractions, the problem often manifests itself in the form of pass-through methods. A pass-through method is one that does little except invoke another method, whose signature is similar or identical to that of the calling method. For example, a student project implementing a GUI text editor contained a class consisting almost entirely of pass-through methods. Here is an extract from that class:

public class TextDocument ... {
    private TextArea textArea;
    private TextDocumentListener listener;
    ...
    public Character getLastTypedCharacter() {
        return textArea.getLastTypedCharacter();
    }
    public int getCursorOffset() {
        return textArea.getCursorOffset();
    }
    public void insertString(String textToInsert, int offset) {
        textArea.insertString(textToInsert, offset);
    }
    public void willInsertString(String stringToInsert, int offset) {
        if (listener != null) {
            listener.willInsertString(this, stringToInsert, offset);
        }
    }
    ...
}

13 of the 15 public methods in that class were pass-through methods.

A pass-through method is one that does nothing except pass its arguments to another method, usually with the same API as the pass-through method. This typically indicates that there is not a clean division of responsibility between the classes.

Pass-through methods make classes shallower: they increase the interface complexity of the class, which adds complexity, but they don’t increase the total functionality of the system. Of the four methods above, only the last one has any functionality, and even there it is trivial: the method checks the validity of one variable. Pass-through methods also create dependencies between classes: if the signature changes for the insertString method in TextArea, then the insertString method in TextDocument will have to change to match.

Pass-through methods indicate that there is confusion over the division of responsibility between classes. In the example above, the TextDocument class offers an insertString method, but the functionality for inserting text is implemented entirely in TextArea. This is usually a bad idea: the interface to a piece of functionality should be in the same class that implements the functionality. When you see pass-through methods from one class to another, consider the two classes and ask yourself “Exactly which features and abstractions is each of these classes responsible for?” You will probably notice that there is an overlap in responsibility between the classes.

The solution is to refactor the classes so that each class has a distinct and coherent set of responsibilities. Figure 7.1 illustrates several ways to do this. One approach, shown in Figure 7.1(b), is to expose the lower level class directly to the callers of the higher level class, removing all responsibility for the feature from the higher level class. Another approach is to redistribute the functionality between the classes, as in Figure 7.1(c). Finally, if the classes can’t be disentangled, the best solution may be to merge them as in Figure 7.1(d).

In the example above, there were three classes with intertwined responsibilities: TextDocument, TextArea, and TextDocumentListener. The student eliminated the pass-through methods by moving methods between classes and collapsing the three classes into just two, whose responsibilities were more clearly differentiated.