ドメインエラーを扱う
from Implementation: Working with Errors
LT;DR
システムには以下の 3 種類のエラーが存在する
ドメインエラー
ビジネスプロセス の一部として、予想されるエラー
ドメインモデリング に組み込み、ドメインエキスパート と議論する
e.g. 請求の段階で却下された注文 や 無効な製品コードを含む注文
パニック
処理不可能なシステムエラーやプログラマによる ヒューマンエラー
例外を発生させ、上位レベル(main 関数)で捕捉する
e.g. メモリ不足、ゼロによる演算、null 参照
インフラストラクチャエラー
アーキテクチャ の一部として、予想されるエラー
ドメインエラー と同じように扱っても、パニック と同じように扱っても良い
ただし、 ドメインエラー として扱ったほうが役立つ
ドメインとして考えることで、見落としを無くすため
エラーの多くは ドメインエキスパート に相談せざるを得ないため
e.g. ネットワークのタイムアウト や 認証失敗
ドメインエラー は ドメインモデル と同じようにモデリングする
一般的に、選択型 としモデリングする
理由: ドメインエラーを扱う#66af2e9d75d04f0000cc14af
例外ではなくエラーを返すと、コードの 可読性 が落ちるので、別途対応が必要
Result を生成する関数の連鎖(bind と errorMap、map)
hr.icon
システムに現れる 3 種類のエラー
ドメインエラー
ビジネスプロセス の一部として、予想されるエラー
実際の現場で、このような自体に対処するための手順がすでに用意されている場合、コードにはこれを反映させるべき
e.g. 請求の段階で却下された注文 や 無効な製品コードを含む注文
パニック
処理不可能なシステムエラー や プログラマの見落としによるエラー
e.g. メモリ不足、ゼロによる演算、null 参照
インフラストラクチャエラー
アーキテクチャ の一部として、予想されるエラー
ビジネスプロセスの一部ではなく、ドメイン にも含まれていない
e.g. ネットワークのタイムアウト や 認証失敗
それぞれのエラーの実装方法
ドメインエラー
ドメイン の一部なので、ドメインモデリング にも組み込み、ドメインエキスパート と議論する
可能であれば、型システム で文書化する
パニック
ワークフロー を放棄して例外を発生させる
適切かつ最も高いレベル(e.g. main)で捕捉すべき
Go の panic をイメージすると分かりやすい
code:fsharp
type workflowPart1 = ...
type workflowPart2 input =
if input = 0 then
raise (DivideByZeroException())
...
let main() =
try
let result1 = workflowPart1()
let result2 = workflowPart2 result1
printfn "the result is %A" result2
with
| :? OutofMemoryException ->
printfn "exited with OutofMemoryException"
| :? DivideByZeroException ->
printfn "exited with DivideByZeroException"
| ex ->
printfn "exited with %s" ex.Message
インフラストラクチャエラー
上記のどちらの方法でも処理できるが、システムアーキテクチャ によって異なる
マイクロサービス: 例外処理によるアプローチ(パニック)でも良い
モノリスサービス: ドメインエラーによるアプローチ
ただし、インフラストラクチャエラーの多くは、ドメインエラーと同等に扱ったほうが役立つ
ドメイン として扱うことで、「どんなエラーが起こる可能性があるか」を考えざるをえなくするため
また、インフラストラクチャエラーのほとんどは、 ドメインエキスパート に相談する必要がある
e.g. 外部サービスを利用できない場合
ビジネスプロセスをどう変更すべきか
顧客にはどう伝えるべきか
これらは開発チームだけで対処できるものではなく、ドメインエキスパートや プロダクトオーナー も含めて検討する
型によるドメインエラーのモデリング
ドメインエラー の モデリング も、ドメイン と同じように行う
プリミティブ型を使わず、ユビキタス言語を用いて、ドメインに特化した型を作成する
Domain Modeling with Types
一般的に、エラーは 選択型 としてモデリングし、別途対応が必要なエラーの種類ごとにケースを用意する
e.g. 注文確定の ワークフロー
code:fsharp
type PlaceOrderError =
| ValidationError of string
| ProductOutOfStock of ProductCode
| RemoteServiceError of RemoteServiceError
ValidationError: 長さやフォーマットのエラー、プロパティのチェックに利用
ProductOutOfStock: 顧客が在庫切れの製品を購入しようとしたときに利用
RemoteServiceError: インフラストラクチャエラー の一例
なぜ 選択型 か?
1. うまくいかない可能性のあるすべての事柄について、コード内で明示的なドキュメントとして機能する
2. エラーに関連する追加情報も明示的に示される
3. 要件の変換に応じて、型を簡単かつ安全に拡張(縮小)できる
一般的に、エラーケースは前もって定義するのではなく、開発していく中で浮かんでくるものである
もしそれが ドメインエラー なら型を拡張する必要がある
なぜ安全か?
コンパイラ によるパターンマッチの 網羅性 チェック
警告が表示されたら、そのケースに対して具体的にどう対処するか ドメインエキスパート や プロダクトオーナー と相談するチャンス
エラー処理はコードの見た目を悪くする
例外の利点は、正常処理のコードがきれいになる点
code:fsharp
let validateOrder unvalidatedOrder =
try
let orderId = ...
let customerInfo = ...
let shippingAddress = ...
with
...
各ステップでエラーを返すと煩雑になる
code:fsharp
let validateOrder unvalidatedOrder =
let orderIdResult = ...
if orderIdResult is Error then
return
let customerInfoResult = ...
if customerInfoResult is Error then
return
try
let shippingAddressResult = ...
if shippingAddressResult is Error then
return
...
with
...
潜在的なエラーを捕捉する try ~ catch が組み合わされると、より 可読性 が落ちる
解決策: Result を生成する関数の連鎖(bind と errorMap、map)