# 9.7 例子：编辑器撤销机制

在[第6.2节](https://placeboy.gitbook.io/a-philosophy-of-software-design/ch6-general-purpose-modules-are-deeper/6.2-li-zi-wei-yi-ge-bian-ji-qi-cun-chu-wen-ben)的GUI编辑器项目中，其中一个要求是支持多级撤销/重做，不仅是对文本本身的改变，也包括对选中区、插入光标和视图的改变。例如，如果用户选择了一些文本，将其删除，滚动到文件的不同位置，然后调用撤销，编辑器必须将其状态恢复到删除前的状态。这包括恢复被删除的文本，再次选择它，并使被选择的文本在窗口中可见。

一些学生项目将整个撤销机制作为文本类的一部分来实现。文本类维护了一个所有可撤销的修改的列表。每当文本被改变时，它就会自动向这个列表添加条目。对于选中区、插入光标和视图的改变，用户界面代码会调用文本类中附加的方法，然后将这些改变的条目添加到撤销列表中。当用户要求撤销或重做时，用户界面代码调用文本类中的一个方法，它随后处理撤销列表中的条目。对于与文本有关的条目，它会更新文本类的内部结构；对于与其他事物有关的条目，如选中区，文本类会回调到用户界面代码来执行撤销或重做。

这种方法导致了文本类中一系列尴尬的功能。撤销/重做的核心包括一个通用的机制，用于管理已经执行的动作列表，并在撤销和重做操作中对它们进行遍历。该核心位于文本类中，同时还有专用的处理程序，用于实现对特定事物的撤销和重做，如文本和选中区。用于选中区和光标的专用撤销处理程序与文本类中的其他东西无关；它们导致了文本类和用户界面之间的信息泄露，同时导致每个模块中需要额外的方法以来回传递撤销信息。如果将来在系统中加入一种新的可撤销实体，就需要对文本类进行修改，包括针对该实体的新方法。此外，通用的撤销核心代码与该类中的通用的文本功能没有什么关系。

这些问题可以通过提取撤销/重做机制的通用核心代码并将其放在一个单独的类中来解决：

```java
public class History {
    public interface Action { 
        public void redo(); 
        public void undo(); 
    }

    History() {...}

    void addAction(Action action) {...} 
    void addFence() {...}

    void undo() {...} 
    void redo() {...}
}
```

在这种设计中，`History`类管理着一个实现`History.Action`接口的对象集合。每个`History.Action`描述一个单一的操作，例如文本插入或光标位置的改变，并且它提供了可以撤销或重做操作的方法。`History`类对存储在操作中的信息或它们如何实现其`undo`和`redo`方法一无所知。`History`维护一个历史列表，描述在一个应用程序的生命周期内执行的所有操作，它提供`undo`和`redo`方法，通过在列表中前后移动，以响应用户要求的撤销和重做，调用`History.Actions`中的`undo`和`redo`方法。

`History.Actions`是特殊用途的对象：每个对象都能理解一种特殊的可撤销操作。它们在`History`类之外实现，在理解特定种类的可撤销操作的模块中。文本类可以实现`UndoableInsert`和`UndoableDelete`对象来描述文本的插入和删除。每当它插入文本时，文本类会创建一个新的`UndoableInsert`对象来描述插入，并调用 `History.addAction` 来将其添加到历史列表中。编辑器的用户界面代码可能创建`UndoableSelection`和`UndoableCursor`对象，描述对选中区和插入光标的变更。

`History`类也允许对操作进行分组，因此，比如说，用户的单个撤销请求可以恢复被删除的文本，重新选择被删除的文本，以及重新定位插入光标。有许多方法来将操作进行分组；`History`类使用栅栏（*fence*），这是放置在历史列表中的标记，用来分隔相关操作组。每次调用 `History.redo` 都会向后遍历历史列表，撤销操作，直到到达下一个栅栏。栅栏的位置由高层代码通过调用`History.addFence`决定。

这种方法将撤销动作的功能分为三类，每一类都在不同的地方实现：

* 用于管理动作和对动作进行分组以及调用撤消/重做动作的通用机制（由 `History` 类实现）。
* 特定动作的具体细节（由各种类实现，每个类都能理解少量的动作类型）。
* 动作分组的策略（由高级用户界面代码实现，以提供正确的整体应用行为）。

这些类别中的每一个都可以在不了解其他类别的情况下实现。`History`类不知道什么样的动作被撤销；它可以被用于各种应用。每个动作类只理解一种动作，`History`类和动作类都不需要知道动作分组的策略。

关键的设计决定是将撤销机制的通用部分与专用部分分开，并将通用部分放在一个单独的类中。一旦这样做了，其余的设计就自然而然地出来了。

注意：建议将通用代码与专用代码分开，这里的专用代码是指与特定机制有关的代码。例如，专用的撤销代码（如撤销文本插入的代码）应该与通用的撤销代码（如管理历史列表的代码）分开。然而，将一种机制的专用代码与另一种机制的通用代码结合起来往往是有意义的。文本类就是一个例子：它实现了一个管理文本的通用机制，但它包括与撤销有关的专用代码。撤销代码是专用的，因为它只处理对文本修改的撤销操作。把这段代码与`History`类中的通用的撤销基础结构结合起来是没有意义的，但把它放在文本类中是有意义的，因为它与其他文本功能密切相关。&#x20;
