1. 程式人生 > 其它 >框架設計原則和規範(完)

框架設計原則和規範(完)

祝大家聖誕節快樂!有事沒事別出門,外面太!擠!了!

此文是《.NET:框架設計原則、規範》的讀書筆記,本文內容較多,共分九章,今天推送最後一章。

1. 什麼是好的框架

2. 框架設計原則

3. 命名規範

4. 型別設計規範

5. 成員設計規範

6. 擴充套件性設計

7. 異常

8. 使用規範

9. 設計模式

一、設計模式

1. 聚合元件

Aggregate Component:

把多個底層型別集中到一個高層型別中,以此來支援常用場景。例如E-mail元件、System.Net.WebClient、System.Messaging.MessageQueue、System.IO.SeralPort、System.Diagnostics.EventLog

1) 面向元件的設計

component-oriented design:

通過型別來暴露API,而型別由函式、屬性、方法及事件組成。

其使用模式為:Create-Set-Call

注意不要讓物件處於不可用的狀態,或者對方法的呼叫有先後順序依賴

A. 應該有預設建構函式

B. 建構函式的所有引數應該與屬性相對應,並用來對屬性進行初始化

C. 大多數屬性應該有getter和setter

D.所有屬性都有合理的預設值

E. 如果引數在主要場景的方法呼叫之間不會改變,那麼方法就不應該帶這樣的引數。這樣的選項應該通過屬性來指定。

F. 方法不以委託為引數。所有回撥函式都通過事件來實現。

2) 因子型別

組成聚合型別的子型別,稱為因子型別(factoredtype)

A. 沒有狀態

B. 有非常清晰的生命週期

C. 可用通過聚合元件的屬性或方法訪問

D.用於高階場景或與系統的不同部分整合

3) 聚合元件規範

A.考慮為常用的特性域提供聚合元件

B. 要用聚合元件來對高層的概念(物理物件)進行建模,而不是對系統級的任務進行建模

比如應該對檔案、目錄、驅動器建模,而不應該對流(stream)、格式化器(formatter)、比較器(comparer)進行建模

C.要讓聚合元件的名字與眾所周知的系統實體相對應

如MessageQueue,Process,EventLog

D. 要在設計聚合元件時使初始化儘量地簡單

E. 不要要求聚合元件的使用者在一個場景中顯式的例項化多個物件

API的使用者數量與簡單場景中的new語句數目成反比

F.要保證讓聚合元件支援Create-Set-Call使用模式

使用者可以先例項化元件,然後設定屬性,呼叫方法,以實現大多數場景

G. 要為所有聚合元件提供預設建構函式或非常簡單的建構函式

H.要為聚合元件提供可讀寫的屬性來與建構函式中的所有引數相對應

I. 要在聚合元件中使用事件,不要使用基於委託的API

J. 考慮用事件來代替需要被覆蓋的虛成員

K. 不要要求聚合元件的使用者在常用場景中使用繼承、覆蓋方法及實現介面。

應該主要依靠屬性以及屬性的組合來改變自己的行為

L. 不要要求使用者在常用場景中除了寫程式碼,還要搞配置檔案、資原始檔等其他工作

M. 考慮讓聚合元件能夠自動切換狀態

MessageQueue既可以收也可以發訊息,使用者感覺不到模式切換

N. 不要設計有多種狀態的因子型別

O.考慮將聚合元件整合到Visual Stuio的設計器中。

只要實現IComponent介面即可

P.考慮把聚合元件和因子型別分開,各自放到不通的程式集中。防止迴圈依賴。

Q.考慮把聚合元件內部的因子型別暴露給外界訪問

2. Async模式

非同步API建模:

一個是“經典的”,一個是“基於事件的”

經典模式使用回撥函式,在任意執行緒中執行。更加靈活強大,效能也更高。

基於事件模式使用事件,更容易學習。可以和VisualStudio整合。

1) 選擇合適的Async模式

