GUI 架構
組織豐富客戶端系統程式碼的方法有很多種。在此,我將討論一些我認為最有影響力的方法,並介紹它們與模式的關聯性。
2006 年 7 月 18 日
這是 進階企業應用程式架構開發 的一部分,我在 2000 年代中期撰寫。遺憾的是,自那時起,太多其他事情吸引了我的注意力,所以我沒有時間進一步研究它們,我也看不到未來會有太多時間。因此,這份材料仍處於草稿形式,而且在我有時間再次研究它之前,我不會進行任何更正或更新。
圖形使用者介面已成為我們軟體環境中熟悉的一部分,無論是使用者或開發人員。從設計角度來看,它們代表系統設計中的一組特定問題,這些問題導致許多不同但相似的解決方案。
我的興趣是找出常見且有用的模式,供應用程式開發人員在豐富客戶端開發中使用。我在專案審查中看過各種設計,也看過以更永久方式撰寫的各種設計。在這些設計中包含有用的模式,但要描述它們通常不容易。以模型-檢視-控制器為例。它通常被稱為一種模式,但我認為將它視為一種模式並無太大用處,因為它包含許多不同的想法。在不同地方閱讀 MVC 的不同人會從中獲得不同的想法,並將這些想法描述為「MVC」。如果這還不足以造成混淆,那麼你會受到 語意擴散 所產生的 MVC 誤解影響。
在本文中,我想探討許多有趣的架構,並描述我對它們最有趣特色的詮釋。我希望這能提供一個背景,以了解我所描述的模式。
在某種程度上,你可以將本文視為一種知識史,追溯多年來 UI 設計中的各種架構中的想法。但我必須對此提出警告。理解架構並不容易,特別是當其中許多架構都在改變和消亡時。追蹤想法的傳播甚至更困難,因為人們從同一個架構中讀到不同的東西。特別是,我並沒有對我所描述的架構進行詳盡的檢查。我所做的是參考設計的常見描述。如果那些描述遺漏了一些東西,我完全不知道。所以不要將我的描述視為權威。此外,如果我不認為它們特別相關,我就會省略或簡化一些東西。請記住,我的主要興趣是基礎模式,而不是這些設計的歷史。
(這裡有一個例外,就是我確實有機會使用正在執行的 Smalltalk-80 來檢視 MVC。我不會將我的檢視描述為詳盡無遺,但它確實揭露了常見描述中所遺漏的重要事項,這讓我對我這裡所描述的其他架構更加謹慎。如果您熟悉其中一種架構,而且您發現我遺漏了某些重要的資訊,請務必告訴我。我也認為對此領域進行更詳盡的調查將會是良好的學術研究主題。)
表單與控制項
我將從一個既簡單又熟悉的架構開始探討。它沒有通用的名稱,因此在本文中,我將稱之為「表單與控制項」。它是一個熟悉的架構,因為它是由 90 年代的客戶端伺服器開發環境所推廣的,例如 Visual Basic、Delphi 和 Powerbuilder 等工具。它持續被廣泛使用,儘管也常被像我這樣的設計怪咖所貶低。
為了探討它,以及其他架構,我將使用一個常見的範例。在我居住的新英格蘭,有一個政府計畫用來監控大氣中冰淇淋微粒的數量。如果濃度過低,這表示我們吃的冰淇淋不夠多,這對我們的經濟和公共秩序構成嚴重的風險。(我喜歡使用與您通常在這類書籍中發現的範例一樣不切實際的範例。)
為了監控我們的冰淇淋健康,政府在新英格蘭各州設置了監測站。該部門使用複雜的大氣模型為每個監測站設定目標。工作人員會定期進行評估,前往各個監測站並記錄實際的冰淇淋微粒濃度。此使用者介面允許他們選擇一個監測站,並輸入日期和實際值。然後,系統會計算並顯示與目標的差異。當差異低於目標 10% 或更多時,系統會以紅色突顯差異;高於目標 5% 或更多時,則以綠色突顯差異。

