当设计一个应用程序时, 清楚的分离该程序的不同逻辑组件, 总是被证实是有益的. 同时也存在许多不同的模式来帮助开发者实现这个目标。其中最有名同时也最常用的自然是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.
*/
}
}