# 例外処理の設計
## 定義
例外処理は、ソフトウェアシステムにおける複雑性の最悪の原因の一つである。特殊な条件を扱うコードは正常系を扱うコードより本質的に書きにくい。例外が発生した箇所ではシステム状態が不整合になっている可能性があり、処理を先へ進める(再送・冗長コピーからの回復)か中断して上位へ報告するかのいずれの対応も複雑になりうるうえ、例外処理コード自身が新たな例外を生むことがある。加えて例外処理コードは実運用でほとんど実行されないため、テストが不足しがちで、バグが長期間気づかれずに残りやすい。Ding Yuan らの2014年の調査では、分散データ集約システムにおける致命的障害の90%超が誤った例外処理に起因していた。ここでいう「例外」は言語の formal な例外機構に限らず、メソッドが特殊な戻り値で失敗を伝える場合も含む、プログラムの正常な制御フローを変えるあらゆる非常事態を指す。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]] §10.1)
開発者は「エラーは多く検知するほどよい」という発想から過剰に例外を定義しがちであり、判断に窮した状況をそのまま呼び出し元に押しつけてしまう。しかし、ある状況の処理方法を決めかねているのは開発者自身であり、呼び出し元がそれを知っている保証はない。例外を投げるという行為は、単に問題を他者に押しつけて複雑性を増しているだけのことが多い。あるクラスが投げる例外の集合はそのインターフェースの一部であり、例外はスタックを複数階層伝播しうるため、直接の呼び出し元だけでなくさらに上位の呼び出し元にも影響する、特に複雑なインターフェース要素である。したがって例外の多いクラスはインターフェースが複雑で、より浅いクラスになる。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]] §10.2)
こうした複雑性を減らす最善の策は「例外を処理しなければならない箇所の数」そのものを減らすことであり、これは「例外を投げるな」という単純な標語には還元できない。本書は4つの具体的な手段を提示する。
1. **エラーを存在しないものとして定義する(§10.3〜10.5)**: API の意味論そのものを変えて、かつてエラーだった状況を正常な振る舞いに含めてしまう。実装の工夫ではなくインターフェースの再定義によって、エラーケースを丸ごと消す。著者自身が Tcl の `unset` コマンドで犯した失敗が典型例で、「変数を削除する」という意味論では存在しない変数の削除がエラーになるが、「変数が存在しないことを保証する」という意味論に変えれば、対象の変数が最初から存在しない場合も正常終了になる。より具体的な対比として、Unix の `unlink` は使用中のファイルの削除を、ディレクトリからエントリを外すだけで実データはオープン中のプロセスが全て閉じるまで保持するという意味論に組み込み、Windows 流の「使用中は削除不可でエラーを返す」設計が生む2種類のエラーを両方とも消している。Java の `String.substring` は範囲外のインデックスに `IndexOutOfBoundsException` を投げるが、Python のリストスライスのように範囲外を単に切り詰めて返す意味論にすれば、この例外は不要になり呼び出し側の事前チェックも消える。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]] §10.3〜10.5)
2. **例外の隠蔽(masking, §10.6)**: 意味論の変更で消せない例外を、システムの低いレイヤーで検知・処理し切り、上位のソフトウェアに存在を意識させない。TCP のパケット再送、NFS のサーバ障害時のハング継続がその例である。隠蔽が最も効果的なのは、多くの上位コードから使われる低レイヤーの汎用メソッドで処理する場合である。
3. **例外の集約(aggregation, §10.7)**: 個別の例外ごとに分散したハンドラを書くのではなく、多数の例外を1箇所の汎用ハンドラでまとめて処理する。Web サーバの `getParameter` の例(図10.1・図10.2、個々のサービスメソッドで `try`/`catch` を重複させる代わりに最上位のディスパッチャで一括捕捉する)と、RAMCloud が個々のオブジェクト破損を専用の回復コードではなく既存のサーバクラッシュ回復機構へ「昇格」させる例が示される。集約は例外が複数階層を伝播してから処理されるときに最も効果的であり、低レイヤーの1箇所で処理する隠蔽とは向きが逆である。
4. **クラッシュさせる(§10.8)**: 回復が困難で頻度も低いエラー(メモリ不足、I/O エラー、内部矛盾の検知など)は、回復コードを書くよりも診断メッセージを出してアプリケーションをクラッシュさせる方が単純で安全なことが多い。C の `malloc` が NULL を返す設計は呼び出し全箇所でのチェックを要求し複雑性を増すが、`ckalloc` のようなラッパーで確認とクラッシュを一箇所に集約できる。ただしこの判断はアプリケーションの性質に依存し、複製ストレージシステムのように回復自体が提供価値の一部である場合には妥当ではない。
同じ発想はエラーに限らず特殊ケース一般にも適用できる(§10.9)。テキストエディタの選択範囲を例に、「選択なし」という状態変数を導入する代わりに、開始位置と終了位置が一致する空の選択として選択範囲を常に存在するものと定義すれば、「選択なし」を検査する分岐が丸ごと不要になる。ユーザーが認識する「選択なし」という概念は、実装内部で明示的に表現する必要はない。
ただし、この手法にはやりすぎの危険がある(§10.10)。エラーを存在しないものとして定義したり隠蔽したりしてよいのは、その情報がモジュールの外側で本当に必要とされない場合に限られる。ある学生チームがネットワーク通信モジュールで全ての例外を握りつぶした結果、それを利用するアプリケーション側はメッセージ消失やピアサーバの障害を知る手段を失い、堅牢なアプリケーションを構築できなくなった。著者は「重要でないものは隠すべきで、隠せるものは多いほどよい。しかし重要なものは、インターフェースの複雑性が増えても必ず露出させなければならない」とまとめている。つまり本章の主張は「例外を投げるな」ではなく「例外を投げる必要がある状況の数を減らせ、ただし本当に重要な情報は隠すな」という、2つの力の釣り合いを取る設計判断である。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]] §10.6〜§10.10)
## 横断的知見
- 例外の隠蔽(masking)は、第8章「複雑性を下方に押し込む(pull complexity downwards)」の一具体例として位置づけられる。第8章は「モジュールの実装者がより多くの複雑性を引き受けることで、利用者の負担を減らす」という一般原則を論じており、例外の隠蔽はその原則を「例外処理という特定の複雑性」に適用したものである。両章を並べると、隠蔽が機能する条件(下位レイヤーが多数の利用者に共有される汎用メソッドであること)は、第8章が論じる「複雑性を押し込む先は、その複雑性の実装知識を最も多く持つ場所であるべき」という条件と一致する。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 8 Pull Complexity Downwards]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]])
- 第4章「深いモジュール」の基準(小さなインターフェースで大きな機能を提供するほど深い)と本章の例外設計は同じ尺度で測れる。あるクラスが投げる例外の集合はインターフェースの一部であるため、例外を存在しないものとして定義する・隠蔽する・集約するというどの手段も、インターフェースを縮小させながら機能(エラーを扱いきる能力)を実装側に残すという点で、モジュールを深くする操作として統一的に説明できる。第4章はこの一般原則を抽象的に述べるにとどまり、例外という具体的な複雑性の発生源に対してどう適用するかは本章で初めて詳細化される。(Source: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 4 Modules Should Be Deep]], [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]])
## 未解決の問い
- 「エラーを存在しないものとして定義する」ことと「例外を隠蔽する」ことは、どちらも例外情報を利用者から遠ざける点で似ているが、本書は両者を使い分ける明確な基準(意味論の再定義で対応できる場合と、実装側での処理が必要な場合の境界)を体系的には示していない。Tcl の `unset` と TCP のパケット再送を比べたときに、どちらの手段を選ぶべきかを事前に判定する基準は今後の検討課題である。
- §10.10 のやりすぎの事例(ネットワーク通信モジュールが全例外を握りつぶした事例)は失敗として提示されるが、「その情報が本当に重要かどうか」をレビュー時や設計時に判定する具体的な手法は本章では与えられていない。
- RAMCloud のエラー昇格(error promotion)は障害頻度が低い場合にのみ有効と述べられるが、「頻度が低い」をどう定量的に見積もるかは本章の範囲外である。第20章(性能を意識した設計)でこの種のトレードオフがどう扱われるかは要追跡。
## 関連
- ソース: [[@2018__YaknyamPress__A Philosophy of Software Design - Chapter 10 Define Errors Out Of Existence]]
- 概念: [[抽象化(ソフトウェア設計)]] / [[深いモジュール]] / [[情報隠蔽]]
- 実体: [[John Ousterhout]]
## 出典
- John Ousterhout, *A Philosophy of Software Design*, Yaknyam Press, 2018, Chapter 10.