圖 1:我將用作範例的使用者介面。
當我們檢視這個畫面時,我們可以看到在組合它的過程中有一個重要的區分。表單是特定於我們的應用程式,但它使用的是通用的控制項。大多數 GUI 環境都附帶許多我們可以在應用程式中使用的常用控制項。我們可以自行建構新的控制項,而且通常這是一個好主意,但通用的可重複使用控制項和特定表單之間仍然存在區別。即使是特別編寫的控制項也可以在多個表單中重複使用。
表單包含兩個主要職責
- 畫面配置:定義控制項在畫面上的排列方式,以及它們彼此之間的階層結構。
- 表單邏輯:無法輕易編寫到控制項本身的行為。
大多數 GUI 開發環境允許開發人員使用圖形編輯器定義螢幕配置,讓您可以在表單中的空間上拖放控制項。這幾乎可以處理表單配置。這樣可以輕鬆在表單上設定令人滿意的控制項配置(儘管這並不總是最好的方法 - 我們稍後會探討)。
控制項顯示資料 - 在這種情況下,關於讀數。這些資料幾乎總是來自其他地方,在這種情況下,我們假設是 SQL 資料庫,因為這是大多數這些客戶端伺服器工具假設的環境。在大多數情況下,涉及三個資料副本
- 一個資料副本位於資料庫本身。此副本是資料的永久記錄,因此我稱之為記錄狀態。記錄狀態通常透過各種機制與多個人共用且可見。
- 另一個副本位於應用程式內的記憶體中記錄集。大多數客戶端伺服器環境提供讓此操作變得容易的工具。這些資料僅與應用程式和資料庫之間的一個特定階段相關,因此我稱之為階段狀態。基本上,這提供了一個暫時的資料本機版本,使用者會處理此版本,直到他們將其儲存或提交回資料庫 - 在這一點上,它會與記錄狀態合併。我不會擔心這裡協調記錄狀態和階段狀態的問題:我在[P of EAA]中探討了各種技術。
- 最後一個副本位於 GUI 元件本身內部。嚴格來說,這是他們在螢幕上看到的資料,因此我稱之為螢幕狀態。螢幕狀態和階段狀態如何保持同步對 UI 而言很重要。
保持螢幕狀態和階段狀態同步是一項重要的任務。一個有助於讓此操作變得更簡單的工具是資料繫結。這個想法是,對控制項資料或基礎記錄集的任何變更都會立即傳播到另一個。因此,如果我變更螢幕上的實際讀數,文字欄位控制項會有效地更新基礎記錄集中正確的欄位。
一般資料繫結會變得棘手,因為如果你必須避免控制項的變更、變更記錄集、更新控制項、更新記錄集……的循環。使用流程有助於避免這些問題 - 我們在開啟畫面時從會話狀態載入至畫面,之後畫面狀態的任何變更都會傳播回會話狀態。畫面開啟後,會話狀態直接更新的情況並不常見。因此,資料繫結可能不會完全雙向 - 僅限於初始上傳,然後將變更從控制項傳播至會話狀態。
資料繫結 很好地處理了用戶端伺服器應用程式的許多功能。如果我變更實際值,欄位會更新,甚至變更所選車站也會變更記錄集中目前選取的列,這會導致其他控制項更新。
這許多行為是由架構建構器內建的,他們會檢視常見需求並讓滿足這些需求變得容易。特別是透過設定控制項上的值(通常稱為屬性)來完成這項工作。控制項透過簡單的屬性編輯器設定其欄位名稱,以繫結至記錄集中的特定欄位。
使用資料繫結,搭配正確的參數化類型,可以幫助您走很長一段路。但是,它無法帶您走完全程 - 幾乎總是有些邏輯不符合參數化選項。在此情況下,計算變異數就是一個不符合此內建行為的範例 - 由於它是應用程式特定的,因此通常會存在於表單中。
為了讓這項功能運作,表單需要在實際欄位的值變更時收到警示,這需要通用文字欄位在表單上呼叫一些特定行為。這比採用類別庫並透過呼叫它來使用它還要複雜一些,因為這會涉及控制反轉。
有各種方法可以讓這類事情運作 - 用於用戶端伺服器工具組的常見方法是事件的概念。每個控制項都有一個它可以引發的事件清單。任何外部物件都可以告訴控制項它對某個事件有興趣 - 在這種情況下,控制項會在事件引發時呼叫該外部物件。這基本上只是觀察者模式的重新表述,其中表單正在觀察控制項。架構通常會提供一些機制,讓表單開發人員可以在事件發生時呼叫的子常式中撰寫程式碼。事件和常式之間的連結方式會因平台而異,而且對此討論並不重要 - 重點是存在一些機制來讓它發生。
當表單中的例程取得控制權後,它便可以執行任何必要的動作。它可以執行特定行為,並視需要修改控制項,並依賴資料繫結將這些變更傳播回工作階段狀態。
這也是必要的,因為資料繫結並非總是存在。Windows 控制項有一個龐大的市場,並非所有控制項都執行資料繫結。如果沒有資料繫結,則由表單執行同步。這可以透過在初始化時將資料從記錄集拉出到小工具中,並在按下儲存按鈕時將變更的資料複製回記錄集來執行。
讓我們檢查我們對實際值的編輯,假設存在資料繫結。表單物件持有對一般控制項的直接參照。螢幕上每個控制項都會有一個,但我這裡只對實際、差異和目標欄位感興趣。

