Document-View-Presentation模式

 

Ku-Yaw Chang等 著,透明

 

抽象

 

    在设计一个交互式系统时,我们将面临这样的挑战:我们必须一致性地保存数据模型,同时我们必须提供独立于用户界面(user interface)而操作数据的功能核心。我们通常会把翻译函数(rendering function)独立出来,用翻译函数将数据翻译成合适的形式,用经过翻译的数据作为用户界面的输出部分。换句话说,数据的翻译过程和翻译结果的输出方式是紧耦合的。这个局限性造成的结果是,每当数据发生改变,我们都必须为每个用户界面重新翻译一次数据(甚至在多个用户界面共享同一个翻译结果的情况下)。

    本文描述了用于交互式软件系统的“文档-视图-表示(Document-View-Presentation,DVP)”模式。本模式建立在Document-View模式的基础上,并且很有效的将翻译函数与翻译结果的输出相解耦。解耦的效果是,我们可以只对数据做一次翻译,然后用不同的方法重复输出翻译的结果。对于那些有翻译算法代价昂贵的交互式系统——例如图形图象处理系统——来说,本模式尤其适用。而且,DVP模式还为交互式系统引入了三层结构(3-tier)的概念,使交互式系统拥有“瘦”客户(即用户界面)。这样的瘦用户界面可以分布在另外的进程或者另外的机器上,而这不会对系统产生重大的影响。这样的分布式性能可以让一个交互式系统进化成一个基于WEB的应用程序(Web-based application),而不需要开发者做太多的努力。

 

Document-View-Presentation模式

 

    Document-View-Presentation(DVP)结构模式将交互式应用程序分割成三个组件(component)。文档(document)组件包含了核心功能和数据;视图(view)组件处理服务请求并翻译文档中的数据;表示(presentation)组件接收事件或服务请求,并输出视图翻译的结果。这样的“变化-传播(change-propagation)”机制可以帮助:(1)文档与它的视图,(2)视图与它的表示--保持状态同步。

 

例子

   


请想象一个用于三维图象生成和诊断分析的医学应用系统,例如[1]中提到的Discover系统。这样的系统通常提供好几个窗口来表示三维图象,让用户可以方便的对三维图象进行交互式操作。这些窗口通常包含各自不同的翻译算法,比如体积翻译(volume redering)或表面翻译(surface rendering)。通常,这些翻译算法的运算量是如此之大,以至于有时候我们要通过分布式运算来缩短运算时间。对于同一个翻译算法的结果,系统支持三种不同的输出方法,包括在屏幕(窗口)上显示、保存到磁盘上、传递给另一个组件做进一步处理。当三维图象数据发生变化时,所有的输出方法必须立刻反映出这一变化,它们必须马上运行一次(而不是三次)翻译算法(如图1所示)。在特定情况下,对于那些翻译算法代价非常昂贵的交互式系统来说,这些算法被执行一次和三次对于系统的性能表现是有很大不同的。

1:为反映数据变化而对三维数据进行(a)一次(b)三次翻译

 

    在上述案例中,我们需要分别处理不同的输出方法,但又不能对系统的性能造成明显的影响。在运行时,系统对不同的输出方法(甚至于输入方法)应该是可以“即插即用(plug-and-play)”的。

 

环境

 

       数据翻译算法代价昂贵的交互式应用系统,比如图形图象处理系统。

 

问题

 

    在设计交互式系统结构时,请注意一个要点:保持数据及功能核心与包含输入输出的用户界面相独立。通常我们的设计会让数据模型有许多不同的用户界面与之相关联。当数据发生变化时,所有与之相关联的用户界面必须立刻反映这一变化。而且,负责将数据翻译成合适的视觉形式的翻译函数通常被看做用户界面的一部分。这样的设计思路的局限性是,每当一个用户界面需要被更新时,我们都不可避免的需要执行翻译函数。

    假想我们的三个不同的用户界面有同一个翻译函数,并且它们通过这个翻译函数被关联到同样的数据。所有的用户界面都需要在数据发生变化时被更新。前面的设计的局限性就是,当数据发生变化时,翻译函数必须被执行三次——为每个用户界面执行一次。但是,如果这些用户界面拥有同一个翻译函数,它们就可以共享同一个翻译结果。也就是说,翻译函数只需要运行一次,翻译的结果可以被不同的用户界面复用(reuse)。简单的说,如果把翻译函数看成是用户界面的一部分,在反映数据变化时为了更新所有用户界面,就可能需要成倍的运算时间。尤其当翻译算法的运算代价非常昂贵时,运算时间的浪费就更加严重。

    在确定解决方案时,需要注意以下几点:

