在 6.2. 为编辑器存储文本 的 GUI 编辑器项目中,要求之一是支持多级撤消/重做,不仅要更改文本本身,还要更改选择,插入光标和视图。例如,如果用户选择了一些文本,将其删除,滚动到文件中的其他位置,然后调用 undo,则编辑器必须将其状态恢复为删除前的状态。这包括还原已删除的文本,再次选择它,并使所选的文本在窗口中可见。
一些学生项目将整个撤消机制实现为文本类的一部分。文本类维护所有不可撤消更改的列表。每当更改文本时,它将自动将条目添加到此列表中。为了更改选择,插入光标和视图,用户界面代码调用了文本类中的其他方法,然后将这些更改的条目添加到撤消列表中。当用户请求撤消或重做时,用户界面代码将调用文本类中的方法,该方法然后处理撤消列表中的条目。对于与文本相关的条目,它更新了文本类的内部。对于与其他事物(例如选择)相关的条目,将调用返回到用户界面代码的文本类来执行撤消或重做。
这种方法在文本类中导致了一系列尴尬的功能。撤消/重做的核心由通用机制组成,用于管理已执行的动作列表,并在撤消和重做操作期间逐步执行这些动作。核心与专用处理程序一起位于 text 类中,该专用处理程序对诸如文本和选择之类的特定内容实现了撤消和重做。用于选择和光标的专用撤消处理程序与文本类中的任何其他内容均无关。它们导致文本类和用户界面之间的信息泄漏,以及每个模块中来回传递撤消信息的额外方法。如果将来将新的可撤消实体添加到系统中,则将需要更改文本类,包括特定于该实体的新方法。
通过提取撤消/重做机制的通用核心并将其放在单独的类中,可以解决这些问题:
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 类对操作中存储的信息或它们如何实现其撤消和重做方法一无所知。历史记录维护一个历史记录列表,该列表描述了应用程序整个生命周期中执行的所有操作,并且它提供了撤消和重做方法,以响应用户请求的撤消和重做而在列表中前后移动,并在应用程序中调用撤消和重做方法。历史动作。
历史。动作是特殊目的的对象:每个人都了解一种特殊的不可操作。它们在 History 类之外的模块中实现,这些模块可以理解特定类型的可撤销操作。文本类可能实现 UndoableInsert 和 UndoableDelete 对象,以描述文本的插入和删除。每当插入文本时,文本类都会创建一个描述该插入的新 UndoableInsert 对象,并调用 History.addAction 将其添加到历史列表中。编辑器的用户界面代码可能会创建 UndoableSelection 和 UndoableCursor 对象,这些对象描述对选择和插入光标的更改。
History 类还允许对操作进行分组,例如,来自用户的单个撤消请求可以恢复已删除的文本,重新选择已删除的文本以及重新放置插入光标。有多种将动作分组的方法。历史记录类使用围栏,围栏是放置在历史记录列表中的标记,用于分隔相关动作的组。每次对 History.redo 的调用都会向后浏览历史记录列表,撤消操作,直到到达下一个栅栏。围栏的位置由更高级别的代码通过调用 History.addFence 确定。
这种方法将撤消功能分为三类,每类都在不同的地方实现:
一种用于管理和分组动作以及调用撤消/重做操作的通用机制(由 History 类实现)。特定操作的细节(由各种类实现,每个类都了解少量的操作类型)。分组操作的策略(由高级用户界面代码实现,以提供正确的整体应用程序行为)。这些类别中的每一个都可以在不了解其他类别的情况下实施。历史课不知道要撤消哪种操作;它可以用于多种应用。每个动作类仅理解一种动作,并且历史记录类和动作类都不需要知道将动作分组的策略。
关键的设计决策是将撤消机制的通用部分与专用部分分开,然后将通用部分单独放在一个类中的决定。一旦完成,其余的设计就会自然消失。
注意:将通用代码与专用代码分离的建议是指与特定机制相关的代码。例如,特殊用途的撤消代码(例如撤消文本插入的代码)应该与通用用途的撤消代码(例如管理历史记录列表的代码)分开。然而,将一种机制的专用代码与另一种机制的通用代码组合起来通常是有意义的。text 类就是这样一个例子:它实现了一种管理文本的通用机制,但是它包含了与撤销相关的专用代码。撤消代码是专用的,因为它只处理文本修改的撤消操作。将这段代码与 History 类中通用的 undo 基础结构结合在一起是没有意义的,但是将它放在 text 类中是有意义的,因为它与其他文本函数密切相关。
In the GUI editor project from Section 6.2, one of the requirements was to support multi-level undo/redo, not just for changes to the text itself, but also for changes in the selection, insertion cursor, and view. For example, if a user selected some text, deleted it, scrolled to a different place in the file, and then invoked undo, the editor had to restore its state to what it was just before the deletion. This included restoring the deleted text, selecting it again, and also making the selected text visible in the window.
Some of the student projects implemented the entire undo mechanism as part of the text class. The text class maintained a list of all the undoable changes. It automatically added entries to this list whenever the text was changed. For changes to the selection, insertion cursor, and view, the user interface code invoked additional methods in the text class, which then added entries for those changes to the undo list. When undo or redo was requested by the user, the user interface code invoked a method in the text class, which then processed the entries in the undo list. For entries related to text, it updated the internals of the text class; for entries related to other things, such as the selection, the text class called back to the user interface code to carry out the undo or redo.
This approach resulted in an awkward set of features in the text class. The core of undo/redo consists of a general-purpose mechanism for managing a list of actions that have been executed and stepping through them during undo and redo operations. The core was located in the text class along with special-purpose handlers that implemented undo and redo for specific things such as text and the selection. The special-purpose undo handlers for the selection and the cursor had nothing to do with anything else in the text class; they resulted in information leakage between the text class and the user interface, as well as extra methods in each module to pass undo information back and forth. If a new sort of undoable entity were added to the system in the future, it would require changes to the text class, including new methods specific to that entity. In addition, the general-purpose undo core had little to do with the general-purpose text facilities in the class.
These problems can be solved by extracting the general-purpose core of the undo/redo mechanism and placing it in a separate class:
public class History {
public interface Action {
public void redo();
public void undo();
}
History() {...}
void addAction(Action action) {...}
void addFence() {...}
void undo() {...}
void redo() {...}
}
In this design, the History class manages a collection of objects that implement the interface History.Action. Each History.Action describes a single operation, such as a text insertion or a change in the cursor location, and it provides methods that can undo or redo the operation. The History class knows nothing about the information stored in the actions or how they implement their undo and redo methods. History maintains a history list describing all of the actions executed over the lifetime of an application, and it provides undo and redo methods that walk backwards and forwards through the list in response to user-requested undos and redos, calling undo and redo methods in the History.Actions.
History.Actions are special-purpose objects: each one understands a particular kind of undoable operation. They are implemented outside the History class, in modules that understand particular kinds of undoable actions. The text class might implement UndoableInsert and UndoableDelete objects to describe text insertions and deletions. Whenever it inserts text, the text class creates a new UndoableInsert object describing the insertion and invokes History.addAction to add it to the history list. The editor’s user interface code might create UndoableSelection and UndoableCursor objects that describe changes to the selection and insertion cursor.
The History class also allows actions to be grouped so that, for example, a single undo request from the user can restore deleted text, reselect the deleted text, and reposition the insertion cursor. There are a number of ways to group actions; the History class uses fences, which are markers placed in the history list to separate groups of related actions. Each call to History.redo walks backwards through the history list, undoing actions until it reaches the next fence. The placement of fences is determined by higher-level code by invoking History.addFence.
This approach divides the functionality of undo into three categories, each of which is implemented in a different place:
A general-purpose mechanism for managing and grouping actions and invoking undo/redo operations (implemented by the History class). The specifics of particular actions (implemented by a variety of classes, each of which understands a small number of action types). The policy for grouping actions (implemented by high-level user interface code to provide the right overall application behavior). Each of these categories can be implemented without any understanding of the other categories. The History class does not know what kind of actions are being undone; it could be used in a variety of applications. Each action class understands only a single kind of action, and neither the History class nor the action classes needs to be aware of the policy for grouping actions.
The key design decision was the one that separated the general-purpose part of the undo mechanism from the special-purpose parts and put the general-purpose part in a class by itself. Once that was done, the rest of the design fell out naturally.
Note: the suggestion to separate general-purpose code from special-purpose code refers to code related to a particular mechanism. For example, special-purpose undo code (such as code to undo a text insertion) should be separated from general-purpose undo code (such as code to manage the history list). However, it often makes sense to combine special-purpose code for one mechanism with general-purpose code for another. The text class is an example of this: it implements a general-purpose mechanism for managing text, but it includes special-purpose code related to undoing. The undo code is special-purpose because it only handles undo operations for text modifications. It doesn’t make sense to combine this code with the general-purpose undo infrastructure in the History class, but it does make sense to put it in the text class, since it is closely related to other text functions.