為類提供軟件約定
根據一種很好的舊軟件開發做法,應在每個方法的頂部(即實現任何重要行為之前)放置一個條件語句作為屏障。 每個條件語句都檢查輸入值必須驗證的不同條件。 如果條件未通過驗證,代碼會引發異常。 這種模式通常稱為 If-Then-Throw。
但是,有了 If-Then-Throw,我們就可以編寫出高效正確的代碼嗎? 是不是在所有情況下,這都足夠了?
If-Then-Throw 不是在所有情況下都能解決所有問題,這不是什么新觀點。 根據約定設計 (DbC) 是 Bertrand Meyer 幾年前提出的方法,這種方法基于這樣一種想法,即每個軟件都擁有一個正式描述其輸入和輸出的約定。 If-Then-Throw 模式幾乎涵蓋了約定的第一部分,但它完全不涉及第二部分。 任何主流編程語言都不是天然支持 DbC 的。 不過,通過現有的一些框架,您可以嘗試在常用語言(如 Java、Perl、Ruby、JavaScript 語言,當然還有 Microsoft .NET Framework 語言)中采用 DbC 方法。 在 .NET 中,可以通過 .NET Framework 4 中增加的代碼約定庫實現 DbC,該代碼約定庫位于 mscorlib 程序集中。 請注意,該庫可用于 Silverlight 4 應用程序,不能用于 Windows Phone 應用程序。
我相信幾乎每個開發人員都原則上同意,約定優先開發方法是一種極好的開發方法。 不過我認為,在 .NET 4 應用程序中積極使用代碼約定的人并不多,因為 Microsoft 已提供了軟件約定并將其集成在 Visual Studio 中。本文著重介紹約定優先方法在代碼維護和簡化開發方面的優勢。 在開發下一個項目時,您可以借鑒本文觀點向領導推薦代碼約定。 以后,我將在本專欄中對某些方面深入探討,如配置、運行時工具和編程功能(如繼承)。
有關簡單 Calculator 類的推論
代碼約定關乎心態;您不應等到必須設計需要超級體系結構并采用很多前沿技術的大型應用程序時才想起使用代碼約定。 請注意,如果管理不善,再強大的技術也可能帶來問題。 只要熟練掌握代碼約定,代碼約定就適用于幾乎任何類型的應用程序。 我們從一個簡單的類開始,一個經典的 Calculator 類,如下所示:
{
public Int32 Sum(Int32 x, Int32 y)
{
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
return x / y;
}
}
您可能會同意,此處不是實際的代碼,因為它至少缺少一個重要的部分:檢查是否要嘗試執行除數為零的運算。 我們要完善代碼,因此,我們還假定還有一個問題要處理:該計算器不支持負值。 圖 1 是該代碼的更新版本,其中添加了一些 If-Then-Throw 語句。
圖 1 實現 If-Then-Throw 模式的 Calculator 類
{
public Int32 Sum(Int32 x, Int32 y)
{
// Check input values
if (x <0 || y <0)
throw new ArgumentException();
// Perform the operation
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
// Check input values
if (x < 0 || y < 0)
throw new ArgumentException();
if (y == 0)
throw new ArgumentException();
// Perform the operation
return x / y;
}
}
到現在,可以這樣說,我們的類或者開始處理其輸入數據,或者(如果輸入無效)在執行任何運算之前引發異常。 該類生成的結果如何? 我們了解它們的哪些情況? 根據規范,這兩個方法都應返回不小于零的值。 我們如何強制實現這一點并在它未發生時失敗? 我們需要該代碼的第三個版本,如圖 2 所示。
圖 2 檢查前置條件和后置條件的 Calculator 類
{
public Int32 Sum(Int32 x, Int32 y)
{
// Check input values
if (x <0 || y <0)
throw new ArgumentException();
// Perform the operation
Int32 result = x + y;
// Check output
if (result <0)
throw new ArgumentException();
return result;
}
public Int32 Divide(Int32 x, Int32 y)
{
// Check input values
if (x < 0 || y < 0)
throw new ArgumentException();
if (y == 0)
throw new ArgumentException();
// Perform the operation
Int32 result = x / y;
// Check output
if (result < 0)
throw new ArgumentException();
return result;
}
}
兩個方法現在都包含三個不同階段:檢查輸入值、執行運算和檢查輸出。 對輸入和輸出的檢查分別有不同的目的。 輸入檢查可標記調用方代碼中的 Bug。 輸出檢查可發現您自己的代碼中的 Bug。 您是否真的需要對輸出進行檢查? 當然,通過某些單元測試中的聲明可以驗證檢查條件。 在這種情況下,您不一定需要在運行時代碼中包含這種檢查。 但是,代碼中的檢查可使類具有自我描述性,可以清楚地表明它可以和不可以執行的操作,這與約定服務的條件十分相似。
如果將圖 2 中的源代碼與我們開始時介紹的簡單類進行比較,就會看到源代碼增加了很多行,這是一個只需要滿足很少要求的簡單類。 讓我們再進一步。
在圖 2 中,我們確定的三個步驟(檢查輸入、運算和檢查輸出)是順序執行的。 如果運算的執行十分復雜,需要加入其他退出點,該怎么處理? 如果某些退出點引用會產生其他結果的錯誤條件,該怎么處理? 事情確實可能會很復雜。 但為了進行說明,向其中一個方法添加一個快捷退出就足夠了,如圖 3 所示。
圖 3 快捷退出復制后置條件代碼
{
// Check input values
if (x <0 || y <0)
throw new ArgumentException();
// Shortcut exit
if (x == y)
{
// Perform the operation
var temp = x <<1; // Optimization for 2*x
// Check output
if (temp <0)
throw new ArgumentException();
return temp;
}
// Perform the operation
var result = x + y;
// Check output
if (result <0)
throw new ArgumentException();
return result;
}
在示例代碼中(這只是 一個示例),如果兩個值相等(相乘而不是相加),則方法 Sum 嘗試一種快捷方式。 但是,必須針對代碼中的每個提前退出路徑復制輸出值檢查代碼。
關鍵是沒有人可以合理地設想采用一種約定優先的軟件開發方法而不使用一些重要工具,至少是使用特定的幫助程序框架。 檢查初步條件相對容易且成本較低;如果手動處理執行后的條件,整個基本代碼會難以處理,并且容易出錯。 更不用說,對于開發人員而言,約定的其他一些附屬方面會使類的源代碼一片混亂,例如,在輸入參數是集合時檢查條件,以及確保每當調用方法或屬性時類總是處于已知有效狀態。
輸入代碼約定
在 .NET Framework 4 中,代碼約定是一個框架,它提供了更加方便的語法來表達類約定。 具體而言,代碼約定支持三種約定:前置條件、后置條件和固定條件。 前置條件指為了安全執行方法而應該驗證的初步條件。 后置條件指方法正確執行或引發異常后應該驗證的條件。 最后,固定條件指在任何類實例的生存期內始終為真的條件。 更準確地說,固定條件是在類與客戶端之間的每個可能交互之后(即在執行包括構造函數在內的公共成員之后)都必須保持的條件。 固定條件是不會檢查的,因此,在調用私有成員之后,可能暫時違反這種條件。
代碼約定 API 由一組在類約定上定義的靜態方法組成。 您可以使用 Requires 方法表示前置條件,使用 Ensures 方法表示后置條件。 圖 4 說明如何使用代碼約定重新編寫 Calculator 類。
圖 4 使用代碼約定重新編寫的 Calculator 類
public class Calculator
{
public Int32 Sum(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
Contract.Ensures(Contract.Result<Int32>() >= 0);
if (x == y)
return 2 * x;
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
Contract.Requires<ArgumentOutOfRangeException>(y > 0);
Contract.Ensures(Contract.Result<Int32>() >= 0);
return x / y;
}
}
粗略比較一下圖 3 和圖 4,可以看出 API 實現 DbC 的有效性。 方法代碼重新回到一種高度可讀的形式,在其中只有兩個層次:約定信息(同時包括前置條件和后置條件)和實際行為。 您不必像圖 3 中那樣,將條件和行為混在一起。 因此,可讀性大大提高,對于團隊來說,代碼維護容易多了。 例如,您可以根據需要,快速安全地添加新的前置條件或編輯后置條件,您只需在一個位置進行更改,還可以清楚地對更改進行跟蹤。
約定信息可以通過普通 C# 或 Visual Basic 代碼表示。 約定說明不同于經典的聲明性屬性,但它們保留了很強的聲明性風格。 使用純代碼而不使用屬性可提高開發人員的編程能力,因為在這種方式下,可以更自然地表達心中所想的條件。 同時,在重構代碼時,代碼約定更具指導性。 實際上,代碼約定會指出方法的可用行為。 代碼約定有助于在編寫方法時遵循編碼準則,有助于在前置條件和后置條件較多時保持代碼的可讀性。 即使可以像圖 4 那樣使用簡潔的語法表達約定,在實際編譯代碼時,所獲得的代碼流也可能與圖 3 中的代碼差不多。 那么訣竅在哪里?
Visual Studio 生成過程中集成的另一個工具(代碼約定重寫程序)可以重塑代碼,了解所表達的前置條件和后置條件的預定用途,并將它們的邏輯置入正確的代碼塊中。 作為開發人員,如果您在某處編輯代碼以添加另一個退出點,則不必擔心在哪里放置后置條件和在哪里將其復制。
表示條件
您可以參考代碼約定文檔確定前置條件和后置條件的準確語法;可以從 DevLabs 網站 bit.ly/f4LxHi 獲取最新 PDF。 我簡單概述一下。 您可以使用以下方法指示所需條件,不符合條件則引發指定的異常:
該方法有幾個可能需要考慮的重載。 方法 Ensures 表示后置條件:
在編寫前置條件時,表達式通常僅包含輸入參數,可能包含同一個類中的某個其他方法或屬性。 如果是這種情況,則需要使用 Pure 屬性對該方法進行修飾,以指明執行該方法不會改變對象的狀態。 請注意,代碼約定工具假定屬性 getters 是純的。
在編寫后置條件時,可能需要訪問其他信息,如局部變量的返回值或初始值。 為此,可以使用一些特定方法,例如,使用 Contract.Result<T> 獲取方法的返回值(類型為 T),使用 Contract.OldValue<T> 獲取方法開始執行時存儲在指定局部變量中的值。 最后,還可以在方法執行期間引發異常時對條件進行驗證。 在這種情況下,可以使用方法 Contract.EnsuresOnThrow<TException>。
縮寫方法
約定語法肯定比使用純代碼更加緊湊,但也可能變得很大。 發生這種情況時,可讀性又會受到影響。 一個自然的補救方法是將多個約定指令組合在一個子例程中,如圖 5 所示。
圖 5 使用 ContractAbbreviator
{
public Int32 Sum(Int32 x, Int32 y)
{
// Check input values
ValidateOperands(x, y);
ValidateResult();
// Perform the operation
if (x == y)
return x<<1;
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
// Check input values
ValidateOperandsForDivision(x, y);
ValidateResult();
// Perform the operation
return x / y;
}
[ContractAbbreviator]
private void ValidateOperands(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
}
[ContractAbbreviator]
private void ValidateOperandsForDivision(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
Contract.Requires<ArgumentOutOfRangeException>(y > 0);
}
[ContractAbbreviator]
private void ValidateResult()
{
Contract.Ensures(Contract.Result<Int32>() >= 0);
}
}
ContractAbbreviator 屬性指示重寫程序如何正確解釋帶相應修飾符的方法。 實際上,如果不使用該屬性將其限定為一種可擴展的宏,ValidateResult 方法(以及圖 5 中的其他 ValidateXxx 方法)可能包含相當復雜的代碼。 例如,在 void 方法中使用時,Contract.Result<T> 會引用什么內容? 目前,mscorlib 程序集不包含 ContractAbbreviator 屬性,因此,開發人員必須在項目中進行明確定義。 該類很簡單:
{
[AttributeUsage(AttributeTargets.Method,
AllowMultiple = false)]
[Conditional("CONTRACTS_FULL")]
internal sealed class
ContractAbbreviatorAttribute :
System.Attribute
{
}
}
改進后的簡潔代碼
綜上所述,代碼約定 API(基本上就是 Contract 類)是 .NET Framework 4 本身的一部分,因為它屬于 mscorlib 程序集。 Visual Studio 2010 在特定于代碼約定配置的項目屬性中提供了一個配置頁。 對于每個項目,您必須明確啟用約定的運行時檢查。 您還需要從 DevLabs 網站下載運行時工具。 在該網站上,您可以選擇適用于所用 Visual Studio 版本的安裝程序。 運行時工具包括代碼約定重寫程序和接口生成器以及靜態檢查程序。
代碼約定通過強制指示每個方法的行為和結果,有助于編寫簡潔的代碼。 至少,它可在重構和改進代碼時提供指南。 關于代碼約定,還有很多內容需要討論。 具體而言,在本文中,我只是簡單提到固定條件,完全沒有提及強大的功能約定繼承。 在以后的文章中,我準備介紹這些內容以及相關內容。 請繼續關注!