l         结果的输出和应用程序的行为必须立刻反映数据的变化。

l         翻译结果应该可以被不同的输出方法复用。

l         对于同一个翻译结果,添加、分离不同输出方法都应该是容易的,甚至可以在运行时进行。

l         支持不同的输出方法不应该对应用程序的核心(包括表现数据的翻译函数)有影响。

 

解决方案

 

    本模式建立在Document-View模式[2,3]的基础上,将交互式系统分割成三个组件:document、view和presentation。

    document组件包含了数据和处理数据的核心功能。本组件独立于任何特定输入输出方法。

    view组件包含了应用程序的翻译算法。它们被隐藏在presentation组件后面,不与最终用户发生直接交互。view组件接收用户请求,然后调用document提供的核心功能或自己的翻译算法实现相关的服务。view组件还从document组件获取数据,然后用不同的算法来翻译数据。一个document组件可以有一个或多个view组件与之相连。

    presentation组件是view组件输入输出的代理。它们从其他组件接收用户事件或消息,再把这些事件和消息处理成服务请求。presentation组件不直接实现这些服务,而是把这些服务请求转发给相关联的view组件。另外,presentation组件还从相关联的view组件获取翻译结果,再根据自己的特征把翻译结果输出到合适的目的地,例如显示在屏幕上或保存到磁盘上。一个presentation组件可以只负责输入、只负责输出、或者同时负责输入和输出。presentation组件对于用户可以是可见的或者不可见的。用户可以通过那些可见的presentation组件与系统直接交互。

    每个presentation组件只能与一个view组件相关联。但是,一个view组件可以拥有几个presentation组件,这取决于应用程序的需要。实际上,DVP模式中的view组件和Command Processor模式[2]中的controller组件扮演着相似的角色。它允许系统支持不同样式的输入输出(即不同的presentation组件)。

    将document组件与view组件分离,于是我们的系统就可以对同样的数据进行不同方法的翻译。当presentation组件接收到用户触发的事件时,它首先把事件翻译成服务请求,然后把请求转发给与自己相连的view。view响应服务请求,根据请求改变document中的数据。一旦document中的数据被改变,document就通知所有依赖于它的view组件,要求view组件翻译数据。当翻译结果发生改变的时候,view就通知所有与它相连的presentation组件。每个presentation组件都将从view组件接收到当前的翻译结果,并将这些结果输出到不同的设备(例如屏幕、磁盘或网络)上。这样的变化-传播机制可以用Publisher-Subscriber模式[2]或Observer模式[4]来实现。

 

结构

 

    document组件维护数据模型,并且包含处理数据的功能核心。它的导出函数用于执行应用程序指定的处理,所以依赖于它的view组件可以调用这些函数提供服务、访问数据。当然,document提供了变化-传播机制。document组件维护一个注册表,记录依赖于它的view组件信息。数据的改变将触发变化-传播机制,于是document组件会向所有记录的view组件发出通告。

    view组件对document组件中的数据进行翻译,并保存翻译结果。每个view组件都应定义一个update函数,由变化-传播机制调用这个函数。当update函数被调用的时候,每个view组件会从document组件接收到数据,然后对这些数据进行翻译。实际上,每个view组件也提供另一个变化-传播机制。view组件也维护一个注册表,记录依赖于它的presentation组件。翻译结果的改变将触发变化-传播机制,于是view组件会向所有记录的presentation组件发出通告。view组件还接收来自presentation的服务请求,并通过调用document提供的核心功能或自身的翻译算法来实现presentation所请求的服务。

    presentation组件接收输入消息并将输入消息翻译成不同的服务请求。每个presentation组件也定义一个update函数,这个函数也由变化-传播机制来调用。当update函数被调用时,每个presentation组件都从与之相连的view组件接收当前的翻译结果,并依照自己的特征输出翻译结果(比如将翻译结果显示在屏幕上)。


    document、view和presentation三个组件的类-责任-协作(Class-Responsibility-Collaborator,CRC)卡片如图2所示。

