文章目录
- 五、行为模式
- 5.1 CHAIN OF RESPONSIBILITY(职责链)
- 1.意图
- 2.动机
- 3.适用性
- 4.结构
- 5.参与者
- 6.协作
- 7.效果
- 8.实现
- 9.代码示例
- 10.相关模式
- 5.2 COMMAND(命令)
- 1.意图
- 2.别名
- 3.动机
- 4.适用性
- 5.结构
- 6.参与者.
- 7.协作
- 8.效果
- 9.实现
- 10.代码示例
- 11.相关模式
- 5.3 INTERPRETER(解释器)
- 1.意图
- 2.动机
- 3.适用性
- 4.结构
- 5.参与者
- 6.协作
- 7.效果
- 8.实现
- 9.代码实例
- 10.相关模式
- 5.4 ITERATOR(迭代器)
- 1.意图
- 2.别名
- 3.动机
- 4.适用性
- 5.结构
- 6.参与者
- 7.协作
- 8.效果
- 9.实现
- 10.代码示例
- 11.相关模式
- 资料
五、行为模式
行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻划了在运行时难以跟踪的复杂的控制流。它们将你的注意力从控制流转移到对象间的联系方式上来。
行为类模式使用继承机制在类间分派行为。本章包括两个这样的模式。
其中TemplateMethod (5.10)较为简单和常用。模板方法是一个算法的抽象定义,它逐步地定义该算法,每一步调用一个抽象操作或一个原语操作,子类定义抽象操作以具体实现该算法。另一种行为类模式是Interpreter (5.3)。它将一个文法表示为一个类层次,并实现一个解释器作为这些类的实例上的一个操作。
行为对象模式使用对象复合而不是继承。一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一个对象都无法单独完成的任务。这里一个重要的问题是对等的对象如何互相了解对方。对等对象可以保持显式的对对方的引用,但那会增加它们的耦合度。在极端情况下,每一个对象都要了解所有其他的对象。
Mediator(5.5)在对等对象间引入一个mediator对象以避免这种情况的出现。mediator提供了松耦合所需的间接性。
Chain of Responsibility(5.1)提供更松的耦合。它让你通过一条候选对象链隐式的向一个对象发送请求。根据运行时刻情况任一候选者都可以响应相应的请求。候选者的数目是任意的,你可以在运行时刻决定哪些候选者参与到链中。
Observer(5.7)模式定义并保持对象间的依赖关系。典型的Observer的例子是Smalltalk 中的模型/视图/控制器,其中一旦模型的状态发生变化,模型的所有视图都会得到通知。
其他的行为对象模式常将行为封装在一个对象中并将请求指派给它。
Strategy(5.9)模式将算法封装在对象中,这样可以方便地指定和改变一个对象所使用的算法。
Command(5.2)模式将请求封装在对象中,这样它就可作为参数来传递,也可以被存储在历史列表里,或者以其他方式使用。
State(5.8)模式封装一个对象的状态,使得当这个对象的状态对象变化时,该对象可改变它的行为。
Visitor(5.11)封装分布于多个类之间的行为,而Iterator(5.4)则抽象了访问和遍历一个集合中的对象的方式。
对类
- Interpreter
- Template Method
对对象
- Chain of Responsibility
- Command
- Iterator
- Mediator
- Memento
- Obeserver
- State
- Strategy
- Visitor
5.1 CHAIN OF RESPONSIBILITY(职责链)
1.意图
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
过滤器或者流水线。
由上一个对象,将处理内容传给下一个对象,直到末尾。形成一条链表一样的结构。
2.动机
考虑一个图形用户界面中的上下文有关的帮助机制。用户在界面的任一部分上点击就可以得到帮助信息,所提供的帮助依赖于点击的是界面的哪一部分以及其上下文。例如,对话框中的按钮的帮助信息就可能和主窗口中类似的按钮不同。
如果对那一部分界面没有特定的帮助信息,那么帮助系统应该显示一个关于当前上下文的较一般的帮助信息一比如说,整个对话框。
因此很自然地,应根据普遍性(generality)即从最特殊到最普遍的顺序来组织帮助信息。而且,很明显,在这些用户界面对象中会有一个对象来处理帮助请求;至于是哪–个对象则取决于上下文以及可用的帮助具体到何种程度。
这儿的问题是提交帮助请求的对象(如按钮)并不明确知道谁是最终提供帮助的对象。我们要有一种办法将提交帮助请求的对象与可能提供帮助信息的对象解耦(decouple)。Chain ofResponsibility模式告诉我们应该怎么做。
这一模式的想法是,给多个对象处理一个请求的机会,从而解耦发送者和接受者。该请求沿对象链传递直至其中一个对象处理它,如下图所示。
从第一个对象开始,链中收到请求的对象要么亲自处理它,要么转发给链中的下一个候选者。提交请求的对象并不明确地知道哪一个对象将会处理它一我们说该请求有一个隐式的接收者(implicit receiver)。
假设用户在一个标有“Print”的按钮窗口组件上单击帮助,而该按钮包含在一个PrintDialog的实例中,该实例知道它所属的应用对象(见前面的对象框图)。下面的交互框图(diagram)说明了帮助请求怎样沿链传递:
在这个例子中,既不是aPrintButton也不是aPrintDialog 处理该请求;它一直被传递给anApplication,anApplication处理它或忽略它。提交请求的客户不直接引用最终响应它的对象。
要沿链转发请求,并保证接收者为隐式的(implicit),每个在链上的对象都有一致的处理请求和访问链上后继者的接口。例如,帮助系统可定义一个带有相应的HandleHelp操作的HelpHandler类。HelpHandler可为所有候选对象类的父类,或者它可被定义为一个混入(mixin)类。这样想处理帮助请求的类就可将HelpHandler作为其一个父类,如下页上图所示。
按钮、对话框,和应用类都使用HelpHandler操作来处理帮助请求。HelpHandler的HandleHelp 操作缺省的是将请求转发给后继。子类可重定义这一操作以在适当的情况下提供帮助;否则它们可使用缺省实现转发该请求。
3.适用性
在以下条件下使用Responsibility 链:
- 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
- 你想在不明确指定接收者的情况下,向多个对象中的个提交一个请求。
- 可处理一个请求的对象集合应被动态指定。
4.结构
一个典型对象可能的结构如下:
5.参与者
- Handler (如HelpHandler)
- 定义一个处理请求的接口。
- (可选)实现后继链。
- ConcreteHandler (如PrintButton和PrintDialog )—处理它所负责的请求。
- 可访问它的后继者。
- 如果可处理该请求,就处理之;否则将该请求转发给它的后继者。.
- Client
- 向链上的具体处理者(ConcreteHandler)对象提交请求。
6.协作
- 当客户提交一个请求时,请求沿链传递直至有一个ConcreteHandler对象负责处理它。
7.效果
Responsibility链有下列优点和缺点(liabilities):
- 降低耦合度
该模式使得一个对象无需知道是其他哪一个对象处理其请求。对象仅需知道该请求会被“正确”地处理。接收者和发送者都没有对方的明确的信息,且链中的对象不需知道链的结构。
结果是,职责链可简化对象的相互连接。它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用。
- 增强了给对象指派职责(Responsibility)的灵活性
当在对象中分派职责时,职责链给你更多的灵活性。你可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个请求的那些职责。你可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。
- 不保证被接受
既然一个请求没有明确的接收者,那么就不能保证它一定会被处理一该请求可能一直到链的末端都得不到处理。一个请求也可能因该链没有被正确配置而得不到处理。
8.实现
下面是在职责链模式中要考虑的实现问题:
- 实现后继者链
有两种方法可以实现后继者链。
- 定义新的链接(通常在Handler中定义,但也可由ConcreteHandlers来定义)。
- 使用已有的链接。
我们的例子中定义了新的链接,但你常常可使用已有的对象引用来形成后继者链。
例如,在一个部分-整体层次结构中,父构件引用可定义一个部件的后继者。
窗口组件((Widget )结构可能早已有这样的链接。Composite ( 4.3)更详细地讨论了父构件引用。
当已有的链接能够支持你所需的链时,完全可以使用它们。这样你不需要明确定义链接,而且可以节省空间。但如果该结构不能反映应用所需的职责链,那么你必须定义额外的链接。
- 连接后继者
如果没有已有的引用可定义一个链,那么你必须自己引入它们。这种情况下Handler不仅定义该请求的接口,通常也维护后继链接。
这样Handler就提供了HandleRequest的缺省实现:HandleRequest向后继者(如果有的话)转发请求。如果ConcreteHandler子类对该请求不感兴趣,它不需重定义转发操作,因为它的缺省实现进行无条件的转发。
此处为一个HelpHandler基类,它维护一个后继者链接:
class HelpHandler{
public:
HelpHandler(HelpHandler* s):_successor(s){}
virtual void HandleHelp();
private:
HelpHandler* _successor;
};
void HelpHandler ::HandleHelp(){
if (_successor){
_successor->Hand1eHelp();
}
}
- 表示请求
可以有不同的方法表示请求。最简单的形式,比如在HandleHelp的例子中,请求是一个硬编码的(hard-coded)操作调用。这种形式方便而且安全,但你只能转发Handler类定义的固定的一组请求。
另一选择是使用一个处理函数,这个函数以一个请求码(如一个整型常数或一个字符串)为参数。这种方法支持请求数目不限。唯一的要求是发送方和接受方在请求如何编码问题上应达成一致。
这种方法更为灵活,但它需要用条件语句来区分请求代码以分派请求。另外,无法用类型安全的方法来传递请求参数,因此它们必须被手工打包和解包。显然,相对于直接调用一个操作来说它不太安全。
为解决参数传递问题,我们可使用独立的请求对象来纣装请求参数。Request类可明确地擂述请求,而新类型的请求可用它的子类来定义。这些子类可定义不同的请求参数。处理者必须知道请求的类型(即它们正使用哪一个Request子类)以访问这些参数。
为标识请求,Request可定义一个访问器(accessor)函数以返回该类的标识符。或者,如果实现语言支持的话,接受者可使用运行时的类型信息。
以下为一个分派函数的框架(sketch),它使用请求对象标识请求。定义于基类Request中的GetKind操作识别请求的类型:
void Handler::HandleReqruest(Requent* theRequest){
switch (theRequest->GetKind()){
case Help:
// cast argunent to appropriate type
HandleHelp((HelpRequese*) theReqruest);
break;
case Print:
HandlePrint((PrintRequest*) theRequest);
// ...
break;
default:
// ...
break;
}
}
子类可通过重定义HandleRequest扩展该分派函数。子类只处理它感兴趣的请求;其他的请求被转发给父类。这样就有效的扩展了(而不是重写)HandleRequest操作。例如,一个ExtendedHandler子类扩展了MyHandler版本的HandleRequest:
clase ExtendedHandler:publie Handler{
public:
virtual void HandleRequest(Request* theReqruest)
// ...
};
void ExtendedHandler::HandleRequest(Request* theRequeat){
switch(theRequest->GetKind()){
case Preview:
// handle the Preview request
break;
default:
// let Handler handle other requests
Handler::HandlerRequest(theRequest);
}
}
- 在Smalltalk中自动转发
你可以使用Smalltalk 中的doesNotUnderstand机制转发请求。没有相应方法的消息被doseNotUnderstand的实现捕捉( trap in),此实现可被重定义,从而可向一个对象的后继者转发该消息。这样就不需要手工实现转发;类仅处理它感兴趣的请求,而依赖doesNotUnderstand转发所有其他的请求。
9.代码示例
下面的例子举例说明了在一个像前面描述的在线帮助系统中,职责链是如何处理请求的。帮助请求是一个显式的操作。
我们将使用在窗口组件层次中的已有的父构件引用来在链中的窗口组件间传递请求,并且我们将在Handler类中定义一个引用以在链中的非窗口组件间传递帮助请求。
HelpHandler类定义了处理帮助请求的接口。它维护一个帮助主题(缺省值为空),并保持对帮助处理对象链中它的后继者的引用。关键的操作是HandleHelp,它可被子类重定义。HasHelp是一个辅助操作,用于检查是否有一个相关的帮助主题。
typedef int Topic;
const Topic NO_HELP_TOPIC = -1;
class HelpHandler{
public:
HelpHandler(HelpHandler* = 0,Topic = NO_HELP_TOPIC);
virtual bool HasHelp():
virtual void SetHandler(HelpHandler*,Topic);
virtual void HandleHelp();
private:
HelpHandler* _successor;
Topic _topic;
);
HelpHandler::HelpHandler(HelpHandler* h, Topic t): _successor (h), _topic(t){}
bool HelpHandler::HasHelp(){
return _topic != NO_HELP_TOPIC;
}
void HelpHandler::HandleHelp(){
if(_successor != 0){
_successor->HandleHelp();
}
}
所有的窗口组件都是Widget抽象类的子类。Widget是HelpHandler 的子类,因为所有的用户界面元素都可有相关的帮助。(我们也可以使用另一种基于混入类的实现方式)
claee Widget:public HelpHandler{
protected:
Widget (Widget* parent, Topic t = NO_HELP_TOPIC);
private:
Widget* _parent;
};
widget::Widget (widget* w,Topic t):HelpHandler(w. t) {
_parent = w;
}
在我们的例子中,按钮是链上的第一个处理者。Button类是Widget类的子类。Button构造函数有两个参数:对包含它的窗口组件的引用和其自身的帮助主题。
class Button:public Widget{
public:
Button (Widget* d, Topic t = NO_HELP_TOPIC);
virtual void HandleHelp():
// Widget operations that Button overrides ...
};
Button版本的HandleHelp首先测试检查其自身是否有帮助主题。如果开发者没有定义一个帮助主题,就用HelpHandler中的HandleHelp操作将该请求转发给它的后继者。如果有帮助主题,那么就显示它,并且搜索结束。
Button::Button(Widget* h,Topic t):Widget(h,t){ }
void Button::HandleHelp(){
if(HasHelp()) {
// offer help on the button
}
else{
HelpHandler::HandleHelp();
}
}
Dialog实现了一个类似的策略,只不过它的后继者不是一个窗口组件而是任意的帮助请求处理对象。在我们的应用中这个后继者将是Application的一个实例。
class Dialog :public Widget{
public:
Dialog(HelpHand1er* h,Topic t = NO_HELP_TOPIC);
virtual void HandleHelp();
// widget operations that Dialog overrides...
// ...
};
Dialog::Dialog (HelpHandler* h,Topic t):widget(0){
SetHandler(h,t);
}
void _Dialog::HandleHelp(){
if(HasHelp()){
// offer help on the dialog
}
else {
HelpHandler::HandleHelp();
}
}
在链的末端是Application的一个实例。该应用不是一个窗口组件,因此Application不是HelpHandler的直接子类。当一个帮助请求传递到这一层时,该应用可提供关于该应用的一般性的信息,或者它可以提供一系列不同的帮助主题。
class Application : public HelpHandler{
public:
Application(Topic t):HelpHandler(0, t){};
virtual void HandleHelp();
// application-specific operations ...
};
void Application:HandleHelp(){
// show a list of help topics
}
下面的代码创建并连接这些对象。此处的对话框涉及打印,因此这些对象被赋给与打印相关的主题。
const Topic PRINT_TOPIC = 1 ;
const Topic PAPER_ORIENTATION_TOPIC = 2;
const Topic APPLICATION_TOPIC = 3;
Application* application = new Application (APPLICATION_TOPIC);
Dialog* dialog = new Dialog(application,PRINT_TOPIC);
Button* button = new Button(dialog,PAPER_ORIEVTATION_TOPIC);
我们可对链上的任意对象调用HandleHelp以触发相应的帮助请求。要从按钮对象开始搜索,只需对它调用HandleHelp:
button->HardleHelp();
在这种情况下,按钮会立即处理该请求。注意任何HelpHandler类都可作为Dialog的后继者。此外,它的后继者可以被动态地改变。因此不管对话框被用在何处,你都可以得到它正确的与上下文相关的帮助信息。
10.相关模式
职责链常与Composite ( 4.3)一起使用。这种情况下,一个构件的父构件可作为它的后继。
5.2 COMMAND(命令)
1.意图
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
可以思考线程池的方式,让线程去执行job,调用一个约定好的执行方法,让每一个线程执行job中的执行方法即可达到复用效果。
2.别名
动作(Action)
事务(Transaction)
3.动机
有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。但工具箱不能显式的在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作。而工具箱的设计者无法知道请求的接受者或执行的操作。
命令模式通过将请求本身变成一个对象来使工具箱对象可向未指定的应用对象提出请求。这个对象可被存储并像其他的对象一样被传递。这一模式的关键是一个抽象的Command类,它定义了一个执行操作的接口。其最简单的形式是一个抽象的Execute操作。
具体的Command子类将接收者作为其一个实例变量,并实现Execute操作,指定接收者采取的动作。而接收者有执行该请求所需的具体信息。
用Command对象可很容易的实现菜单(Menu ),每一菜单中的选项都是一个菜单项(Menultem)类的实例。一个Application类创建这些菜单和它们的菜单项以及其余的用户界面。该Application类还跟踪用户已打开的Document对象。
该应用为每一个菜单项配置一个具体的Command子类的实例。当用户选择了一个菜单项时,该Menultem对象调用它的Command对象的Execute方法,而Execute执行相应操作。Menultem对象并不知道它们使用的是Command的哪一个子类。Command子类里存放着请求的接收者,而Excute操作将调用该接收者的一个或多个操作。
例如,PasteCommand支持从剪贴板向一个文档(Document)粘贴正文。PasteCommand的接收者是一个文档对象,该对象是实例化时提供的。Execute操作将调用该Document的Paste操作。
而OpenCommand的Execute操作却有所不同:它提示用户输人一个文档名,创建一个相应的文档对象,将其入作为接收者的应用对象中,并打开该文档。
有时一个Menultem需要执行一系列命令。例如,使一个页面按正常大小居中的Menultem可由一个CenterDocumentCommand对象和一个NormalSizeCommand对象构建。因为这种需将多条命令串接起来的情况很常见,我们定义一个MacroCommand类来让一个MenuItem执行任意数目的命令。MacroCommand是一个具体的Command子类,它执行一个命令序列。MacroCommand没有明确的接收者,而序列中的命令各自定义其接收者。
请注意这些例子中 Command模式是怎样解耦调用操作的对象和具有执行该操作所需信息的那个对象的。这使我们在设计用户界面时拥有很大的灵活性。一个应用如果想让一个菜单与一个按钮代表同一项功能,只需让它们共享相应具体Command子类的同一个实例即可。我们还可以动态地替换Command对象,这可用于实现上下文有关的菜单。我们也可通过将几个命令组成更大的命令的形式来支持命令脚本(command scripting)。所有这些之所以成为可能乃是因为提交一个请求的对象仅需知道如何提交它,而不需知道该请求将会被如何执行。
4.适用性
当你有如下需求时,可使用Command模式
-
像上面讨论的MenuItem对象那样,抽象出待执行的动作以参数化某对象。你可用过程语言中的回调(callback)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Command模式是回调机制的一个面向对象的替代品。
-
在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
-
支持取消操作。Command的Excute操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现重数不限的“取消”和“重做”。
-
支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Execute操作重新执行它们。
-
用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务(transaction)的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。
5.结构
6.参与者.
- Command
- 声明执行操作的接口。
- ConcreteCommand (PasteCommand,OpenCommand)
- 将一个接收者对象绑定于一个动作。
- 调用接收者相应的操作,以实现Execute。
- Client (Appliction)
- 创建一个具体命令对象并设定它的接收者。
- lnvoker (MenuItem)
- 要求该命令执行这个请求。
- Receiver (Document,Application)
- 知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。
7.协作
-
Client创建一个ConcreteCommand对象并指定它的Receiver对象。
-
某Invoker对象存储该ConcreteCommand对象。
-
该Invoker通过调用Command对象的Execute操作来提交一个请求。若该命令是可撤消的,ConcreteCommand就在执行Excute操作之前存储当前状态以用于取消该命令。
-
ConcreteCommand对象对调用它的Receiver的一些操作以执行该请求。
下图展示了这些对象之间的交互。它说明了Command是如何将调用者和接收者(以及它执行的请求)解耦的。
8.效果
Command模式有以下效果:
- Command模式将调用操作的对象与知道如何实现该操作的对象解耦。
- Command是头等的对象。它们可像其他的对象一样被操纵和扩展。
- 你可将多个命令装配成一个复合命令。例如是前面描述的MacroCommand类。一般说来,复合命令是Composite模式的一个实例。
- 增加新的Command很容易,因为这无需改变已有的类。
9.实现
实现Command模式时须考虑以下问题:
- 一个命令对象应达到何种智能程度
命令对象的能力可大可小。-一-个极端是它仅确定一个接收者和执行该请求的动作。另一极端是它自己实现所有功能,根本不需要额外的接收者对象。当需要定义与已有的类无关的命令,当没有合适的接收者,或当一个命令隐式地知道它的接收者时,可以使用后一极端方式。例如,创建另一个应用窗口的命令对象本身可能和任何其他的对象一样有能力创建该窗口。在这两个极端间的情况是命令对象有足够的信息可以动态的找到它们的接收者。
- 支持取消(undo)和重做(redo)
如果Command提供方法逆转(reverse)它们操作的执行(例如Unexecute或Undo操作),就可支持取消和重做功能。为达到这个目的,ConcreteCommand类可能需要存储额外的状态信息。这个状态包括:
- 接收者对象,它真正执行处理该请求的各操作。
- 接收者上执行操作的参数。
- 如果处理请求的操作会改变接收者对象中的某些值,那么这些值也必须先存储起来。接收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态。
若应用只支持一次取消操作,那么只需存储最近一次被执行的命令。而若要支持多级的取消和重做,就需要有一个已被执行命令的历史表列(history list),该表列的最大长度决定了取消和重做的级数。历史表列存储了已被执行的命令序列。向后遍历该表列并逆向执行(reverse-executing)命令是取消它们的结果;向前遍历并执行命令是重执行它们。
有时可能不得不将一个可撤消的命令在它可以被放入历史列表中之前先拷贝下来。这是因为执行原来的请求的命令对象将在稍后执行其他的请求。如果命令的状态在各次调用之间会发生变化,那就必须进行拷贝以区分相同命令的不同调用。
例如,一个删除选定对象的删除命令(DeleteCommand)在它每次被执行时,必须存储不同的对象集合。因此该删除命令对象在执行后必须被拷贝,并且将该拷贝放入历史表列中。如果该命令的状态在执行时从不改变,则不需要拷贝,而仅需将一个对该命令的引用放入历史表列中。在放入历史表列中之前必须被拷贝的那些Command起着原型(参见Prototype模式(3.4))的作用。
- 避免取消操作过程中的错误积累
在实现一个可靠的、能保持原先语义的取消/重做机制时,可能会遇到滞后影响问题。由于命令重复的执行、取消执行,和重执行的过程可能会积累错误,以至一个应用的状态最终偏离初始值。这就有必要在Command中存人更多的信息以保证这些对象可被精确地复原成它们的初始状态。这里可使用Memento模式(5.6)来让该Command访问这些信息而不暴露其他对象的内部信息。
- 使用C++模板
对1.不能被取消2.不需要参数的命令,我们可使用C++模板来实现,这样可以避免为每一种动作和接收者都创建一个Command子类。我们将在代码示例一节说明这种做法。
10.代码示例
此处所示的C++代码给出了动机一节中的Command类的实现的大致框架。我们将定义OpenCommand、PasteCommand和MacroCommand。首先是抽象的Command类:
// 行为对象
class Command{
public:
virtual ~Command();
// 执行
virtual void Execute() = 0;
protected:
Command();
};
OpenCommand打开一个名字由用户指定的文档。注意OpenCommand的构造器需要一个Application对象作为参数。AskUser是一个提示用户输入要打开的文档名的实现例程。
class Opencommand:public Command {
public:
OpenCommand(Application*);
virtual void Execute();
protected:
virtual const char* AskUser () ;
private:
Application* _application;
char* _response;
};
OpenCommand::OpenCommand(Application* a){
_application = a;
}
void OpenCommand::Execute(){
const char* name = AskUser();
if(name != 0){
Document document = new Document(name);
_application->Add(document);
document->open() ;
}
}
PasteCommand需要一个Document对象作为其接收者。该接收者将作为一个参数给PasteCommand的构造器。
class PasteCommand:public Command{
public:
PasteCommand(Docunent*);
virtual void Excecute();
private:
Document* _document;
};
PasteCommand::PasteCommand(Document* doc){
_document = doc;
}
void PasteCommand::Excecute(){
_document->Paste();
}
对于简单的不能取消和不需参数的命令,可以用一个类模板来参数化该命令的接收者。我们将为这些命令定义一个模板子类SimpleCommand
用Receiver类型参数化SimpleCommand,并维护一个接收者对象和一个动作之间的绑定,而这一动作是用指向一个成员函数的指针存储的。
template <class Receiver>
class SimpleCommand:public Command{
public:
typedef void (Receiver::* Action)();
SimpleCommand(Receiver* r,Action a):_receiver(r),_action(a){}
virtual void Execute();
private:
Action _action;
Receiver* _receiver;
};
构造器存储接收者和对应实例变量中的动作。Execute操作实施接收者的这个动作。
template <class Receiver>
void SimpleCommand<Receiver>::Excecute{
(_receiver->*_action)();
}
为创建一个调用Myclass类的一个实例上的Action的Command对象,仅需如下代码:
MyClass* receiver = new Myc1ass;
// ...
Command* aCommand = new SimpleCommand<MyClass>(receiver,&Myclass::Action);
// ...
acomnand->Execute():
记住,这一方案仅适用于简单命令。更复杂的命令不仅要维护它们的接收者,而且还要登记参数,有时还要保存用于取消操作的状态。此时就需要定义一个Command的子类。
MacroCommand管理一个子命令序列,它提供了增加和删除子命令的操作。这里不需要显式的接收者,因为这些子命令已经定义了它们各自的接收者。
class Macrocommand : public Command{
public:
Macrocommand();
virtual ~MacroCommand();
virtual void Add(comnand*);
virtual void Remove(Command*);
virtual void Execute();
private:
List<Command*>*_cmds;
};
MacroCommand的关键是它的Execute成员函数。它遍历所有的子命令并调用其各自的Execute操作。
void MacroCommand::Execute(){
ListIterator<Command*> i( _cmds);
for(i.First ();!i.IsDone();i.Next()){
Command* c = i.CurrentItem() ;
c->Execute();
}
}
注意,如果MacroCommand实现取消操作,那么它的子命令必须以相对于Execute的实现相反的顺序执行各子命令的取消操作。
最后,MacroCommand必须提供管理它的子命令的操作。MacroCommand也负责删除它的子命令。
void MacroCommand::Add(Command* c){
_cmds->Append(c) ;
}
void MacroCommand::Remove(Command* c){
_cmds->Remove(c);
}
11.相关模式
Composite模式(4.3)可被用来实现宏命令。
Memento模式(5.6)可用来保持某个状态,命令用这一状态来取消它的效果。在被放入历史表列前必须被拷贝的命令起到一种原型(3.4)的作用。
5.3 INTERPRETER(解释器)
1.意图
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
本节与编译原理相关。
简单的比如算法中的,去计算一个字符的数学表达式的值(比如:“1 + 2 + 3” )。
复杂的就是一门语言的解释器。
2.动机
如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。
这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
例如,搜索匹配一个模式的字符串是一个常见问题。正则表达式是描述字符串模式的一种标准语言。与其为每一个的模式都构造一个特定的算法,不如使用一种通用的搜索算法来解释执行一个正则表达式,该正则表达式定义了待匹配字符串的集合。
解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。
可参考-编译原理——正规表达式与有限自动机(笔记)
在上面的例子中,本设计模式描述了如何为正则表达式定义一个文法,如何表示一个特定的正则表达式,以及如何解释这个正则表达式。
考虑以下文法定义正则表达式:
符号expression是开始符号, literal是定义简单字的终结符。
解释器模式使用类来表示每一条文法规则。在规则右边的符号是这些类的实例变量。
上面的文法用五个类表示:
一个抽象类
- RegularExpression
它的四个子类
- LiteralExpression
- AlternationExpression
- SequenceExpression
- RepetitionExpression
后三个类定义的变量代表子表达式。
每个用这个文法定义的正则表达式都被表示为一个由这些类的实例构成的抽象语法树。
例如,抽象语法树:
表示正则表达式:
raining & (dogs ! cats) *
如果我们为RegularExpression的每一子类都定义解释(Interpret)操作,那么就得到了为这些正则表达式的一个解释器。解释器将该表达式的上下文做为一个参数。上下文包含输入字符串和关于目前它已有多少已经被匹配等信息。为匹配输入字符串的下一部分,每一RegularExpression的子类都在当前上下文的基础上实现解释操作(Interpret)。
例如,
- LiteralExpression将检查输人是否匹配它定义的字(literal)。
- AlternationExpression将检查输入是否匹配它的任意一个选择项。
- RepetitionExpression将检查输入是否含有多个它所重复的表达式。
- 等等。
3.适用性
当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:
- 该文法简单对于复杂的文法,文法的类层次变得庞大而无法管理。此时语法分析程序生成器这样的工具是更好的选择。它们无需构建抽象语法树即可解释表达式,这样可以节省空间而且还可能节省时间。
- 效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的,而是首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种情况下,转换器仍可用解释器模式实现,该模式仍是有用的。
4.结构
5.参与者
-
AbstractExpression (抽象表达式,如RegularExpression)
- 声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享。
-
TerminalExpression (终结符表达式,如LiteralExpression)
- 实现与文法中的终结符相关联的解释操作。
- 一个句子中的每个终结符需要该类的个实例。
-
NonterminalExpression(非终结符表达式,如AlternationExpression,Repetition-Expression, SequenceExpressions)
- 对文法中的每一条规则R::=R,R.,…R,都需要一个NonterminalExpression类。
- 为从R,到R_的每个符号都维护一个AbstractExpression类型的实例变量。
- 为文法中的非终结符实现解释(Interpret)操作。解释(Interpret)一般要递归地调用表示R,到R的那些对象的解释操作。
-
Context ( 上下文)
- 包含解释器之外的一些全局信息。
-
Client(客户)
- 构建(或被给定)表示该文法定义的语言中一个特定的句子的抽象语法树。该抽象语法树由NonterminalExpression和TerminalExpression的实例装配而成。
- 调用解释操作。
6.协作
- Client构建(或被给定)一个句子,它是NonterminalExpression和TerminalExpression的实例的一个抽象语法树.然后初始化上下文并调用解释操作。
- 每一非终结符表达式节点定义相应子表达式的解释操作。而各终结符表达式的解释操作构成了递归的基础。
- 每一节点的解释操作用上下文来存储和访问解释器的状态。
7.效果
解释器模式有下列的优点和不足:
- 易于改变和扩展文法
因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。已有的表达式可被增量式地改变,而新的表达式可定义为旧表达式的变体。
- 也易于实现文法
定义抽象语法树中各个节点的类的实现大体类似。这些类易于直接编写,通常它们也可用一个编译器或语法分析程序生成器自动生成。
- 复杂的文法难以维护
解释器模式为文法中的每一条规则至少定义了一个类(使用BNF定义的文法规则需要更多的类)。因此包含许多规则的文法可能难以管理和维护。可应用其他的设计模式来缓解这一问题。
但当文法非常复杂时,其他的技术如语法分析程序或编译器生成器更为合适。
- 增加了新的解释表达式的方式
解释器模式使得实现新表达式“计算”变得容易。例如,你可以在表达式类上定义一个新的操作以支持优美打印或表达式的类型检查。如果你经常创建新的解释表达式的方式,那么可以考虑使用Visitor(5.11)模式以避免修改这些代表文法的类。
8.实现
Interpreter和Composite ( 4.3)模式在实现上有许多相通的地方。下面是nterpreter所要考虑的一些特殊问题:
- 创建抽象语法树
解释器模式并未解释如何创建一个抽象的语法树。换言之,它不涉及语法分析。抽象语法树可用一个表驱动的语法分析程序来生成,也可用手写的(通常为递归下降法)语法分析程序创建,或直接由Client提供。
- 定义解释操作
并不一定要在表达式类中定义解释操作。如果经常要创建一种新的解释器,那么使用Visitor (5.11 )模式将解释放入一个独立的“访问者”对象更好一些。例如,一个程序设计语言的会有许多在抽象语法树上的操作,比如类型检查、优化、代码生成,等等。恰当的做法是使用一个访问者以避免在每一个类上都定义这些操作。
- 与Flyweight模式共享终结符
在一些文法中,一个句子可能多次出现同一个终结符。此时最好共享那个符号的单个拷贝。计算机程序的文法是很好的例子——每个程序变量在整个代码中将会出现多次。在动机一节的例子中,一个句子中终结符dog(由LiteralExpression类描述)也可出现多次。
终结节点通常不存储关于它们在抽象语法树中位置的信息。在解释过程中,任何它们所需要的上下文信息都由父节点传递给它们。因此在共享的(内部的)状态和传入的(外部的)状态区分得很明确,这就用到了Flyweight ( 4.6)模式。
例如,dog LiteralExpression的每一实例接收一个包含目前已匹配子串信息的上下文。且每一个这样的LiteralExpression在它的解释操作中做同样一件事(它检查输入的下一部分是否包含一个dog)无论该实例出现在语法树的哪个位置。
9.代码实例
略,可自行翻看。
10.相关模式
Composite模式(4.3):抽象语法树是一个复合模式的实例。
Flyweight模式(4.6):说明了如何在抽象语法树中共享终结符。
Iterator(5.4):解释器可用一个迭代器遍历该结构。
Visitor(5.11):可用来在一个类中维护抽象语法树中的各节点的行为。
5.4 ITERATOR(迭代器)
1.意图
提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
2.别名
游标( Cursor )。
3.动机
一个聚合对象,如列表(list),应该提供一种方法来让别人可以访问它的元素,而又不需暴露它的内部结构.此外,针对不同的需要,可能要以不同的方式遍历这个列表。
但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有时还可能需要在同一个表列上同时进行多个遍历。
迭代器模式都可帮你解决所有这些问题。这一模式的关键思想是将对列表的访问和遍历从列表对象中分离出来并放入一个迭代器((iterator)对象中。迭代器类定义了一个访问该列表元素的接口。迭代器对象负责跟踪当前的元素;即,它知道哪些元素已经遍历过了。
例如,一个列表(List)类可能需要一个列表迭代器(ListIterator ),它们之间的关系如下图:
在实例化列表迭代器之前,必须提供待遍历的列表。一旦有了该列表迭代器的实例,就可以顺序地访问该列表的各个元素。CurrentItem操作返回表列中的当前元素, First操作初始化迭代器,使当前元素指向列表的第一个元素,Next操作将当前元素指针向前推进一步,指向下一个元素,而IsDone检查是否已越过最后一个元素,也就是完成了这次遍历。
将遍历机制与列表对象分离使我们可以定义不同的迭代器来实现不同的遍历策略,而无需在列表接口中列举它们。例如,过滤表列迭代器(FilteringListIterator)可能只访问那些满足特定过滤约束条件的元素。
注意迭代器和列表是耦合在一起的,而且客户对象必须知道遍历的是一个列表而不是其他聚合结构。最好能有一种办法使得不需改变客户代码即可改变该聚合类。可以通过将迭代器的概念推广到多态迭代(polymorphic iteration)来达到这个目标。
例如,假定我们还有一个列表的特殊实现,比如说SkipList。SkipList是一种具有类似于平衡树性质的随机数据结构。我们希望我们的代码对List和SkipList对象都适用。
首先,定义一个抽象列表类AbstractList,它提供操作列表的公共接口。类似地,我们也需要一个抽象的迭代器类Iterator,它定义公共的迭代接口。然后我们可以为每个不同的列表实现定义具体的Iterator子类。这样迭代机制就与具体的聚合类无关了。
余下的问题是如何创建迭代器。既然要使这些代码不依赖于具体的列表子类,就不能仅仅简单地实例化一个特定的类,而要让列表对象负责创建相应的迭代器。这需要列表对象提供CreateIterator这样的操作,客户请求调用该操作以获得一个迭代器对象。
创建迭代器是一个Factory Method模式(3.3)的例子。我们在这里用它来使得一个客户可向一个列表对象请求合适的迭代器。Factory Method模式产生两个类层次,一个是列表的,一个是迭代器的。CreateIterator“联系”这两个类层次。
4.适用性
迭代器模式可用来:
- 访问 一个聚合对象的内容而无需暴露它的内部表示。
- 支持对聚合对象的多种遍历。
- 为遍历不同的聚合结构提供一个统一的接口(即,支持多态迭代)。5.结构
5.结构
6.参与者
- Iterator (迭代器)
- 迭代器定义访问和遍历元素的接口。
- Concretelterator (具体迭代器)
- 具体迭代器实现迭代器接口。
- 对该聚合遍历时跟踪当前位置。
- Aggregate (聚合)
- 聚合定义创建相应迭代器对象的接口。
- ConcreteAggregate (具体聚合)
- 具体聚合实现创建相应迭代器的接口,该操作返回ConcreteIterator的
- 一个适当的实例。
7.协作
- ConcreteIterator跟踪聚合中的当前对象,并能够计算出待遍历的后继对象。
8.效果
迭代器模式有三个重要的作用:
- 它支持以不同的方式遍历一个聚合
复杂的聚合可用多种方式进行遍历。例如,代码生成和语义检查要遍历语法分析树。代码生成可以按中序或者按前序来遍历语法分析树。
迭代器模式使得改变遍历算法变得很容易:仅需用一个不同的迭代器的实例代替原先的实例即可。你也可以自己定义迭代器的子类以支持新的遍历。
- 迭代器简化了聚合的接口
有了迭代器的遍历接口,聚合本身就不再需要类似的遍历接口了。这样就简化了聚合的接口。
- 在同一个聚合上可以有多个遍历
每个迭代器保持它自己的遍历状态。因此你可以同时进行多个遍历。
9.实现
迭代器在实现上有许多变化和选择。下 面是一- 些较重要的实现。实现迭代器模式时常常
需要根据所使用的语言提供的控制结构来进行权衡。一些语言(例如, CLU[LG86])甚至直接支
持这一模式。
- 谁控制该迭代
一个基本的问题是决定由哪-方来控制该迭代,是迭代器还是使用该迭代器的客户。当由客户来控制迭代时,该迭代器称为一个外部迭代器(external iterator), 而当由迭代器控制迭代时,该迭代器称为一个内部迭代器(internal iterator)。使用外部迭代器的客户必须主动推进遍历的步伐,显式地向迭代器请求下一个元素。相反地,若使用内部迭代器,客户只需向其提交一个待执行的操作,而迭代器将对聚合中的每一个元素实施该操作。
外部迭代器比内部迭代器更灵活。例如,若要比较两个集合是否相等,这个功能很容易用外部迭代器实现,而几乎无法用内部迭代器实现。在象C++这样不提供匿名函数、闭包,或象Smalltalk和CLOS这样不提供连续(continuation)的语言中,内部迭代器的弱点更为明显。但另一方 面,内部迭代器的使用较为容易,因为它们已经定义好了迭代逻辑。
- 谁定义遍历算法
迭代器不是唯一可定义遍历算法的地方。聚合本身也可以定义遍历算法,并在遍历过程中用迭代器来存储当前迭代的状态。我们称这种迭代器为一个游标(cursor),因为它仅用来指示当前位置。客户会以这个游标为一个参数调用该聚合的Next操作,而Next操作将改变这个指示器的状态。
如果迭代器负责遍历算法,那么将易于在相同的聚合上使用不同的迭代算法,同时也易于在不同的聚合上重用相同的算法。从另一方面说,遍历算法可能需要访问聚合的私有变量。如果这样,将遍历算法放入迭代器中会破坏聚合的封装性。
- 迭代器健壮程度如何
在遍历一个聚合的同时更改这个聚合可能是危险的。如果在遍历聚合的时候增加或删除该聚合元素,可能会导致两次访问同一个元素或者遗漏掉某个元素。一个简单的解决办法是拷贝该聚合,并对该拷贝实施遍历,但一 般来说这样做代价太高。
**一个健壮的迭代器(robust iterator)**保证插人和删除操作不会干扰遍历,且不需拷贝该聚合。
有许多方法来实现健壮的迭代器。其中大多数需要向这个聚合注册该迭代器。当插人或删除
元素时,该聚合要么调整迭代器的内部状态,要么在内部的维护额外的信息以保证正确的遍
历。
Kofler在ET++中对如何实现健壮的迭代器做了很充分的讨论。Murray讨论了如何
为USL StandardComponents列表类实现健壮的迭代器。
4. 附加的选代器操作
迭代器的最小接口由First、Next、IsDone和CurrentItem噪作组成。
其他一些操作可能也很有用。例如,对有序的聚合可用-个Previous操作将迭代器定位到前一
个元素。SkipTo操作用于已排序并做了索引的聚合中,它将迭代器定位到符合指定条件的元
素对象.上。
- 在C++中使用多态的迭代器
使用多态 迭代器是有代价的。它们要求用一一个Factory Method动态的分配迭代器对象。因此仅当必须多态时才使用它们。否则使用在栈中分配内存的具体的迭代器。
多态迭代器有另一个缺点:客户必须负责删除它们。这容易导致错误,因为你容易忘记释放一个使用堆分配的迭代器对象,当一个操作有多个出口时尤其如此。而且其间如果有异常被触发的话,迭代器对象将永远不会被释放。
Proxy (4.4)模式提供了一个补救方法。我们可使用一个栈分配的Proxy作为实际迭代器的中间代理。该代理在其析构器中删除该迭代器。这样当该代理生命周期结束时,实际迭代器将同它一起被释放。即使是在发生异常时,该代理机制能保证正确地清除迭代器对象。这就是著名的C++.“资源分配即初始化”技术[ES90]的-个应用。下面的代码示例给出了一个例子。
- 选代器可有特权访问.
迭代器可被看为创建它的聚合的-一个扩展。迭代器和聚合紧密耦合。在C++中我们可让迭代器作为它的聚合的一个友元(friend)来表示这种紧密的关系。这样你就不需要在聚合类中定义一些仅为迭代器 所使用的操作。
但是,这样的特权访问可能使定义新的遍历变得很难,因为它将要求改变该聚合的接口增加另一个友元。为避免这一问题,迭代器类可包含一些protected操作来访问聚合类的重要的非公共可见的成员。迭代器子类(且只有迭代器子类)可使用这些protected操作来得到对该聚合的特权访问。
- 用于复合对象的选代器
在Composite(4.3)模式中的那些递归聚 合结构上,外部迭代器可能难以实现,因为在该结构中不同对象处于嵌套聚合的多个不同层次,因此一个外部迭代器为跟踪当前的对象必须存储一条纵贯该Composite的路径。有时使用一个内部迭代器会更容易一些。它仅需递归地调用自己即可,这样就隐式地将路径存储在调用栈中,而无需显式地维护当前对象位置。
如果复合中的节点有一个接口可以从一个节点移到它的兄弟节点、父节点和子节点,那么基于游标的迭代器是个更好的选择。游标只需跟踪当前的节点;它可依赖这种节点接口来遍历该复合对象。复合常常需要用多种方法遍历。前序,后序,中序以及广度优先遍历都是常用的。你可用不同的迭代器类来支持不同的遍历。
- 空迭代器
一个空迭代器(NulIterator)是一个退化的迭代器,它有助于处理边界条件。根据定义,一个NulIterator总是已经完成了遍历:即,它的IsDone操作总是返回true。空迭代器使得更容易遍历树形结构的聚合(如复合对象)。在遍历过程中的每一节点,都可向当前的元素请求遍历其各个子结点的迭代器。该聚合元素将返回一个具体的迭代器。但叶节点元素返回NullIterator的一个实例。这就使我们可以用一种统一的方式实现在整个结构上的遍历。
10.代码示例
我们将看看一个简单List类的实现,它是我们的基础库(附录C)的一部分。我们将给出两个迭代器的实现,一个以从前到后的次序遍历该表列,而另一个以从后到前的次序遍历(基础库只.支持第一种)。然后我们说明如何使用这些迭代器,以及如何避免限定于一种特定的实现。在此之后,我们将改变原来的设计以保证迭代器被正确的删除。最后-个例子示例一个内部迭代器并与其相应的外部迭代器进行比较。
- 列表和迭代器接口
首先让我们看与实现迭代器相关的部分List接口。
template <class Item>
class List {
pub1ic:
List(long size = DEFAULT_ LIST_ CAPACITY);
long Count() const;
Item& Get (long index) const ;
};
该List类通过它的公共接口提供了一个合理的有效的途径以支持迭代。它足以实现这两种遍历。因此没有必再要给迭代器对底层数据结构的访问特权,也就是说,迭代器类不是列表的友元。为确保对不同遍历的透明使用,我们定义一个抽象的迭代器类,它定义了迭代器接口。
temp1ate <class Item>
class Iterator {
public:
virtual void First() =0;
virtual void Next() = 0;
virtual bool IsDone() const = 0;
virtual Item CurrentItem() const = 0;
protected:
Iterator();
};
2)迭代器子类的实现列表迭代器是迭代器的一个子类。
template <class Item>
class ListIterator : public Iterator<Item> {
public:
ListIterator (const List<Item>* aList);
virtual void First();
virtual void Next ();
virtual bool IsDone() const;
virtual Item CurrentItem() const;
private:
const List<Item> _list;
long _current;
);
ListIterator的实现简单直接。它存储List和列表当前位置的索引current。
template <class Item>
ListIterator<Item>::ListIterator (
const List<Item>* aList
):_1ist(aList), _current(0){}
First将迭代器置于第一个元素:
template <class Item>
void ListIterator<Item>::First () { .
_current = 0;
}
Next使当前元素向前推进一步:
template <class Item>
void ListIterator<Item>::Next () {
_current++;
}
IsDone检查指向当前元素的索引是否超出了列表:
template <class Item>
bool ListIterator<Item>::IeDone () const (
return _current >= _list->Count();
}
最后,CurrentItem返回当前索引指向的元素。若迭代已经终止,则抛出一个Iterator OutOfBounds异常:
template <class Item>
Item ListIteratorcItem>::CurrentItem () const {
if(IsDone())
throw IteratorOutofBounds ;
return _list->Get(_current);
}
ReverseListIterator 的实现是几乎是一样的, 只不过它的First操作将_current置于列表的末尾,而Next操作将_ current减一,向表头的方向前进一步。
- 使用迭代器
假定有一个雇 员( Employee )对象的List,而我们想打印出列表包含的所有雇员的信息。Employee类用一个Print操作来打印本身的信息。为打印这个列表,我们定义一个PrintEmployee操作,此操作以一个迭代器为参数,并使用该迭代器遍历和打印这个列表:
void PrintEmployees (Iterat or<Employee*>& i) {
for(i.First(); i.IgDone(); i.Next()){
i.CurrentItem()->Print();
}
}
前面我们已经实现了从后向前和从前向后两种遍历的迭代器,我们可用这个操作以两种次
序打印雇员信息:
List<Bmp1oyee*>* employees;
ListIterator<Employee*> forward (employees);
ReverseListIterator<Employo> backward (employees) :
Print Employees (forward);
PrintEmployees (backward) ;
- 避免限定于一种特定的列表实现
考虑一 个List的变体skiplist会对迭代代码产生什么影响。List的SkipList子类必须提供一个实现Iterator接口的相应的迭代器SkipListerator。在内部,为了进行高效的迭代,SkipListerator必须保持多 个索引。既然SkipListIterator实现了Iterator, PrintEmployee操作 也可用于用SkipList存储的雇员列表。
SkipList<Employee*>+ employees; .
//...
skipListIterator<Employee*> iterator (employees);
PrintEmployees(iterator);.
尽管这种方法是可行的,但最好能够无需明确指定具体的List实现(此处即为SkipList )。为此可以引入一个AbstractList类,它为不同的列表实现给出一一个标准接口。List和SkipList成为AbstractList的子类。为支持多态迭代,AbstractList定 义一个Factory Method,称为CreateIterator。各个列表子类重定义这个方法以返回相应的迭代器。
template <class Item>
class AbstractList{
public:
virtual Iterator<Item>* CreateIterator() const = 0;
};
另一个办法是定义一-个一般的mixin类Traversable,它定义- -个用于创建迭代器接口。聚合类通过混人(继承) Traversable来支持多态迭代。List重定义CreateIterator,返回一个Listiterator对象:
template <class Item>
Iterator<Item> List<Item>::CreateIterator () const {
return new ListIterator<Item> (this);
}
现在我们可以写出不依赖于具体列表表示的打印雇员信息的代码。
// we know only that we have an AbstractList
AbstractList<Employee> employees;
// ...
Iterator <Employee*>* iterator = employees->CreateIterator();
PrintEmployees(*iterator);
delete iterator;
- 保证透代器被删除
注意CreateCreateIterator返回的是一个动态分配的迭代器对象。在使用完毕后,必须删除这个迭代器,否则会造成内存泄漏。为方便客户,我们提供一个IteratorPtr作为迭代器的代理,这个机制可以保证在Iterator对象离开 作用域时清除它。
IteratorPtr总是在栈上分配9。C++自动调用它的析构器,而该析构器将删除真正的迭代器。IteratorPtr重载了操作符“->” 和“*", 使得可将IteratorPtr用作- -个指向迭代器的指针。IteratorPtr的成员都实现为内联的,这样它们不会产生任何额外开销。
template <class' Item>
class IteratorPtr {
public:
IteratorPtr (Iterator<Item>* i):_i(i) { }
~IteratorPtr() { delete _i; }
Iterator<Item>* operatox->() { return _i; }
Iterator<Item>& operator*() { return *_i; }
private:
// disal1ow copy and assignment to avoid
// mu1tiple deletions of i:
IteratorPtr (const Iteratorptr&) ;
IteratorPtre operator = (const Iteratorptr&);
private:
Iterator<Item>*_ 1;
}
IteratorPtr简化 了打印代码:
AbatractList <Bmp1oyee*>* employees;
// ...
IteratorPtr<Bmployee*> iterator (employes->CreateIterator());
PrintEmployees(*iterator);
- 一个内部的Listerator
最后 ,让我们看看一个内部的或被动的Listeratr类是怎么实现的。此时由迭代器来控制迭代,并对列表中的每一个元素施行同一个操作。
问题是如何实现一个抽象的迭代器,可以支持不同的作用于列表各个元素的操作。有些语言支持所谓的匿名函数或闭包,使用这些机制可以较方便地实现抽象的迭代器。但是C++并不支持这些机制。此时,至少有两种办法可供选择:
(1)给迭代器传递一个函数指针(全局的或静态的)。
(2)依赖于子类生成。在第一种情况下,迭代器在迭代过程中的每-步调用传递给它的操作,在第二种情况下,迭代器调用子类重定义了的操作以实现-个特定的行为。
这两种选择都不是尽善尽美。常常需要在迭代时累积(accumulate)状态,而用函数来实现这个功能并不太适合;因为我们将不得不使用静态变量来记住这个状态。Iterator子类 给我们提供了一个方便的存储累积状态的地方,比如存放在一个实例变量中。但为每一个不同的遍 历创建一个子类需要做更多的工作。
下面是第二种实现办法的一个大体框架,它利用了子类生成。这里我们称内部迭代器为一个ListTraverser。
template <class Item>
c1ass ListTraverser{
public:
ListTraverser (List<Item>* aList);
bool Traverse():
protected:
virtual bool ProcessItem(const Item) = 0;
private:
ListIterator<Item> _iterator;
};
ListTraverser以一个List实例为参数。在内部,它使用-个外部ListIterator进行遍历。
Traverse启动遍历并对每一元素项调用Processtem操作。内部迭代器可在某次ProcessItem操作返回false时提前终止本次遍历。而Traverse返 回-个布尔值指示本次遍历是否提前终止。
template <c1ass Item>
ListTraverser<Item>::ListTraverser(
List<Item>* aList
):_iterator (aList) { )
template <c1ass Item>
bool LiatTraverser<Item>::Traverse(){
bool result = false;
for (
_iterator.Firet();
!_iterator. IsDone();
_iterator .Next()
){
result = ProcessItem (_iterator.CurrentItem());
if (result == false) {
break;
}
return result;
}
让我们使用一个ListTraverser来打印雇员列表中的头10个雇员。为达到这个目的,必须定义一个ListTraverser的子类并重定义其Processtem操作。我们用一-个.count实例变量中对已打印的雇员进行计数。
class PrintNEmployees : public ListTraverser<Employee*> {
public:
PrintNEmployees (List<Employee aList,int n) :
ListTraverser<Emp1oyee*>(aList),
_total(n), _count(0) { }
protected:
bool procesaItem (Employee* const&);
private:
int _tota1;
int _count;
}
bool PrintNEmployees::ProcessItem (Employee* const& e) {
_count++:
e->Print();
return _count < _total;
}
下面是PrintNEmployees怎样打印列表中的头10个雇员的代码:
ListcEmployee*>* employees;
PrintNEmployees pa(employee, 10);
pa.Traverse();
注意这里客户不需要说明如何进行迭代循环。整个迭代逻辑可以重用。这是内部迭代器的主要优点。但其实现比外部迭代器要复杂一些, 因为必须定义-一个新的类。与 使用外部迭代器比较:
ListIterator<Employee*> i(employes);
int count = 0;
for (i.First(): i.IsDone(); i.Next()){
count++;
i.CurrentItem()->Print();
if (count >= 10) {
break;
}
}
内部迭代器可以封装不同类型的迭代。例如, FilteringListTraverser封装的迭代仅处理能通过测试的那些列表元素:
template <class Item>
class FilteringListTraverser (
public:
FilteringListTraverser (List<Item>* aList);
bool Traverse();
protected:
virtual bool ProcessItem(const Item&) = 0;
virtual bool TestItem(const Item&) = 0;
private:
ListIterator<Item> _iterator;
};
这个类接口除了增加了用于测试的成员函数TestItem外与ListTraverser相同,它的子类将重定义TestItem以指定所需的测试。Traverse根据测试的结果决定是否越过当前元素继续遍历:
template <class Item>
void FilteringListTraverser<Item>::Traverse () {
bool result = false;
for(
_iterator.First();
_ iterator.IsDone();
_iterator.Next ()
){
if (TestItem( _iterator.CurrentItem())) {
result = ProcessItem(_iterator.CurrentItem());
if (result == false) {
break;
}
return result;
}
这个类的一中变体是让Traverse返回值指示是否至少有一个元素通过测试。
11.相关模式
Composite(4.3):迭代器常被应用到象复合这样的递归结构上。
Factory Method(3.3):多态迭代器靠Factory Method来例化适当的迭代器子类。
Memento(5.6):常与迭代器模式一起使用。 迭代器可使用一个memento来捕获一个迭代的状态。迭代器在其内部存储memento。
资料
[1]《设计模式:可复用面向对象软件的基础》(美) Erich Gamma Richard Helm、Ralph Johnson John Vlissides 著 ; 李英军、马晓星、蔡敏、刘建中 等译; 吕建 审校