読者です 読者をやめる 読者になる 読者になる

すがブロ

sugamasaoのhatenablogだよ

例外戦略

まぁ正しく例外を使いましょうという話ですね。当たり前でしょと思う人は読まなくて良いです!!

前口上

例外に限らず、自分がプログラミングをするにあたって心掛けていることの一つに、誠実なプログラミングというのがある。最近、思いついたので勝手に名前をつけてみたんだけど。
何かっつーと、何が起こっているか、であるとか、これから使うデータはこれです、と言ったものをきちんと伝える・伝わるようにしておく、という心構えです*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のこと

プログラマが知るべき97のこと

*1:心構えっつーか、そういうのをコードに落としておくということ

*2:例外が発生したクラス自身も結構重要な手がかりになるのだよ

*3:この場合backtraceだけど

*4:というかFlex3

*5:AS3忘れすぎて、いわゆるtry〜catchのエラーが出る処理をぱっと思いつかなかったので……

*6:という確認のためにAlert出した

*7:物によっては例外が出たらリトライ処理をするとかあるから、一概に例外を握りつぶすのがダメとは言えないが

*8:ライブラリではまた違う設計になると思うけど、ここはあくまでもアプリケーションレベルの話ね