2:document、view和presentation三个组件的CRC卡片

    图3展示了DVP模式的组件之间的主要关联。view组件和presentation组件分别是document组件和view组件的订户(subscriber)。换句话说,一个document可以有不止一个view与之相连,同时一个view也可以有不止一个presentation组件与之相连。在一个使用C++的实现中,每个组件都被定义为一个单独的类。view类和presentation类拥有一个共同的父类——Subscriber类,它在接口中定义了update函数。相似的,document类和view类拥有另一个共同的父类——Publisher类,它在接口中定义了attach、detach和notify等函数。


 

3:用对象建模技术(Object Modeling Technique,OMT)构造的DVP模式对象模型

 

动态分析

 

    我们将图解下列两种场景下DVP结构的行为。

    场景I展示出输入消息怎样改变document的数据、这一改变又怎样触发变化-传播机制。为简单起见,在这个图表中只使用一个view-presentation组件对。

l         presentation接收到一个输入消息,并将输入消息翻译成view能理解的服务请求。

l         view调用document提供的服务功能。

l         document实现被请求的服务,通常这会导致document中的一些数据发生改变。

l         document调用所有与之相连的view的update函数,对它们发出通告。

l         每个view都接收到来自document的数据,并调用render函数对数据进行翻译。

l         view调用所有与之相连的presentation的update函数,对它们发出通告。

l         每个presentation组件都接收到来自view的翻译结果,并输出翻译结果。



场景I

 


    场景II展示了DVP三元组如何被初始化。这个初始化过程通常由主程序中的代码按照下面步骤进行:

l         用初始化数据结构创建document实例。

l         创建一个view对象,将一个document的引用作为初始化参数。

l         view对象通过调用document对象的attach函数而预定变化-传播机制。

l         创建一个presentation对象,将一个view的引用作为初始化参数。

l         presentation对象通过调用view对象的attach函数而预定变化-传播机制。


 


场景II

 


实现

 

    在实现DVP模式时,请遵循以下几个基本的步骤:

1.定义数据模型和核心功能。分析应用领域并将之映射到一个合适的数据模型上。将核心功能与输入输出行为分离开。设计document组件来包装数据和核心功能。提供访问数据所需的函数。

F 在我们的Discover例子中,一个BS对象(一个二维Binary Slices的集合)被用于表现三维对象。每个document都包含一个BS对象,并提供访问和操作数据(即BS对象)的方法。因为document扮演着发行者(publisher)的角色,它继承自Publisher基类。我们将在第二步中讲到这个基类。

class Document: public Publisher

{

private:

// BSOBJ is the data structure for BS objects

BSOBJ coreData;

public:

Document();

// methods for views to access the core data

BSOBJ * getData();

// methods for views to manipulate the data

void doThreshold(int low, int high);

void doCut(CONTOUR * con2r, int alpha, int beta);

}

2.实现第一个变化-传播机制。在Publisher-Subscriber模式的基础上,我们为document指定了发行者的角色。进一步扩展document,为它加上一个注册表,在其中保存观察对象——view——的引用。提供一组函数,允许view预定和取消预定数据变化通告。document的notify函数调用所有观察对象的update函数。

F 在我们的C++例子中定义了两个抽象类——Publisher和Subscriber。Publisher类保存当前的订户,并提供attach()和detach()方法,允许观察对象预定和取消预定。Subscriber类提供update接口函数。任何会修改document的状态、类似doThreshold()的方法都会调用notify()方法来通知当前的订户。notify()将遍历注册表中所有的Subscriber对象,并调用它们的update()方法。

class Publisher

{

private:

Set<Subscriber *> registry;

public:

virtual void attach(Subscriber * s) { registry.add(s); }

virtual void detach(Subscriber * s) { registry.remove(s); }

protected:

virtual void notify() {

// call update for all subscribers

Iterator<Subscriber *> iter(registry);

While (iter.next()) {

Iter.curr()->update();

}

}

};

class Subscriber

{

public:

// default is to do nothing

virtual void update() { }

};

3.定义翻译结果的内容。分析不同的输出行为需要的信息,定义翻译结果的内容。

F 对于一张三维图象,我们出于不同的目的会需要它的不同方面的信息,比如颜色、深度(z坐标值)和透明度等等。在这里,我们用RENDER_DATA这样一个数据结构来收集所有需要的信息。