A.如果型別是一個支援視覺化設計器的元件,使用“基於事件的Async模式”

B. 如果必須支援等待控制代碼,使用“經典的Async模式""

C. 考慮在高層API使用“基於事件的Async模式”

D.考慮在底層API時使用“經典的Async模式”

E. 避免在同一個型別或者是一組相關的型別中同時實現兩種Async模式

2) 經典Async模式

public class APMTestRun1 
{ 
 publicstatic void AsyncRun() 
 {
          Utility.Log(""APMAsyncRun:start"");
          stringurl = ""http://sports.163.com/nba/""; 
 HttpWebRequestwebRequest =HttpWebRequest.Create(url) as HttpWebRequest;  // webRequest是一個 IAsyncResult
          webRequest.BeginGetResponse(Callback,webRequest);//開始非同步操作,設定回撥Callback,此Callback函式會在另外一個執行緒中執行,在任務完成的時候呼叫
          Utility.Log(""AsyncRun:download_start"");
 }
 // 回撥函式,引數ar用來執行EndGetResponse()
 privatestatic void Callback(IAsyncResult ar) 
 {
          varsource = ar.AsyncState as HttpWebRequest; 

varresponse = source.EndGetResponse(ar); //一直阻塞,直到結束非同步操作。實際上在這裡一般都可以立刻返回,主要是為了清理掉非同步狀態防止記憶體漏洞

          using(var stream = response.GetResponseStream()) 
          {
                   using(var reader = new StreamReader(stream)) 
                   {
                            stringcontent = reader.ReadToEnd(); 
                            Utility.Log(""AsyncRun:result_size=""+ Utility.GetStrLen(content)); 
                   }
          }
 }
} 

A. 模式的主要元素

a) Begin方法,用來開始一個非同步操作
b) End方法,用來完成一個非同步操作
c) 返回自Begin方法的IAsyncResult物件,它本質上表示一個非同步操作。
d) 由使用者提供的非同步回撥函式,使用者把它傳給Begin方法,當非同步操作完成時會被呼叫。
e) 有使用者提供的State物件,使用者可以先把它傳給Begin方法,隨即傳給非同步回撥函式。通常用這個狀態來把資料從呼叫方法傳給非同步回撥函式。

B. 實現規範

a) 非同步操作定義API時要遵循的約定
i. 給定名為Operation的同步方法,應該提供名為BeginOperation和EndOperation的方法

方法簽名標準:

// 同步方法
public <return>Operation(<parameters>, <out params>)
// 非同步方法
public IAsyncResultBeginOperation(<parameters>, AsyncCallback callback, object state)
public <return>EndOperation(IAsyncResult asyncResult, <out params>)

ii. 要確保begin方法的返回型別實現了IAsyncResult介面

iii. 要確保同步方法的按值傳遞和按引用傳遞的引數在Begin方法中都是按值傳遞
iv. 要確保End方法的返回型別和同步方法的返回型別相同
v. 如果Begin方法丟擲異常,不要繼續執行非同步操作
vi. 要依次通過下面的機制來通知呼叫方非同步操作已經完成
將IAsyncResult.IsCompleted設為true
啟用IAsyncResult.AsyncWaitHandler返回的的等待控制代碼
呼叫非同步回撥函式
vii. 要通過從End方法中丟擲異常來表示無法成功的完成非同步操作
viii. 要在End方法被呼叫時同步完成所有尚未完成的操作
ix. 如果使用者用同一個IAsyncResult兩次呼叫一個End方法,或IAsyncResult是從另外一個不想管的Begin方法返回的,考慮丟擲InvalidOperationException異常
x. 當且僅當非同步回撥函式將在呼叫Begin方法的執行緒中執行的時候,要把IAsyncResult.CompletedSynchoronously設為true

C. Async模式的基本實現樣例

