例外戦略
まぁ正しく例外を使いましょうという話ですね。当たり前でしょと思う人は読まなくて良いです!!
前口上
例外に限らず、自分がプログラミングをするにあたって心掛けていることの一つに、誠実なプログラミングというのがある。最近、思いついたので勝手に名前をつけてみたんだけど。
何かっつーと、何が起こっているか、であるとか、これから使うデータはこれです、と言ったものをきちんと伝える・伝わるようにしておく、という心構えです*1。
その中でもとにかく例外はきちんと扱って欲しい事の一つなので、ちょっと自分はこーしているというのを世に残しておくのも良いかなぁと思った次第です。
例外はたくさんの重要なデータを持っている
概ねここらへんの情報は例外機構を持ってる言語なら提供してくれると思います。
Ruby だったら class Exception を見ればどのようなデータが取れるかわかりますね。
コードを見たほうが早いかな。
require 'fileutils' class Hoge def fuga FileUtils.rm('/tmp/hoge/fuga') end end begin Hoge.new.fuga rescue => e puts "e.class => #{e.class}" puts "e.message => #{e.message}" puts "e.backtrace => #{e.backtrace}" end
sugamasao@GRAM% ruby hoge.rb e.class => Errno::ENOENT e.message => No such file or directory - /tmp/hoge/fuga e.backtrace => ["/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:1406:in `unlink'", "/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:1406:in `block in remove_file'", "/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:1411:in `platform_support'", "/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:1405:in `remove_file'", "/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:785:in `remove_file'", "/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:563:in `block in rm'", "/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:562:in `each'", "/Users/sugamasao/.rvm/rubies/ruby-1.9.3-head/lib/ruby/1.9.1/fileutils.rb:562:in `rm'", "hoge.rb:5:in `fuga'", "hoge.rb:10:in `<main>'"]
この例だとメッセージだけでおおよそ見当はつくけれど、存在しないファイルを消そうとしたという例外が発生したってわかりますよね。で、スタックトレース*3を見ると、hoge.rbの5行目で定義されているfugaメソッド内で呼んだfileutilsのrmが呼ばれてるなーとかわかるわけ。そうすっと、今回は一個だけだからすぐに特定できたけれど、複数の処理のどこで例外が起きたとかもわかるよね。
昔はActionScript3*4を書いてる時期があって、ASで書いたらどんな感じになるんだっけ、と思ってちょっとAIRで書いてみたよ*5。
ASの場合、いわゆる例外と、例外イベントの二種類があるのが厄介ですね。
import flash.filesystem.File; import flash.filesystem.FileMode; import flash.filesystem.FileStream; import mx.controls.Alert; protected function button1_clickHandler(event:MouseEvent):void { try { readData(); } catch (error:IOError) { Alert.show("errorID = " + error.errorID.toString()); Alert.show("message = " + error.message); Alert.show("name = " + error.name); Alert.show("stacktrace = " + error.getStackTrace()); Alert.show("errorClass = " + flash.utils.getQualifiedClassName(error)); trace("errorID = ", error.errorID); trace("message = ", error.message); trace("name = ", error.name); trace("stacktrace = ", error.getStackTrace()); trace("errorClass = ", flash.utils.getQualifiedClassName(error)); } } private function readData():void { var file:File=File.desktopDirectory.resolvePath("sample.txt"); var stream:FileStream=new FileStream(); try { stream.open(file, FileMode.READ); } finally { stream.close(); } } protected function button2_clickHandler(event:MouseEvent):void { var loader:URLLoader=new URLLoader(); loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); var request:URLRequest=new URLRequest("MissingFile.xml"); loader.load(request); } private function ioErrorHandler(event:IOErrorEvent):void { var e:Error=new Error(); trace(event.text); trace(event.type); trace(e.getStackTrace()); Alert.show(event.text); Alert.show(event.type); Alert.show(e.getStackTrace()); }
当然、デバッグモードの時しかtraceでない(Flex4からはそうなったのだ!!)けど、リリースモードだってきちんと文字列は取れるので活用しない手は無いよね*6。
# ここは普通の例外 errorID = 3003 message = Error #3003: File or directory does not exist. name = Error stacktrace = Error: Error #3003: File or directory does not exist. at flash.filesystem::FileStream/open() at Hoge/readData()[/Users/sugamasao/Documents/Adobe Flash Builder 4/Hoge/src/Hoge.mxml:39] at Hoge/button1_clickHandler()[/Users/sugamasao/Documents/Adobe Flash Builder 4/Hoge/src/Hoge.mxml:20] at Hoge/___Hoge_Button1_click()[/Users/sugamasao/Documents/Adobe Flash Builder 4/Hoge/src/Hoge.mxml:62] # ここからイベントハンドラ errorClass = flash.errors::IOError Error #2032: Stream Error. URL: app:/MissingFile.xml Error at Hoge/ioErrorHandler()[/Users/sugamasao/Documents/Adobe Flash Builder 4/Hoge/src/Hoge.mxml:53] at flash.events::EventDispatcher/dispatchEventFunction() at flash.events::EventDispatcher/dispatchEvent() at flash.net::URLLoader/onComplete()
薄い記憶通り、後者のイベントハンドラだとろくなメッセージが帰ってこないw だもんでイベントハンドラで受け取った時にスタックトレース出したら結構良いのでは、と思ったけどイベントが発火したことのスタックトレースがでちゃってあんまり役に立たないね。
とはいえ、最低限どのようなエラーが返ってきているかはわかるし、たぶん作りによってはcurrentTargetとかからもっと良い情報が取れる気もする(もう全て忘れたのでテキトウ)。
閑話休題
さて、これらの例外が起こった情報というのは当たり前だけれど、アプリケーションがどのような状況に陥ったかを調べることができる重要な情報だ。とりあえず動けば良いやーというものであればテキトウな対応をして、あとでチマチマ調べるということもできるかもしれないけれど、なにか起こった時に「いやーよくわかりませんねーw」とか言う状況っていうのはプロとしては腹を切って死ぬべき状況だと思う。
……時には対応が漏れたりするときもあると思うけれど、それは発覚した時にきちんと再発しないように対応するのがプロです。
例えば、こんな(笑い話でよく見る)コードを書いていたら腹を切って死ぬ覚悟をしたほうが良い*7。
begin なんとかの処理 rescue => e # 何もしない end
コレ以外にもこのような例も腹を切って死ぬ覚悟が必要になるケース。
begin なんとかの処理 rescue => e raise 独自例外クラス.new end
例えば、イベントドリブンなものではこんな感じか
private function errorEventHandler(e:Error):void { this.dispatchEvent(new 独自イベント(独自イベント.ERROR)); }
あまりに例外が明示的すぎてこれで十分な可能性もあるけれど、既に起こった例外の情報を全て捨てて、新たなイベントや例外を起こすというのはほぼ例外の握りつぶしに等しい。気を効かせたつもりかもしないけれど、大きなおせっかいにしかなっていない例である。やるなら、元の情報をもたせた上での例外送出を行うべきである*8。
おれ自身はRubyで書くことが多いので、こんなふうにしてコンストラクタで渡したりする。例外クラスの情報をシリアライズ化させるメソッドを定義して、そこを通すようにしているっていうのが正解かなぁ。フォーマットとかはテキトウなんだけど、少なくとも値が空だった時に空だったってわかるように、値の部分に何かの囲いを付けるようにしている。
begin なんとかの処理 rescue => e raise 独自例外クラス.new("なんとかしようとしたら例外が出たよ >Class=[#{e.class}], message=[#{e.message}], stacktrace=[#{e.backtrace}]") end
エラーイベントでもmessageとか、取れるものはきちんと取ってイベントをディスパッチしてあげれば良いよね。
どのようにアウトプットすべきか
エラーが起こった情報を取得することができたとして、ではどうやってアウトプットすれば良いか。
サーバサイドアプリケーション
いわゆるサーバサイドではすごく王道の選択肢があるので、それを使うのが良いですよね(さすがにこれが無いのはヤバい!!)。
- 自前のログファイルに出力する
- syslogやEventLogみたいなOSが提供するログシステムに出力する
デスクトップアプリケーション
デスクトップアプリケーションの場合は結構難しい。結局、ユーザから送ってもらうなりしないといけないケースが多いし、ファイルに落とすにしても自力管理するハメになる可能性があるからだ。
- 自前のログファイルに出力する(自前でログファイルの管理が必要かも)
- syslogやEventLogみたいなOSが提供するログシステムに出力する
- レポート送信機能的なもの(アプリケーションやOSが落ちた後に出てくるアレ)
- 独自に定義したエラー番号とかをアプリケーションが終了するときにアラートとかで出しておく
ココらへんはアプリケーションの性質やUIとの兼ね合いもあるけれど、レポート機能みたいのが一番無難なんですかね。
補足:私はデスクトップアプリケーションを開発した経験がないので、こーいうやり方が良いよ!という方法があれば教えて下しあ
この場合とはちょっと話が異なるけれど、JSの場合はonerror関数を使って例外をキャッチして例外が発生した用のURLに情報を投げる、なんてのを聞いたことがありますね。
本当はログの出力方針についての話も書こうと思ったけどなんか既に話が長くなったので辞めた。
よーするに要点を押さえて出す(ファイルI/Oはとても遅いのでたくさん出すのはデバッグ版だけにするんだよ)とか、ログレベルとかビルドオプションとかできちんと切り替える機構を使おうとかそういう話なので、まぁ普通の開発者は知ってることですよね……。
あわせて読みたい
- いわゆる仕様と業務例外について - 急がば回れ、選ぶなら近道
- プログラマが知るべき97のこと
プログラマが知るべき97のことには、カテゴリ別の目次もあるんです。そのカテゴリとして"エラー、例外とその処理"という項目があるので、読んでみると良いでしょう。
え、まだ本を持ってないですって?ここのリンクから買うと良いんじゃないですかね!!
- 作者: 和田卓人,Kevlin Henney,夏目大
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/12/18
- メディア: 単行本(ソフトカバー)
- 購入: 58人 クリック: 2,107回
- この商品を含むブログ (332件) を見る