4.设计并实现view类。为每个view类设计翻译算法。指定并实现一个翻译函数用来翻译document中的数据。update函数调用翻译函数进行实际翻译过程。为每个请求提供一个服务函数。通过调用document的核心函数或自己的算法满足服务请求。view对象在初始化过程中应向document对象提出预定。

F 在Discover的例子里面,所有具体view类共享一个抽象基类view,其中定义了不同的view类的公共行为。view类同时继承Publisher和Subscriber两个基类。view类包含两个成员变量,其中一个表示翻译结果,另一个表示view与document的关联——view的构造函数向document发出预定而建立这一关联;析构函数取消预定,从注册表中移除这一关联。view类还提供从presentation组件接收服务请求的方法。每当update()方法被调用时,view对象就产生一个新的翻译对象,并向所有注册的观察对象通知这一变化。

class View: public Publisher, public Subscriber

{

protected:

RENDER_DATA renderResult;

Document * myDoc;

public:

View (Document * doc) : myDoc(doc) { myDoc->attach(this); }

virtual ~View() { myDoc->detach(this); }

// methods for presentation components to access

// the render data

RENDER_DATA * getResult() { return (&renderResult); }

// methods for presentation components to

// manipulate the data

void doThreshold(int low, int high) {

myDoc->doThreshold(low, high);

}

void doCut(CONTOUR * con2r, int alpha, int beta) {

myDoc->doCut(con2r, alpha, beta);

}

// define the default behavior to reflect changes of

// Document’s data

virtual void update() {

this->render();

this->notify();

}

// abstract method to be redefined:

// default is to do nothing

virtual void render() {}

};

SurfaceRenderingView类的定义给出了一个本系统中具体view类的示例。它重载render()方法,通过基于表面的方法(surface-based approach)对BS对象进行翻译。另外,它还提供形如doRotate()和doSetColor()这样的函数来改变翻译结果,但不真正改动document中的数据。

class SurfaceRenderingView : public View

{

public:

SurfaceRenderingView(Document * doc) : View(doc) {}

// method for surface rendering algorithm

virtual void render() {

// get core data from Document

BSOBJ * bs = myDoc->getData();

// do the surface rendering algorithm

SurfaceRendering(&renderData, bs);

}

// methods to change the rendering attributes

void doRotate(int alpha, int beta);

void doSetColor(int r, int g, int b);

};

5.实现第二个变化-传播机制。为view指定publisher的角色。扩展view类,为它添加一个注册表,在其中保存观察对象(presentation组件)的引用。提供一组函数让presentation组件可以预定和取消预定翻译结果的变化通告。view的notify函数调用所有观察对象的update函数。请注意,对于document来说,view是订户;对于presentation组件来说,view是发行者。

6.设计和实现presentation组件。为每个presentation组件指定具体的输入和输出行为。presentation组件的初始化需要向view组件发出预定请求。

F 所有的具体presentation类共享一个抽象基类presentation。presentation类继承Subscriber基类。presentation类包含一个成员变量,用以表示它与view的关联——presentation对象的构造函数向view对象发出预定,建立这一关联;析构函数取消预定,将关联从注册表中移除。

  WndPresetation类定义了是一个presentation组件的示例,它可以接收用户输入并在屏幕上显示翻译结果。它重载了update()方法,以获取当前的翻译结果并在屏幕上显示。另外,它还提供回调函数来接收用户输入。

class Presentation: public Subscriber

{

protected:

View * myView;

public:

Presentation(View * view) : myView(view) {

myView->attach(this);

}

virtual ~Presentation() { myView->detach(this); }

// define the default behavior to reflect changes of

// View’s rendering result

virtual void update() { this->output ();}

// abstract method to be redefined

virtual void output() {}

};

class WndPresentation : public Presentation

{

public:

WndPresentation (View * view) : Presentation(view) {}

// method to output the rendering result

virtual void output () {

RENDER_DATA * result;

result = myView->getResult();

// display the image on the screen: …..

}

// call-back functions to receive user inputs

void onThreshold() {

int low, high; // required parameters

// open a dialog box to obtain parameters: ..…

myView->doThreshold(low, high);

}

void onRotate() {

int alpha, beta; // required parameters

// open a dialog box to obtain parameters: …..

myView->doRotate(alpha, beta);

}

};

 