//一個菲波拉契數列的生成器,以非同步方式呼叫
public class FiboCalculator {
     delegatevoid Callback(int count,ICollection<decimal> series); //關於非同步執行緒的原型宣告,按業務需求設定
     privateCallback callback = new Callback(GetFibo); //生成非同步處理執行緒物件,注意GetFibo和Callback是兩個型別,GetFibo表示一個業務功能
     //開始生成一個序列的過程,然後返回
     //返回值是由執行緒啟動方法BeginInvoke所產生的,用來表示一次非同步過程
    publicIAsyncResult BeginGetFibo{
               intcount,
               ICollection<decimal>series,
 AsyncCallback callback,
               object state)
     {
               returnthis.callback.BeginInvoke(count, series, callback, state); //BeginInvoke()的引數列表前半段為非同步業務函式的引數,後2個為執行緒處理所需的引數:callback->任務結束後的回撥;state->傳遞給callback函式的引數
     }
     //阻塞,直到生成序列的過程完成。
     // 使用者可以在主執行緒中呼叫此方法阻塞直到返回,也可以放在非同步回撥方法裡面,用來清理非同步呼叫的記憶體漏洞。
     publicvoid EndGetFibo(IAsyncResult asyncResult) {
               this.callback.EndInvoke(asyncResult);//EndInvoke()的作用為阻塞直到callback執行緒退出,引數應該為BeginInvoke()的返回值
     }
     //生成一個第一個引數count的數量的菲波拉契數列數列,此方法可能會造成一段時間的阻塞,因為可能需要很多層遞迴
     publicstatic void GetFibo(
               intcount, ICollection<decimal> series)
     {
               for(int i = 0; i < count; i++) {
                        decimald = GetFiboCore(i);
                        lock(series) {
                                 series.Add(d);
                        }
               }
     }
     //返回第N個菲波拉契數
     staticdecimal getFiboCore(int n) {
               if(n < 0) throw new ArgumentException(""n must be >0"");
               if(n == 0 || n == 1) return 1;
               returnGetFiboCore(n-1) + getFiboCore(n-2);
     }
}

3) 基於事件的Async模式

A.非同步方法的定義

a) 要確保如果元件定義的非同步方法沒有userState引數,那麼在前一次呼叫完成之前試圖再呼叫該方法都將引發InvalidOperationException

userState引數用來區別對於同一個非同步方法的多次呼叫:

public void MethodAsync(stringarg1, string arg2, object userState);

這個userState會傳遞到事件處理函式裡,用來讓事件處理函式分辨是哪一次非同步請求所產生的事件。

b) 要確保在正確的執行緒中呼叫事件處理程式。
c) 要確保無論是操作已經完成,還是操作出錯,還是操作被取消,都始終會呼叫事件處理程式。不應該讓應用程式無休止的等待一件永遠不會發生的事件。
d) 要確保在非同步操作失敗後,訪問事件引數類的屬性會引發異常。——如果有錯誤導致操作無法完成,那麼就不應該允許使用者訪問操作的結果。
e) 不要為返回值為空的方法定義新的事件處理程式或事件引數型別,要用:
i. AsyncCompletedEventArgs
ii. AsyncCompletedeventHandler
iii. EventHandler<AsyncCompletedEventArg>

4) 對於輸出引數和引用引數的支援

A.非同步方法簽名中去掉所有的輸出引數

B. 把輸出引數作為EventArgs類的只讀屬性暴露給使用者

C. 屬性的名字和型別應該和對應的引數相同

5) 對取消操作的支援

A.要確保在將操作取消時,將事件引數類的Cancelled屬性設為true,並確保在使用者試圖訪問結果時引發InvalidOperationException,來告訴使用者操作已經取消

B. 如果無法取消某個特定的操作,要忽略對取消操作的呼叫而不是丟擲異常。

6) 對進度報告的支援

增加一個額外的ProgessChanged事件,這個事件由非同步操作引發。

