Annotations能夠幫助你去掉應用組件之間的耦合摘要Model-View-Controller (MVC)是一種軟件架構,它可以將應用程序的數據模型,用戶接口,以及控制邏輯分開, 使其成為獨立的組件.這樣一來,對其中一個組件的修改而產生的對其他組件的影響能夠被降到最低. 在這篇文章中,你將學習到如何使用annotation來設計一個幾乎能夠完全去掉model和view之間耦合的繼承的MVC的框架.
版權聲明:任何獲得Matrix授權的網站,轉載時請務必保留以下作者信息和鏈接作者:Riccardo Govoni;
wenzi_33原文:
http://www.matrix.org.cn/resource/article/44/44204_Annotations+MVC.html關鍵字:annotations;MVC;Framework
當設計一個應用程序時, 清晰的分離該程序的不同邏輯組件, 總是被證明是有益的. 同時也存在許多不同的模式來幫助開發者實現這個目標。其中最有名同時也最常用的自然是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提供的功能強大的元數據結合在一起所提供的可能性的洞察.
關于作者Riccardo Govoni自從2003年開始就做為一名J2EE的開發者在一家位于意大利北部的金融服務公司工作.在那里,他為遺留的銀行系統,數據庫管理,和繁重的數據處理任務開發了許多Web前臺. Govoni在J2EE多層應用程序方面擁有豐富經驗并且擁有關于Java GUI 庫象Swing 和SWT詳盡的知識.他擁有物理學碩士學位.在閑暇時間,Govoni在網絡上四處尋找最新的Java新聞,或者與朋友和(有時候)他女朋友的狗一起討論新的項目主意.
資源· Stamps項目主頁,及相關源碼文件和文檔:http://sourceforge.net/projects/stamps-mvc/
· Wikipedia上關于MVC的定義:http://en.wikipedia.org/wiki/MVC
· 最有名的MVC實現之一, Struts Web 應用框架:http://struts.apache.org/
· JGoodies 綁定,一個在Swing應用程序中解決model-view-controller綁定的不同方法:https://binding.dev.java.net/
· TikeSwing, 一個基于Swing之上的開源框架, 通過擴展Swing widgets來引入MVC “與結合Swing來使用高層次的MVC和POJOs” Tomi Tuomainen(JavaWorld, 2005年六月):http://www.javaworld.com/javaworld/jw-06-2005/jw-0620-tikeswing.html
· Sun J2SE 5.0 annotations向導:http://java.sun.com/j2se/1.5.0/docs/guide/language/annotations.html
· J2SE 5.0 annotations 入門介紹: “Taming Tiger, 第三部分,” Tarak Modi(JavaWorld, 2004年七月):http://www.javaworld.com/javaworld/jw-07-2004/jw-0719-tiger3.html
· “零距離: J2SE 5.0 Annotations,” Kyle Downey (ONJava.com, 2004年十月):http://www.onjava.com/pub/a/onjava/2004/10/06/anno1.html
· JGroups, 是一個可靠的多點傳送通信的工具包, 在分布式Stamps MVC網絡中象傳輸層一樣工作:http://www.jgroups.org/javagroupsnew/docs/index.html
· “一個Swing架構的一般了解”, 作者: Amy Fowler(Sun開發者網絡)討論Swing’s MVC的根源:http://java.sun.com/products/jfc/tsc/articles/architecture/