觀察者同步
透過讓多個畫面都成為共用網域資料的觀察者,來同步多個畫面。
2004 年 9 月 8 日
這是 進階企業應用程式架構開發 的一部分,我在 2000 年代中期撰寫。很遺憾地,自此之後有太多其他事情吸引了我的注意力,因此我沒有時間進一步研究它們,我也看不到在可預見的未來會有太多時間。因此,這份文件仍處於草稿階段,直到我能找到時間再次研究它之前,我不會進行任何更正或更新。
在某些應用程式中,您有多個可用的畫面,顯示共用資料區域的簡報。如果透過其中一個畫面對資料進行變更,您會希望所有其他畫面都能正確更新。但是,您不希望每個畫面都知道其他畫面,因為這會增加畫面的複雜性,並讓新增新畫面變得更困難。
觀察者同步 使用單一網域導向資料區域,並讓每個畫面成為該資料的觀察者。一個畫面中的任何變更都會傳播到該網域導向資料,然後傳播到其他畫面。這種方法是 Model View Controller 方法的一大部分。
運作方式
這種方法的精髓在於,每個畫面(及其關聯的畫面狀態)都充當共用會話資料區域的 觀察者。對會話資料的所有變更都會產生畫面會傾聽並透過從會話資料重新載入來回應的事件。一旦您設定好這個機制,您就可以透過編寫每個畫面來更新會話資料,進而確保同步。即使進行變更的畫面也不需要明確重新整理自己,因為觀察者機制會觸發重新整理,就像另一個畫面進行變更一樣。
或許此設計中最大的問題在於決定要使用什麼粒度的事件,以及如何設定傳播和觀察者關係。在非常細緻的層級中,每個網域資料位元都可以有獨立的事件來明確指出變更了什麼。每個畫面只會註冊可能會使該畫面資料失效的事件。最粗略的替代方案是使用事件聚合器將所有事件傳送至單一頻道。這樣一來,每個畫面在任何網域資料變更時都會重新載入,無論是否會影響畫面。
一如往常,取捨在於複雜度和效能。粗略的方法設定起來容易許多,也不太容易產生錯誤。然而,粗略的方法會導致畫面不必要的重新整理,這可能會影響效能。一如往常,我的建議是從粗略的方法開始,並在測量出實際效能問題後,適時導入適當的細緻機制。
事件通常難以除錯,因為無法透過查看程式碼來查看呼叫鏈。因此,保持事件傳播機制盡可能簡單非常重要,這也是我偏好粗略機制的原因。一個好的經驗法則,就是將物件視為分層,並只允許層級之間的觀察者關係。因此,一個網域物件不應觀察另一個網域物件,只有簡報物件應觀察網域物件。
另一件需要非常注意的事情是事件鏈,其中一個事件會導致另一個事件觸發。此類事件鏈很快就會變得非常難以追蹤,因為無法透過查看程式碼來了解行為。因此,我傾向於不鼓勵層級內的事件,而偏好單一行事件,或透過事件聚合器。
當觀察者註冊事件時,您會從主體取得對觀察者的參照。如果您在移除畫面時,觀察者沒有從主體中移除自己,您就會有一個殭屍參照和一個記憶體外洩。如果您在每個工作階段中毀損並建立網域資料,而且您的工作階段很短,這可能不會導致問題。然而,長駐網域物件可能會導致嚴重的外洩。
何時使用
觀察者同步在您有多個共用一般資料的活動視窗時,是一個特別重要的模式。在這種情況下,讓視窗彼此發出訊號來更新的替代方案會變得相當複雜,因為每個視窗都需要知道其他視窗,以及它們何時可能需要更新。新增視窗表示更新資訊。使用這種方法,新增視窗非常簡單,而且每個視窗都可以設定為維護它自己與一般網域資料的關係。
觀察者同步的主要缺點是事件觸發的隱含行為,這很難從程式碼中視覺化。因此,事件傳播中的錯誤可能非常難以尋找和修正。
觀察者同步也可以用於更簡單的導覽樣式,儘管在這些情況下,流程同步是一個合理的替代方案。觀察者同步的價值可能無法超過使用事件進行更新所帶來的複雜性。
進一步閱讀
這個模式顯然與觀察者非常相似,並且是模型檢視控制器的核心部分。我認為這兩者之間的主要差別在於,這是觀察者的特定用途,儘管是最常見的用途。我認為這是構成模型檢視控制器的幾個模式的一部分。
致謝
Patrik Nordwall 指出了觀察者和記憶體外洩的問題。
範例:專輯和表演者 (C#)

圖 1:顯示專輯和表演者的畫面。
考慮類似圖 1的應用程式。我們有活動畫面來編輯表演者的名稱和他們出現的專輯。如果我編輯專輯「藍色情懷」的標題,我希望我的編輯不僅出現在文字方塊和專輯畫面的標題中,也出現在 Miles Davis 和 John Coltrane 的清單項目中。

圖 2:專輯和表演者的網域物件。
在這個案例中,我將網域資料保存在表演者和專輯的幾個簡單網域物件中圖 2。當我變更這些類別中的一個的資料時,我需要傳播一個事件。
class Album : DomainObject
public string Title { get { return _title; } set { _title = value; SignalChanged(); } } string _title;
class DomainObject...
public void SignalChanged() { if (Changed != null) Changed (this, null); } public event DomainChangeHandler Changed;
public delegate void DomainChangeHandler (DomainObject source, EventArgs e);
在這裡,我僅在 圖層超類別 中定義了一個簡單的變更事件。此事件不會提供任何有關變更的資訊,只會指出已發生某些變更,適合從客戶端進行粗略同步。
在表單中,我透過傳入相簿來建立新的相簿表單。建構函式會執行其一般的 GUI 工作,設定相簿參考,連接事件監聽器,並最後從相簿載入資料。
類別 FrmAlbum...
public FrmAlbum(Album album) { InitializeComponent(); this._album = album; observeDomain(); load(); } private Album _album; private void observeDomain() { _album.Changed += new DomainChangeHandler(Subject_Changed); foreach (Performer p in _album.Performers) p.Changed +=new DomainChangeHandler(Subject_Changed); } private void Subject_Changed(DomainObject source, EventArgs e) { load(); }
事件監聽已連接,因此相依屬性物件中的任何變更都會導致表單重新載入其資料。
類別 FrmAlbum...
private void load() { txtTitle.Text = _album.Title; this.Text = _album.Title; lstPerformers.DataSource = performerNames(); } private string[] performerNames() { ArrayList result = new ArrayList(); foreach (Performer p in _album.Performers) result.Add(p.Name); return (string[]) result.ToArray(typeof (string)); }
如果我變更相簿中的標題,我會直接在基礎屬性物件上進行變更。
類別 FrmAlbum...
private void txtTitle_TextChanged(object sender, EventArgs e) { this._album.Title = txtTitle.Text; }
只要每個表單都可以與共用屬性物件參與繫結,您就可以使用資料繫結來達成大部分的這些工作。另一種變化是使用 事件聚合器,這會允許每個表單僅向聚合器註冊,而不必向每個屬性物件註冊。在沒有效能問題的情況下,我會這樣做,我沒有這樣做是因為我比較喜歡讓範例盡可能地彼此獨立。
如果您正在使用 監督控制器 或 被動檢視,則控制器會充當屬性事件的觀察者。如果您正在使用 表示模型,則 表示模型 會充當觀察者。