傳給此事件的處理程式的事件引數:ProgressChangedEventArgs引數中有一個表示進度的屬性,該屬性為0-100。

A. 要確保如果在一個非同步操作中實現了ProgressChanged事件,那麼在操作的完成事件被觸發之後,不應該再出現此類事件。

B. 要確保如果使用了標準的ProgessChangedEventArgs,那麼ProgressPercentage始終能用來表示進度的百分比。

7) 對增量結果的支援

在少數情況下,非同步操作可以在操作完成之前不定期的返回增量結果(incrementalresult)。

A. 要在有增量節誒過需要報告的時候觸發ProgressChanged事件

B. 要對ProgressChangedEventArgs進行擴充套件來儲存增量結果資料,並用擴充套件後的事件引數類來定義ProgessChanged事件

C. 多個非同步操作返回不通型別的資料

a) 要把增量結果報告與進度報告分開
b) 要為每個非同步操作定義單獨的<MethodName>ProgressChanged事件和響應的事件引數類,來處理該操作的增量結果資料。

3. 依賴屬性

Dependency Properties

依賴屬性的值不儲存在型別的欄位中,而是放在一個屬性儲存區中。比如物件和物件容器的關係;Panel容器中的一個Button物件。

拿Button來講,它的繼承樹是Button->ButtonBase->ContentControl->Control->FrameworkElement->UIElement->Visual->DependencyObject->…

每次繼承,父類的私有欄位都被繼承下來。當然,這個繼承是有意思的,不過以Button來說,大多數屬性並沒有被修改,仍然保持著父類定義時的預設值。通常情況,在整個Button物件的生命週期裡,也只有少部分屬性被修改,大多數屬性一直保持著初始值。每個欄位,都需要佔用4K等不等的記憶體,這裡,就出現了期望可以優化的地方:

因繼承而帶來的物件膨脹。每次繼承,父類的欄位都被繼承,這樣,繼承樹的低端物件不可避免的膨脹。

大多數字段並沒有被修改,一直保持著構造時的預設值,可否把這些欄位從物件中剝離開來,減少物件的體積。

1) 如果需要支援各種WPE特性,比如樣式、觸發器、資料保定、動畫、動態資源以及繼承,要提供依賴屬性

2) 依賴屬性的設計

A.繼承DependencyObject或其子型別實現依賴屬性

B. 要為每個依賴屬性提供常規的CLR屬性和存放System.Windows.DependencyProperty例項的公有靜態只讀欄位

C. 使用DependencyObject.GetValue和SetValue的方式來實現依賴屬性

D.要用依賴屬性的名字加上“Property“字尾來命名依賴屬性的靜態欄位

E. 不要顯式的在程式碼中設定依賴屬性的預設值,應該在元資料中設定預設值

F. 不要在屬性的訪問器中新增額外的程式碼,而應該使用標準程式碼來訪問靜態欄位

G.不要依賴書香來儲存保密資料。任何程式碼都能訪問依賴屬性,即使他們是私有的。

3) 附加依賴屬性的設計

A.依賴屬性的驗證

a) 不要把依賴屬性的驗證邏輯放在訪問器中,而應該把驗證回撥函式傳給DependencyProperty.Register方法

B. 依賴屬性的改變通知

a) 不要在依賴屬性的訪問器中實現屬性改變的通知,而應該向PropertyMetadata註冊改變通知的回撥函式

C. 依賴屬性的強制賦值

a) 不要再依賴屬性的訪問器中實現屬性強制賦值邏輯,而應該向PropertyMetadata註冊強制賦值的回到函式

4. Dispose模式

參見: IDisposable

手動釋放非託管資源的方法:

呼叫.Dispose()方法

使用using(DisposableObjectobj = new DisposableObject()){ ... }

在資源類的解構函式寫釋放程式碼,但是無法確定什麼時候被釋放

1) 要為含有可處置例項的型別實現基本Dispose模式。

