Skip to content

Instantly share code, notes, and snippets.

@sunaot
Created August 2, 2013 09:13
Show Gist options
  • Save sunaot/6138546 to your computer and use it in GitHub Desktop.
Save sunaot/6138546 to your computer and use it in GitHub Desktop.
例外設計の話

例外設計の話。

こんな指針がいいのかなー 2013 夏 ver.

例外の目的とは?

.NET の「例外のデザインのガイドライン」にもこう書いてある。

特定の例外が特定のコンテキストでスローされる理由を把握できている場合は、その例外をキャッチするようにしてください。

回復可能な例外だけをキャッチする必要があります。たとえば、存在しないファイルを開こうとした場合に発生する FileNotFoundException は、アプリケーションで処理できる例外です。それは、アプリケーションがユーザーに問題を知らせ、ユーザーが別のファイル名を指定したり、ファイルを作成したりできるようにすることが可能だからです。ExecutionEngineException を生成するような、ファイルのオープン要求は、例外の根本原因が把握できず、実行を継続することの安全性をアプリケーションが保証できないため、処理しないでください。

例外ではない Exception の使われどころ

try ~ finally のような事後処理を漏らさずやるための機構として、Exception が使われることがある。これは明確にわけて考える。事後処理のための catch をしたい場合、広い範囲を広い例外で捕捉し、拾えるかぎりの場合に事後処理が行われることを保証する必要がある。ただ、これは例外の本来の考え方とは真逆の発想。なので、Exception を catch している場合に、どちらの目的で使用しているかを意識し、それぞれの目的に絞って適切に使う必要がある (なるべく Ruby のブロックによる close の隠ぺいのように設計として組み込んでしまって、明示的に finally をすべきケースを減らすほうがいい)。

参考: http://blogs.msdn.com/b/nakama/archive/2009/01/02/net-part-2.aspx

例外と返り値の違い

例外は投げっぱなしにできる。どうせリカバリの効かない異常の場合には、発生直後でストップしそのまま正常系のコードとしてはその処理を無視したまま、コードの可読性を損なわずに書くことができる。返り値だと、リカバリ不能なケースでも明示的に上位層へ受け渡しをしていかないとどこで問題が発生したかの情報が途切れてしまう (もしくはそのためにアプリケーションの深いところでのロギングが必要となる)。

指針

この辺りを考慮した結果の例外設計の指針としてはこんなところ。

  • なるべく例外で止めない
  • 正常な処理、考慮済の処理として扱えるものはなるべく例外を使わず表現する
  • 「リカバリ可能」な「異常な状態」の場合に、アプリケーション例外を使用する
    • リカバリの例:修正、リトライ、スキップ
  • 例外の catch は「リカバリ」を担当すべき層が早めに拾ってその中で閉じて処理をする
  • それ以外の例外については投げっぱなしにして最上層で復帰できない例外として処理する
  • 例外は可能なかぎり狭い例外で catch し (catch の目的を明確にすると、当然のこと)、基底クラスでまとめて catch するような曖昧な運用は最上位層以外では避ける
  • SystemError を catch してハンドルしようとしない (上位で最後に拾わせる)
  • リカバリ可能な例外 (意図的に上の層で catch し対処が決まっている例外) については、そのケースだけを catch できるようにアプリケーション例外のクラスを定義する
  • 検査例外的な発想で、明示的に上位層へそのアプリケーション例外が投げられることを知っていてほしいケースというのを無くす。アプリケーション例外は局所的に閉じて使い、その影響を漏らさない。これで、上位層では RuntimeError が起きていれば、復帰不可能なエラーだという判断して、ロギングして処理を停止した通知のみをクライアントへ返すことができる。
    • 逆にいうと、あえて定義されているアプリケーション例外は、一つ上の層で必ず catch しリカバリをする必要がある、という設計上のメッセージを表現できる
  • 投げっぱなしでいいって言ってるんだから、わざわざ握りつぶさないこと
  • リカバリするか投げっぱなしにするかと言ってるので、例外の読み替えはほとんど必要ない。自分で処理しないならそのまま投げる (読み替えようがなにしようがログ吐いて死ぬだけなんだからいっしょ)
    • 読み替えが発生しうるケースとして、どうしても重要な情報をロギングしたいためなど、あらたな情報を付加した例外へと拡張したいときなど
  • 経路をたどるのはスタックトレースに任せる。例外発生時の経路をたどれるように例外 (を catch したところでのロギングや例外の読み替えによる情報追加) でサポートしようとするとどんどん処理が深くなり、必要な情報が埋もれる。
  • クラスをつくるの? RuntimeException のメッセージにエラータイプを書けばいいの? よくある悩みだが、これもすぐ上の層で catch してハンドルするときの利用のされ方を考えればいい。エラーの内容によって処理がわかれるならクラスとしてわける。処理はいっしょで内容が伝わればいいだけなら、メッセージに書いておけば ok。

その他のトピック

ログイン失敗は例外?

たとえば、下記の例ではパスワード不一致によりログインが失敗するケースを LoginException として扱い、ログイン処理回りでの諸々のエラーをまとめているがこれは乱用といってよさそう(執筆者は牛尾さんなので、けしてそこらの未熟な人による設計ではなくやってしまいがちな罠)。

ログインでパスワード不一致なのは、当然考えられるべきケースなので、例外ではなく認証の失敗時のフローとして扱われるべき。

http://www.geocities.jp/objectbrain/exceptionbrain.html

ArgumentError は例外的な引数不正

  • ArgumentError Exception はシグネチャのエラーであり、値の validation error とは別のものとしたほうがいい
  • 引数にたいして、不要なものは読み捨て、必要なものを取得し、必要なものの値について制限におさまるかのチェックを行い、パスしないものについてはデフォルト値や null object パターン、無視などで対処できれば済ませたい
  • どうにも復帰できない場合は、ArgumentError Exception を投げ、コンパイル時のシグネチャ違反に相当の重たい失敗として即時失敗させる
  • 逆にいえば、可能な範囲では、ArgumentError Exception に頼らず処理を続けられるほうがいい
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment