使用Annotations設計一個MVC架構

來源:互聯網
上載者:User
設計   當設計一個應用程式時, 清晰的分離該程式的不同邏輯組件, 總是被證明是有益的. 同時也存在許多不同的模式來協助開發人員實現這個目標。其中最有名同時也最常用的自然是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提供的功能強大的中繼資料結合在一起所提供的可能性的洞察.



相關文章

Cloud Intelligence Leading the Digital Future

Alibaba Cloud ACtivate Online Conference, Nov. 20th & 21st, 2019 (UTC+08)

Register Now >

Starter Package

SSD Cloud server and data transfer for only $2.50 a month

Get Started >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。