文章目录
- 五、行为模式
- 5.5 MEDIATOR(中介者)
- 1.意图
- 补充部分
- 2.动机
- 3.适用性
- 4.结构
- 5.参与者
- 6.协作
- 7.效果
- 8.实现
- 9.代码示例
- 10.相关模式
- 5.6 MEMENTO ( 备忘录)
- 1.意图
- 2.别名
- 3.动机
- 4.适用性
- 5.结构
- 6.参与者
- 7.协作
- 8.效果
- 9.实现
- 10.代码示例
- 11.相关模式
- 5.7 OBSERVER (观察者)
- 1.意图
- 2.别名
- 3.动机
- 4.适用性
- 5.结构
- 6.参与者
- 7.协作
- 8.效果
- 9.实现
- 10.代码示例
- 11.相关模式
- 5.8 STATE (状态)
- 1.意图
- 2.别名
- 3.动机
- 4.适用性
- 5.结构
- 6.参与者
- 7.协作
- 8.效果
- 9.实现
- 10.代码示例
- 11.相关模式
- 资料
五、行为模式
5.5 MEDIATOR(中介者)
1.意图
用一个中介对象来封装一系列的对象交互。 中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
封装需要相互调用的部分,避免彼此之间显示相互依赖。它使得编译时不相互依赖,而改为运行时依赖。
补充部分
可以理解为将如下逻辑
改为了
2.动机
面向对象设计鼓励将行为分布到各个对象中。这种分布可能会导致对象间有许多连接。在最坏的情况下,每一个对象都知道其他所有 对象。虽然将一个系统分割成许多对象通常可以增强可复用性,但是对象间相互连接的激增又会降低其可复用性。大量的相互连接使得一个对象似乎不太可能在没有其他对象的支持下工作——系统表现为一个不可分割的整体。而且,对系统的行为进行任何较大的改动都十分困难,因为行为被分布在许多对象中。结果是,你可能不得不定义很多子类以定制系统的行为。
例如,考虑一个图形用户界面中对话框的实现。对话框使用一个窗口来展现一系列的窗口组件,如按钮、菜单和输人域等,如下图所示。
通常对话框中的窗口组件间存在依赖关系。例如,当一个特定的输入域为空时,某个按钮不能使用;在称为列表框的一列选项中选择一个表目可能会改变一个输人域的内容;反过来,在输人域中输人正文可能会自动的选择一个或多个列表框中相应的表目;一旦正文出现在输人域中,其他一些按钮可能就变得能够使用了,这些按钮允许用户做一些操作, 比如改变或删除这些正文所指的东西。
不同的对话框会有不同的窗口组件间的依赖关系。因此即使对话框显示相同类型的窗口组件,也不能简单地直接重用已有的窗口组件类;而必须定制它们以反映特定对话框的依赖关系。由于涉及很多个类,用逐个生成子类的办法来定制它们会很冗长。
可以通过将集体行为封装在一个单独的中介者(mediator)对象中以避免这个问题。中介者负责控制和协调一组对象间的交互。中介者充当一个中介以使组中的对象不再相互显式引用。
这些对象仅知道中介者,从而减少了相互连接的数目。
例如,FontDialogDirector可 作为一个对话框中的窗口组件间的中介者。FontDialogDirector对象知道对话框中的各窗口组件,并协调它们之间的交互。它充当窗口组件间通信的中转中心,如下图所示。
下面的交互图说明了各对象如何协作处理一个列表框中选项的变化。
下面一系列事件使一个列表框的选择被传送给一个输人域:
1)列表框告诉它的操作者它被改变了。
2)导控者从列表框中得到选中的选择项。
3)导控者将该选择项传递给入口域。
4)现在人口域已有正文,导控者使得用于发起一个动作(如“半黑体” ,“斜体”)的某个(某些)按钮可用。
注意导控者是如何在对话框和人口域间进行中介的。窗口组件间的通信都通过导控者间接地进行。它们不必互相知道;它们仅需知道导控者。而且,由于所有这些行为都局部于一个类中,只要扩展或替换这个类,就可以改变和替换这些行为。
这里展示的是FontDialogDirector抽象怎样被集成到一个类库中,如下图所示。
DialogDirector是一个抽象类,它定义了一个对话框的总体行为。客户调用ShowDialog操作将对话框显示在屏幕上。CreateWidgets是创建-一个对话框的窗口组件的抽象操作。
WidgetChanged是另一个抽象操作;窗口组件调用它来通知它的导控者它们被改变了。
DialogDirector的子类将重定义CreateWidgets以创建正确的窗口组件,并重定义WidgetChanged
以处理其变化。
3.适用性
在下列情况下使用中介者模式:
- 一组对象以定义良好但是复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理解。
- 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。
- 想定制一个分布在多个类中的行为,而又不想生成太多的子类。
4.结构
5.参与者
- Mediator(中介者,如DialogDirector)
- 中介者定义一个接口用于与各同事( Colleague )对象通信。
- ConcreteMediator(具体中介者,如FontDialogDirector)
- 具体中介者通过协调各同事对象实现协作行为。
- 了解并维护它的各个同事。
- Colleague class(同事类,如ListBox, EntryField)
- 每一个同事类都知道它的中介者对象。
- 每一个同事对象在需与其他的同事通信的时候,与它的中介者通信。
6.协作
- 同事向一个中介者对象发送和接收请求。中介者在各同事间适当地转发请求以实现协作行为。
7.效果
中介者模式有以下优点和缺点:
- 减少了子类生成
Mediator将原本分布于多个对象间的行为集中在一起。改变这些行为只需生成Meditator的子类即可。这样各个Colleague类可被重用。
- 它将各Colleague解耦
Mediator有 利于各Colleague间的松耦合.你可以独立的改变和复用各Colleague类和Mediator类。
- 它简化了对象协议
用Mediator和各Colleague间的- -对多的交互来代替多对多的交互。一对多的关系更易于理解、维护和扩展。
- 它对对象如何协作进行了抽象
将中介作为一个独立的概念并将其封装在一一个对象中, 使你将注意力从对象各自本身的行为转移到它们之间的交互上来。这有助于弄清楚一个系统中的对象是如何交互的。
- 它使控制集中化
中介者模式将交互的复杂性变为中介者的复杂性。因为中介者封装了协议,它可能变得比任一个Colleague都复杂。这可能使得中介者自身成为一个难于维护的庞然大物。
8.实现
下面是与中介者模式有关的一些实现问题:
- 忽略抽象的Mediator类
当各Colleague仅与一个Mediator一起工作时,没有必要定义一个抽象的Mediator类。Mediator类 提供的抽象耦合已经使各Colleague可与不同的Mediator子类一起工作,反之亦然。
- Colleague——Mediator通信
当一个感兴趣的事 件发生时, Colleague必须与其Mediator通信。一种实现方法是使用Observer(5.7)模式,将Mediator实现为一个Observer,各Colleague作为Subject,一旦其状态改变就发送通知给Mediator。Mediator作出的响应是将状态改变的结果传播给其他的Colleague。
另一个方法是在Mediator中定义一个特殊的通知接口,各Colleague在通信时直接调用该接口。Windows 下的Smaltalk/V使用某种形式的代理机制:当与Mediator通信时,Colleague将自身作为一个参数传递给Mediator,使其可以识别发送者。代码示例一节使用这种方法。而Smalltalk/V的实现方法将稍后在已知应用一节中讨论。
9.代码示例
我们将使用一个DialogDirector来实现在动机一节中所示的字体对话框。抽象类DialogDirector为导控者定义了一个接口。
class DialogDirector {
public:virtual ~DialogDirector();virtual void ShowDialog();virtual void WidgetChanged (Widget*) = 0;
protected:DialogDirector();virtual void Createwidgets() = 0;
};
Widget是窗口组件的抽象基类。一个窗口组件知道它的导控者。
class Widget (
public:Widget (DialogDirector*);virtual void Changed();virtual void HandleMouse (MouseEvent& event);
private:DialogDirector& _director;
};
Changed调用导控者的WidgetChanged操作。通知导控者某个重要事件发生了。
void Widget::Changed () {_director->WidgetChanged(this);
}
DialogDirector的子类重定义WidgetChanged以导控相应的窗口组件。窗口组件把对自身的一个引用作为WidgetChanged的参数,使得导控者可以识别哪个窗口组件改变了。
DialogDirector子类重定义纯虚函数CreateWidgets,在对话框中构建窗口组件。
ListBox、EntryField和Button是Widget的子类,用作特定的用户界面构成元素。ListBox提供了一个GetSelection操作来得到当前的选择项,而EntryField的SetText操作则将新的正文放人该域中。
class ListBox:public widget {
public:ListBox (DialogDirector*);virtual const chart Getselection() ;virtual void SetList (List<char*>* listItems) ;virtual vold HandleMouse (MouseEvent& event);// ...
};class EntryField:public Widget {
public:EntryField(DialogDirector*); virtual void setText (const char* text);virtual const char GetText();virtual void HandleMouse (MouseEvent& event);// ..
};
Button是一个简单的窗口组件,它一旦被按下就调用Changed。这是在其HandleMouse的实现中完成的:
class Button:public Widget {
public;Button (DialogDirector*);virtual void SetText (const char* text);virtual void HandleMouse (MouseEvent& event);// ...
};void Buttont::HandleMouse(MouseEvent& event) {// ...Changed();
}
FontDialogDirectator类在对话框中的窗口组件间进行中介。
FontDialogDirectator是DialogDirector的子类:
class FontDialogDirector : public DialogDirector {
public:FontDialogDirector();virtual ~FontDialogDirector();virtual void widgetChanged (Widget*);
protected:virtual void CreateWidgets();
private:Button* _ok;Button* _cancel;ListBox* _fontList;EntryField* _fontName;
}
FontDialogDirector跟踪它显示的窗口组件。它重定义CreateWidgets以创建窗口组件并初始化对它们的引用:
vold FontDlalogDirector::Createwidgets () (_ok = new Button(this);_cancel = new Button(this); _fontList = new ListBox (this);_fontName = new EntryField(this);// fill the listBox with the available font names// assemble the widgets in the dialog
}
WidgetChanged保证窗口组件正确地协同工作:
void FontDialogDirector::WidgetChanged (Widget* theChangedwidget){if(theChangedwidget == _fontList) {_fontName->SetText(_fontList->GetSelection());}else if (theChangedWidget == _ok) {// apply font change and dismiss dialog// ...} else if (theChangedwidget == _cancel) {// dismiss dialog}
}
WidgetChanged的复杂度随对话框的复杂度的增加而增加。在实践中,大对话框并不受欢迎,其原因是多方面的,其中一个重要原因是中介者的复杂性可能会抵消该模式在其他方面的带来的好处。
10.相关模式
Facade(4.5)与中介者的不同之处在于它是对一个对象子系统进行抽象,从而提供了一个更为方便的接口。
它的协议是单向的,即Facade对象对这个子系统类提出请求,但反之则不行。相反,Mediator提供了各Colleague对象不支持或不能支持的协作行为,而且协议是多向的。
Colleague可使用Observer(5.7)模式与Mediator通信。
5.6 MEMENTO ( 备忘录)
1.意图
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
就是方便“撤销”。
2.别名
Token
3.动机
有时有必要记录一个对象的内部状态。为了允许用户取消不确定的操作或从错误中恢复过来,需要实现检查点和取消机制,而要实现这些机制,你必须事先将状态信息保存在某处,这样才能将对象恢复到它们先前的状态。但是对象通常封装了其部分或所有的状态信息,使得其状态不能被其他对象访问,也就不可能在该对象之外保存其状态。而暴露其内部状态又将违反封装的原则,可能有损应用的可靠性和可扩展性。
例如,考虑一个图形编辑器,它支持图形对象间的连线。用户可用一条直线连接两个矩形,而当用户移动任意一个矩形时,这两个矩形仍能保持连接。在移动过程中,编辑器自动伸展这条直线以保持该连接。
一个众所周知的保持对象间连接关系的方法是使用一个约束解释系统。我们可将这一功能封装在一个ConstraintSolver对象中。ConstraintSolver在连 接生成时,记录这些连接并产生描述它们的数学方程。当用户生成一个连接或修改图形时,ConstraintSolver就求解这些方程。
并根据它的计算结果重新调整图形,使各个对象保持正确的连接。在这一应用中,支持取消操并不象看起那么容易。一个显而易见的方法是,每次移动时保存移动的距离,而在取消这次移动时该对象移回相等的距离。然而,这不能保证所有的对象都会出现在它们原先出现的地方。设想在移动过程中某连接中有一些松弛。在这种情况下,简单地将矩形移回它原来的位置并不一定能得到预想的结果。
一般来说, ConstraintSolver的公共接口可能不足以精确地逆转它对其他对象的作用。为重建先前的状态,取消操作机制必须与ConstraintSolver更紧密的结合,但我们同时也应避免将ConstraintSolver的内部暴露给取消操作机制。
我们可用备忘录(Memento)模式解决这一问题。一个备忘录( memento)是一个对象,它存储另一个对象在某个瞬间的内部状态,而后者称为备忘录的原发器(originator)。当需要设置原发器的检查点时,取消操作机制会向原发器请求一个备忘录。原发器用描述当前状态的信息初始化该备忘录。只有原发器可以向备忘录中存取信息,备忘录对其他的对象“不可见" 。
在刚才讨论的图形编辑器的例子中, ConstraintSolver可作为一个原发器。下面的事件序列描述了取消操作的过程:
-
作为移动操作的一个副作用, 编辑器向ConstraintSolver请求一个备忘录。
-
ConstraintSolver创建并返回一个备忘录,在这个例子中该备忘录是SolverState类的一个实例。SolverState备 忘录包含一些描述ConstraintSolver的内部等式和变量当前状态的数据结构。
-
此后当用户取消移动操作时,编辑器将SolverState备忘录送回给ConstraintSolver。
-
根据SolverState备忘录中的信息,ConstraintSolver改变它的内部结构以精确地将它的等式和变量返回到它们各自先前的状态。这一方案允许ConstraintSolver把恢复先前状态所需的信息交给其他的对象,而又不暴露它的内部结构和表示。
4.适用性
在以下情况下使用备忘录模式:
-
必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状。
-
如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。
5.结构
6.参与者
-
Memento(备忘录,如SolverState)
- 备忘录存储原发器对象的内部状态。原发器根据需要决定备忘录存储原发器的哪些内部状态。
- 防止原发器以外的其他对象访问备忘录。备忘录实际上有两个接口,管理者(caretaker)只能看到备忘录的窄接口一-它只能将备忘录传递给其他对象。相反,原发器能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据。理想的情况是只允许生成本备忘录的那个原发器访问本备忘录的内部状态。
-
Originator(原发器,如ConstraintSolver)
- 原发器创建一个备忘录,用以记录当前时刻它的内部状态。
- 使用备忘录恢复内部状态.。
-
Caretaker(负责人,如undo mechanism)
- 负责保存好备忘录。
- 不能对备忘录的内容进行操作或检查。
7.协作
- 管理器向原发器请求一个备忘录,保留一段时间后,将其送回给原发器,如下面的交互图所示。
有时管理者不会将备忘录返回给原发器,因为原发器可能根本不需要退到先前的状态。
- 备忘录是被动的。只有创建备忘录的原发器会对它的状态进行赋值和检索。
8.效果
备忘录模式有以下一些效果:
- 保持封装边界
使用备忘录可以避免暴露–些只应由原发器管理却又必须存储在原发器之外的信息。该模式把可能很复杂的Originator内部信息对其他对象屏蔽起来,从而保持了封装边界。
- 它简化了原发器
在其他的保持封装 性的设计中,Originator负责保持客户请求过的内部状态版本。这就把所有存储管理的重任交给了Originator。让客户管理它们请求的状态将会简化Originator,并且使得客户工作结束时无需通知原发器。
- 使用备忘录可能代价很高
如果原发器 在生成备忘录时必须拷贝并存储大量的信息,或者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。除非封装和恢复Originator状态的开销不大,否则该模式可能并不合适。参见实现一节中关于增量式改变的讨论。
- 定义窄接口和宽接口
在一些语言中可能难以保证只有原发器可访问备忘录的状态。
- 维护备忘录的潜在代价
管理器负责删除它所维护的备忘录。然而,管理器不知道备忘录中有多少个状态。因此当存储备忘录时,-一个本来很小的管理器,可能会产生大量的存储
开销。
9.实现
下面是当实现备忘录模式时应考虑的两个问题:
- 语言支持备忘录有两个接口
一个为原发器所使用的宽接口,一个为其他对象所使用的窄接口。理想的实现语言应可支持两级的静态保护。在C++中,可将Originator作为Memento的一个友元, 并使Memento宽接口为私有的。只有窄接口应该被声明为公共的。例如:
class State;
class originator {
public:Memento* Creat eMemento{) ;void SetMemento(const Memento*);
private:State* _state;// internal data structures
};class Memento {
public:// narrow public interfacevirtual ~Memento() ;
private:// private members accessible only to Originatorfriend class Originator;Memento(); void SetState (State*);State* GetState() ;// ...
private:State* _state;
};
- 存储增量式改变
如果备忘录的创建及其返回( 给它们的原发器)的顺序是可预测的,备忘录可以仅存储原发器内部状态的增量改变。
例如,一个包含可撤消的命令的历史列表可使用备忘录以保证当命令被取消时,它们可以被恢复到正确的状态(参见Command(5.2))。历史列表定义了一个特定的顺序,按照这个顺序命令可以被取消和重做。这意味着备忘录可以只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。在前面动机一节给出的例子中,约束解释器可以仅存储那些变化了的内部结构,以保持直线与矩形相连,而不是存储这些对象的绝对位置。
10.代码示例
此处给出的C++代码展示的是前面讨论过的ConstraintSolver的例子。我们使用MoveCommand命令对象(参见Command(5.2))来执行(取消)一个图形对象从一个位置到另一个位置的移动变换。图形编辑器调用命令对象的Execute操作来移动一个图形对象,而用Unexecute来取消该移动。命令对象存储它的目标、移动的距离和一个CostraintSolverMemento的实例,它是一个包含约束解释器状态的备忘录。
class Graphic;
// base class for graphical objects in the graphical editor
class MoveCommand {
public:MoveComnand (Graphic* target, const Point& delta);void Execute();void Unexecute();
private:ConstraintSolverMemento* _state;Point delta;Graphic* _target;
};
连接约束由ConstraintSolver类创建。它的关键成员函数是Solve,它解释那些由AddConstraint操作注册的约束。为支持取消操作, ConstraintSolver用CreateMemento操作将自身状态存储在外部的一个ConstraintSolverMemento实例中。调用SetMemento可 使约束解释器返回到先前某个状态。ConstraintSolver是一个Singleton(3.5)。
class ConstraintSolver {
public:static Constraintsolver* Instance(); .void Solve();void AddConstraint (Graphic* startConnection, Graphic* endConnection);void RemoveConstraint(Graphic* startConnection, Graphic* endConnection);ConstraintsolverMemento* CreateMemento();void SetMemento (ConstraintsolverMemento*);
private://nontrivial state and operations for enforcing// connectivity semantics
};
class ConstraintSolverMemento {
public:virtual ~ConstraintsolverMemento();
private:friend class Constraintso1ver;ConstraintsolverMemento();// private constraint solver state
};
给定这些接口,我们可以实现MoveCommand的成员函数Execute和Unexecute如下:
void MoveCommand::Execute () {ConstraintSolver* solver = ConstraintSolver::Instance();_state = solver->CreateMemento(); // create a memento_target->Move(_delta);solver->Solve ();
}
void MoveCommand::Unexecute () {ConstraintSolver* solver = ConstraintSolver::Instance();_target->Move(_delta) ;solver->SetMemento(_state); // restore solver statesolver->Solve() ;
}
Execute在移动图形前先获取一个ConstraintSolverMemento备忘录。Unexecute先将 图形移回,再将约束解释器的状态设回原先的状态,并最后让约束解释器解释这些约束。
11.相关模式
Command(5.2):命令可使用备忘录来为可撤消的操作维护状态。
Iterator(5.4):如前所述备忘录可用于迭代.
5.7 OBSERVER (观察者)
1.意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
也就是订阅模式。关注一个对象,一旦对象发送消息、信息,你都可以收到。就是把所有的关注者放入一个数组中,然后遍历通知。
2.别名
依赖(Dependents),发布订阅(Publish-Subscribe)
3.动机
将一个系统分割成一系列相互协作的类有一个常见的副作用:需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,因为这样降低了它们的可重用性。
例如,许多图形用户界面工具箱将用户应用的界面表示与底下的应用数据分离。定义应用数据的类和负责界面表示的类可以各自独立地复用。当然它们也可一起工作。一个表格对象和一个柱状图对象可使用不同的表示形式描述同一个应用数据对象的信息。表格对象和柱状图对象互相并不知道对方的存在,这样使你可以根据需要单独复用表格或柱状图。但在这里是它们表现的似乎互相知道。当用户改变表格中的信息时,柱状图能立即反映这一变化, 反过来也是如此。
这一行为意味着表格对象和棒状图对象都依赖于数据对象,因此数据对象的任何状态改变都应立即通知它们。同时也没有理由将依赖于该数据对象的对象的数目限定为两个,对相同的数据可以有任意数目的不同用户界面。
Observer模式描述了如何建立这种关系。这一模式中的关键对象是目标(subject)和观察者(observer)。一个目标可以有任意数目的依赖它的观察者。一旦目标的状态发生改变,所有的观察者都得到通知。作为对这个通知的响应,每个观察者都将查询目标以使其状态与目标的状态同步。
这种交互也称为发布-订阅( publish-subscribe )。目标是通知的发布者。它发出通知时并不需知道谁是它的观察者。可以有任意数目的观察者订阅并接收通知。
4.适用性
在以下任一情况下可以使用观察者模式:
- 当一个抽象模型有两个方面,其中一一个方面依赖于另一 方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。
- 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之,你不希望这些对象是紧密耦合的。
5.结构
6.参与者
- Subject(目标)
- 目标知道它的观察者。可以有任意多个观察者观察同一个目标。
- 提供注册和删除观察者对象的接口。
- Observer (观察者)
- 为那些在目标发生改变时需获得通知的对象定义一个更新接口。
- ConcreteSubject (具体目标)
- 将有 关状态存人各ConcreteObserver对象。
- 当它 的状态发生改变时,向它的各个观察者发出通知。
- ConcreteObserver (具体观察者)
- 维护一个指 向ConcreteSubject对象的引用。
- 存储有关状态,这些状态应与目标的状态保持一致。
- 实现Observer的更新接口以使自身状态与目标的状态保持一致。
7.协作
- 当ConcreteSubject发 生任何可能导致其观察者与其本身状态不一致的改变时,它将通知它的各个观察者。
- 在得到一个具体目标的改变通知后,ConcreteObserver对象可向目标对象查询信息。ConcreteObserver使用这些信息以使它的状态与目标对象的状态一致。
下面的交互图说明了一个目标对象和两个观察者之间的协作:
注意发出改变请求的Observer对象并不立即更新,而是将其推迟到它从目标得到一个通知之后。Notify不总 是由目标对象调用。它也可被一个观察者 或其它对象调用。实现一节将讨论一些常用的变化。
8.效果
Observer模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用其观察者,反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。下面是观察者模式其它一些优缺点:
- 目标和观察者间的抽象耦合
一个目标所知道的仅仅是它有一系列观察者,每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪-一个 具体的类。这样目标和观察者之间的耦合是抽象的和最小的。因为目标和观察者不是紧密耦合的,它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它,这样就保持了系统层次的完整。如果目标和观察者混在一块,那么得到的对象要么横贯两个层次(违反了层次性),要么必须放在这两层的某一层中(这可能会损害层次抽象)。
- 支持广播通信
不像通常的请求,目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。
- 意外的更新
因为一个观察者并不知道其它观察者的存在,它可能对改变目标的最终代价一无所知。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新,这种错误通常很难捕捉。
简单的更新协议不提供具体细节说明目标中什么被改变了,这就使得上述问题更加严重。如果没有其他协议帮助观察者发现什么发生了改变,它们可能会被迫尽力减少改变。
9.实现
这一节讨论一些与实现依赖机制相关的问题。
- 创建目标到其观察者之间的映射
一个目标对象跟踪它应通知的观察者的最简单的方法是:显式地在目标中保存对它们的引用。然而,当目标很多而观察者较少时,这样存储可能代价太高。一个解决办法是用时间换空间,用一个关联查找机制(例如一个hash表)来维护目标到观察者的映射。这样一个没有观察者的目标就不产生存储开销。但另一方面,这-方法增加了访问观察者的开销。
- 观察多个目标
在某些情况下, 一个观察者依赖于多个目标可能是有意义的。例如,一个表格对象可能依赖于多个数据源。在这种情况下,必须扩展Update接口以使观察者知道是哪一个目标送来的通知。目标对象可以简单地将自己作为Update操作的个参数,让观察者知道应去检查哪一个目标。
- 谁触发更新
目标和它的观察者依赖于通知机制来保持-致。但到底哪一个对象调用Notify来触发更新?此时有两个选择:
a)由目标对象的状态设定操作在改变目标对象的状态后自动调用Notify。这种方法的优点是客户不需要记住要在目标对象上调用Notify,缺点是多个连续的操作会产生多次连续的更新,可能效率较低。
b)让客户负责在适当的时候调用Notify。这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新,避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用Notify,这种方式较易出错。
- 对已删除目标的悬挂引用
删除一个目标时应注意不要在其观察者中遗留对该目标的悬挂引用。一种避免悬挂引用的方法是,当一个目标被删除时,让它通知它的观察者将对该目标的引用复位。一般来说,不能简单地删除观察者,因为其他的对象可能会引用它们,或者也可能它们还在观察其他的目标。
- 在发出通知前确保目标的状态自身是一致的
在发出通知前确保状态自身一致这一点很重要,因为观察者在更新其状态的过程中需要查询目标的当前状态。
当Subject的子类调用继承的该项操作时,很容易无意中违反这条自身一致的准则。例如,下面的代码序列中,在目标尚处于一种不一致的状态时,通知就被触发了:
void MySubj ect::Operation {int newValue) {BaseClassSubject::Operation (newValue) ;// trigger notification_myInstVar += newValue;// update subclass state (too late!)
}
你可以用抽象的Subject类中的模板方法(Template Method(5. 10))发送通知来避免这种错误。定义那些子类可以重定义的原语操作,并将Notify作为模板方法中的最后一个操作,这样当子类重定义了Subject的操作时,还可以保证该对象的状态是自身一致的。
void Text::Cut (TextRange r) {ReplaceRange (r) ; // redefined in subclassesNotify() ;
}
顺便提一句,在文档中记录是哪一个Subject操作触发通知总是应该的。
- 避免特定于观察者的更新协议一推/拉模型
观察者模式的实现经常 需要让目标广播关于其改变的其他一些信息。目标将这些信息作为Update操作一个参数传递出去。这些信息的量可能很小,也可能很大。
- 一个极端情况是,目标向观察者发送关于改变的详细信息,而不管它们需要与否。我们称之为推模型(push model)。
- 一个极端是拉模型(pull model);目标除最小通知外什么也不送出,而在此之后由观察者显式地向目标询问细节。
拉模型强调的是目标不知道它的观察者,而推模型假定目标知道一些观察者的需要的信息。推模型可能使得观察者相对难以复用,因为目标对观察者的假定可能并不总是正确的。
另一方面。拉模型可能效率较差,因为观察者对象需在没有目标对象帮助的情况下确定什么改变了。
- 显式地指定感兴趣的改变
你可以扩展目标的注册接口,让各观察者注册为仅对特定事件感兴趣,以提高更新的效率。当一个事件发生时,目标仅通知那些已注册为对该事件感兴趣
的观察者。支持这种做法一种途径是,对使用目标对象的方面( aspects)的概念。可用如下代码将观察者对象注册为对目标对象的某特定事件感兴趣:
void Subject::Attach(Observer*, Aspect& interest) ;
此处interest指定感兴趣的事件。在通知的时刻,目标将这方面的改变作为Update操作的一个参数提供给它的观察者,例如:
void Observer::Update(Subject*,Aspect& interest) ;
- 封装复杂的更新语义
当目标和观察者间的依赖关系特别复杂时,可能需要一个维护这些关系的对象。我们称这样的对象为更改管理器( ChangeManager )。它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。
例如,如果一个操作涉 及到对几个相互依赖的目标进行改动,就必须保证仅在所有的目标都已更改完毕后,才一次性地通知它们的观察者,而不是每个目标都通知观察者。
ChangeManager有三个责任:
a)它将一个目标映射到它的观察者并提供一个接口来维护这个映射。这就不需要由目标来维护对其观察者的引用,反之亦然。
b)它定义一个特定的更新策略。
c)根据一个目标的请求,它更新所有依赖于这个目标的观察者。
框图描述了一个简单的基于ChangeManager的Observer模式的实现。有两种特殊的ChangeManager。SimpleChangeManager总是更新每- 一个目标的所有观察者,比较简单。相反,DAGChangeManager处理目标及其观察者之间依赖关系构成的无环有向图。当一个观察者观察多个目标时, DAGChangeManager要比SimpleChangeManager更好一些。 在这种情况下,两个或更多个目标中产生的改变可能会产生冗余的更新。DAGChangeManager保证观察者仅接收一个更新。当然,当不存在多重更新的问题时, SimpleChangeManager更好一些。ChangeManager是一个Mediator(5.5)模式的实例。 通常只有一个ChangeManager, 并且它是全局可见的。这里Singleton(3.5)模式可能有用。
- 结合目标类和观察者类
用不支持多重继承的语言(如Smalltalk)书写的类库通常不单独定义Subject和Observer类,而是将它们的接口结合到一个类中。这就允许你定义一个既是一个目标又是一个观察者的对象,而不需要多重继承。例如在Smalltalk中, Subject和Observer接口定义于根类Object中,使得它对所有类都可用。
10.代码示例
一个抽象类定义了Observer接口:
class Subject;
class Observer{
public:virtual ~Observer();virtual void Update (subject* theChangedSubject) = 0;
protected:Observer();
};
这种实现方式支持-一个观察者有多个目标。当观察者观察多个目标时,作为参数传递给Update操作的目标让观察者可以判定是哪一个目标发生了改变。.
类似地,一个抽象类定义了Subject接口:
class Subject {
public:virtual ~Subject() ; virtual void Attach (observer*); .virtual void Detach (observer*);virtual void Notify();
protected:Subject();
private:List<Observer*> *_observers;
};void Subject::Attach (Observer* o) {_observers->Append(o) ;
}void Subiect::Detach. (Observer* 0) {_observers->Remove(o);
}void Subject::Notify (){ListIterator<Observer*> i(_observers); for (i.First(); !i.IsDone(); i.Next()) {i.CurrentIten()->Update (this);}
}
ClockTimer是一个用于存储和维护一天时间的具体目标。它每秒钟通知一次它的观察者。ClockTimer提供了一个接口用于取出单个的时间单位如小时,分钟和秒。
class ClockTimer : publie subject {
public:ClockTiner(); .virtual int GetHour();virtual int GetMinute();virtwal int Getsecond();void Tick();
};
Tick操作由一个内部计时器以固定的时间间隔调用,从而提供一个精确的时间基准。Tick更新ClockTimer的内部状态并调用Notify通知观察者:
vold clockrimer:Tick(){// update Internal time-keeping stateNiotity();
}
现在我们可以定义一个DigitalClock类来显示时间。它从一个用户界面工具箱提供的Widget类继承了它的图形功能。通过继承Observer, Observer接口被融入DigitalClock的接口。
class DigitalClock:public Widget,public Observer{
public:DigitalClock(ClockTimer*);virtual ~Digitalclock();virtual void update (Subject*);// overrides observer cperationvirtual void Draw();// overridea Widget operat 1on;// definea how to draw the digital clock
private:ClockTimer* _subject;
};Digitalclockt ;Digitalclock (ClockTimer* s) {_subject = s;_subject->Attach(this);
}DigitalClock::~DigitalClock(){_subject->Detach(this);
}
在Update操作画出时钟图形之前,它进行检查,以保证发出通知的目标是该时钟的目标:
vold Digitalclock::Update(Subject* theChangedSubject){if(theChangedsubject == _subject) {Draw();}
}
void DigitalClock::Draw () {// get the new values from the subjectint hour = _subject->GetHour();int minute = _subject->GetMinute();// etc.// draw the digital clock
}
一个AnalogClock可用相同的方法定义
class AnalogClock:public Widget, public Observer {
public:AnalogClock (ClockTimer*) ;virtual void Update (Subject*) ;virtual void Draw() ;// ...
};
下面的代码创建一个AnalogClock和一个DigitalClock, 它们总是显示相同时间:
ClockTimer* timer = new ClockTimer;
AnalogClock* analogClock = new AnalogClock(timer) ;
DigitalClock* digitalClock = new DigitalClock(timer) ;
一旦timer走动,两个时钟都会被更新并正确地重新显示。
11.相关模式
Mediator(5.5):通过封装复杂的更新语义, ChangeManager充当目标和观察者之间的中介者。
Singleton(3.5): ChangeManager可使用Singleton模式来保证它是唯一的并且是可全局访问的。
5.8 STATE (状态)
1.意图
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
依据状态,改变执行的具体内容。
2.别名
状态对象(Objects for States )
3.动机
考虑一个表示网络连接的类TCPConnection。一个TCPConnection对象 的状态处于若干不同状态之一:连接已建立( Established )正在监听(Listening)、连接已关闭(Closed)。当一个TCPConnection对象收到其他对象的请求时,它根据自身的当前状态作出不同的反应。
例如,一个Open请求的结果依赖于该连接是处于连接已关闭状态还是连接已建立状态。State模式描述了TCPConnection如何在每一种状态下表现出不同的行为。
这一模式的关键思想是引人了一个称为TCPState的抽象类来表示网络的连接状态。
TCPState类为各表示不同的操作状态的子类声明了一个公共接口。TCPState的子类实现与特定状态相关的行为。例如, TCPEstablished和TCPClosed类分别实现了特定于TCPConnection的连接已建立状态和连接已关闭状态的行为。
TCPConnection类维护一个表示TCP连接当前状态的状态对象(一个TCPState子类的实例)。TCPConnection类将所有与状态相关的请求委托给这个状态对象。TCPConnection使用它 的TCPState子类实例来执行特定于连接状态的操作。
一旦连接状态改变,TCPConnection对 象就会改变它所使用的状态对象。例如当连接从已建立状态转为已关闭状态时,TCPConnection会用一个TCPClosed的实例来代替原来的TCPEstablished的实例。
4.适用性
在下面的两种情况下均可使用State模式:
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常,有多个操作包含这-相同的条件结构。State模式将每一个条件分支放人一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
5.结构
6.参与者
- Context(环境,如TCPConnection)
- 定义客户感兴趣的接口。
- 维护一个ConcreteState子类的实例,这个实例定义当前状态。
- State(状态,如TCPState)
- 定义一个接口以封装与Context的一个特定状态相关的行为。
- ConcreteState subclasses(具体状态子类,如TCPEstablished, TCPListen, TCPClosed)
- 每一子类实现一个 与Context的一个状态相关的行为。
7.协作
- Context将与状态相关的请求委托给当前的ConcreteState对象处理。
- Context可将自身作为一个参数传递给处理该请求的状态对象。这使得状态对象在必要时可访向Context。
- Context是客户使用的主要接口。客户可用状态对象来配置一个Context,一旦一个Context配置完毕,它的客户不再需要直接与状态对象打交道。
- Context或ConcreteState子类都可决定哪个状态是另外哪一个的后继者,以及是在何种条件下进行状态转换。
8.效果
State模式有下面一些效果:
- 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来
State模式将所有与一个特定的状态相关的行为都放人一个对象中。因为所有与状态相关的代码都存在于某一个State子类中,所以通过定义新的子类可以很容易的增加新的状态和转换。
另一个方法是使用数据值定义内部状态并且让Context操作来显式地检查这些数据。但这样将会使整个Context的实现中遍布看起来很相似的条件语句或case语句。增加一个新的状态可能需要改变若千个操作,这就使得维护变得复杂了。
State模式避免了这个问题,但可能会引入另一个问题,因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些,否则需要使用巨大的条件语句。
正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码不够清晰,这又使得它们难以修改和扩展。State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的if或switch语句中,而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。
- 它使得状态转换显式化
当一个对象仅以内部数据值来定义当前状态时,其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引人独立的对象使得转换变得更加明确。而且, State对象可保证Context不会发生内部状态不一致的情况,因为从Context的角度看,状态转换是原子的一只需 重新绑定一个变量(即Context的State对象变量),而无需为多个变量赋值。
- State对象可被共享
如果State对象没有实例变量一即它们表示 的状态完全以它们的类型来编码一那 么各Context对象可以共享一个State对象。当状态以这种方式被共享时,它们必然是没有内部状态,只有行为的轻量级对象(参见Flyweight (4.6) )。
9.实现
实现State模式有多方面的考虑:
- 谁定义状态转换
State模式不指定哪一 个参与者定义状态转换准则。如果该准则是固定的,那么它们可在Context中完全实现。然而若让State子类自身指定它们的后继状态以及何时进行转换,通常更灵活更合适。这需要Context增加一个接口,让State对象显式地设定Context的当前状态。
用这种方法分散转换逻辑可以很容易地定义新的State子类来修改和扩展该逻辑。这样做的一个缺点是,一个State子类至少拥有-一个其他子类的信息,这就再各子类之间产生了实现依赖。
- 基于表的另一种方法
在C++ Programming Style中, Cargil描述了另一种将结构加载在状态驱动的代码上的方法:他使用表将输入映射到状态转换。对每一个状态, 一张表将每一个可能的输人映射到-个后继状态。实际上,这种方法将条件代码(和State模式下的虛函数)映射为一个查找表。
表的主要好处是它们的规则性:你可以通过更改数据而不是更改程序代码来改变状态转换的准则。然而它也有一些缺点:
- 对表的查找通常不如(虚)函数调用效率高。
- 用统一的、表格的形式表示转换逻辑使得转换准则变得不够明确而难以理解。
- 通常难以加人伴随状态转换的一些动作。表驱动的方法描述了状态和它们之间的转换,但必须扩充这个机制以便在每一个转换上能够进行任意的计算。
表驱动的状态机和State模式的主要区别可以被总结如下: State模式对与状态相关的行为进行建模,而表驱动的方法着重于定义状态转换。
- 创建和销毁State对象
一个常见的值得考虑的实现上的权衡是,究竟是1. 仅当需要State对象时才创建它们并随后销毁它们,还是2. 提前创建它们并且始终不销毁它们。当将要进入的状态在运行时是不可知的,并且上下文不经常改变状态时,第一种 选择较为可取。这种方法避免创建不会被用到的对象,如果State对象存储大量的信息时这一点很重要。
当状态改变很频繁时,第二种方法较好。在这种情况下最好避免销毁状态,因为可能很快再次需要用到它们。此时可以预先一次付清创建各个状态对象的开销,并且在运行过程中根本不存在销毁状态对象的开销。但是这种方法可能不太方便,因为Context必须保存对所有可能会进人的那些状态的引用。
- 使用动态继承
改变一个响应 特定请求的行为可以用在运行时刻改变这个对象的类的办法实现,但这在大多数面向对象程序设计语言中都是不可能的。Self和其他一些基于委托的语言却是例外,它们提供这种机制,从而直接支持State模式。Self中 的对象可将操作委托给其他对象以达到某种形式的动态继承。在运行时刻改变委托的目标有效地改变了继承的结构。这一机制允许对象改变它们的行为,也就是改变它们的类。
10.代码示例
下面的例子给出了在动机一节描述的TCP连接例子的C++代码。这个例子是TCP协议的一个简化版本,它并未完整描述TCP连接的协议及其所有状态。
首先,我们定义类TCPConnection,它提供了一个传送数据的接口并处理改变状态的请求。
class TCPOctetStream;
class TCPState;
class TCPConnection {
pub1ic;TCPConnection();void ActiveOpen();void PassiveOpen();void Close();void Send();void Acknowledge();void Synchronize();void ProcessOctet (TCPOctetStream*) ;
private:friend class TCPState;void ChangeState (TCPState*) ;
private:TCPState* _state;
};
TCPConnection在_state成员变量中保持一个TCPState类的实例。类TCPState复制了TCPConnection的状态改变接口。每一个TCPState操作都以一个TCPConnection实例作为一个参数,从而让TCPState可以访问TCPConnection中的数据和改变连接的状态。
class TCPState {
public:virtual void Transmit (TCPConnection*, TCPOctetStream*) ;virtual void ActiveOpen (TCPConnection*) ;virtual void PassiveOpen (TCPConnection*) ;virtual void Close (TCPConnection*) ;virtual void Synchronize (TCPConnection*) ;virtual void Acknowledge (TCPConnection*) ;virtual void Send (TCPConnection*) ;
protected:void ChangeState (TCPConnection*, TCPState*);
};
TCPConnection将所有与状态相关的请求委托给它的TCPState实例_state。 TCPConnection还提供了一个操作用于将这个变量设为一个新的TCPState。TCPConnection的构 造器将该状态对象初始化为TCPClosed状态(在后面定义)。
TCPConnection::TCPConnection(){_state = TCPClosed::Instance();
}
void TCPConnection::ChangeState(TCPState* s){_state = s;
}
void TCPConnection::Activeopen(){_state->ActiveOpen(this);
}
void TCPConnection::Passive0pen(){_state->PassiveOpen(this);
}
void TCPConnection::Close (){_state->Close(this);
}void TCPConnection::Acknowledge () {_state->Acknowledge (this) ;
}
void TCPConnection::Synchronize(){_state->Synchronize (this) ;
}
TCPState为所有委托给它的请求实现缺省的行为。它也可以调用ChangeState操作来改变TCPConnection的状态。TCPState被定 义为TCPConnection的友元,从而给了它访问这一操作的特权。
void TCPState::Transmit(TCPConnection*, TCPOctetStream*) { }
void TCPState::Active0pen(TCPConnection*) { }
void TCPState::PassiveOpen(TCPConnection*) { }
void TCPState::Close(TCPConnection*) { }
void TCPState:: Synchronize(TCPConnection*) { }
void TCPState::ChangeState(TCPConnection* t, TCPState* s) {t->ChangeState(s);
}
TCPState的子类实现与状态有关的行为。一个TCP连接可处于多种状态:已建立、监听、已关闭等等,对每一个状态都有一个TCPState的子类。我们将详细讨论三个子类:TCPEstablished、TCPListen和TCPClosed。
class TCPEstablished:public TCPState [
public:static TCPState* Instance() ;virtual void Transmit (TCPConnect ion* ," TCPOctetStream*) ;virtual void Close (TCPConnection*) ;
};
class TCPListen:public TCPState {
public:static TCPState* Instance() ;virtual void Send (TCPConnection*);// ...
};
class TCPClosed:public TCPState {
public:static TCPState* Instance() ;virtual void ActiveOpen (TCPConnection*);virtual void PassiveOpen (TCPConnection*) ;// ...
};
TCPState的子类没有局部状态,因此它们可以被共享,并且每个子类只需一个实例。每个TCPState子类的唯一实例 由静态的Instance操作日得到。每一个TCPState子类为该状态下的合法请求实现与特定状态相关的行为:
void TCPClosed::ActiveOpen (TCPConnection* t) {// send SYN, receive SYN, ACK, etc.Changestate(t, TCPEstablished::Instance());
void TCPClosed::PassiveOpen (TCPConnection* t) ChangeState(t, TCPListen::Instance()) ;
}
void TCPEstablished::Close (TCPConnection* t) {// send FIN, receive ACK of FINChangeState(t, TCPListen::Instance());
}
void TCPEstablished::Transmit(TCPConnection* t,TCPOctetStream* o){t->Process0ctet(o);
}
void TCPListen::Send (TCPConnection* t) {// send SYN,receive SYN,ACK, etc. ChangeState (t,TCPEstablished::Instance());
}
在完成与状态相关的工作后,这些操作调用ChangeState操作来改变TCPConnection的状态。TCPConnection本身对TCP连接协议一无所知;是由TCPState子类来定义TCP中的每一个状态转换和动作。
11.相关模式
Flyweight模式(4.6)解释了何时以及怎样共享状态对象。
状态对象通常是Singleton(3.5)。
资料
[1]《设计模式:可复用面向对象软件的基础》(美) Erich Gamma Richard Helm、Ralph Johnson John Vlissides 著 ; 李英军、马晓星、蔡敏、刘建中 等译; 吕建 审校