2) 如果型別持有需要開發人員顯式釋放的型別,而且其本事沒有終結方法,要為其實現基本Dispose模式並提供終結方法

3) 如果類本身並不持有非託管資源或可處置物件,但是它的子型別卻可能會持有,那麼考慮為此基類實現基本Dispose模式

4) 基本Dispose模式

參見: 要為所有的可終結型別實現“基本Dispose模式”

基本Dispose模式的一個簡單實現:

public classDisposableResourceHolder : IDisposable {
 privateSafeHandle resource; // 掌握一個資源
 publicDisposableResourceHolder() {
          this.resource= ... // 分配資源
 }
 publicvoid Dispose() {
          Dispose(true);
          GC.SuppressFinalize(this);
 }
 //此方法被IDisposable.Dispose方法所呼叫時disposing 為 true,所以應該檢查資源是否還可用
 //此方法被終結器(垃圾回收機制)呼叫時 disposing 為false。
 protectedvirtual void Dispose(bool disposing) {
          if(disposing) {
                   if(resource != null) resource.Dispose();
          }
 }
}

A. 要宣告protected virtual void Dispose(bool disposing)方法,來把所有與非託管資源有關的清理工作集中在一起。

B. 要先呼叫Dispose(true),然後再呼叫GC.SuppressFinalize(this)

C. 不要把無引數的Dispose方法定義為虛方法

D.不要為Dispose方法宣告除了Dispose()和Dispose(bool)之外的任何其他過載放啊分。

E. 要允許多次呼叫Dispose(bool)方法。可以讓它在第一次呼叫後就什麼都不錯。

F. 避免從Dispose(bool)方法中丟擲異常,除非是緊急情況,所處程序已經早到破壞。

G.如果方法在物件終結之後(被呼叫了Dispose方法後)就無法繼續使用,要從成員中丟擲ObjectDisposedException異常

H.如果Close是該領域中的一個標準術語,考慮在Dispose()方法之外再提供一個Close()方法

5) 可終結型別

如果型別覆蓋了終結方法(解構函式),並在Dispose(bool)中加入支援終結的程式碼,以此來擴充套件基本Dispose模式,那麼這些型別就是可終結型別。

這種是把非託管資源封裝成託管資源的做法。效能不高

A. 避免定義可終結類

B. 不要定義可終結的值型別

C. 如果一個型別要負責釋放非託管資源,且非託管資源本身不具備終結方法,要將該型別定位為可終結型別

D.要為所有的可終結型別實現“基本Dispose模式”

參見: 基本Dispose模式

E.不要在終結方法中訪問任何可終結物件,因為被訪問的物件可能已經被終結了

F. 要將Finalize方法(解構函式)定義為受保護的

G.不要在終結方法中放過任何異常,除非是致命的系統錯誤。

如果從終結方法丟擲異常,那麼CLR會關閉整個程序。

H. 考慮建立一個用於緊急情況的可終結物件——如果終結方法在應用程式域被強制解除安裝或執行緒異常退出的情況下都務必執行。

5. Factory模式

1) 要優先使用建構函式,而不是優先使用工廠,建構函式更容易使用,更一致,更方便

2) 如果建構函式提供的物件建立機制不能滿足要求,才考慮使用工廠

3) 如果開發人員可能不清楚待建立物件的確切型別,比如對基類或介面程式設計就屬於這種情況,要使用工長

4) 如果這是讓操作不言自明的唯一辦法,要考慮使用工廠方法

5) 要在轉換風格的操作中使用factory

所謂轉換風格:

int i =int.Parse(""35"");

DateTime d =DateTime.Parse(""10/10/1999"");

6) 要儘量將工廠操作實現為方法,而不是實現為屬性

7) 要通過方法的返回值而不是方法的輸出引數來返回新建立的物件例項

8) 考慮把Create和要建立的型別名連在一起,來命名工廠方法

9) 考慮把建立的型別名和Factory連在一起,以此來命名工廠型別。

