A Philosophy of Software Design ch10 Define errors out of existence
Define errors out of existence
- 例外処理はソフトウェアの複雑性の源として最悪のもののひとつ
- 特殊ケースは通常ケースよりも本質的に取り回しが難しい
- どう処理されるか考えずに軽率に例外を投げがち
- 本章のテーマ
- なぜ例外は不釣り合いに複雑性を生んでしまうのか
- 例外処理をどうシンプルにするか
- 要点
- 例外が処理されなければならない箇所を減らす
- セマンティクスを変えることで、例外そのものをなくせることが多い
- 【補】冪等な削除処理とか
- セマンティクスを変えることで、例外そのものをなくせることが多い
- 例外が処理されなければならない箇所を減らす
Why exceptions add complexity
- 本書における「例外」という言葉の定義
- 通常の制御フローを変えるもの全般
- 言語の例外機構
- 「通常の振る舞いが完了していない」ことを示す特殊な値
- 【補】nullとかのことかな
- 通常の制御フローを変えるもの全般
- こんなときに例外が発生する:
- 引数やコンフィグの誤り
- メソッドを起動したが、要求された操作を完了できなかった
- I/Oとか
- 分散システム
- パケットがロストまたは遅延した
- サーバーが即応しなかった
- ピアが予期せぬ応答をした
- 大きなシステムは、扱う例外的条件も多い
- 特に、分散システムであったり、フォールトトレランスが要件にある場合
- 例外処理がコードベースの大きな割合を占める
- 例外処理は本質的に通常の処理よりも書くのが難しい
- 何かが期待通りに動かなかったということだから
- 2つの選択肢
- 例外が発生したにもかかわらず突き進む
- 例
- パケットがロストしたら再送する
- データが破損したら、冗長コピーから復元する
- 例
- アボートして上の層にエラーを報告する
- inconsistencyが発生したりするので厄介
- 上の層の例外処理で一貫性の確保を要する
- 例外発生前の状態に巻き戻すとか
- 例外が発生したにもかかわらず突き進む
- 例外処理中にさらなる例外を生むことなかれ
- 例
- パケットの遅延
- パケットを再送して2重に届いてしまったら、受信側は例外にみまわれる
- データの復元
- 冗長コピーも壊れてたら?
- パケットの遅延
- 二次の例外は最初の例外よりも捉え難く複雑なもの
- 例
- 例外機構は冗長で不格好
- 膨大な定形try-catchコードにまみれ、例外処理本体が読みづらくなる
- 例外処理は動作を保証しづらい
- 例外処理は滅多に実行されない
- I/Oエラー等、テスト環境で再現しづらいものがある
- 動いているシステムでは滅多に発生しない
- 「実行したことのないコードは動かない」
- 滅多に実行されないのでデバッグ困難
- 【所感】マルチスレッドも再現しづらくて辛い
- 例外処理は滅多に実行されない
Too many exceptions
- むやみに例外を定義して事態を悪化させがち
- 「例外は多く検出されるほどよい」
- 過剰防衛に陥る
- 著者がTclスクリプトを開発したときの昔話
unset
: 「変数を削除する」- 指定の変数が存在しなければ仕事を遂行できないので例外送出
- この仕様、「一時変数を始末する」という最も多いユースケースで使いづらかった
- 存在するかしないか予測することが難しい
- 存在しない場合例外が発生してしまうため
catch
でいちいち囲む必要がある
- 著者最大のミス
- 難しい局面を避けるために例外を投げがち
- 呼び出し元に丸投げ
- そのモジュールが処理方法を知らない場合、呼び出し元も知らないことがままある
- システムの複雑性が増してしまう
- 例外はインタフェースの一部
- 例外が多いクラスはインタフェースが複雑である
- (機能が同じなら)例外の少ないクラスに比べて浅いクラスである
- 投げるは楽、処理するは大変
- 例外が処理されなければならない箇所を減らすことで、例外による複雑性を軽減する
Define errors out of existence
- 例外処理をなくす最良の方法は、例外をそもそも定義しないこと
unset
の定義は次のようであるべきだった:- 「指定の変数が存在しない状態にする」
- 【補】冪等
- もともと存在しなければ単に何もしない
Example: file deletion in windows
- Windowsのファイル削除
- 誰かが開いていたら例外
- ファイルを開いているプロセスを探し、killしてようやく削除できる
- 諦めて再起動することも
- Unixのファイル削除
- 誰かが開いていたら、ファイルデータは削除せず、名前だけ削除する
- 誰も新たに触れないようにする
- 同じファイル名で新しくファイルを作れるようにする
- ファイルを開いていた人は依然として読み書きできる
- 全員がファイルを閉じたら、ファイルデータは削除・解放される
- 誰かが開いていたら、ファイルデータは削除せず、名前だけ削除する
- Unixのアプローチでは、2つの例外が排除されている
- 使用中のファイルを削除しようとしたときのエラー
- 使用中のファイルを削除されたプロセスのエラー
- 削除されたファイルが閉じられるまで読み書き可能なのはおかしな感じがするが、とりたてて問題になることはない
- 開発者にもユーザにもシンプルな定義
Example: Java substring methoda
- Java: 範囲外エラーが出る
- 並み居る言語: 範囲外の場合、空文字を返すなどする
- 「エラーで不具合を見つけるのであって、例外を定義しないと不具合の多いソフトウェアになるのでは?」
- エラーいっぱいアプローチにより不具合は見つかるかもしれないが、複雑性が増してしまう
- エラーを避ける/無視するための追加コードが必要
- 「不具合ありそう感」が増す
- 追加コードを忘れると実行時エラーが出てしまう
- エラーを避ける/無視するための追加コードが必要
- エラーいっぱいアプローチにより不具合は見つかるかもしれないが、複雑性が増してしまう
- ソフトウェアの不具合を減らす最良の方法は、シンプルに作ることである
Mask exceptions
- 例: TCPの高信頼転送
- 例: NFS
- クラス内部で例外をマスクするのは、「複雑性を下位レイヤに引き下げる」一例である
- クラスの利用者はその例外について知らない
- インタフェースが小さくなる
- 深いモジュールになる
Exception aggregation
class Dispatcher... try { if (...) { handleUrl1(...); } else if (...) { handleUrl2(...); } ... } catch (NoSuchParameter e) { // send error response; } handleUrl1 ... getParameter('photo_id'); getParameter('message'); handleUrl2 ... getParameter('user_id');
- 例外ハンドラをまとめる
- WebサーバーならURLのディスパッチャ等で
- 個々のサービスメソッドで例外を捕まえるのではなく、最上位レイヤのディスパッチャに委ねる
- 【補】HTTP Middleware層に例外ハンドラを書くのもこの類ですね
- WebサーバーならURLのディスパッチャ等で
- 知識のカプセル化の好例
- 最上位レイヤのディスパッチャ
- 知っている
- エラーレスポンスの生成方法
- 例外に格納されているメッセージを利用するだけ
- エラーレスポンスの生成方法
- 知らない
- 特定のエラーのこと
- 知っている
- getParameterメソッド
- 知っている
- human-readableなエラーメッセージを例外に格納する方法
- 知らない
- エラーレスポンスの生成方法
- 知っている
- 最上位レイヤのディスパッチャ
- 新機能を追加するときは、getParameter()と同様の方法で例外を送出しさえすればよい
- 最上位レイヤのハンドラが良しなにエラーレスポンスを生成してくれる
- 最上位レイヤのハンドラで処理対象とする、基底の例外クラスを定義すると有用
- 個々の例外はこの基底の例外から派生する
- fatalな例外と区別しつつ、まとめて処理できる
- 【補】PHPなら\Throwable interfaceを継承するインタフェースを定義するとよい
@startuml interface Throwable interface MyDomainException MyDomainException -u-|> Throwable Exception .u.|> Throwable RuntimeException -u-|> Exception ASpecificException -u-|> RuntimeException ASpecificException .u.|> MyDomainException MyDomainExceptionHandler --> MyDomainException @enduml
- 例外の集約は、例外のマスキングの対極
- 例外の集約
- 例外を上位のレイヤに伝播
- 最上位で多くのメソッドの多くの例外をまとめて処理
- 例外のマスキング
- 例外を下位レイヤで処理
- 上位のレイヤには知らせない
- 例外ハンドラをむやみに増やさないという点は共通している
- 例外の集約
- 一旦クラッシュしてリカバリするという選択肢
- RAMCloudの話(略)
- メリット
- 多岐にわたるリカバリのシナリオを絞り、実装をシンプルにできる
- デメリット
- コストが高くつく
- パケットがロストするたびにサーバ再起動したらたまらない
- コストが高くつく
Just crash?
- リトライする価値がない場合に有効
- システムの要件によっては許容できない
Design special cases out of existence
- 「例外をそもそも定義しない」に通ずる
- 特殊ケースも処理できるように通常ケースを設計する
- GUIエディタの例
- 「選択範囲がない」特殊ケースを避ける
- startとendが同じ「空の選択範囲」で表現する
- 「選択範囲がない」特殊ケースを避ける
- different layer, different abstractionの一例
- ユーザのメンタルモデル的には「選択範囲がない」ことは道理
- アプリケーションの実装もそうなっている必要はない
- 「選択範囲は常にある」
Taking it too far
- 例外を定義しなかったり、モジュール内でマスキングすることは、モジュール外でその知識が必要ない場合のみ道理にかなっている
- やりすぎてしまうことも
- 例: ネットワーク通信のエラーを全部マスキング
- モジュール利用側からはすべてうまく行っているように見え、下記のような情報は得られない
- メッセージがロストしたのか
- ピアサーバーが落ちているのか
- 堅牢なアプリケーションを作るためには、たとえインタフェースが複雑化するとしても、例外を公開すべき
- モジュール利用側からはすべてうまく行っているように見え、下記のような情報は得られない
- 例: ネットワーク通信のエラーを全部マスキング
- 例外設計においても、何が重要で何が重要でないか判断する必要がある
- ソフトウェア設計の他の領域と同じ
- 重要でないことは隠蔽すべきで、その部分は多ければ多いほどよい
- 【補】深いモジュールになる
- 重要なものは公開しなければならない
- 【補】さもないと「わからないことがわからない」複雑性につながる
Conclusion
- 特殊ケースはコードを理解しづらくし、不具合のおそれを増やす
- 最たるものが例外
- 本章のテーマは、例外を処理しなければならない箇所を減らすこと
- 例外を定義しなくていいようにセマンティクスを再定義する
- 「あるものを削除する」->「あるものが削除された状態であることを保証する」
- 無くせない例外については、処理する場所を絞る
- 下位レイヤでマスキング
- 最上位レイヤの例外ハンドラで一元的に処理する
- 例外を定義しなくていいようにセマンティクスを再定義する
英語
- out of existence
- Vして...をなくす
- disproportionately
- 不釣り合いに
- disrupt
- 混乱させる
- exacerbate
- 悪化させる
- sacrilegious
- 神聖を犯す