第 9 章:常用的設計模式 9.1 聚合組件 考慮為常用的特性域提供聚合組件。 要用聚合組件來對高層的概念(物理對象)進行建模,而不是對系統級的任務進行建模。 要讓聚合組件的名字與眾所周知的系統實體相對應,比如 MessageQueue、Process 或 EventLog,這樣就能使類型更加引人註 ...
第 9 章:常用的設計模式
9.1 聚合組件
考慮為常用的特性域提供聚合組件。
要用聚合組件來對高層的概念(物理對象)進行建模,而不是對系統級的任務進行建模。
要讓聚合組件的名字與眾所周知的系統實體相對應,比如 MessageQueue、Process 或 EventLog,這樣就能使類型更加引人註目。
要在設計聚合組件時使初始化儘可能地簡單,這樣用戶只需進行簡單的初始化就可以使用組件。如果某一項初始化是必需的,那麼由於沒有對組件進行初始化而引發的異常應該明確地告訴用戶應該怎麼做。
不要要求聚合組件的用戶在一個場景中顯式地實例化多個對象。
要保證讓聚合組件支持 Create-Set-Call 使用模式,這樣用戶就可以先實例化組件,然後設置它的屬性,最後調用一些簡單的方法,以實現大多數場景。
要為所有的聚合組件提供預設構造函數或非常簡單的構造函數。
要為聚合組件提供可讀寫的屬性來與構造函數中的所有參數相對應。
要在聚合組件中使用事件,不要使用基於委托的 API。
考慮用事件來代替需要被覆蓋的虛成員。
不要要求聚合組件的用戶在常用場景中使用繼承、覆蓋方法及實現介面。
不要要求聚合組件的用戶在常用場景中除了編寫代碼之外,還要做其他的做工。例如,不應該讓用戶用配置文件來配置組件,也不應該讓用戶生成資源文件,等等。
考慮讓聚合組件能夠自動切換狀態。
不要涉及有多種狀態的因數類型。
考慮將聚合組件集成到 VS 的設計器中。
考慮把聚合組件和因數類型分開,各自放在不同的程式集中。
考慮把聚合組件內部的因數類型暴露給外界訪問。
9.2 Async 模式
要實現基於事件的 Async 模式 - 如果類型是一個支持可視化設計器的組件(也就是說類型實現了 IComponent)。
要實現經典的 Async 模式 - 如果必須支持等待句柄。
考慮在實現高層 API 時使用基於事件的 Async 模式。例如,聚合組件就應該實現該模式。
考慮在實現底層 API 時使用經典的 Async 模式,在這種情況下更強大的功能、更少的記憶體消耗、更好的靈活性、更少的磁碟占用要比可用性更重要。
避免在同一個類型中甚至是一組相關的類型中同時實現兩種 Async 模式。
要在為非同步操作定義 API 時遵循下麵的約定。給定名為 Operation 的同步方法,應該提供名為 BeginOperation 和 EndOperation 的方法,它們的方法簽名如下麵所示(註意,輸出參數不是必需的)。
要確保 Begin 方法的返回類型實現了 IAsyncResult 介面。
要確保同步方法的按值傳遞和按引用傳遞的參數在 Begin 方法中都是按值傳遞的。同步方法的輸出參數不應該出現在 Begin 方法的簽名中。
要確保 End 方法的返回類型與同步方法的返回類型相同。
要確保同步方法的任何輸出參數和按引用傳遞的參數都作為 End 方法的輸出參數。同步方法中安置傳遞的參數不應該出現在 End 方法的簽名中。
不要繼續執行非同步操作 - 如果 Begin 方法拋出了異常。
要一次通過下麵的機制來通知調用方非同步操作已經完成。
將 IAsyncResult.IsCompleted 設為 true。
激活 IAsyncResult.AsyncWaitHandle 返回的等待句柄。
調用非同步回調函數。
要通過從 End 方法中拋出異常來表示無法成功地完成非同步操作。
要在 End 方法被調用時同步完成所有尚未完成的操作。。
考慮拋出 InvalidOperationException 異常 - 如果用戶用同一個 IAsyncResult 兩次調用 End 方法,或 IAsyncResult 是從另一個不相關的 Begin 方法返回的。
要把 IAsyncResult.CompletedSynchronously 設為 true - 當且僅當非同步回調函數將在調用 Begin 方法的線程中運行的時候。
要確保在正確的線程中調用事件處理程式。與經典 Async 模式相比,這是使用基於事件的 Async 模式的主要好處之一。
要確保無論是操作已經完成,還是操作出錯,還是操作被取消,都是種會調用事件處理程式。不應該讓應用程式無休止地等待一間永遠不會發生的事件
要確保在非同步操作失敗後,訪問時間參數類的屬性會引發異常。換句話說,如果有錯誤導致操作無法完成,那麼就不應該允許用戶訪問操作的結果。
不要為返回值為空的方法定義新的事件處理程式或事件參數類型。要使用 AsyncCompletedEventArgs,AsyncCompletedEventHandler 或 EventHandler<AsyncCompletedEventArg>。
要確保如果在一個一步操作中實現了 PaogressChanged 事件,那麼在操作的完成事件被觸發之後,不應該再出現此類事件。
要確保如果使用了標準的 ProgressChangedEventArgs,那麼 ProgressPercentage 始終能用來表示進度的百分比(不一定要完全精確,但表示的一定要百分比)。如果使用的不是標準進度,那麼從 ProgressChangedEventArgs 派生一個子類會更合適,這種情況下應該保持 ProgressPercentage 為 0 ;
要在有增量結果需要報告的時候出發 ProgressChanged 事件。
要對 ProgressChangedEventArgs 進行擴展來保存增量結果數據,並用擴展後的時間參數類來定義 ProgressChanged 事件。
要把增量結果報告與進度報告分開。
要為每個非同步操作定義單獨的 <MethodName>ProgreessChanged 事件和相應的事件參數類,來處理該操作的增量結果數據。
9.3 依賴屬性
要提供依賴屬性 - 如果需要用他們來支持各種 WPF 特性,比如樣式、觸發器、數據綁定、動畫、動態資源以及繼承。
要在設計依賴屬性的時候繼承自 DependencyObject 或它的子類型。該類型實現的屬性存儲區非常高效,它還自動支持 WPF 的數據綁定。
要為每個依賴屬性提供常規的 CLR 屬性和存放 System.Windows.DependencyProperty 實例的公有靜態只讀欄位。
要通過調用 DependencyObject.GetValue 和 DependencyObject.SetValue 的方式來實現依賴屬性。
要用依賴屬性的名字加上“Property”尾碼來命名依賴屬性的靜態欄位。
不要顯式地在代碼中設置依賴屬性的預設值,應該在元數據中設置預設值。
不要在屬性的訪問器中添加額外的代碼,而應該使用標準代碼來訪問靜態欄位。
不要使用依賴屬性來保存保密數據。任何代碼都能訪問依賴屬性,即使它們是私有的。
不要把依賴屬性的驗證邏輯放在訪問器中,而應該把驗證毀掉函數傳給 DependencyProperty.Register 方法。
不要在依賴屬性的訪問器中實現屬性改變的通知,而應該向 PropertyMetadata 註冊改變通知的回調函數,後者是依賴屬性本身提供的一項特性,為了支持改變通知,必須使用該特性。
不要在依賴屬性的訪問器中實現屬性強制賦值邏輯,而應該向 PropertyMetadata 註冊強制賦值的回調函數。後者是依賴屬性本身提供的一項特性,為了支持強制賦值,必須使用該特性。
9.4 Disopse 模式
要為含有可處置類型實例的類型實現基本 Dispose 模式。
要為類型實現基本 Dispose 模式並提供終結方法 - 如果類型持有需求由開發人員顯式釋放的類型,而且後者本身沒有終結方法。
考慮為類實現基本 Dispose 模式 - 如果類本身並不持有非托管資源或可處置對象,但是它的子類型卻可能會持有非托管資源或可處置對象。
要按下麵的方法來實現 IDisposable 介面,即先調用 Dispose(true),然後再調用 GC.SuppressFinalize(this)。
不要將無參數的 Dispose 方法定義為虛方法。
不要為 Dispose 方法聲明除了 Dispose() 和 Dispose(bool) 之外的任何其它重載方法。
要允許多次調用 Dispose(bool) 方法。他可以在第一次調用之後就什麼也不做。
避免從 Dispose(bool) 方法中拋出異常,除非是緊急情況,所處的進程已經遭到破壞(比如泄漏、共用狀態不一致,等等)。
要從成員中拋出 ObjectDisposedException 異常 - 如果該成員在對象終結之後就無法繼續使用。
考慮在 Dispose() 方法之外在提供一個 Close() 方法 - 如果 close 是該領域中的一個標準術語。
避免定義可終結類型。
不要定義可終結的值類型。
要將類型定義為可終結類型 - 如果該類型要負責釋放非托管資源,且非托管資源本身不具備終結方法。
要為所有的可終結類型實現基本 Dispose 模式。
不要在終結方法中訪問任何可終結對象,這樣做存在很大的風險,因為被訪問的對象可能已經被終結了。
要將 Finalize 方法定義為受保護的。
不要在終結方法中放過任何異常,除非是致命的系統錯誤。
考慮創建一個用於緊急情況的可終結對象 - 如果終結方法在應用程式域被強制卸載或線程異常退出的情況下都務必要執行。
9.5 Factory 模式
要優先使用構造函數,而不是優先使用工廠,因為與特殊的對象構造機制相比,構造函數一般來說更容易使用、更一致,也更方便。
考慮使用工廠 - 如果構造函數提供的對象創建機制不能滿足要求。
要使用工廠 - 如果開發人員可能不清楚待創建的對象的確切類型,比如對基類或介面編程就屬於這種情況。
考慮使用工廠方法 - 如果這是讓操作不言自明的唯一方法。
要在轉換風格的操作中使用 factory。
要儘量將工廠操作方法實現為方法,而不是實現為屬性。
要通過方法的返回值而不是方法的輸出參數來返回新創建的對象實例。
考慮把 Create 和要創建的類型名連在一起,一次來命名工廠方法。
考慮把要創建的類型名和 Factory 連在一起,一次來命名工廠類型。例如,可以考慮把創建 Control 對象的工廠類型命名為 ControlFactory。
9.6 對 LINQ 的支持
要實現 IEnumerabl<T>,其目的是為了得到基本的 LINQ 支持。
考慮實現 ICollection<T>,其目的是為了提高查詢的性能。
考慮實現 IQueryable<T> - 如果必須要訪問傳給 IQueryable 的成員的查詢表達式。
不要草率地實現 IQueryable<T>,要理解這樣做可能會對性能產生什麼影響。
要在 IQueryable<T> 的方法中拋出 NotSupportedException - 如果你的數據源上不支持該操作。
要在新類型中將 Query 模式實現為實例方法 - 如果在 LINQ 以外的場合,這些方法在類型中仍然有存在的意義。否則,應該將它們實現為擴展方法。
要讓實現了 Query 模式在類型實現了 IEnumerable<T>。
考慮在設計 LINQ 操作符時,讓它們返回領域特有的可枚舉類型。雖然從本質上來說,Select 查詢方法可以返回任何類型,但是大家通常都希望查詢的結果是可枚舉類型。
避免只實現 Query 模式的一部分 - 如果不希望退回到基本的 IEnuerable<T> 實現。
要為有序序列定義單獨的類型,從而將它和對應的無序序列分開。這樣的類型應該定義 ThenBy 方法。
要推遲執行實際的查詢操作。對 Query 模式的大多數成員來說,我希望它們只是創建一個新的對象,併在枚舉的時候才產生集合重負荷查詢條件的元素。
要將用於查詢的擴展方法放在主命名空間中的一個名為“Linq” 的子命名空間中。例如,為 System.Data 特性定義的擴展方法被放在 System.Data.Linq 命名空間。
要在參數中使用 Expression<Func<...>>,而不是 Func<...> - 如果需要查詢查詢表達式。
9.7 Optional Feature 模式
考慮將 Optional Feature 模式用於抽象中的可選特性。
要提供一個簡單的布爾屬性來讓用戶檢測對象是否支持可選特性。
要在積累中將可選特性定義為虛方法,併在該方法中拋出 NotSupportedException 異常。
9.8 Simulated Convariance 模式
考慮使用 Simulated Convariance 模式 - 如果需要有一種統一的類型來表示泛型類型的所有實例。
要確保以等價的方式來實現根基類型成員和對應的泛型類型成員。
考慮使用抽象基類來表示根基類型,而不是使用介面來表示根基類型。
考慮用非泛型類型作為根基類型 - 如果這樣的類型已經存在。
9.9 Template Method 模式
避免將公有成員定義為虛成員。
考慮使用 Template Method 模式來更好地控制擴展性。
考慮以非秀成員的名字加“Core”尾碼為名字,來命名為該費虛成員提供擴展點的受保護的虛成員。
9.10 超時
要優先讓用戶通過參數來制定超時長度。
要優先使用 TimeSpan 來表示超時長度。
要在超時後拋出 System.TimeoutException 異常。
不要通過返回錯誤碼的方式來告訴用戶發生了超時。
9.11 可供 XAML 使用的類型
考慮提供預設構造函數 - 如果想讓類型能用於 XAML。
要提供標記擴展 - 如果想讓 XAML 讀取程式能夠創建不可變的類型。。
避免定義新的類型轉換器,除非這樣的轉換是自然而直觀的。一般來說,應該將類型轉換器的使用範圍限制在 .NET 框架中已經使用了類型轉換器的地方。
考慮將 ContentPropertyAttribute 用於最常用的屬性,從而得到更方便的 XAML 語法。