圖 2:表單和控制項的類別圖
文字欄位宣告一個文字變更事件,當表單在初始化期間組裝螢幕時,它會訂閱該事件,將其繫結到它自己的方法 - 這裡是 actual_textChanged
。

圖 3:使用表單和控制項變更類別的順序圖。
當使用者變更實際值時,文字欄位控制項會引發其事件,並透過架構繫結的魔力執行 actual_textChanged
。此方法從實際和目標文字欄位取得文字,執行減法,並將值放入差異欄位。它也會找出值應顯示的顏色,並適當地調整文字顏色。
我們可以用一些簡短的說明摘要架構
- 開發人員撰寫使用一般控制項的應用程式特定表單。
- 表單描述其上控制項的配置。
- 表單觀察控制項,並有處理程序方法來回應控制項引發的有趣事件。
- 簡單的資料編輯透過資料繫結來處理。
- 複雜的變更會在表單的事件處理程序方法中完成。
模型檢視控制器
UI 開發中最廣為引用的模式可能是 Model View Controller (MVC) - 它也是最常被錯誤引用的。我已經記不清有多少次看到某些東西被描述為 MVC,但結果根本不像。坦白說,造成這種情況的原因在於,經典 MVC 的部分內容對現今的豐富用戶端來說並不太有意義。但我們現在先來看看它的起源。
當我們檢視 MVC 時,重要的是要記住這是首次嘗試在任何規模上執行嚴肅的 UI 工作。在 70 年代,圖形使用者介面並非普遍存在。我剛剛描述的表單和控制項模型出現在 MVC 之後,我之所以先描述它,是因為它比較簡單,但並不總是好的。我將再次使用評估範例來討論 Smalltalk 80 的 MVC,但請注意,我將對 Smalltalk 80 的實際細節做一些自由發揮,因為它是一個單色系統。
MVC 的核心,以及對後續架構影響最大的概念,就是我稱之為分離式簡報。 分離式簡報背後的想法是,在模擬我們對真實世界認知的網域物件,以及我們在螢幕上看到的 GUI 元素簡報物件之間做出明確的區分。網域物件應該是完全獨立的,並且可以在不參考簡報的情況下工作,它們也應該能夠支援多個簡報,甚至可以同時支援。這種方法也是 Unix 文化的重要組成部分,並且持續至今,允許透過圖形和命令列介面操作許多應用程式。
在 MVC 中,網域元素稱為模型。模型物件完全不知道 UI。為了開始討論我們的評估 UI 範例,我們將模型視為讀數,其中包含所有有趣資料的欄位。(我們稍後將看到,由於清單方塊的存在,這使得模型是什麼這個問題變得更加複雜,但我們將暫時忽略該清單方塊。)
在 MVC 中,我假設的是常規物件的網域模型,而不是表單和控制項中我所擁有的記錄集概念。這反映了設計背後的普遍假設。表單和控制項假設大多數人都希望輕鬆地操作來自關聯式資料庫的資料,MVC 假設我們正在操作常規 Smalltalk 物件。
MVC 的簡報部分由兩個剩餘元素組成:檢視和控制器。控制器的任務是接收使用者的輸入,並找出如何處理它。
在這裡,我應該強調的是,不只有一個檢視和控制器,您會為螢幕的每個元素、每個控制項和整個螢幕準備一對檢視控制器。因此,對使用者的輸入做出反應的第一部分是各種控制器協作以查看誰被編輯了。在這種情況下,那是實際文字欄位,因此該文字欄位控制器現在將處理接下來會發生的事情。

圖 4:模型、檢視和控制器之間的基本相依性。(我稱之為基本,因為事實上檢視和控制器確實直接連結彼此,但開發人員大多不會使用這個事實。)
與後續環境一樣,Smalltalk 發現您想要可以重複使用的通用 UI 元件。在這種情況下,元件將是檢視控制器對。兩者都是通用類別,因此需要插入應用程式特定的行為。將會有評估檢視,它將表示整個螢幕並定義較低層級控制項的配置,在這個意義上類似於表單和控制器中的表單。然而,與表單不同,MVC 在評估控制器上沒有較低層級元件的事件處理常式。

