領域驅動設計中的異常 - Michał

banq發表於2022-07-08

異常已經被引入來處理函式層面的錯誤。其目的是為了避免返回錯誤程式碼和消除返回型別的模糊性。異常的力量來自於它們透過堆疊向下傳播的能力。你沒有義務直接處理異常。它允許你將你的正常程式碼流與錯誤處理分開。

當函式對引數的假設被打破或者函式不能履行其承諾時,應該丟擲一個異常。最簡單的例子是除以0。我們必須向呼叫者發出訊號,表示我們不能提供答案。這個異常構成了函式契約的一部分。在語言學中,這有時被稱為預設失敗。考慮一下法國國王是否是禿頭的問題。沒有法國國王,所以我們不能用是或不是來回答。任何一個答案都會產生誤導。

異常是領域語言的一部分
把異常作為領域語言本身的一部分來考慮是很方便的。這是一種很好的交流方式。像OrderLimitExceeded這樣的業務異常是很重要的,而且資訊量很大。透過翻閱異常列表,你可以獲得關於業務預期的基本資訊。有必要建立一個一致的領域異常的層次結構。例如,如果你的訂單集合可能會引發許多不同的異常,作為其API的一部分,所有這些異常都應該擴充套件為一個單一的異常,如OrderException。這使它更容易處理。事實上,控制異常流並不是一件容易的事,它需要大量的紀律性。否則,它遲早會失去控制。

異常的問題
不幸的是,異常並不理想。它們破壞了程式碼的自然控制流。它是變相的GOTO。使用異常還增加了介面的複雜性,增強了耦合性。丟擲異常會破壞引用的透明度,所以我們的方法並不純粹。我們的函式沒有單一的返回點,這大大阻礙了推理。

異常還有一個巨大的問題。異常可能導致模型的不一致狀態。如果某些操作無法完成,模型應該保持與之前相同的狀態。開發者並不總是注意到這一點。在計算過程中,模型狀態只被部分改變並且不一致的情況下,就會引發異常。你的IDE和編譯器都不會幫助你解決這個問題。

最近的一項研究表明,分散式系統中90%的故障是指錯誤處理。

功能性方法
使用類似單體的容器型別,如Option、Either或Result,可讀性和優雅性要好得多。這些都是注重功能的語言中眾所周知的結構。我們可以在設計我們的API時只考慮單一的返回型別,而不去扭曲控制流。

Order.addItem(Item $item): Either[Order, Error]


新增一個專案可能導致一個新的訂單(包含該專案)或一個錯誤。指定我們可能從該方法得到的錯誤型別(OrderItemsNumberExceed, CanNotAddItemToFinishedOrder)是至關重要的。該API是優雅而完整的。返回型別明確地告訴我們,我們可能沒有任何引數的有效答案。我們可以使用聯合型別或通用的OrderError,並在方法文件中描述這些錯誤,但依賴型別級的安全總是更好。

在語言層面上,必須可以輕鬆地對許多呼叫進行排序,而不必為錯誤處理而煩惱。如果是這樣的話,那是非常好的。
選項、任一、結果--保留單體特徵的型別很容易被組合。這不會破壞正常的控制流或參考透明性。但是如果沒有語言層面上的語法支援,可能會變成很麻煩。否則,例外可能是更有吸引力的選擇。

免責宣告
這篇文章只提到了商業領域的環境,可能不適用於低階別的或基礎設施的情況。

相關文章