組織簡報邏輯
有數種方式可以分割簡報邏輯。
2006 年 7 月 11 日
這是 進階企業應用程式架構開發 的一部分,我於 2000 年代中期撰寫。遺憾的是,自此以後,太多其他事情吸引我的注意力,所以我沒有時間進一步研究,也看不到未來有太多時間。因此,這份文件仍是草稿形式,直到我找到時間重新研究為止,我不會進行任何更正或更新。
在設計任何簡報圖層時,最實用的方法之一是強制執行 分離簡報。完成此步驟後,下一步就是思考簡報邏輯本身的組織方式。對於一個簡單的視窗,一個類別可能就夠了。但更複雜的邏輯會導致更廣泛的細分。
最常見的方法是為應用程式中的每個視窗設計一個類別。此類別通常繼承自 GUI 函式庫的視窗類別,並包含處理該視窗所需的所有程式碼。如果視窗包含一個複雜的面板,您可能會為該面板建立一個獨立的類別,形成一個複合結構。我不會深入探討這種複合結構,因為這很簡單,我將更專注於組織單一視窗內基本行為的方法。
專輯清單執行範例
對於這裡的大部分討論和範例,我將使用一個單一的範例畫面來討論遇到的問題 (圖 1)。此視窗顯示音樂錄音的資訊。它會為每張專輯顯示歌手、標題、是否為古典錄音,以及如果是古典錄音:作曲家。我選擇這個範例來包含一些簡報邏輯元素。
- 清單選取中的選擇決定要將哪張專輯的資料顯示在欄位中。
- 視窗標題來自目前顯示專輯的標題。
- 只有在勾選古典核取方塊時,才會啟用作曲家欄位。
- 套用和取消按鈕僅在編輯資料後才會啟用。