已知应用

 

    Discover[1,7]是一个用于科学显像的分布式交互系统,该系统从1993年开始已经在台湾Cheng-Kung大学医院投入试用。该系统就是建立在DVP模式结构基础之上的。除了普通的成像分析、图象生成的功能之外,Discover还提供如下的功能:

 

1.图象综合(image integration)

 

    不同的翻译算法或者不同的医学成像设备会生成不同但是相关的三维图形对象。有时候,医师为了进行诊断分析,需要找出这些不同三维图形之间的关联。Discover可以让医师把几个现有的三维对象(即:源对象source object)综合成一个新的三维对象(即:综合对象integrated object),以达到医师需要的观察要求。

    如图4所示:在图象综合过程开始之前,每个view对象都用一个可见的default presentation组件(这个组件可以接收用户事件、在屏幕上显示翻译结果)进行初始化。当一个三维对象被选做源对象时,系统在运行时创建一个output presentation对象,并将之附加给源对象所属的view对象。output presentation组件对用户是不可见的,它完全不接收输入消息。同时,output presentation也是一个发行者,它将view组件的翻译对象输出给它的订户。另外,系统还为综合对象创建一个新的document、view和default presentation三元组。新的document对象注册所有源对象的output presentation,这样document对象就可以分别获取源对象的翻译结果。


图4:添加每个源对象的output presentation的引用,就可以轻松的获取一个综合对象

 


    因此,当医师通过任何一个源对象的default presentation修改源对象时,这个源对象所属view的翻译结果会得到更新,这个过程只需要执行一次翻译算法。default presentation组件和output presentation组件都必须立刻反映这一变化:前者为医师显示新的翻译结果,后者向综合文档(integrated document,综合对象的document)发出变化通告。综合文档从output presentation取回新的翻译结果,更新自己的数据。然后,综合对象的view对综合数据进行翻译,并通知自己的default presentation对象向医师显示翻译结果。

 

2.基于WEB的应用(web-based application)

 

    通常Discover提供的图象生成和诊断分析功能对实时性要求是非常强烈的。尽管我们可以为每个医师的桌面计算机都安装Discover系统,但也不是每台桌面计算机都有足够的运算能力——它必须进行如此沉重的运算,还必须有可以接受的响应时间。所以,Discover还允许医师使用web浏览器来控制它。因此,我们可以只在一台计算能力很强的服务器上安装Discover系统。医师们可以用他们的运算能力不同的桌面计算机通过网络来控制Discover系统。如果不考虑网络流量和服务器负荷的影响,所有的桌面计算机都将可以获得几乎相同的响应时间。

    如图5所示:当web服务器接收到一个来自浏览器的控制Discover的请求时,它运行一个通用网关接口(Common Gateway Interface, CGI)程序来实现服务。CGI程序首先通过自动化机制(automation mechanism,[8])为view组件创建一个input presentation和一个output presentation,然后将服务请求转发给input presentation。当view保存的翻译对象被改变时,output presentation将翻译结果交付给CGI,CGI再将翻译结果转发给web服务器。最后,翻译结果被发送给web浏览器,医师就可以在浏览器上看到所需的图象了。


 

 


5:通过input presentation和output presentation的合作,可以用web浏览器控制Discover系统

 

效果

 

    Document-View-Presentation结构模式有几个优点:

1.复用翻译结果。当多个用户界面(即presentation组件)共享同一个翻译结果时,只需要执行一次翻译函数。于是我们可以节省大量运算时间,从而得到更快的响应,特别是当翻译函数的运算量很大的时候。

2.要点分解。对翻译过程和输入/输出行为进行分解,这使得软件开发者将注意力集中在翻译算法上,而不必操心任何输入/输出操作。当然,他们还可以将注意力集中于提供不同的输入/输出方法,而不必陷入翻译算法的开发中。

3.“可插入的”表示组件。可以实现多个presentation组件,并用一个view组件使用它们。这些presentation组件可以在运行时添加甚至替换,而不会对view组件造成任何影响。

4.瘦用户接口。将翻译过程从输入/输出过程中移除,这使用户接口变得简练。这样的瘦用户接口可以被容易的分布到其他进程或其他机器上。比如说,如果我们通过HTTP协议来分布用户接口,一个交互式系统就可以变成一个基于web的应用。