圖 5:冰淇淋監控器顯示的 MVC 版本類別
文字欄位的組態來自於提供連結至其模型(讀取),並告知在文字變更時要呼叫哪個方法。當畫面初始化時,會設定為「#actual:」(開頭的「#」表示 Smalltalk 中的符號或內部字串)。然後,文字欄位控制器會對讀取執行該方法的反射呼叫,以進行變更。基本上,這與資料繫結發生的機制相同,控制項會連結至基礎物件(列),並告知它會控制哪個方法(欄)。

圖 6:變更 MVC 的實際值。
因此,沒有整體物件會觀察低階小工具,而是低階小工具會觀察模型,而模型本身會處理許多表單會做出的決策。在這種情況下,當要找出差異時,讀取物件本身就是執行此動作的自然位置。
觀察者確實會出現在 MVC 中,這確實是歸功於 MVC 的其中一個概念。在這種情況下,所有檢視和控制器都會觀察模型。當模型變更時,檢視會做出反應。在這種情況下,實際文字欄位檢視會收到通知,表示讀取物件已變更,並呼叫定義為該文字欄位的外觀方法(在本例中為 #actual),然後將其值設定為結果。(它會對顏色執行類似的動作,但這會引發我稍後會提到的問題。)
您會注意到,文字欄位控制器並未設定檢視本身的值,而是更新模型,然後讓觀察者機制處理更新。這與表單和控制項方法有很大不同,後者會更新控制項,並依賴資料繫結來更新基礎記錄集。我將這兩種風格描述為模式:流程同步和觀察者同步。這兩種模式描述了處理畫面狀態和工作階段狀態之間同步觸發的替代方法。表單和控制項會透過應用程式流程來執行此動作,直接控制需要更新的各種控制項。MVC 會透過更新模型來執行此動作,然後依賴觀察者關係來更新觀察該模型的檢視。
當資料繫結不存在時,流程同步會更加明顯。如果應用程式需要自行執行同步,則通常會在應用程式流程的重要時間點執行,例如開啟畫面或按下儲存按鈕時。
觀察者同步的其中一個後果是,控制器對於使用者控制特定小工具時需要變更的其他小工具非常無知。雖然表單需要持續追蹤事項,並確保整體畫面狀態在變更時保持一致(這可能會讓複雜畫面變得相當複雜),但觀察者同步中的控制器可以忽略所有這些事項。
如果有多個開啟的螢幕檢視同一個模型物件,這種有用的無知會特別方便。經典的 MVC 範例是一個資料試算表般的螢幕,其中有幾個不同的資料圖表在不同的視窗中。試算表視窗不需要知道其他哪些視窗是開啟的,它只會變更模型,而 觀察者同步 會處理其餘部分。使用 流程同步 時,它需要一些方法來知道哪些其他視窗是開啟的,才能告訴它們更新。
雖然 觀察者同步 很棒,但它確實有一個缺點。 觀察者同步 的問題是觀察者模式本身的核心問題 - 你無法透過閱讀程式碼來判斷發生了什麼事。當我試圖找出一些 Smalltalk 80 螢幕是如何運作時,我被這一點強烈地提醒了。我可以透過閱讀程式碼來了解到目前為止的狀況,但一旦觀察者機制啟動,我唯一能看到發生什麼事的方法就是透過除錯程式和追蹤陳述。觀察者行為很難理解和除錯,因為它是隱含的行為。
雖然從順序圖中可以特別明顯地看出同步的不同方法,但最重要的也是最有影響力的差異是 MVC 使用 分離呈現。計算實際值與目標值之間的差異是網域行為,它與 UI 無關。因此,遵循 分離呈現 表示我們應該將其置於系統的網域層中 - 這正是讀取物件所代表的。當我們檢視讀取物件時,差異功能在沒有任何使用者介面概念的情況下就能完全說得通。
然而,在這個時候,我們可以開始檢視一些複雜性。有兩個區域是我略過一些妨礙 MVC 理論的尷尬點。第一個問題區域是要處理差異的顏色設定。這不應該真正放入網域物件中,因為我們用來顯示值的顏色並非網域的一部分。處理此問題的第一步是了解邏輯的一部分是網域邏輯。我們在這裡所做的是對差異做出定性的陳述,我們可以將其稱為好(超過 5%)、壞(低於 10%)和正常(其餘)。做出該評估肯定屬於網域語言,將其對應到顏色並變更差異欄位則是檢視邏輯。問題在於我們將這個檢視邏輯放在哪裡 - 它不是我們標準文字欄位的一部分。
早期 Smalltalk 使用者曾面臨此類問題,並提出了一些解決方案。上面顯示的解決方案是較不理想的,會犧牲部分領域的純粹性以使運作正常。我承認偶爾會執行不純粹的動作,但我試著不要養成習慣。
我們可以執行與 Forms and Controls 類似的方式,讓評估畫面檢視觀察差異欄位檢視,當差異欄位變更時,評估畫面可以做出反應並設定差異欄位文字顏色。此處的問題包括更頻繁使用觀察者機制,使用次數越多,複雜度就會呈指數增加,以及各種檢視之間的額外耦合。
我比較偏好的方式是建立一種新的 UI 控制類型。基本上,我們需要的是一種 UI 控制,會向領域詢問品質值,將其與一些內部值和顏色表格進行比較,並據此設定字型顏色。表格和詢問領域物件的訊息會由評估檢視在組裝時設定,就像設定要監控的欄位方面一樣。如果我可以輕鬆地將文字欄位子類別化以新增額外行為,這種方法會非常有效。這顯然取決於元件的設計是否能順利進行子類別化,Smalltalk 讓這變得非常容易,其他環境則可能讓這變得更困難。

圖 7:使用可以設定為判斷顏色的文字欄位特殊子類別。
最後一種途徑是建立一種新的模型物件,一種以畫面為導向,但仍獨立於小工具的模型物件。這將會是畫面的模型。與閱讀物件上相同的函式會委派給閱讀,但會新增支援僅與 UI 相關行為的函式,例如文字顏色。

圖 8:使用中介 簡報模型 來處理檢視邏輯。
最後一個選項適用於許多情況,而且正如我們將看到的,已成為 Smalltalk 使用者遵循的常見途徑,我稱之為 簡報模型,因為它是一種專為簡報層設計,因此是簡報層一部分的模型。
「表示模型」也能有效解決另一個表示邏輯問題 - 表示狀態。基本的 MVC 概念假設檢視的所有狀態都能從模型的狀態衍生。在這種情況下,我們如何找出在方塊清單中選取哪個電台?「表示模型」透過提供我們一個放置這類狀態的地方來解決這個問題。如果我們有只有在資料變更時才會啟用的儲存按鈕,也會發生類似的問題 - 這又是關於我們與模型互動的狀態,而不是模型本身。
所以,現在我想是時候針對 MVC 提供一些精簡的資訊了。
VisualWorks 應用程式模型
如同我在上面所討論的,Smalltalk 80 的 MVC 非常有影響力,並具有一些出色的功能,但也有一些缺點。隨著 Smalltalk 在 80 年代和 90 年代的發展,這導致經典 MVC 模型出現了一些顯著的變化。事實上,幾乎可以說 MVC 已經消失了,如果你認為檢視/控制器分離是 MVC 的一個重要部分 - 這個名稱確實暗示了這一點。
從 MVC 中明顯有效的概念是 分離表示 和 觀察者同步。因此,隨著 Smalltalk 的發展,這些概念仍然存在 - 事實上,對許多人來說,它們是 MVC 的關鍵要素。
Smalltalk 在這些年也出現了分歧。Smalltalk 的基本概念,包括(最小的)語言定義保持不變,但我們看到多個 Smalltalk 隨著不同的函式庫而發展。從 UI 的角度來看,這變得很重要,因為多個函式庫開始使用原生小工具,也就是「表單和控制項」樣式所使用的控制項。
Smalltalk 最初是由 Xerox Parc 實驗室開發的,他們分拆出一家獨立的公司 ParcPlace 來行銷和開發 Smalltalk。ParcPlace Smalltalk 被稱為 VisualWorks,並強調成為一個跨平台系統。早在 Java 出現之前,你就可以在 Windows 中編寫 Smalltalk 程式,並立即在 Solaris 上執行它。因此,VisualWorks 沒有使用原生小工具,並將 GUI 完全保留在 Smalltalk 中。
在對 MVC 的討論中,我以一些 MVC 問題作結,特別是如何處理檢視邏輯和檢視狀態。VisualWorks 透過提出稱為應用程式模型的建構來改善其架構以處理此問題,這項建構朝向簡報模型邁進。使用類似簡報模型之類事物的概念對 VisualWorks 來說並不新鮮,原始的 Smalltalk 80 程式碼瀏覽器非常類似,但 VisualWorks 應用程式模型將其完全納入架構中。
此類 Smalltalk 的關鍵元素是將屬性轉換為物件的概念。在我們對具有屬性的物件的通常概念中,我們會想到 Person 物件具有名稱和地址的屬性。這些屬性可能是欄位,但也可以是其他東西。通常有存取屬性的標準慣例:在 Java 中,我們會看到 temp = aPerson.getName()
和 aPerson.setName("martin")
,在 C# 中,會是 temp = aPerson.name
和 aPerson.name = "martin"
。
屬性物件透過讓屬性傳回包裝實際值的物件來改變此情況。因此,在 VisualWorks 中,當我們要求名稱時,我們會收到一個包裝物件。然後,我們透過要求包裝物件提供其值來取得實際值。因此,存取一個人的名稱會使用 temp = aPerson name value
和 aPerson name value: 'martin'
屬性物件讓小工具和模型之間的對應變得更容易。我們只需要告訴小工具要傳送什麼訊息才能取得對應的屬性,而小工具就會知道使用 value
和 value:
存取適當的值。VisualWorks 的屬性物件也允許您使用訊息 onChangeSend: aMessage to: anObserver 設定觀察者。
您實際上不會在 Visual Works 中找到稱為屬性物件的類別。相反地,有許多類別遵循 value/value:/onChangeSend: 協定。最簡單的是 ValueHolder,它只包含其值。與此討論更相關的是 AspectAdaptor。AspectAdaptor 允許屬性物件完全包裝另一個物件的屬性。這樣,您就可以在 PersonUI 類別中定義一個屬性物件,透過類似這樣的程式碼包裝 Person 物件上的屬性
adaptor := AspectAdaptor subject: person adaptor forAspect: #name adaptor onChangeSend: #redisplay to: self
因此,讓我們看看應用程式模型如何符合我們的執行範例。

圖 9:執行範例中 Visual Works 應用程式模型的類別圖
使用應用程式模型和傳統 MVC 之間的主要差異在於,我們現在在網域模型類別 (Reader) 和小工具之間有一個中間類別,這就是應用程式模型類別。小工具不會直接存取網域物件,它們的模型是應用程式模型。小工具仍然細分為檢視和控制器,但除非您正在建置新的,否則此區別並不重要。
當您組裝 UI 時,您會在 UI 繪製器中執行此操作,同時在該繪製器中為每個小工具設定外觀。外觀對應於應用程式模型中傳回屬性物件的方法。

圖 10:顯示如何更新實際值來更新變異文字的順序圖。
圖 10 顯示基本更新順序如何運作。當我在文字欄位中變更值時,該欄位會更新應用程式模型中屬性物件中的值。該更新會傳遞到基礎網域物件,更新其實際值。
此時,觀察者關係會啟動。我們需要設定事項,以便更新實際值會導致讀取指示它已變更。我們透過在實際值修改器中置入呼叫來執行此操作,以指示讀取物件已變更,特別是變異外觀已變更。在設定變異的外觀適配器時,很容易指示它觀察讀取器,因此它會接收更新訊息,然後將其轉發至其文字欄位。然後,文字欄位會啟動取得新值,同樣透過外觀適配器。
使用應用程式模型和屬性物件有助於我們連結更新,而無需撰寫大量程式碼。它也支援細緻同步(我不認為這是一件好事)。
應用程式模型讓我們能夠將特定於 UI 的行為和狀態與真正的網域邏輯分開。因此,我先前提到的其中一個問題,在清單中保留目前選取的項目,可以使用特定類型的外觀適配器來解決,該適配器會包裝網域模型的清單,並儲存目前選取的項目。
然而,所有這些的限制在於,對於更複雜的行為,您需要建構特殊的小工具和屬性物件。舉例來說,提供的物件組不提供將變異的文字顏色連結到變異程度的方法。將應用程式和網域模型分開確實讓我們能以正確的方式區分決策制定,但接著為了使用觀察方面適配器的工具,我們需要建立一些新的類別。這通常被視為過於繁瑣的工作,因此我們可以透過允許應用程式模型直接存取小工具來簡化這類事情,如同 圖 11 所示。

圖 11:應用程式模型透過直接操作小工具來更新顏色。
像這樣直接更新小工具並非 簡報模型 的一部分,這就是為什麼視覺化工作應用程式模型並非真正的 簡報模型。許多人將這種直接操作小工具的需求視為一種不太光彩的權宜之計,並有助於發展模型-檢視-簡報者方法。
因此,現在關於應用程式模型的重點整理如下:
模型-檢視-簡報者 (MVP)
MVP 是一種架構,最初出現在 IBM,並在 1990 年代於 Taligent 更加廣為人知。最常見的引用方式是透過 Potel 論文。這個想法進一步由 Dolphin Smalltalk 的開發人員普及並描述。正如我們所見,這兩種描述並非完全吻合,但其基本概念已廣受歡迎。
為了接近 MVP,我發現思考 UI 思考的兩條支流之間的重大不匹配很有幫助。一方面是表單和控制器架構,這是 UI 設計的主流方法,另一方面是 MVC 及其衍生品。表單和控制項模型提供了一個易於理解的設計,並在可重複使用的小工具和特定於應用程式的程式碼之間進行了良好的區分。它所缺乏的,而 MVC 卻如此強大的是分離表示,以及使用Domain Model進行編程的背景。我將 MVP 視為統一這些流的一個步驟,試圖從中汲取最好的部分。
Potel 的第一個元素是將檢視視為小工具結構,這些小工具對應於表單和控制項模型的控制項,並移除任何檢視/控制器分離。MVP 的檢視是這些小工具的結構。它不包含任何描述小工具如何對使用者互動做出反應的行為。
對使用者行為的積極反應存在於一個獨立的簡報物件中。使用者手勢的基本處理程式仍然存在於小工具中,但這些處理程式僅將控制權傳遞給簡報者。
然後簡報者決定如何對事件做出反應。Potel 主要根據對模型的動作來討論這種互動,它通過命令和選擇系統來執行此操作。這裡要強調的一件有用的事情是在命令中封裝對模型的所有編輯的方法 - 這為提供復原/重做行為提供了良好的基礎。
當簡報者更新模型時,檢視會透過與 MVC 使用相同的觀察者同步方法進行更新。
「Dolphin」的說明類似。再次強調,主要相似點在於簡報者的存在。在「Dolphin」說明中,沒有簡報者透過命令和選取對模型進行操作的結構。也明確討論簡報者直接操作檢視。Potel 沒有討論簡報者是否應該這麼做,但對「Dolphin」來說,這個能力對於克服應用程式模型中讓我在變異欄位中為文字上色的缺陷至關重要。
思考 MVP 的變異之一,是簡報者控制檢視中的小工具的程度。一方面,所有檢視邏輯都留在檢視中,而簡報者不會參與決定如何呈現模型。這種風格是 Potel 暗示的風格。在 Bower 和 McGlashan 背後的方針是我稱之為「監督控制器」的方針,其中檢視會處理大量可以用宣告式描述的檢視邏輯,然後簡報者再介入處理更複雜的案例。
你也可以一路走到讓簡報者執行所有小工具操作。這種風格我稱之為「被動檢視」,它並非 MVP 原始說明的一部分,但隨著人們探索可測試性問題而發展出來。我稍後會討論這種風格,但這種風格是 MVP 的一種。
在我對比 MVP 和我之前討論的內容之前,我應該提到這兩篇 MVP 論文也這麼做,但我的詮釋與他們不太一樣。Potel 暗示 MVC 控制器是整體協調者,但我並非如此看待它們。「Dolphin」大量討論 MVC 中的問題,但他們所指的 MVC 是 VisualWorks 應用程式模型設計,而非我所描述的傳統 MVC(我並不責怪他們,因為現在要取得傳統 MVC 的資訊並不容易,更何況是當時)。
現在是時候做一些對比了
- 表單和控制項:MVP 有模型,而簡報者預期會透過「觀察者同步」操作這個模型,然後更新檢視。儘管允許直接存取小工具,但這應該是除了使用模型之外的附加選項,而不是首選。
- MVC:MVP 使用「監督控制器」來操作模型。小工具將使用者手勢傳遞給「監督控制器」。小工具不會區分為檢視和控制器。你可以將簡報者視為控制器,但沒有最初處理使用者手勢的功能。不過,重要的是要注意,簡報者通常在表單層級,而不是小工具層級,這也許是更大的差異。
- 應用程式模型:檢視會將事件傳遞給簡報者,就像傳遞給應用程式模型一樣。不過,檢視可能會直接從網域模型更新自身,簡報者不會充當簡報模型。此外,簡報者可以自由直接存取小工具,以執行不符合觀察者同步化的行為。
MVP 簡報者和 MVC 控制器之間有明顯的相似性,而簡報者是 MVC 控制器的一種鬆散形式。因此,許多設計會遵循 MVP 風格,但將「控制器」用作簡報者的同義詞。當我們討論處理使用者輸入時,使用控制器通常是合理的論點。

圖 12:MVP 中實際讀取更新的順序圖。
讓我們來看一下冰淇淋監視器的 MVP(監督控制器)版本(圖 12)。它的開頭與表單和控制項版本非常相似 - 實際文字欄位在文字變更時會引發事件,簡報者會偵聽此事件並取得欄位的最新值。此時,簡報者會更新讀取網域物件,而變異欄位會觀察並更新其文字。最後一部分是設定變異欄位的顏色,這項工作由簡報者執行。它會從讀取中取得類別,然後更新變異欄位的顏色。
以下是 MVP 的重點摘要
謙卑檢視
在過去幾年中,撰寫自測試程式碼已成為一股強勁的風潮。儘管我是最後一個詢問時尚品味的人,但我徹底沉浸在這股風潮中。我的許多同事都是 xUnit 架構、自動回歸測試、測試驅動開發、持續整合和類似流行語的忠實粉絲。
當人們討論自測試程式碼時,使用者介面會迅速成為一個問題。許多人發現測試 GUI 介於困難和不可能之間。這主要是因為 UI 與整體 UI 環境緊密結合,而且難以拆解和分段測試。
有時會過度誇大測試難度。您通常可以透過在測試程式碼中建立小工具並操作它們來達到驚人的進展。但有時這是不可能的,您會錯過重要的互動,出現執行緒問題,而且測試執行速度太慢。
因此,一直有穩定的動作來設計 UI,以將難以測試的物件行為降到最低。Michael Feathers 在 The Humble Dialog Box 中簡潔地總結了此方法。Gerard Meszaros 將此概念概括為Humble Object 的概念 - 任何難以測試的物件都應具有最小的行為。這樣一來,如果我們無法將其納入我們的測試套件中,我們可以將未偵測到失敗的機率降到最低。
The Humble Dialog Box 論文使用簡報者,但方式比原始 MVP 深入許多。簡報者不僅決定如何對使用者事件做出反應,還處理 UI 小工具本身的資料填充。因此,小工具不再具有可見性,也不需要可見性來查看模型;它們形成由簡報者操作的 Passive View。
這並非讓 UI 變得謙遜的唯一方式。另一種方法是使用 Presentation Model,儘管如此,您確實需要在小工具中增加一點行為,足夠讓小工具知道如何將自己對應到 Presentation Model。
這兩種方法的關鍵在於,透過測試簡報者或測試簡報模型,您可以在不觸及難以測試的小工具的情況下,測試 UI 的大部分風險。
使用 簡報模型 時,您可以透過讓 簡報模型 執行所有實際決策來達成此目的。所有使用者事件和顯示邏輯都會路由到 簡報模型,因此所有小工具所要做的就是將自己對應到 簡報模型 的屬性。然後,您可以在沒有任何小工具的情況下測試 簡報模型 的大部分行為 - 唯一剩下的風險在於小工具對應。只要這很簡單,您就可以不測試它。在這種情況下,畫面並不完全像 被動檢視 方法那樣謙虛,但差異很小。
由於 被動檢視 使小工具完全謙虛,甚至沒有對應,被動檢視 消除了 簡報模型 存在的小風險。然而,代價是您需要一個 測試替身 來在測試執行期間模擬畫面 - 這是您需要建置的額外機制。
使用 監督控制器 時存在類似的權衡。讓檢視執行簡單對應會帶來一些風險,但好處是(與 簡報模型 一樣)能夠以宣告方式指定簡單對應。與 簡報模型 相比,監督控制器 的對應會趨於較小,因為即使是複雜的更新也會由 簡報模型 決定並對應,而 監督控制器 會在沒有任何對應的情況下操作複雜案例的小工具。
進一步閱讀
對於進一步發展這些想法的近期文章,請查看 我的 bliki。
致謝
Vassili Bykov 大方地讓我取得 Hobbes 的副本 - 他實作的 Smalltalk-80 版本 2(來自 1980 年代初期),在現代 VisualWorks 中執行。這為我提供了一個模型-檢視-控制器的實際範例,對於回答有關其運作方式和在預設映像中如何使用它的詳細問題非常有幫助。在那些日子裡,許多人認為使用虛擬機器是不切實際的。我想知道我們以前的自己會怎麼想,看到我在 Windows XP 上執行 VisualWorks 虛擬機器中的 VisualWorks 虛擬機器中執行的 Smalltalk 80,而 Windows XP 正在 Ubuntu 上執行的 VMware 虛擬機器中執行。
重大修訂
2006 年 7 月 18 日:首次發佈於開發網站。