圖 1:一個簡單的專輯資訊視窗
將簡報邏輯從檢視中分離
儘管將所有簡報邏輯放入自主檢視中既常見又可行,但確實會帶來缺點。現今與自主檢視相關最常見的缺點在於測試。透過 GUI 視窗測試簡報通常很尷尬,在某些情況下甚至不可能。您必須建構某種 UI 驅動程式來驅動 GUI。有些人使用模擬原始滑鼠和鍵盤事件的 GUI 工具,但這些工具通常會建立脆弱的測試,只要簡報發生細微變更就會產生假陽性。更詳細的工具會更直接地處理 UI 控制項,這些工具較不脆弱,但仍然很麻煩。它們還依賴 GUI 架構是否具有足夠的支援,才能透過 API 直接控制存取,而且並非所有架構都具備此功能。
因此,許多基於程式設計師測試的倡導者主張採用簡約對話方塊(也稱為極簡 GUI)。此處的核心概念是透過將所有邏輯移至其他簡報層級類別,讓包含 UI 控制項的類別盡可能小而愚蠢。此 GUI 控制項類別通常稱為檢視,原因我稍後會說明。然後,您可以在不使用任何 GUI 控制項的情況下對智慧型類別執行測試,必要時存根簡約檢視。由於檢視非常愚蠢,因此不太可能出錯,而且您可以透過處理智慧型類別來找出大多數錯誤。我喜歡用皮下測試這個術語來表示這種測試風格,因為測試在應用程式的皮下執行。
儘管皮下測試是現今拆分簡報類別的主要原因,但還有其他幾個原因值得考慮這種拆分。智慧型類別可以獨立於檢視的幾個面向,例如控制項的選擇、控制項的配置,甚至可能是精確的 UI 架構本身。這讓您可以使用相同的邏輯行為支援多個不同的檢視。儘管這對支援應用程式的多個「外觀」很有用,但您只能透過更換簡約檢視來執行有限的變更。
在某些方面,分離簡報邏輯可以讓編寫簡報變得更容易。在撰寫行為時,您可以忽略檢視配置的詳細資料,實際上可以讓您更輕鬆地使用檢視的控制項。然而,與此相對的是,分離簡報邏輯確實會導致額外的機制來支援分離(其性質會根據您使用的模式而有所不同)。因此,兩者都有合理的論點,表示分離會簡化或增加簡報的複雜性。
將此分割進行處理有歷史先例,這是 Model View Controller (MVC) 的一部分。正如我在 [P of EAA] 中所討論的,MVC 方法進行了兩個區分。最重要的區分是 分離呈現 - 將模型與檢視/控制器分開。檢視與控制器的另一個區分在富用戶端 GUI 框架中並不流行,儘管它在基於網路的使用者介面中重新出現。在 MVC 中,檢視是模型中資訊的簡單顯示,而控制器處理各種使用者輸入事件。這不適用於大多數 GUI 框架,因為它們的設計使得 UI 控制項同時顯示和接收使用者輸入事件。
若要建立一個簡潔的檢視,設計必須將所有行為移出檢視 - 處理使用者事件和網域資訊的任何顯示邏輯。有兩種主要方法可以做到這一點。首先是 Model-View-Presenter 樣式,其中行為會移到簡報者中,你可以將其視為一種控制器。簡報者處理使用者事件,並且在更新檢視中也扮演一些角色。 監督控制器 和 被動檢視 是此方法的兩種樣式。 監督控制器 將簡單的檢視邏輯放入檢視中,而 被動檢視 將所有檢視邏輯放入控制器中。另一種樣式是 簡報模型,它建立一種模型形式,擷取所有資料和行為,以便檢視只需要一個簡單的同步。
在這兩種樣式中,檢視都是使用者事件的初始處理程式,但隨即將控制權移交給控制器。
這三種模式透過引入額外類別,產生可說是更複雜的設計。將執行太多功能的類別拆分成個別類別以履行各項職責,是一種良好的做法,但問題是 Autonomous View 是否過於複雜。其他模式肯定提供其他測試選項,以及支援多個檢視的能力。如果您不需要多個檢視,而且滿意透過檢視進行測試,那麼 Autonomous View 可能很適合,特別是如果視窗不太複雜的話。
在 Presentation Model、Supervising Controller 和 Passive View 之間的選擇比較隨意,實際上取決於在您的 GUI 環境中執行模式的容易程度,以及您個人的喜好。
每當我們使用 MVC 中的術語時,難免會產生模型是什麼的問題。在傳統的 Smalltalk MVC 中,模型是 Domain Model。一般來說,現今 MVC 討論中的模型是指領域層的介面;它可以是傳統的領域模型,或是服務層、交易腳本、表格模組或領域的任何其他表示方式。事實上,如果沒有獨立的領域層,模型很可能是資料庫的介面。
畫面、圖層和資料
大多數企業應用程式都涉及編輯資料。此資料通常會在應用程式的多個層級之間複製,而且可能在使用相同系統的多個使用者之間複製。企業應用程式的許多行為取決於如何協調這些資料的變更,以及如何在層級之間同步資料。對於如何思考這一點,沒有普遍接受的術語,為了本書的目的,我將套用以下內容。
資料圖層
要思考的第一個問題是不同層級中資料的不同副本。從物理角度思考這一點,通常有記憶體中的資料和資料庫或檔案中的資料之間的差異。您可以將這視為暫時資料和持久資料之間的差異。然而,我發現通常不只如此。即使是記憶體中的資料,也常常會出現在兩個地方。您經常會發現螢幕上的資料與作為螢幕後盾的某種形式記憶體儲存體中的資料之間的差異。這可能是從資料庫中擷取的記錄集(但尚未提交回資料庫),或是在領域模型中。
考慮在文字處理器中處理文件。文件在磁碟上,您已在文字處理器中開啟文件並編輯文件的文字。這會產生與磁碟上不同的記憶體中的文字。現在開啟一個對話方塊,以變更某些文字的格式。您通常可以在對話方塊中變更格式,但它不會變更基礎文字,直到您按下套用按鈕。對話方塊中的格式化資料是記憶體中的資料,但它與主記憶體文件中的資料不同。
我在本書中將使用的術語為畫面狀態、會話狀態和記錄狀態。畫面狀態是使用者介面上顯示的資料。會話狀態是使用者目前正在處理的資料。使用者會將會話狀態視為暫時的,他們通常可以儲存或捨棄自己的工作。記錄狀態是指較為永久的資料,預期會在各個會話之間存在。
會話狀態主要在記憶體中作用,但通常會儲存在磁碟中。現代文字處理器通常會儲存救援檔案,以避免因斷電或系統崩潰而遺失工作。企業應用程式可以在本地端儲存會話狀態於檢查點檔案中,或伺服器可以在要求之間將狀態儲存在磁碟中。
在企業應用程式中,會話狀態和記錄狀態之間的一個特定區別在於,記錄狀態會在系統的多個使用者之間共用,而會話狀態是私人狀態,僅對正在處理它的使用者可見。因此,使用者不僅決定將變更儲存為較為永久的形式,他們也決定與同事共用變更。會話狀態通常與單一業務交易相關,儘管它通常跨越多個系統交易,這種情況通常需要離線並行處理。
並非所有應用程式都有會話狀態。有些應用程式只有畫面狀態和記錄狀態 - 儲存資料的任何變更都會直接轉到記錄狀態。在這些情況下,您可能完全沒有任何會話狀態,或任何變更都會立即寫入記錄狀態,因此會話狀態始終與記錄狀態同步。沒有會話狀態會大幅簡化應用程式,因為您不必擔心管理會話狀態。許多應用程式的使用者甚至更喜歡這種方式,因為使用者不必擔心遺失工作。然而,省略會話狀態並非十全十美。使用者失去在工作狀態中試驗某個情境,然後在不喜歡時捨棄該情境的能力。它也會阻止人們在多使用者應用程式中獨立工作。
您也可以獲得額外的狀態層級。一個範例是會話狀態同時儲存在用戶端層和伺服器層。這些狀態可以獨立變更,儘管通常有相當嚴格的規則說明它們如何同步,這使得管理它們變得更為簡單。
額外層級的範例為開發人員如何於團隊中工作。在此情況下,記錄狀態為共用原始碼儲存庫的狀態。開發人員本機上的工作副本為暫存在磁碟上的會話狀態形式。在此情況下,它本身有點暫時性。IDE 中還有其他表示方式。在現代 IDE 中,會在記憶體中保存並持續更新語法樹,您有該語法樹,加上畫面中顯示的文字。在此情況下,有超過三個層級,但仍可思考開發人員在畫面中看到什麼、他的私人會話資料,以及共用的永久資料。為了有效地推理,我會為每組資料命名,並將它們視為獨立的層級。特定應用程式永遠有自己的設定,在討論中,我會專注於畫面、會話和記錄狀態這三個常見的組合。
使用者大部分時間會一次在單一會話中工作。有時,使用者會同時在多個會話中操作。這通常會造成混淆,因為在兩個會話都與記錄狀態同步之前,一個會話中的變更不會顯示在另一個會話中。您可以透過同步兩個會話來解決這個問題,但通常很麻煩。
這些多重狀態通常對應到企業應用程式的各種層級。在使用簡報、網域和資料來源層級的理想應用程式中,您會讓網域邏輯只在會話狀態中運作。在實務上,這個區別會變得模糊,通常是出於不良原因,但有時是出於正當理由。在網域層級與簡報層級位於不同程序的應用程式中,您可能希望讓一些網域邏輯在簡報程序中執行,以讓應用程式適當地回應。此類邏輯可能涉及從主要網域程序複製一些會話狀態,或者您可能必須針對簡報控制項中的資料執行網域邏輯。類似地,如果您需要處理大量資料,您可能需要透過類似儲存程序之類的機制將網域邏輯嵌入資料庫中。此類邏輯會在記錄狀態中運作。然而,大部分時間您會希望網域邏輯在會話狀態中運作。
圖層間同步
在這些不同的內容之間同步資料是建置應用程式的重點。當您處理使用者介面時,您可以將畫面狀態同步到兩個不同的深度:會話狀態或記錄狀態。如果您同步到會話狀態,您將需要一些控制項,讓使用者可以將會話狀態儲存到記錄狀態。
同步可以以不同的頻率發生,我發現以下三種很有用。按鍵同步表示您在每次按鍵或滑鼠點擊時同步。欄位同步表示您在完成編輯欄位時同步。畫面同步表示您在完成一畫面資訊時同步,這時您會在 UI 中按一些特殊按鈕(通常標示為「套用」、「確定」、「取消」或「提交」)。
一旦您需要同步,下一個問題便是您要同步多少內容。當您考慮將畫面資料與會話資料同步時,我看到兩個主要的方案。粗略同步表示每當您在使用者介面上變更任何內容時,整個使用者介面都會同步;因此變更藝術家欄位表示同步整個視窗,即使其他內容不需要變更。細緻同步表示只變更確實需要更新的欄位。因此變更標題欄位會涉及同步標題欄位、視窗標題和清單方塊,但不會變更其他內容。
會話資料與記錄資料之間的同步通常會使用不同的方法。會話資料通常不會由多個人同時使用,因此您不必擔心並行問題。會話資料與記錄資料之間的同步通常會在畫面同步時發生,而且通常需要較長的時間。因此您會執行標記資料元素為已變更或使用工作單元等動作。
所有這些面向在使用者介面的內部設計和互動設計中都會互相權衡。最明顯的權衡發生在同步的頻率和深度之間。將關鍵同步降低到記錄狀態會導致無法接受的效能,以及其他問題。因此,我看到大部分時間畫面同步都使用在那個深度。事實上,畫面同步在會話深度也是最常見的。這通常是最容易執行的,而且許多應用程式都是這樣運作,因此使用者已經習慣了。然而,互動設計也常常確實需要欄位同步。如果網域邏輯與簡報在同一個程序中,欄位同步相當容易,如果在不同的程序中,要獲得良好的效能則比較困難。因此,對於網域層而言,畫面同步是合理的預設值,但預期會相當頻繁地執行欄位同步。
關鍵同步似乎比較少見,但如果網域在同一個程序中,執行起來相當容易。
雖然計時選擇會隨著深度和應用程式設計而有所不同,但我幾乎總是贊成畫面狀態與會話狀態之間的粗略同步。許多人避開粗略同步,因為他們擔心效能影響。但細緻同步難以維護,因為有許多程式碼經常重複。所有這些程式碼中的錯誤都很難發現,因此也很難修正。大部分時間粗略同步的效能都相當好,因此我的建議是永遠先使用它。如果您確實遇到效能問題,而且您已進行剖析以檢查它確實是同步問題,那麼您必須引入一點細緻同步來修正它。在那個時間點,執行您需要執行的最低限度動作來處理效能問題。
這種同步需求非常普遍,因此人們開發架構來嘗試處理它是不可避免的。其中一個備受關注的是 .NET 中的資料繫結架構,它會自動同步畫面和會話狀態。資料繫結有許多優點,理論上應該能夠妥善處理同步。到目前為止(至 1.0 版),我發現它適用於簡單的案例,但在中等複雜的案例中會中斷。我曾對談的專案一開始使用資料繫結,但一段時間後就放棄了,因為無法控制繫結運作的方式。因此,除非你的需求非常簡單,否則我建議你謹慎使用它。不過,請在後續版本中重新評估它 - 我可以輕易地看到這會變成同步問題的非常有效解決方案。
同步和多個畫面
同步的一部分是關於在狀態層之間同步,另一部分是處理在同一個層的複數分支之間的同步。你經常會在單一記錄狀態之上找到多個會話,以及在每個會話之上找到多個畫面。這些每一個都是獨立的內容,你必須思考一個內容中的變更如何傳播到其他內容中。
由於我在這裡談論簡報,因此我不會多談論同步多個會話。無論如何,那是一個更為人理解且相對單純的主題。大部分時間會話彼此隔離,而且只會與記錄狀態同步。它們使用交易或某種形式的離線並行控制來執行此操作。
簡報更為複雜,因為使用者預期較少的隔離和更快速的同步。
如何最佳地同步多個畫面在很大程度上取決於畫面的組織方式以及畫面之間的流程結構。從兩個極端來看,我們可以思考對比精靈與完全非模式介面,例如檔案系統瀏覽器。
對於精靈使用者介面,系統會引導使用者瀏覽受嚴格控制的畫面流程。任何時候只會顯示一個畫面,而且使用者通常只能從每個畫面向前或向後移動。在此情況下,畫面的設計者確切知道顯示哪些資料,以及確切何時開啟和關閉畫面。
使用者可以使用檔案系統瀏覽器在畫面間任意移動。更重要的是,使用者可以開啟多個瀏覽器視窗,顯示相同的檔案。如果使用者在一個視窗中變更資料夾名稱,其他視窗也應該更新。使用者介面的程式設計人員永遠無法確定視窗何時開啟,以及是否在多個視窗中顯示相同的資料。
這兩個極端情況建議使用兩種不同的方式,在畫面之間協調資訊。使用流程同步時,每個畫面會根據應用程式的流程,決定何時將其畫面狀態與任何基礎的階段狀態同步。因此,使用精靈時,畫面通常會在從一個畫面移至另一個畫面時同步;寫出舊畫面並讀入新畫面的資料。流程同步最適合在畫面之間的流程很簡單,而且有明確的點可以將資料從畫面狀態儲存並載入到階段狀態時使用。
對於檔案瀏覽器而言,流程同步會很困難。一個畫面永遠無法真正判斷另一個畫面是否已變更基礎資料。在這種情況下,畫面需要彼此不知情,並在基礎資料變更時同步。使用觀察者同步時,基礎畫面狀態會作為資料的主來源。每當它變更時,顯示的畫面就會收到通知,並可以使用觀察者模式更新其畫面狀態。在此形式中,觀察者同步是模型檢視控制器樣式的基本部分。
觀察者同步的好處是,所有畫面總是完全獨立於彼此,它們不需要知道彼此才能同步,也不需要彼此告知同步事件。這使得應用程式可以輕鬆地進行非常臨時且複雜的流程。觀察者同步的缺點是,它依賴於使用觀察者,而且會引入一些隱含的行為,如果您讓它失控,可能會變得非常棘手。
然而,整體而言,觀察者同步是複雜使用者介面的主要選擇。流程同步只有在應用程式流程非常簡單時才真正可用:通常一次只有一個活動畫面,而且畫面之間的流程很簡單。即使如此,一旦您習慣觀察者同步,您可能更喜歡在這些簡單的情況下使用它。
觀察者陷阱
豐富用戶端簡報中的許多互動都使用觀察者模式。觀察者是有用的模式,但它會帶來一些您需要了解的重要問題。
觀察者的最大優點,也是最大缺點,在於控制權會從主體隱含地轉移到觀察者。從程式碼中無法得知觀察者何時會觸發,唯一能看到發生什麼事的方法就是使用除錯工具。因此,複雜的觀察者鏈可能難以理解、變更或除錯,因為動作會觸發其他動作,但卻很少說明原因。因此,我強烈建議只在非常簡單的情況下使用觀察者。
- 不要讓物件的鏈觀察其他物件,而這些物件又觀察其他物件。最好只使用一層觀察者關係(除非使用事件聚合器)。
- 不要在同層中的物件之間建立觀察者關係。網域物件不應觀察其他網域物件,簡報也不應觀察其他簡報。觀察者最適合用於跨層界線,經典用法是讓簡報觀察網域。
觀察者的另一個問題在於記憶體管理。假設我們有一些畫面觀察一些網域物件。當我們關閉畫面時,我們希望畫面被刪除,但網域物件實際上會透過觀察者關係保留對畫面的參考。在記憶體管理的環境中,存活時間長的網域物件可能會保留許多殭屍畫面,導致顯著的記憶體外洩。因此,當觀察者要被刪除時,從其主體取消註冊非常重要。
當您要刪除網域物件時,也會發生類似的問題。如果您依賴於中斷網域物件之間的所有連結,這可能還不夠,因為畫面可能會觀察網域。實際上,這不太會成為問題,因為畫面會離開,而且網域物件的存活時間通常會透過資料來源層控制。但一般來說,值得注意的是,觀察者關係經常被遺忘,而且是殭屍的常見原因。使用事件聚合器通常會簡化這些關係,雖然不是萬靈丹,但可以讓生活更輕鬆。
我特別感謝我的同事 Xiao Guo,他透過分析他在視窗導覽和資料同步方面的經驗,激發了本章的大部分思考。Patrik Nordwall 指出了觀察者和記憶體外洩的問題。
重大修訂
2006 年 7 月 11 日:初始更新以處理 MVP 風格中的分割
2004 年 11 月 20 日:新增流程同步討論。
2004 年 8 月 4 日:新增受與蕭國對話啟發而來的螢幕、圖層和資料素材。
2004 年 7 月 19 日:首次公開發布。主要關於簡報模型和 MVP 比較。
2004 年 5 月 15 日:對 TW 內部發布