5.使用先进的Windows应用程序框架很容易实现DVP模式。好几个开发Windows应用程序的框架——例如Visual C++的MFC和Borland C++的OWL——都采用Document-View模式作为它们的默认系统结构[3]。从本质上来说,DVP模式是Document-View模式的扩展。所以,我们只需使用这些框架提供的document-view结构相关类和通信机制就可以很容易的实现DVP模式。我们可以禁用框架提供的view类的输入/输出函数,并提供附加的presentation组件。当然,我们还需要实现view和presentation之间的通信机制。

本模式的缺点如下:

1.复杂性增加。用户界面被分割到view和presentation两个组件中,这会导致系统结构变得复杂。如果对document中的数据的翻译算法比较简单,这一分割只会增加复杂性,却得不到明显的性能改善。

 

相关模式

 

l        

Model-View-Controller(MVC)模式[2]将应用程序分割成三个组件:model包含核心算法和数据;view向用户显示信息;controller处理用户输入。Document-View(DV)模式——MVC的一个变体——将MVC中的view和controller结合成一个组件。在这两个模式中,向用户显示信息的过程实际上包括两个过程:翻译(或解释)数据、在屏幕上显示结果。
表1总结了DVP模式和上述两个模式中的组件以及它们的主要责任。这三个模式的共同点是:它们都把核心功能和数据放在一个组件(即model或document)中。DVP模式与MVC/DV模式最大的不同点是:DVP模式将“原始”输入/输出与其他组件分离。这包括(1)对document中的数据显示分成两部分:数据翻译和输出,(2)将用户输入和服务请求的内部联系解耦。

1:交互式系统的三个模式的特点总结

    另外,MVC/DV模式对交互式系统结构的分割可以看成是在应用程序内部形成了两层的client/server体系结构(如图6所示)。数据和功能核心运行在server端,翻译函数和图形用户界面则运行在client端。这样的两层系统如果有大运算量的翻译算法,则client端会变得非常“肥胖(fat)”。与之相对的,DVP模式可以看成一个三层体系结构(如图7所示)。在三层系统中,中间层由业务逻辑单元组成。这些业务逻辑单元通常不依赖于终端应用程序,因此它们可以被不同的应用程序复用。在Discover系统中,数据翻译过程(计算机图形应用的业务逻辑单元)从client端被移到了server端的中间层,从而保证了client端的精简。同时,这样的变化还使得不同的client(presentation组件)可以复用中间层(view组件)的翻译结果。

 


 


6:MVC模式和DV模式都是把交互式应用程序分解成两层

 

 

 

7:DVP模式将交互式应用程序分解成三层

 

参考书目

 

[1]. P. W. Liu et al., "Distributed Computing: New Power for Scientific Visualization," IEEE Computer Graphics and Applications, Vol. 16, No. 3, May 1996, pp.42-51.

[2]. F. Buschmann, R. Meunier, H. Rohnert, P. Sommerlad and M. Stal, A System of Patterns - Pattern-Oriented Software Architecture, John Wiley & Sons, New York, 1996.

[3]. D. Kruglinski: Inside Visual C++, Microsoft Press, 1996. 中文版:《Visual C++技术内幕(第四版)》,潘爱民等译,清华大学出版社1999年1月。

[4]. E. Gamma, E. Helm, R. Johnson and J. Vlissides, Design Patterns -Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995. 中文版:《设计模式:可复用面向对象软件的基础》,李英军等译,机械工业出版社2000年9月。

[5]. K. Beck and W. Cunningham, “A Laboratory For Teaching Object-Oriented Thinking,” Proceedings of OOPSLA’89, N. Meyrowitz(Ed), Special Issue of SIGPLAN Notices, Vol. 24, No. 10, October 1989, pp. 1- 6.

[6]. J. Rumbaugh et al., Object-Oriented Modeling and Design, Prentice Hall, 1991.

[7]. K. Y. Chang and L. S. Chen, "Using Design Patterns to Develop a Hyper-controllable Medical Image Application,” Proceedings of PLoP'98, Monticello, Illinois, Aug 11-14, 1998.

[8]. R. M. Adler, "Emerging Standards for Computing Software," Computer, March 1995, pp. 68-77.

 

致谢

 

    在此特别向Rosana T. Vaccare Braga表示感谢,她为改善这个模式给了我们很多有价值的建议。