本文提出了一種界面設計中的架構模式-界面組裝器模式,它致力于分解界面,將界面和組裝行為解耦,將界面邏輯處理與領域邏輯處理解耦,這樣我們在開發GUI胖客戶端界面應用時可以從眾多的界面控制管理中解脫出來,而專注于我們的后臺業務邏輯的開發。通過該模式,我們可以動態地組裝我們的界面,我們甚至還可以在我們的界面中輕松地插入 transaction 事務或 session 會話管理。
本文將通過分析設計一個架構的過程來講解該模式,從一個簡單的設計模型開始,一步步走向一個完整的架構。借此也向大家展示一個架構設計的思維歷程。另外,本文給出了 Eclipse SWT(Standard Widget Toolkit) 的示例。
問題引出
界面設計常常是模式產生的根源,無論是架構模式,還是設計模式,比如 MVC 模式,Observer,Facade 等,也是整個軟件行業向前發展的動力。遺憾的是,即使在軟件技術發達的今天,界面設計仍是軟件設計中的難以突破的瓶頸之一。我們用過 Java swing 或 Eclipse SWT 作過項目的都知道,要將界面進行分解是很困難的,它不像我們的業務邏輯,可以方便地按職責分解到不同的類中去實現,因為各個業務邏輯之間耦合度很低。但界面邏輯不一樣,你不可能將一個文本框的讀取操作委任到另一個類中去,而且各個界面元素之間相互依賴,無法去除耦合,一般的做法只能是在界面元素的事件觸發(比如按鈕點擊事件)時,將輸入數據封裝成一個數據對象傳給后臺的邏輯處理類來處理。
Eclipse 的 Wizard 框架在界面分解上提供了一種很好的實踐,它可以將按鈕區和其他界面區分離出來,用類似 MVC 的方式實現了 Wizard 框架。但這個實現并非沒有瑕疵,一個缺點是 wizard 是一個 plug-in,這樣的話就減少了可重用性,不能移植到 eclipse 以外的環境。另一個缺點就是它引入了很大的復雜性,而且在一些對界面元素的控制上喪失了一些精細控制的能力,這可能是它過度地強調了自動化和用戶擴展的方便性的緣故。比如,用戶不能將自己的邏輯插入按鈕區的按鈕事件控制中,而只能在自定義區的界面元素 Listener 中設定按鈕區狀態,如果用戶自定義的界面元素很多,就需要很多個 Listener 來組合判斷一個按鈕狀態(如是否進行“下一步”),這樣的話就很影響性能,而且無端地多了一堆復雜的邏輯判斷,也就是說本來只需在按鈕 Listener 事件中處理的邏輯現在要分散在各個界面元素的 Listener 中去處理。這也正是設計上一個值得反復強調的普遍問題:當你要保持架構或設計的完美性時必然會以喪失其他特性為代價。世上永遠沒有完美的東西,我們只關注適合我們的。
我下面要提出的這個架構模式的靈感來自于我的一個真實項目,一個用 RSA(Rational Software Architect)/Eclipse 建模的項目,在 RSA 環境中,讀寫模型都必須在一個特有的 context 下才能操作,這就意味著我在界面的啟動之前必須封裝好輸入數據,關閉之后返回輸出數據,而不是直接處理數據,必須對輸入/輸出數據對象進行封裝。正如前面提到的,這種情況界面設計中很普遍。所以,在模式命名時我用了組裝器-assembler 這個詞,有一層意思是輸入/輸出數據對象的組裝,另一層意思就是界面部件(界面元素的集合)的組裝,這里的組裝還有更深層次的涵義就是指界面部件的可裝配性,可以在運行時動態組裝。而且這個模式可以用任何語言(Java,C++ 等)來實現。在這里我會從一個簡單的設計模型開始,一步步走向一個完整的架構。借此也向大家展示一個架構設計的思維歷程。本文中給出了 Eclipse SWT(Standard Widget Toolkit) 的示例。
![]() ![]() |
![]()
|
界面分解
在 Eclipse SWT 中,有幾個重要的界面部件,一個是Shell-界面的最外層容器,類似 Java Swing 中的 Frame,另一個就是 Composite-界面元素的集合的容器,類似 Java Swing 中的 Panel。我們的界面分解將從 Composite 開始,(Shell 本身是不需要分解的)。我們可以在 Shell 中裝配上一個空的 Composite ,然后我們的具體界面元素都定義在這個 Composite 里。這樣就把 Composite 邏輯從 Shell 中分離出來了,因此我們現在有了 2 個類(目前我們用概念類來表示):
Editor : 該類處理 Shell 的邏輯,如顯示-show,關閉-close,它負責創建和銷毀 EditorComposite。
EditorComposite: 該類處理 Composite 的界面邏輯,如創建界面元素。
有兩點值得注意,第一,Editor 負責 EditorComposite 的創建和銷毀,也就是生命周期的管理。那么我們可以想到,如果我們的界面需要 transaction-事務或 session-會話的管理,那么我們完全可以讓 Editor 來負責這項職責,而不是分散在各個 EditorComposite 中。怎么擴展界面的事務功能可能會很復雜,這已經超出本文的討論范圍,我只是從架構的層面來分析可能有的可擴展性。第二,一個 Editor 可以包括多個 EditorComposite,比如我們的屬性頁,此時我們在Shell中定義的空的 Composite 將會是一個 TabFolder. 還有一種情況,就是我們可以根據某種邏輯來判斷我們需要裝配哪個 EditorComposite。這就要求我們有一個裝配的行為。
![]() ![]() |
![]()
|
界面部件裝配
當我們的裝配邏輯很簡單時,我們可以定義一個 assemble() 方法來負責裝配行為。但是當我們的界面需要組裝一系列 EditorComposite 時,就會牽涉到選擇邏輯,選擇邏輯不一定很復雜,但我們還是應該把這種行為從 Editor 中分離出來,這樣 Editor 可以集中精力負責與用戶交互方面的職責,而裝配行為被分配到一個新的類 EditorAssembler 中,這樣做還有一個好處,就是我們一旦有新的 EditorComposite 需要添加時,我們只需要改變 EditorAssembler 的代碼,而不用修改 Editor 的代碼,這就把變化隔離出來,對 Editor 的修改關閉,對裝配行為的擴展開放。這正是面向對象設計領域反復強調的基本原則-開放-封閉原則(Open-Close Principle)。經過重構后的架構如下圖:
EditorAssembler:該類處理 EditorComposite 的創建,還包括多個 EditorComposite 的選擇邏輯。
這里的選擇邏輯我們可以用 if/else 或 switch/case 來硬編碼,如果邏輯不是很復雜而且今后的修改不會太頻繁的話,用這種方法就足夠了,當然可以考慮將多個 EditorComposite 的裝載信息專門用一個資源/信息類來存儲,這在 EditorComposite 比較多的情況下很有效,這樣每次添加 EditorComposite 就只需要改變這個資源類,這是一個很有用的建模原則(為了簡化我們的核心模型,我在這里不將這個資源類表示出來)。
如果進一步考慮到我們的組裝邏輯會比較復雜,或會比較容易改變,甚至在運行時動態改變,我們就可以將眾多的 EditorComposite 和復雜的邏輯存儲在一個元數據文件中,如 XML 或配置文件。這樣,有新的 EditorComposite 需要支持,或修改裝配邏輯時,不用修改 EditorAssembler 類,只要修改元數據文件即可。這樣就可以很動態的配置我們的界面。這里會有一個架構權衡的問題,元數據由它的優點,也有它的缺點,其一,必須編寫解析它的類,復雜性增加了,其二,不需要編譯是它的優點也是它的缺點,對 XML 或配置文件我們可以隨意修改,只有在運行時發現異常才知道改錯了,而且也可能被人蓄意破壞掉。所以我們只在真的需要很頻繁地修改 EditorComposite 的配置或經常需要增加 EditorComposite 時才采用元數據方案。在這里我傾向于采用資源類方案。
![]() ![]() |
![]()
|
IO 數據裝配
模型設計進行到這里,我們似乎缺少了對數據流的建模,在一個標準的界面程序中,我們首先會有一組輸出數據,比如按”OK”按鈕之后,我們需要將界面元素上的輸入信息輸出到后臺邏輯類來處理或直接調用好幾個邏輯類分別處理不同的界面元素輸入信息了。我們一般習慣上可能直接將這個數據傳遞到邏輯類來處理。這樣做三個缺點:其一,如果我們的數據讀寫處理要求必須在特定的 context 中才能進行,這樣的話我們不能在界面中直接調用后臺邏輯處理類了。其實這種限制并不罕見,在一些涉及底層(比如協議層)的開發時,經常會碰到只能讀不能寫的情況。其二,UI 的可替代性差,假如我們今后需要一種方案可以在運行時可以替換不同的 UI 但輸出的數據是一樣的,也就是說后臺邏輯處理完全一致,那么這種情況我們就需要每一個 UI 自己去調用后臺邏輯類,重復編碼,而且可能由于程序員的失誤每一個 UI 用了一個邏輯類,從而導致一個完全相同行為的類有了好幾個不一致實現版本,這樣不僅嚴重違反了面向對象設計,而且還可能產生難以預料的 bug,難以維護。其三,UI 的可重用性差,對于上面多個 UI 對應一種邏輯處理的例子,由于 UI 依賴了后臺邏輯類,如果今后要修改邏輯類結構的話,我們就需要修改每一個 UI。如果我們還有一種需求是要支持一個 UI 在不同的環境下需要不同的后臺邏輯類時,我們可能要專門在一個 UI 中設置一個屬性來標識后臺將要使用的邏輯類。這會很復雜。
解決上面幾個缺點只有一種方法,就是將后臺邏輯類與 UI 解耦。如果我們把要處理的輸出數據打包成一個輸出數據對象從界面統一輸出,再由 UI 的調用者決定調用哪一個后臺邏輯類來處理數據,而不是 UI 自己決定調用行為。
還有一個輸入數據對象就很好理解了,我們調用 UI 時,可能某些界面元素需要的從環境中動態裝載數據,比如一個下列列表,還有一些我們上一次配置好的數據這次需要更新,也需要將已有數據導入。所以我們需要一個輸入數據對象。這就得到下面的模型:
InputDataObject:該類封裝了輸入數據。由 EditorComposite 負責解析這些數據。
OutputDataObject:該類封裝了輸出數據。由 EditorComposite 負責產生這些數據。
Editor 負責傳輸這兩個數據對象。
![]() ![]() |
![]()
|
重構架構
從上面的模型我們可以看出 Editor 類其實相當于一個 Facade,所有的界面與用戶的交互都由它負責集中調度管理,Editor 會將裝配行為分配給 EditorAssembler 類來處理,它還負責臨時存儲輸入輸出數據,當然如果我們有類似 transaction 或 session 之類的處理會由 Editor 委派到別的相關類去處理。應用 Facade 設計模式,我們可以給 Editor 改個名字叫 EditorFacade,這樣更能體現設計者的意圖,千萬不要忽視類的命名,設計是一門嚴肅的科學,每一個細節我們都不能茍且,對架構的設計更要嚴謹。命名可以起到溝通的作用,還能起到提醒的功能,EditorFacade 提醒我們以后要給它添加新的行為是記住它是一個 Facade,不能將不相干的職責分配進來。
另外,我發現添加了 InputDataObject 類后,EditorComposite 就有兩個職責:裝載界面元素初始化數據(一些需要從環境中動態獲得的輸入數據,從 InputDataObject 對象中獲得)和顯示上一次編輯的數據(也從 InputDataObject 對象中獲得),我們定義兩個方法來分別處理:loadDataInfo()-裝載初始化數據;showPreInfo()-顯示上一次編輯的數據。當然,一般來說這兩個方法是私有的-private,因為這是 EditorComposite 自身的內部邏輯,但我們在這個架構中讓它成為公有的-public,是因為我們可以在 EditorAssembler 類中集中控制它的調用,而且每一個 EditorComposite 都會有裝載初始化數據和顯示已有數據的行為,那么為什么不抽象出來呢,以便讓 EditorComposite 的開發提供者更清楚自己的職責,雖然這么做有點破壞 EditorComposite 的封裝性和其中方法的私密性,但從架構的角度來講這種破壞是合適的,值得的。
再看看前面的 EditorAssembler 類,它其實有兩個職責,一個是創建 EditorComposite,還有一個就是從幾個 EditorComposite 選擇出一個的判斷邏輯。如果我們把這兩個不相干的職責解耦,應用 Factory 設計模式,就可以將創建 EditorComposite 的工作委任給一個 EditorCompositeFactory 的新類。
經過以上幾項重構后得到以下概念類模型:
![]() ![]() |
![]()
|
實現架構
經過上面的分析建模,我們可以開始實現架構了,從上面的概念模型我們可以很容易地抽象出相應的接口來。首先,我們看看 EditorFacade 類,基于我們上面的討論,不同的界面可能有不同的需求,比如有的要支持 transaction-事務,那么 EditorFacade 的實現就會不同,所以我們有必要提取出一個接口來表示,下面列出了這個接口 IEditorFacade:
|
那么 EditorFacade 類的部分代碼如下:
|
下一步,我們將兩個 IO 數據類定義出來,很顯然,不同的界面會有不同的輸入輸出數據,在這里我們只能定義出兩個抽象的接口 IInputDataObject 和 IOutputDataObject,它們繼承了序列化 java.io.Serializable 接口,里面并無其它內容。這里注意一點,空的接口并非無意義,它可以起到標識的作用,另外,它隱藏了具體實現,在傳遞數據時傳遞者不用知道具體數據內容,這樣傳遞者類具有更好的重用性,而且具體數據類也不用暴露給不該知道它的類-傳遞者類,這正是另一個面向對象的基本原則-迪米特法則(LoD):不要和陌生人說話。下面給出 IInputDataObject 的清單:
|
接下來,我們看看 EditorAssembler 類的實現,根據前面的討論,它封裝了界面的裝配邏輯,一定會被修改的,那么我們就需要一個接口 IEditorAssembler 來規范它的行為,在這里我還給出了一個抽象類 AbstractEditorAssembler,實現了裝載單個 EditorComposite 的方法,另外我還給出了一個具體的 EditorAssembler 類,這是一個每次只裝載一個 EditorComposite 的例子,代碼清單如下:
|
|
|
接下來,是 EditorCompositeFactory 的實現,這個類的實現比較簡單,只是根據類名產生類:
|
最后,就是 EditorComposite 的實現了,很顯然每個界面的 EditorComposite 都不一樣,所以我們在這里只定義了一個接口來規范一下行為,具體的 EditorComposite 實現我會在代碼附件中的測試包中給出。
|
下面,我們編寫一些測試代碼來測試它,這個測試應用是要編寫一個電話簿,為了簡單起見我只定義了一個 EditorComposite-PhoneBookComposite, 在編寫組裝邏輯時也只是示例性地改變了一下界面的標題和尺寸。(詳細代碼見代碼下載)
|
|
接下來,我們可以看一下架構的實現模型,注意,我在畫下面的 UML 圖時采用了分層的方式,所有的接口都會在上面一層,實現在下面一層,這種分層畫 UML 圖的方法有助于我們理清架構的思路,也便于與開發組的其他成員溝通。
至此,我們完成了界面組裝器的核心架構的實現,注意,這只是一種實現,并不是界面組裝模式的全部,作為一種模式,它必須有更廣的外延,下面我們將要探討它的模式本質。
![]() ![]() |
![]()
|
模式與價值觀
這個模式是一種架構模式,模式的定義有三個要素:問題,環境,解決方案,這在前面我們已經詳細地論述過了,在這里我們討論一下其他的參量。每個模式都有它自己獨特的價值觀,那么界面組裝器模式給我們提供了什么樣的價值觀呢?
首先,它的精髓在于這種分解界面,將界面和組裝行為解耦的設計思想,這在擁有多個界面的應用中很有益處,當界面多的時候,如果沒有一個比較集中的調度控制方式來對這些界面進行管理,就會形成界面行為無法規范,風格各異,更難以作 transaction 事務或 session 會話控制。這在小型應用開發中也許不很明顯,但在一個大中型應用中對分散的不規范的界面行為進行控制將會是一場惡夢,到最后可能整個開發組都沉浸于 bug 的修復和界面修改中,而無暇顧及領域邏輯代碼的編寫。而通過將界面和組裝行為解耦就可以讓開發人員集中精力于界面邏輯和領域邏輯的開發,而不用每一個界面都去編寫管理界面的代碼。其實這也是模式化的一個優點,模式可以優化我們的架構,可以規范開發行為,因此也會節省開發成本。
其二,它將界面邏輯處理與領域邏輯處理(也就是數據邏輯處理)解耦。我們將數據輸入輸出從界面模型中抽取出來,沒有與界面耦合在一起,這就獲得巨大的好處,第一,我們可以在界面之外來處理數據,在我們的領域類中處理這些數據,也就是說界面只是提供了一個定義數據的載體,而這些數據是被領域邏輯類使用的,而我們開發的主要精力也應該放在處理業務邏輯的領域類上。第二,現在我們將界面和領域類解耦,這樣我們的界面和領域類都可以獨立地變化,相互之間沒有任何依賴,這就很方便于我們開發人員的分工,編寫界面的開發組不用依賴于編寫后臺邏輯類的開發組。第三,在做單元測試-unit test 時,開發后臺邏輯類的人員可以單獨測試領域類,而開發界面的人員也可以單獨測試界面邏輯。第四,當我們有多套界面機制時,我們的后臺邏輯類可以很方便地接插上去,比如我們要支持 GUI(SWT/Java Swing)和 Web 方式,那么我們的領域類和數據類無需任何更改就可以方便的切換。第五,我們還能獲得好處,就是數據類的可重用,如果我們沒有輸入輸出數據類的封裝行為,那可能我們會將各條數據散落在界面類中直接處理,這樣當你要換一種界面機制時就必須重寫這部分邏輯,無法重用。
作為一種模式,它會有很多的變體,也就是說它不拘泥于我們給出的這種外在實現方式,它還有其它的實現,例子中我們只是組裝一個 EditorComposite,我們當然可以一次組裝幾個 EditorComposite,比如一個復雜的界面會有好幾個 EditorComposite 組成,或者像屬性頁,并列著有好幾個 EditorComposite,我們只需要自己實現一個組裝器類 Assembler 就可以。又或者我們可以在運行界面時動態地在幾個界面之間切換界面,這可能會復雜一些,也受限于平臺或語言的技術實現,但也并非不可實現。
對于該模式的適用性,我想它主要適用于那些每次裝載一個 EditorComposite 或屬性頁的情況,至于是否可以作為 Wizard 向導界面的實現架構,還需進一步探索,不過從這個模式的概念層次上來看,它的關鍵的價值觀是完全可以用于實現 Wizard 向導界面的,只不過具體實現時可能會對現在的架構變動較大。另外這個模式主要適用于 GUI 客戶端界面,對于 Web 形式的界面,已經有別的模式可以考慮。
我們還可以討論一下界面組裝器模式與別的模式之間的關系。在界面架構界我們已經有了大名鼎鼎的 MVC 模式,為什么還需要界面組裝器模式呢?雖然 MVC 模式解決的也是界面與領域邏輯處理的解耦,但它的出發點主要是針對一個業務邏輯處理后會有好幾個界面同時需要更新顯示,也就是說它的貢獻在于他的及時傳播數據變更的能力,這和我們的模式是不一致的,我們主要解決界面的分解組裝和數據剝離的問題,當然他們在結構上有些相似之處,我們的 EditorFacade 有點像 MVC 中的控制器。
![]() ![]() |
![]()
|
結束語
本文所講述的界面組裝器模式為我們提供了將界面和組裝行為解耦,將界面邏輯處理與領域邏輯處理解耦的價值觀,在 GUI 胖客戶端型界面中可以大量應用,筆者已經在幾個大型項目中應用了它,所以它的可行性是經過實踐檢驗的。當然,任何模式,不管是設計模式還是架構模式,都有它的適用性,只有合適的,沒有絕對的優劣,我們是否應用模式是在于模式為我們提供的價值觀是否和我們的需求期望符合,而不是因為別的原因。
![]() ![]() |
![]()
|
參考文獻
![]() ![]() |
![]()
|
下載
描述 | 名字 | 大小 | 下載方法 |
---|---|---|---|
Source code | UIAssembler.jar | 26KB | HTTP |