当设计一个应用程序时, 清晰的分离该程序的不同逻辑组件, 总是被证明是有益的. 同时也存在许多不同的模式来帮助开发者实现这个目标。其中最有名同时也最常用的自然是Model-View-Controller (MVC)了, 它能够将每个应用程序(或者应用程序的一部分)分成三个不同功能的组件,并且定义了把他们联结在一起的规则。Swing本身就是基于这个模式的,而且每个使用Struts,这个流行的开发Web应用框架的人也都了解隐藏在MVC后面的理论.
这篇文章介绍了怎么样通过使用annotation而增加一个新的组件来加强MVC,使其能够更加方便地去掉models跟views之间的耦合。这篇文章介绍了一个叫Stamps的开源库, 它是基于MVC组件之上的,但它去除了所有在开发MVC时所需的, 在models, views和controllers之间建立联系的负担。
基础知识: MVC和annotations
正如MVC这个名字所指出的, Model-View-Controller模式建议将一个应用程序分成以下三个组件:
·Model: 包含了数据模型和所有用来确定应用程序状态的信息。 它一般来说是有条理的并且独立于其他组件的。
·View: 从不同于model的角度出发,它定义了存储在模型中数据的展现方式。它通常被认为是你的应用程序的用户界面(或者GUI),或者以Web应用为例,场景就是你通过浏览器看到的页面。
·Controller: 它代表应用程序的逻辑部分。在这里,它定义了一个用户如何和应用程序进行交互并且也定义了用户行为是如何映射到model的改变。
这些组件紧密的联系在一起: 用户影响view, 反过来view通知controller来更新model.最终model又更新view来反映它的新状态。图1就展现了这种典型的MVC结构。
图1. 一个典型的MVC结构
作为J2SE 5.0所提供的一个新的功能,annotations允许开发者往classes,methods,fields,和其他程序元素中增加元数据。就像反射机制一样,之后很多应用程序为了某些原因能在运行时期获取并使用那些元数据。因为J2SE 5.0只是定义了怎么样编写和读取annotations,并没有说明在哪里使用他们(象@Override这样的用于提前定义的例外),开发者拥有无穷多的在许多不同场合使用他们的可能性:文档编写,与对象相关的映射,代码生成,等等.. Annotations已经变的十分流行,以至于大多数框架和库都更新自己来支持他们。至于更多的关于MVC和annotations的信息请参见资源。
超越MVC: dispatcher
就像前文提到的一样,models和views之间的一些耦合是必要的因为后者必须反映前者的状态。普通Java程序使用直接或间接的耦合将组件绑定在一起。直接耦合发生在当view和model之间有一个直接相关的时候,model包含一列需要维持的views。间接耦合通常发生在一个基于事件分派的机制中。Model会在它状态改变时激发事件,同时一些独立的views会将他们自己注册成事件侦听器。
通常我们比较青睐间接耦合因为它使model完全不知道view的存在,相反view必须和model保持一定的联系从而将自己注册到model上。在这篇文章里我将介绍的框架就是使用间接耦合,但是为了更好的降低组件之间的耦合,view必须不知道model的存在;也就是说,model和view没有被绑定在一起。
为了实现这个目标,我已经定义了一个新的组件,就是dispatcher,它能作为一个存在于views和models之间的分离层。它能处理models和views双方之间的注册并且分派由model激发的事件到注册的views上。它使用java.beans.PropertyChangeEvent对象来表现由model传送到view的事件;然而,这个框架的设计是足够开放的,它可以支持不同事件类型的实现。
管理注册的views列表的负担于是就从model上移开了,同时,因为view只和这个独立于应用程序的dispatcher有关,view不知道model的存在。如果你熟悉Struts内部,你也许能够看出Struts的controller就是在履行这样一个任务,它将Actions和他们关联的JSP(JavaServer Pages)表现页面联系在一起。
现在,我们所设计的MVC框架就像图2所描述的一样。Dispatcher在其中担当了一个于controller相称的角色。
图2.拥有额外dispatcher组件的改进的MVC框架
由于dispatcher必须是独立于应用程序的,所以必须定义一些通用的联结models和views的规范。我们将使用annotations来实现这种联结,它将会被用来标注views并且确定哪个view是受哪个model的影响的,及这种影响是怎么样的。通过这种方式,annotations就像是贴在明信片上的邮票一样,驱动dispatcher来执行传递model事件的任务(这就是这一框架名字的由来)。
应用实例
我们将使用一个简单的计秒器应用程序做该框架的一个应用实例:它允许用户设置时间周期来记数和启动/停止这个定时器。 一旦过去规定的时间,用户将会被询问是否取消或者重启这个定时器。这个应用程序的完全源代码可以从项目主页上找到。
图3.一个简单的应用程序
这个modle是非常简单的,它只存储两个属性:周期和已经过去的秒数。注意当它其中一个属性发生变化时它是如何使用java.beans.PropertyChangeSuppor来激发事件。
public class TimeModel {
public static final int DEFAULT_PERIOD = 60;
private Timer timer;
private boolean running;
private int period;
private int seconds;
private PropertyChangeSupport propSupport;
/**
* Getters and setters for model properties.
*/
/**
* Returns the number of counted seconds.
*
* @return the number of counted seconds.
*/
public int getSeconds() {
return seconds;
}
/**
* Sets the number of counted seconds. propSupport is an instance of PropertyChangeSupport
* used to dispatch model state change events.
*
* @param seconds the number of counted seconds.
*/
public void setSeconds(int seconds) {
propSupport.firePropertyChange("seconds",this.seconds,seconds);
this.seconds = seconds;
}
/**
* Sets the period that the timer will count. propSupport is an instance of PropertyChangeSupport
* used to dispatch model state change events.
*
* @param period the period that the timer will count.
*/
public void setPeriod(Integer period){
propSupport.firePropertyChange("period",this.period,period);
this.period = period;
}
/**
* Returns the period that the timer will count.
*
* @return the period that the timer will count.
*/
public int getPeriod() {
return period;
}
/**
* Decides if the timer must restart, depending on the user answer. This method
* is invoked by the controller once the view has been notified that the timer has
* counted all the seconds defined in the period.
*
* @param answer the user answer.
*/
public void questionAnswer(boolean answer){
if (answer) {
timer = new Timer();
timer.schedule(new SecondsTask(this),1000,1000);
running = true;
}
}
/**
* Starts/stop the timer. This method is invoked by the controller on user input.
*/
public void setTimer(){
if (running) {
timer.cancel();
timer.purge();
}
else {
setSeconds(0);
timer = new Timer();
timer.schedule(new SecondsTask(this),1000,1000);
}
running = !running;
}
/**
* The task that counts the seconds.
*/
private class SecondsTask extends TimerTask {
/**
* We're not interested in the implementation so I omit it.
*/
}
}
Controller只定义了用户可以执行的并且能够从下列接口抽象出来的actions。
public interface TimeController {
/**
* Action invoked when the user wants to start/stop the timer
*/
void userStartStopTimer();
/**
* Action invoked when the user wants to restart the timer
*/
void userRestartTimer();
/**
* Action invoked when the user wants to modify the timer period
*
* @param newPeriod the new period
*/
void userModifyPeriod(Integer newPeriod);
}
你可以使用你自己喜欢的GUI编辑器来画这个view。出于我们自身的情况,我们只需要几个公共的methods就可以提供足够的功能来更新view的fields,如下面的这个例子所示:
/**
* Updates the GUI seconds fields
*/
public void setScnFld(Integer sec){
// scnFld is a Swing text field
SwingUtilities.invokeLater(new Runnable() {
public void run() {
scnFld.setText(sec.toString());
}
});
}
在这里我们注意到我们正在使用POJOs (plain-old Java objects),同时我们不用遵守任何编码习惯或者实现特定的接口(事件激发代码除外)。剩下的就只有定义组件之间的绑定了。
事件分派annotations
绑定机制的核心就是@ModelDependent annotation的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ModelDependent {
String modelKey() default "";
String propertyKey() default "";
boolean runtimeModel() default false;
boolean runtimeProperty() default false;
}
这个annotation能被用在view的methods上,同时dispatcher也会使用这些提供的参数(即modelKey和propertyKey)来确定这个view将会响应的model事件。这个view既使用modelKey参数来指定它感兴趣的可利用的models又使用propertyKey参数来匹配分配的java.beans.PropertyChangeEvents的属性名称。
View method setScnFld()因此被标注以下信息(这里,timeModel提供了用来将model注册到dispatcher上的key):
/**
* Updates the GUI seconds fields
*/
@ModelDependent(modelKey = "timeModel", propertyKey = "seconds")
public void setScnFld(final Integer sec){
// scnFld is a Swing text field
SwingUtilities.invokeLater(new Runnable() {
public void run() {
scnFld.setText(sec.toString());
}
});
}
由于dispatcher既知道model激发的事件又知道事件本身-例如,它知道关联的modelKey和propertyKey-这是唯一需要用来绑定views和models的信息。Model和view甚至不需要分享通信接口或者共用的数据库。
借助我们讨论的绑定机制,我们可以轻易的改变潜在的view而不改变其他任何东西。下面的代码是按照使用SWT(Standard Widget Toolkit)而不是Swing实现的同一个method:
@ModelDependent(modelKey = "timeModel", propertyKey = "seconds")
public void setScnFld(final Integer sec){
Display.getDefault().asyncExec(new Runnable() {
public void run() {
secondsField.setText(sec.toString());
}
});
}
一个完全没有耦合的系统存在以下优点:View可以更加容易地适应model地改变,尽管model通常都是稳定地,相反view是经常被改变。加上系统可以通过使用GUI编辑器或者其他源码生成器来设计,避免了将生成地代码与model-view通信代码混合在一起。又由于model-view的绑定信息是和源码关联的元数据,于是也相对容易把它应用到IDE生成的GUIs或者将已经存在的应用程序转化成这个框架。加之拥有单独的基础代码,view和model可以被当作是独立组件来开发,这很可能简化了应用程序的开发过程。组件测试也可以被简化,因为每个组件可以被单独地测试,并且出于调试的目的,我们可以用假的model和view来代替真实的组件。
然而,这里也存在许多缺点。因为现在当使用接口和公共的classes来绑定model和view时,我们不能再提供编译时期的安全性了,可能出现的打字错误将导致组件之间一个绑定的遗漏,从而导致出现运行时期的错误。
通过使用@ModelDependent的讨论过的modelKey和propertyKey元素,你可以定义model和view之间静态的联系。然而,现实世界的应用程序证明view必须能够经常动态的适应变化的models和应用程序的状态:考虑到用户界面的不同部分能够在应用程序的生命周期内被创造和删除。因此我将介绍怎么使用这个框架与其他常用技术一起来处理此类情形。
动态MVC绑定
对于那些依赖XML绑定(或者其他一些基于配置文件的声明性绑定)的框架,存在一个问题那就是静态绑定规则。在这些框架下,动态变化是不可能的,于是通常开发者决定每次将冗余的绑定信息与一些使用正确绑定的判定算法耦合在一起。
为了巧妙的解决这个问题,Stamps框架提供了两种方式在运行时期改变绑定。 第一种方式是,views和models可以采用事件监听器与GUI窗口小部件联合的方式在dispatcher上注册和注销。这样允许特定的views只在需要他们的时候被通知到。例如,一个与应用程序有联系的监视控制台可以只在用户请求的时候与被它监视的对象绑定在一起。
第二种方式是利用@ModelDependent annotation提供的两个元素runtimeModel() 和 runtimeProperty()。他们指明了某个确定的model和它的分配事件会在运行时期被确定。如果这两个设定中有一个是正确的,那么各自的key(modelKey 或propertyKey)会在view上被method调用来得到需要使用的值。例如:一个负责显示一组新channels (每个channel就是一个model)的view,它就依赖于用户的输入来确定需要绑定的channel。
这种情形的实例如下:
// This method is invoked to display all the messages of one news channel
@ModelDependent(modelKey = "dynamicChannel", propertyKey = "allmessages" , runtimeModel = true)
public void setAllMessages(java.util.List messages) {
// Updates the user interface
}
public String getDynamicChannel() {
// Returns the channel requested by the user
}
附加的annotations
由于世界并不完美,一些附加的annotations被定义来帮助解决现实世界的案例。@Namespace允许开发者为了更好的管理model domain将其再细分成不同的部分。由于单独一个dispatcher可以处理多个models,model keys中将出现的冲突。因此,它能将成群的models和相关的views分到不同的但同属一个namespace下的domains中去, 这样一来,他们就不会干扰对方。
@Transform annotation提供了on-the-fly对象转化, 从包含在model事件中的对象到被receiving views接受的对象的。因而,这个框架就可以适应已存的代码而不需要做任何的改动。这个annotation接受一个注册在有效转化上的单一参数(被定义成一个特殊接口的实现)。
@Refreshable annotation能通过标注model的属性来支持前面讨论的动态连接和分离views。使用这个annotation,该框架可以处理静态和动态的MVC布局,在不同的时间把不同的views绑定到model上。
要理解@Refreshable的使用,我们必须回到之前的那个监控控制台的例子。这个控制台(用MVC的术语来说就是一个view)可以动态地绑定和离开model,取决于用户的需要。当控制器连接到model的时候@Refreshable annotation可以被用来让这个控制器随时了解其model的状态。当一个view连接到这个框架时,它必须在当前model的状态下被更新。因此,dispatcher扫描model寻找@Refreshable annotations并且生成与view它本身从model普通接受到的相同的事件。这些事件接着被之前讨论过的绑定机制分派。
分布式MVC网络
Dispatcher有一个很重的负担那就是它负责处理事件的传送周期中所有重型信息的传递:
· Model激发一个事件用来确定它已经经历过的一些改变, dispatcher处理通知model.
· Dispatcher扫描所有注册在它那里的views, 寻找@ModelDependent annotations, 这些annotations明确了views希望通知的改变及当每个model事件发生时,需要在views上调用的method.
· 如果需要,转化将会被用于事件数据上.
· view method在被调用时会从被激发的事件里抽取参数,接着view会更新自己.
从另一个方面来讲,当一个新view在dispatcher上注册时:
· View告诉dispatcher有关modelKey的信息,modelkey能确定它将被连接到哪一个model上(该model的事件将负责组装view)
· 如果需要,dispatcher扫描model寻找@Refreshable annotations并使用他们来生产将要及时更新view假的model事件
· 这些事件将通过使用上述的顺序被分派, 接着view被更新.
所有这些既不涉及view也不涉及model的工作,他们站在他们各自的信息通信渠道的两端.无所谓这些信息是在一个本地JVM内部传输还是在多个远程主机上的JVM之间传输.如果想将本地应用程序转化成Client/Server应用程序所需的只是简单地改变dispatcher里面的逻辑,而model和view都不会受影响.下图就是一个示例:
图4. 一个基于分布式网路建立的MVC,点击缩略图查看全图
如上图所示,单一的dispatcher被一个与model处在同一个host上的transmitter(it.battlehorse.stamps.impl.BroadcastDispatcher的一个instance)和一个(或多个) 与view处在同一个host上的receiver(it.battlehorse.stamps.impl.FunnelDispatcher)所取代. Stamps 框架默认的实现使用了一个创建于JGroups上的消息传送层, JGroups是一个可靠的多点传送通信的工具包,象网络传输机制(但是不同的实现和使用)一样工作. 通过使用它可以获得一个稳定可靠的, 多协议的, 失败警觉的通信.
对我们应用程序(dispatcher)初步建立的一个改变, 使我们从一个单一用户界面的独立运行的应用程序转移到一个多用户分布式的应用程序.当model进入或离开这个网络(想象一个通信失败)的时候,框架可以通知无数的监听接口, 于是远程views可以采取适当的响应.例如,显示一个警告信息给用户. 这个框架也可以提供有用的methods来帮助将本地的controllers转化成远程的.
总结和摘要
仍有许多元素需要被探索,就像设计controllers的方式一样,它在目前和dispatchers具有一致的普遍性.该框架假设普通的controller-model绑定,由于前者需要知道如何去驱动后者.未来的开发方向将是支持不同类型的views,例如使用一个Web浏览器, 网络警觉的applets,和Java与JavaScript的通信.
已经讨论的Stamps库说明如何在一个MVC架构中降低views和models之间的耦合以及这个框架可以有效的利用Java annotations将绑定信息从实际开发程序组件分离开.拥有隔离的绑定逻辑允许你在物理上将元件分离开并且能提供一个本地和一个client/server结构而不需要改变应用逻辑或者表示层. 这些目标提供对由一个象MVC一样坚固的设计模式与由annotations提供的功能强大的元数据结合在一起所提供的可能性的洞察.