6. 對LINQ的支援

語言整合查詢:Language-IntegratedQuery

1) LINQ概要

2) 支援LINQ的幾種方法

3) 通過IEnumerable來支援LINQ

4) 通過IQueryable來支援LINQ

7. Optional Feature模式

抽象的一部分實現支援某種特性,而其他實現則不支援該特性。如stream的實現可能會支援讀、寫、定位或其他組合。

// 可選功能在繼承的時候按所提供的範圍來覆蓋
    public abstract class Stream {
     publicabstract void Close();
     publicabstract int Position { get; }
//可選的寫入功能
     publicvirtual bool CanWrite { get { return false; } }
     publicvirtual void Write(byte[] bytes) {
              thrownew NotSupportedException(...);
     }
     //可選的搜尋功能
     publicvirtual bool CanSeek { get {return false;} }
     publicvirtual void Seek(int position) {
              thrownew NotSupportedException(...);
     }
     //其他可選功能
}

1) 考慮將Optional Feature模式用於抽象中的可選特性

2) 要提供一個簡單的布林屬性來讓使用者檢測物件是否支援可選特性

3) 要在基類中將可選特性定義為虛方法,並在方法中丟擲NotSupportedException異常

8. Simulated Covariance模式

泛型生成的類因為沒有一個公共的基類,在某些情況下很不好操作。比如List<T>就不是List<object>的子類,在需要管理多個List的時候會很麻煩。

因此,我們需要用SimulatedCovariance模式:

  • 宣告一個IFoo<T>的介面模板,在此介面中以T作為型別宣告各種所需的公共的方法。
  • 然後讓具體對於泛型類實現的時候,用Bar<T>: IFoo<object>來繼承
  • 這樣所有的Bar<T>型別都有一個公共的基類:IFoo<object>,因此也可以呼叫此基類的公共方法。

1) 如果需要有一種同意的型別來表示泛型型別的所有例項,考慮使用SimulatedCovariance模式

2) 要確保以等價的方式來實現根基型別成員和對應的泛型型別成員

3) 考慮使用抽象基類來表達根基型別,而不是使用介面來表示根基型別

4) 如果這樣的型別已經存在,考慮用非泛型類作為根基型別

9. Template Method模式

最常見的形式由一個過著多過非虛(通常是公有)成員組成,這些成員通過呼叫一個或者多個受保護的虛成員來實現。

目標是對擴充套件性加以控制。

1) 避免將公有成員定義為虛成員

2) 考慮使用Template Method模式來更好的控制擴充套件性

3) 考慮以非虛成員的名字加""Core“字尾,來命名該非虛成員提供擴充套件點的受保護虛成員

public void SetBounds(...) {
 ...
 SetBoundsCore(...);
}
protected virtual voidSetBoundsCore(...) {...}

10. 超時

需要支援超時的API的設計規範

1) 要優先讓使用者通過引數來指定超時長度

用方法的引數比屬性好,這使操作與超市長度之間的關係更加明確。

2) 優先使用TimeSpan來表示超時長度

用整數來表示超時長度有幾個缺點:

  • 超時長度的度量單位不明顯
  • 不太容易把時間單位轉換為常用的毫秒

如果要用整數,需要滿足:

  • 引數或屬性的名字能夠描述相應的時間單位,如XxxMillisenconds
  • 常用的值非常小,使用者不需要用計算器來得出最終的值,如單位是毫秒,常用的數量應該是小於1秒

3) 要在超時後丟擲System.TimeoutException異常

4) 不要通過返回錯誤碼的方式來告訴使用者發生了超時

11. 可供XAML使用的型別

XAML是WPF用來表示物件圖的一種XML格式,一般用於畫UI

感謝大家的閱讀,如覺得此文對你有那麼一丁點的作用,麻煩動動手指轉發或分享至朋友圈。如有不同意見,歡迎後臺留言探討。