交互式系统(MVC模式) 伪代码实现
下面将利用伪代码编写MVC创建流程,其中(1)到(6)是编写MVC软件框架的基本步骤,第(7)~第(10)步骤是可选的,可以提高灵活性,适合用于打造高度灵活的应用程序或者应用程序框架。
(1)将人机交互与核心功能分离。分析应用领域,将核心功能与所需的输入输出行为进行分离。设计应用程序的模型组件,使其封装所需的核心数据和功能,并提供函数用于访问要显示的数据。确定要通过控制器将模型的那些功能暴露给客户,进而给模型添加相应的接口。
在我们的实例中,模型在两个等长的列表中分别存储选举人和得票数,并提供两个返回迭代器的方法,用于访问这些列表。该模型还提供了修改投票数据的接口。
class Model
{
List<long> votes;
List<string> parties;
public:
Model(List<string> partyNames);
void clearVotes();
void changeVotes(string party, long vote);
Iterator<long> makeVoteIterator()
{
return Iterator<long>(votes);
}
Iterator<string> makePartyIterator()
{
return Iterator<string>(partyNames);
}
//未完待续
};
(2)实现变更传播机制。为此,可采用Publisher-Subscriber设计模式,并让模型充当发布者(publisher)。在模型中添加一个用来存储观察对象的容器,并提供让视图和控制器能够向变更传播机制注册和卸载的过程。模型的通知过程调用所有观察对象的更新过程。模型中每个修改模型状态的过程都在执行修改后调用通知过程。
根据C++使用指南,应定义一个提供更新接口的抽象类Observer,并让视图和控制器都继承Observe。对第(1)步的Model类进行扩展,使其包含一个观察者的引用集合以及让观察者能够注册和卸载attach()和detach()接口。接口notify()被修改模型状态的接口调用。
class Observer
{
public:
/*接口 用来更新视图和控制器
*/
virtual void update();
}
class Model
{
//待续
public:
void attach(Observer *obs)
{
registry.add(s);
}
void detach(Observer *obs)
{
registry.remove(obs);
}
protected:
virtual void notify();
private:
List<Observer *> registry;
};
我们的notify()接口实现了遍历所有已经注册的Observe对象并调用其更新接口。由于存储注册对象的List仅供内部使用,因此我们没有提供创建注册对象的迭代器对象。
void Model::notify()
{
Iterator<Observer *>iter(registry);
while(iter.next()) iter.curr()->update();
}
接口changeVote和clearVotes在修改投票数据后调用notify();
(3)设计并实现视图。设计每个视图的外观,规范并实现在屏幕上显示视图的绘图(draw)过程。这个过程首先从模型那里获取要显示的数据,余下的部分与用户界面平台无关,如调用绘制线或渲染文本的过程。
实现反映模型变化的更新过程,为此最简单的方法是调用绘图过程,让它取回视图所需的数据。对于需要频繁更新的复杂视图,这种简单的更新可能效率低下。在这种情况下,有多种优化策略可供使用。一种方法是给更新过程提供额外的参数,让视图能够判断是否需要重绘;另一种解决方案,在后面还有要求重绘视图的事件时,不马上绘制视图,而等到没有未处理的事件时再重绘视图。
除了更新和绘图过程外,每个视图都需要包含一个初始化过程。初始化过程注册模型的变更传播机制,并关联到控制器,如第(5)步所示。初始化控制器后,视图将自己显示到屏幕上。平台或控制器可能要求视图提供其他功能,如调整试图窗口大小的过程。
在投票系统中,我们定义了基类View,所有视图都从它派生而来。这个基类包含两个成员变量(用于存储于视图相关联的模型和控制器),并提供了获取它们的接口。View的构造函数通过注册变更传播机制来与模型建立关系,而析构函数通过卸载来中断这种关系。View还提供了一个update(),这个方法很简单,未经优化。
class View
: public Observer
{
public:
View(Model *m)
: model(m)
, controller(0)
{
model->attach(this);
}
virtual ~View()
{
model->detach(this);
}
virtual void update()
{
this->draw();
}
virtual void initialize();
virtual void draw();
//未完待续
Model *getModel()
{
return model;
}
Controller *getController()
{
return controller;
}
protected:
Model *model;
Controller *controller;
};
class BarChartView
: public View
{
public:
BarChartView(Model *m)
: View(m)
{
}
virtual void draw();
}
void BarChartView::draw()
{
Iterator<string> party = model->makePartyIterator();
Iterator<long> vote = model->makeVoteIterator();
List<long> dl; //存储充满整个屏幕时的缩放比例
long max = 1;
while(vote.next())
{
if(vote.curr() > max) max = vote.curr();
}
vote.reset();
while(vote.next())
{
dl.append((MAXBARSIZE * vote.curr()) / max);
}
vote = dl;
vote.reset();
while(party.next() && vote.next())
{
//绘制文本
//绘制矩形
}
}
类BarChartview的定义演示了该系统的一个具体视图,它重写了方法draw(),使其使用柱状显示投票数据。
(4)设计并实现控制器。对于应用程序的每个视图,确定系统如何响应用户操作。我们规定底层平图以事件的方法传递用户执行的每项操作。控制器接受这些事件,并使用一个专用过程进行解读。对于重要的控制起来说,这种解读依赖于模型的状态。
初始化控制器时应关联到模型和视图,并启动事件处理。如何完成这些工作取决去用户界面平台,例如,控制器可向窗口系统注册,并将事件处理过程指定为回调函数。
在我们的示例中,大部分视图都用于显示结果,不需要处理事件,因此我们定义了基类Controller,它包含了一个空的handleEvent()接口。其中的构造函数将控制其关联到模型,而析构函数断开关联。
class Controller
: public Observer
{
public:
virtual void handleEvent(Event *)
{
}
Controller(View *v)
: view(v)
{
model = view->getModel();
model->attach(this);
}
virtual ~Controller()
{
model->detach(this);
}
virtual void update()
{
}
protected:
Model *model;
View *view;
}
这里没有提供独立的控制器初始化方法,因为已经在构造函数中关联到了视图和模型。
调用功能核心导致控制器与模型联系紧密,因为控制器依赖于应用程序特定的模型接口。如果你打算修改功能或希望控制器可重用,必须让控制器独立于特定的接口,为此可使用Command Processor设计模式。这种情况下,MVC模型将扮演Command Processor供应者(supplier)角色;在模型和控制器之间新增了命令类和命令处理器组件;而MVC控制器将扮演Command Processor控制器角色。
(5)设计并实现视图-控制器关系。视图通常在初始化期间创建与之相关联的控制器。创建视图和控制器类层次结构遵循Factory Method设计模式,在视图类中定义接口makeController()。如果视图所需的控制器与其超类所需的控制器不同,就必须重写工厂接口makeController()。
在下面的C++示例代码中,基类View实现了接口initialize(),而这个方法调用了工厂接口makeController()。不能在View类的构造函数中调用makeController(),否则将不会调用子类中重写的makeController()。在View的子类中,只有TableView需要的特殊控制器,因此他重写了makeController(),使其返回一个可接受用户输入的TableController。
class View
: public Observer
{
public:
virtual void initialize()
{
controller = makeController();
}
virtual Controller *makeController()
{
return new Controller(this);
}
};
class TableController
: public Controller
{
public:
TableController(TableView *tv)
: Controller(tv)
{
}
virtual void handleEvent(Event *e)
{
//释放事件e
//如更新等票数
if(vote && party)
{
model->changeVotes(party, vote);
}
}
};
class TableView
: public View
{
public:
TableView(Model *m)
: model(m)
{
}
virtual void draw();
virtual Controller *makeController()
{
return new TableController(this);
}
};
(6)实现搭建MVC的代码。搭建MVC的代码首先初始化模型,在创建并初始化视图。完成初始化后,启动事件处理,这通常是一个循环,也可能是一个包含循环的过程。由于模型应独立于视图和控制器,这种搭建代码应位于模型外部,如主程序中。
在下面的简单代码示例中,函数main()初始化模型和多个视图。事件处理将事件交给表视图的控制器,让用户能够输入和修改投票数据。
main()
{
List<string> partys;
partys.append("black");
partys.append("red ");
partys.append("oth. ");
partys.append("blue ");
partys.append("green");
Model m(parties);
//初始化视图
TableView *vl = new TableView(&m);
vl->initialize();
BarChartView *v2 = new BarChartView(&m);
v2->initialize();
//开始处理事件
}
(7)动态创建视图。如果应用程序允许动态地打开和关闭视图,最好提供一个负责对打开的试图进行管理的组件。该组件还可以负责在最后一个试图关闭后保存数据和终止应用程序。要实现这种管理组件可以使用View Handle设计模式。
(8)“可插入式”的控制器。通过将控制方面与视图分离,可将试图与不同的控制器组合。可以利用这种灵活性实现不同的运行模式(如提供新用户和专家级用户使用的模式),还可以使用忽略所有输入的控制器来构造只读视图。这种灵活性的另一种用途是,集成新的输入和输出设备。例如:提供残障人士使用的眼睛跟踪设备的控制器可利用既有模型和视图的功能,很容易集成到系统中。
在我们示例代码中,只有TableView类支持多种控制器,其默认控制器TableController让用户能够输入投票数据。如果TableView只用来显示信息,那就可以给它配置一个忽略所有用户输入的控制。下面的代码演示了如果更换控制器。请注意,setController返回以前使用的控制器对象,这里不再需要改控制器对象,因此马上将它删除掉。
class View
: public Observer
{
public:
virtual Controller *setController(Controller *ctrll);
};
main()
{
//*****
//更换控制器
delete v1->setController(new Controller(v1));
//*****
//打开另一个只读的视图
TableView *v3 = new TableView(&m);
v3->initialize();
delete v3-setController(new Controller(v3));
//继续事件处理
//*****
};
(9)创建视图和创建控制器层次结构。基于MVC的框架实现了可重用的视图类和控制器类,其中的视图类通常表示常用的用户界面元素,如按钮、菜单和文本编辑器。这样,创建应用程序的用户界面时,主要工作是组合预先定义好的试图对象。可使用Composite模式来创建层次型组合视图。如果有多个视图处于活动状态,这时可能同时有多个控制器对事件感兴趣。例如,对话框中的按钮响应鼠标单击,但不响应键盘输入。如果该对话框还包含了一个文本框,键盘输入将被发送这个文本框的控制器。事件按指定顺序依次传递给所有活动控制器的事件处理过程。要管理这种事件委托,可以使用Chain of Responsibility模式。如果正确地设置了责任链,控制器将把未处理的事件传递给夫视图或者兄弟视图的控制器。
(10)进一步降低系统的依赖性。打造框架需精心制作一系列视图和控制器类,代价不菲。因此,你可能希望这些类独立与平台,有些系统就是这样做得。为此可使用Bridge模式在系统和平台之间再添加一层:让视图使用表示窗口display类,让控制器使用负责处理事件的sensor类。
抽象类display定义了用于创建窗口、绘制线条和文本、修改鼠标外观等操作的方法;抽象类sensor定义了独立于平台的事件,每个sensor子类都将系统特定的市价映射到独立于平台的事件。为支持的每个平台实现display和sensor子类。他们封装了与系统相关的细节。
抽象类display和sensor的设计关系重大,因为他们将影响最终代码效率,还将影响在不同平台上可实现的具体类的效率。一种方法是抽象类sensor和display只定义所有用户界面平台都提供的基本功能。另一个极端是让display和sensor提供更高级的抽象,这种类使用的用户界面平台原生代码更多,移至工作量更大。采用第一种方法时,应用程序的外观在不同平台上看起来类似;采用第二种方法时,应用程序更严格地遵循了平台特定的指导原则。