代碼協定中的固定條件和繼承
固定條件
一般來說,固定條件就是一種在給定的上下文中始終為 true 的條件。 在應用于面向對象的軟件時,固定條件指示一種針對類的各個實例始終為 true 的條件。 固定條件是一種強大的工具,每當給定類的任何實例的狀態失效時,它都會及時通知您。 換言之,固定條件協定正式定義據以推測類的實例處于良好狀態的條件。 雖然聽起來很重要,但這只是在通過類對業務域建模時要先了解后實施的第一個概念。 域驅動設計 (DDD) 是目前用于為復雜業務方案建模的成熟方法,而且可在設計時為固定條件邏輯分配一個重要位置。 實際上,DDD 極力建議您永遠都不要處理處于無效狀態的類的實例。 同樣,DDD 還建議您編寫返回有效狀態的對象的類的工廠,并且您的對象在每次操作后都以有效狀態返回。
DDD 是一種方法,協定的實際實施應該由您來完成。 在 .NET Framework 4 中,代碼協定可最大限度地減少您的工作量,有效幫助您成功地進行實施。 我們來更詳細地了解一下 .NET Framework 4 中的固定條件。
固定條件邏輯在哪里?
固定條件是否在某種程度上與優秀的對象建模和軟件設計有關? 深入了解域至關重要。 深入了解域自然會指導您找到您的固定條件。 有些類根本不需要固定條件。 從根本上說,缺乏固定條件并不是一種警告信號。 對應該包含什么內容以及應該執行什么操作沒有限制的類就沒有固定條件。 如果這是您的分析得出的結果,那么再好不過了。
假定有一個類代表要發布的新聞。 該類可能有標題、摘要和發布日期。 此處的固定條件在哪里? 這取決于業務域。 發布日期是必需的嗎? 如果是,則您應該確保新聞始終有有效日期,而“有效日期”的定義也來源于上下文。 如果日期是可選的,則您可以保存一個固定條件,并確保先驗證屬性的內容,然后再將該屬性應用于要在其中使用它的上下文中。 標題和摘要也可以以同樣的方式處理。 既沒有標題也沒有內容的新聞是否有意義? 如果這在您正在考慮的業務方案中有意義,則您有一個無固定條件的類。 如果沒有意義,則要準備好添加幾項檢查,以防標題和內容為空。
更常見的情況是,無任何行為且充當松散關系數據的容器的類可能缺乏固定條件。 如有疑問,我建議您對該類的每個屬性都問問“我是否可以在這里存儲值?”,而不需要考慮屬性是公用的、受保護的還是私用的(只要是通過方法設定的)。 這樣做應該有助于具體了解您是否會遺漏模型的要點。
與設計的許多其他方面一樣,如果在設計過程的早期查找固定條件,則會更富有成效。 在開發過程的晚期添加固定條件始終都是可行的,但這樣做會增加您在重構方面的成本。 如果要這樣做,則必須小心,當心回歸。
代碼協定中的固定條件
在 .NET Framework 4 中,類的固定條件協定是對該類的任何實例始終應為 true 的條件的集合。 向類中添加協定時,前置條件用于在該類的調用程序中查找錯誤,而后置條件和固定條件則用于在類及其子類中查找錯誤。
您需要通過一個或多個專用的方法來定義固定條件協定。 這類方法是實例方法,它們是私有的,返回 void 且有特殊屬性(ContractInvariantMethod 屬性)加以修飾。 此外,固定條件方法不得包含定義固定條件所需的調用之外的代碼。 例如,您不能在固定條件方法中添加任何類型的邏輯,無論邏輯是否純凈都不行。 您甚至不能添加只是用于記錄類的狀態的邏輯。 下面介紹如何為類定義固定條件協定:
public String Title {get; set;}
public String Body {get; set;}
[ContractInvariantMethod]
privatevoid ObjectInvariant()
{
Contract.Invariant(!String.IsNullOrEmpty(Title));
Contract.Invariant(!String.IsNullOrEmpty(Body));
}
}
News 類的固定條件是 Title 和 Body 永遠不能為 null 或空。 請注意,為了使此代碼起作用,您需要根據情況在各種內部版本的項目配置中啟用全面的運行時檢查(請參見圖 1)。
圖 1 固定條件要求對協定執行全面的運行時檢查
現在,嘗試以下簡單代碼:
收到協定失敗異常會讓您感到很吃驚。 您已成功創建 News 類的新實例;遺憾的是該實例的狀態無效。 我們需要從一個新的角度看待固定條件。
在 DDD 中,固定條件與工廠的概念相關。 工廠只是負責創建類的實例的公共方法。 在 DDD 中,各個工廠負責返回處于有效狀態的域實體的實例。 關鍵在于,當您使用固定條件時,您應該保證在任意給定時間內均符合條件。
但是有哪些特定時間呢? DDD 及代碼協定的實際實施均同意在退出任何公共方法(包括構造函數和 setter)時檢查固定條件。 圖 2 顯示了添加構造函數的 News 類的修訂版本。 除了以下一點外,工廠與構造函數基本相同:工廠是靜態方法,可擁有自定義且與上下文相關的名稱并可產生更具可讀性的代碼。
圖 2 固定條件和可識別固定條件的構造函數
{
public News(String title, String body)
{
Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(title));
Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(body));
Body = body;
}
public String Body { get; set; }
[ContractInvariantMethod]
{
Contract.Invariant(!String.IsNullOrEmpty(Title));
Contract.Invariant(!String.IsNullOrEmpty(Body));
}
}
使用 News 類的代碼可以如下所示:
因為實例是在滿足固定條件的狀態下創建并返回的,所以此代碼不會引發任何異常。 如果添加以下行,情況會怎樣:
n.Title ="";
將 Title 屬性設置為空字符串會使對象處于無效狀態。 因為固定條件是在退出公共方法時檢查的,而屬性 setter 是公共方法,所以您會再次收到異常消息。 有趣的是,如果您使用公用字段而不是公用屬性,您會注意到不會檢查固定條件,而且代碼運行狀況良好。 但您的對象處于無效狀態。
請注意,使對象處于無效狀態不一定是問題的根源。 但在大型系統中,為了安全起見,您可能需要妥善地進行管理,以便在遇到無效狀態時自動收到異常信息。 這有助于您管理開發工作和執行測試。 在小型應用程序中,固定條件可能不是必需的,甚至在分析時出現的一些固定條件也不是必需的。
盡管在退出公共方法時必須驗證固定條件,但任何公共方法的主體內部的狀態都可能是暫時無效的。 重點在于,固定條件在執行公共方法前后都為 true。
如何防止對象進入無效狀態? 靜態分析工具(如 Microsoft Static Code Checker)能夠檢測到給定任務是否會違反固定條件。 固定條件使您免受中斷行為的危害,同時還可以幫助確定未明確指定的輸入。 通過正確指定這些輸入,您可以更輕松地在使用給定類的代碼中找到錯誤。
協定繼承
圖 3 顯示了另一個定義了固定條件方法的類。 此類可充當域模型的根。
圖 3 域模型的基于固定條件的根類
{
public abstract Boolean IsValid();
[Pure]
private Boolean IsValidState()
{
return IsValid();
}
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(IsValidState());
}
}
在 DomainObject 類中,固定條件通過被聲明為純方法(即不發出狀態警報)的私有方法表示。 在內部,私有方法調用派生的類將用于指示其自身固定條件的抽象方法。 圖 4 顯示了可能派生自覆蓋 IsValid 方法的 DomainObject 的類。
圖 4 覆蓋固定條件使用的方法
{
private Int32 _id;
private String _companyName, _contact;
public Customer(Int32 id, String company)
{
Contract.Requires(id >0);
Contract.Requires(company.Length >5);
Contract.Requires(!String.IsNullOrWhiteSpace(company));
Id = id;
CompanyName = company;
}
...
public override bool IsValid()
{
return (Id >0&&!String.IsNullOrWhiteSpace(CompanyName));
}
}
該解決方案簡單而有效。 我們來嘗試獲取傳遞有效數據的 Customer 類的一個新實例:
如果我們停止查看 Customer 構造函數,一切都顯得完美無暇。 但是,因為 Customer 從 DomainObject 進行繼承,所以要調用 DomainObject 構造函數并檢查固定條件。 因為 DomainObject 上的 IsValid 是虛擬的(實際上,是抽象的),所以會根據針對 Customer 的定義將調用重定向到 IsValid。 遺憾的是,檢查的實例尚未完全初始化。 您會收到異常消息,但這不是您的錯。 (在最新發布的代碼協定中,此問題已得到解決,而且直到調用最外部構造函數后才會對構造函數執行固定條件檢查。)
此方案映射了一個已知問題:不要從構造函數中調用虛擬成員。 在此例中,發生這種情形的原因并不是您直接以這種方式進行編碼,而是協定繼承產生了負面影響。 您有兩個解決方案:從基類中刪除抽象 IsValid,或借助圖 5 中的代碼。
圖 5 覆蓋固定條件使用的方法
{
protected Boolean Initialized;
publicabstract Boolean IsValid();
[Pure]
private Boolean IsInValidState()
{
return!Initialized || IsValid();
}
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(IsInValidState());
}
}
public class Customer : DomainObject
{
public Customer(Int32 id, String company)
{
...
Id = id;
CompanyName = company;
Initialized = true;
}
}
Initialized 保護的成員充當安保成員,它不會調入已覆蓋的 IsValid,直至初始化了實際對象。 Initialized 成員可以是字段,也可以是屬性。 不過,如果是屬性,您會得到第二輪固定條件 — 嚴格說來這并不是必需的,因為已經將一切檢查了一遍。 在這一方面,使用字段會使代碼的運行速度略勝一籌。
派生的類自動接收針對其基類定義的協定,因此從這個意義上來說,協定繼承是自動的。 這樣,派生的類便會將其自己的前置條件添加到基類的前置條件。 后置條件和固定條件也一樣。 處理繼承鏈時,中間語言重寫程序會累加協定,并在適當的位置和時間以正確的順序調用協定。
注意
固定條件并非無懈可擊。 有時,固定條件一方面可提供幫助,另一方面則會引起問題,尤其是用在每個類和類層次結構的上下文中時。 盡管您始終都應盡力確定類中的固定條件邏輯,但如果實施固定條件會遇到極端情況,那么我建議您最好從實施中將其排除。 但是,請記住,您遇到的極端情況可能是由模型中的復雜性所導致的。 固定條件是您管理實施問題所需的工具;它們本身并不是問題。
至少有一個理由可以說明累加整個類層次結構的協定是一種微妙的操作。 這不是一兩句話可以說清楚的,我們在這里就不討論了,不過這為下個月的文章提供了很好的素材。