通知
收集領域層中錯誤和其他資訊的物件,並將其傳達給簡報。
2004 年 8 月 9 日
這是 進階企業應用程式架構開發 文章的一部分,我是在 2000 年代中期撰寫的。遺憾的是,從那以後,太多其他事情吸引了我的注意力,所以我沒有時間進一步研究它們,而且在可預見的未來,我也沒有看到太多時間。因此,這份文件基本上是草稿形式,直到我有時間再次研究它之前,我不會進行任何更正或更新。
常見的應用程式情境是簡報擷取使用者的資料,並將該資料提交給領域進行驗證。領域需要進行多項檢查,如果其中任何一項失敗,則讓簡報知道。然而,分離簡報 不允許領域直接與簡報溝通。
通知 是領域用來收集驗證期間錯誤資訊的物件。當錯誤出現時,通知 會傳送回簡報,以便簡報可以顯示有關錯誤的更多資訊。
運作方式
在最簡單的形式中,通知可以只是一個字串集合,這些字串是領域在執行工作時產生的錯誤訊息。隨著領域層進行的每次驗證,每次失敗都會導致錯誤新增到通知中。然而,讓通知有比這更明確的介面是有意義的。通知物件通常會有方法來新增使用錯誤碼而非字串的錯誤,以及判斷通知中是否有任何錯誤的便利方法。
如果您正在使用 資料傳輸物件 (DTO),則將通知新增至 層超級類型 中的 DTO 是有意義的。這允許所有互動使用通知來進行清除。
如果您的網域邏輯相對簡單,例如使用交易指令碼,則邏輯可以對 通知 進行直接參照。這使得在新增錯誤時可以輕鬆地參照它。在使用 網域模型 的更多層級系統中,參照 通知 可能會更加有問題,因為此類網域模型通常無法看到諸如傳入 DTO 之類的事物。在這種情況下,有必要將 通知 放置在網域物件可以輕鬆存取的某種類型的階段物件中。
錯誤碼需要出現在表示層和網域之間共用的類別上。使用錯誤碼提供預期錯誤的更明確陳述,並使表示層能夠以比僅列印錯誤訊息更互動的方式呈現錯誤。對於簡單的網域層,通常足以將這些碼嵌入資料傳輸物件 (如果您正在使用一個) 或特定互動的錯誤碼組中。使用網域模型時,錯誤碼需要圍繞網域模型本身的詞彙進行設計。
雖然錯誤通常是 通知 最需要的方面,但 通知 也可傳回網域希望傳達給呼叫者的任何其他資訊,這也很有用。這些可能包括警告 (不嚴重到足以停止互動的問題) 和要顯示給使用者的資訊訊息。這些需要在 通知 上分開,以便表示層可以輕鬆判斷是否存在任何錯誤。
如果您在表示層和網域邏輯位於不同程序的系統中使用 通知,則需要確保 通知 僅包含可以安全地透過網路傳輸的資訊,這通常表示您無法在這些 通知 中嵌入對網域物件的參照。
當簡報收到驗證回應時,它需要檢查 通知 以確定是否有錯誤。如果有,它可以從 通知 中提取資訊,以向使用者顯示這些錯誤。如果出現錯誤,其中一個選項是讓網域引發例外狀況,以便簡報可以使用例外狀況處理來處理錯誤。整體而言,我認為驗證錯誤很常見,不值得為這些情況使用例外狀況處理機制,但這不是壓倒性的偏好。
何時使用
當驗證是由無法直接依賴於啟動驗證的模組的程式碼層執行時,您應該使用 通知。這在分層架構中非常常見,即 分離簡報。
使用 通知 最明顯的替代方法是讓網域使用例外狀況處理來指出錯誤。這種方法讓網域在驗證檢查失敗時擲回例外狀況。這樣做問題在於它只指出第一個驗證錯誤。通常顯示每個驗證錯誤會更有幫助,特別是如果驗證需要往返於遠端網域層。
另一種替代方法是讓網域層為驗證錯誤引發事件。這允許多個錯誤被標記。然而,這對於遠端網域層來說不太好,因為每個事件都會導致網路呼叫。
範例:檢查視窗錯誤 (C#)

在 圖 1 中,我有一個簡化的表單,用於提交評斷保險理賠。我必須提交三項資料:保單號碼 (字串)、理賠類型 (從選取清單中的文字) 和事故日期 (DateTime)。
如果我的資料很簡化,只需等待我的有效性檢查
- 檢查這三項資料是否都沒有遺漏 (字串為 null 或空白。
- 檢查資料儲存庫中是否有保單號碼。
- 檢查事故日期是否晚於保單生效日期。
我們希望盡可能向使用者提供更多資訊,因此如果我們可以合理地偵測到多個錯誤,我們就應該這麼做。
我將從領域層開始討論。領域邏輯的基本介面在服務層中。
類別 ClaimService...
public void RegisterClaim (RegisterClaimDTO claim) { RegisterClaim cmd = new RegisterClaim(claim); cmd.Run(); }
此方法只建立並執行命令物件來執行實際工作。將命令物件包裝在方法呼叫服務層之後,有助於簡化伺服器 API,並讓建立遠端外觀變得更容易。
我使用資料傳輸物件來傳輸資料。RegisterClaimDTO 包含主要資料。
RegisterClaimDTO : DataTransferObject
private string _policyID; private string _Type; private DateTime _incidentDate = BLANK_DATE; public string PolicyId { get { return _policyID; } set { _policyID = value; } } public string Type { get { return _Type; } set { _Type = value; } } public DateTime IncidentDate { get { return _incidentDate; } set { _incidentDate = value; } }
DataTransferObject 是所有 DTO 的層級父類別。此類別包含用於建立和存取通知的通用程式碼,以配合互動。
類別 DataTransferObject...
private Notification _notification = new Notification(); public Notification Notification { get { return _notification; } set { _notification = value; } }
通知類別是我們用來擷取領域層中錯誤的類別。它基本上是錯誤的集合,每個錯誤都是字串周圍的簡單包裝。
類別 Notification...
private IList _errors = new ArrayList(); public IList Errors { get { return _errors; } set { _errors = value; } } public bool HasErrors { get {return 0 != Errors.Count;} }
類別 Notification.Error
private string message; public Error(string message) { this.message = message; }
命令的執行方法非常簡單。
類別 class RegisterClaim : ServerCommand...
public RegisterClaim(RegisterClaimDTO claim) : base(claim) {} public void Run() { Validate(); if (!notification.HasErrors) RegisterClaimInBackendSystems(); }
層級父類別再次提供一些通用功能來儲存 DTO,並存取通知。
類別 ServerCommand...
public ServerCommand(DataTransferObject data){ this._data = data; } protected DataTransferObject _data; protected Notification notification { get {return _data.Notification;} }
驗證方法執行我上面提到的驗證。它基本上只執行一系列條件檢查,如果任何檢查失敗,就會將錯誤新增到通知中。
類別 RegisterClaim...
private void Validate() { failIfNullOrBlank(Data.PolicyId, RegisterClaimDTO.MISSING_POLICY_NUMBER); failIfNullOrBlank(Data.Type, RegisterClaimDTO.MISSING_INCIDENT_TYPE); fail (Data.IncidentDate == RegisterClaimDTO.BLANK_DATE, RegisterClaimDTO.MISSING_INCIDENT_DATE); if (isNullOrBlank(Data.PolicyId)) return; Policy policy = FindPolicy(Data.PolicyId); if (policy == null) { notification.Errors.Add(RegisterClaimDTO.UNKNOWN_POLICY_NUMBER); } else { fail ((Data.IncidentDate.CompareTo(policy.InceptionDate) < 0), RegisterClaimDTO.DATE_BEFORE_POLICY_START); } }
這大部分最複雜的事情是,某些驗證檢查只有在其他檢查沒有失敗時才有意義,這會導致驗證方法中的條件邏輯。如果方法的規模更逼真,就必須將它們分解成更小的區塊。
驗證的常見通用位元可以(而且應該)萃取出來,並放入層級父類別中。
protected bool isNullOrBlank(String s) { return (s == null || s == ""); } protected void failIfNullOrBlank (string s, Notification.Error error) { fail (isNullOrBlank(s), error); } protected void fail(bool condition, Notification.Error error) { if (condition) notification.Errors.Add(error); }
最簡單的通知錯誤形式就是只使用字串作為錯誤訊息。我比較喜歡至少進行最少的包裝,定義一個簡單的錯誤類別,並在 DTO 中定義互動的固定錯誤清單。
類別 RegisterClaimDTO...
public static Notification.Error MISSING_POLICY_NUMBER = new Notification.Error("Policy number is missing"); public static Notification.Error UNKNOWN_POLICY_NUMBER = new Notification.Error("This policy number is unknown"); public static Notification.Error MISSING_INCIDENT_TYPE = new Notification.Error("Incident type is missing"); public static Notification.Error MISSING_INCIDENT_DATE = new Notification.Error("Incident Date is missing"); public static Notification.Error DATE_BEFORE_POLICY_START = new Notification.Error("Incident Date is before we started doing this business");
如果您要跨層級進行通訊,可能需要將 ID 欄位新增到錯誤中,以讓錯誤透過網路序列化時,比較可以正常運作。
這幾乎就是領域層中所有有趣的行為。對於簡報,我將使用自主檢視。我們有興趣的行為會在按下送出按鈕時發生。
類別 FrmRegisterClaim...
RegisterClaimDTO claim; public void Submit() { saveToClaim(); service.RegisterClaim(claim); if (claim.Notification.HasErrors) { txtResponse.Text = "Not registered, see errors"; indicateErrors(); } else txtResponse.Text = "Registration Succeeded"; } private void saveToClaim() { claim = new RegisterClaimDTO(); claim.PolicyId = txtPolicyNumber.Text; claim.IncidentDate = pkIncidentDate.Value; claim.Type = (string) cmbType.SelectedItem; }
此方法會從控制項中擷取資訊來填入 DTO,並將資料傳送至領域層。如果回傳的 DTO 包含錯誤,我們就需要顯示這些錯誤。
類別 FrmRegisterClaim...
private void indicateErrors() { checkError(RegisterClaimDTO.MISSING_POLICY_NUMBER, txtPolicyNumber); checkError(RegisterClaimDTO.MISSING_INCIDENT_TYPE, cmbType); checkError(RegisterClaimDTO.DATE_BEFORE_POLICY_START, pkIncidentDate); checkError(RegisterClaimDTO.MISSING_INCIDENT_DATE, pkIncidentDate); checkError(RegisterClaimDTO.DATE_BEFORE_POLICY_START, pkIncidentDate); } private void checkError (Notification.Error error, Control control) { if (claim.Notification.IncludesError(error)) showError(control, error.ToString()); }
我這裡使用在 DTO 中定義的錯誤,並將它們對應到表單中的欄位,這樣正確的欄位就會顯示正確的錯誤。
為了實際顯示錯誤,我使用 .NET 附帶的標準錯誤提供者。這會在有問題的欄位旁邊顯示錯誤圖示,並顯示提示工具提示,以揭露造成問題的錯誤訊息。
類別 FrmRegisterClaim...
private ErrorProvider errorProvider = new ErrorProvider(); void showError (Control arg, string message) { errorProvider.SetError(arg, message); }
如果欄位中的任何內容變更,我會清除錯誤資訊。
類別 FrmRegisterClaim...
void clearError (Control arg) { errorProvider.SetError(arg, null); } private void txtPolicyNumber_TextChanged(object sender, EventArgs e) { clearError((Control)sender); } private void cmbType_TextChanged(object sender, EventArgs e) { clearError((Control)sender); } private void pkIncidentDate_ValueChanged(object sender, EventArgs e) { clearError((Control)sender); }