原文 http://www.lemanix.com/nick/articles/1374.aspx
第三章 例外の正しい使い方(前編)
ここまで「こんな例外はいらない」を見てきました。以下は、Delphiの例外処理システムを適切に使う、いくつかのTIPSです。
例外を使用するのは、開発者のコードを、例外処理コードに邪魔されずに、よどみなく流す為
例外処理の主要な目的の1つは、例外チェックのコードを完全に取り払い、アプリケーションの主要なロジックから、例外処理コードを分離する事です。例外処理によって、開発者は何の問題も無いかのように、コードを記述する事が出来ます。そして発生する可能性のあるエラーや問題を処理したい場合は、try exceptブロックでコードを包み込む事が出来ます。これにより、処理を行う前に、適切な形式になっているかどうか、パラメータや他のデータを常にチェックしなくても、コードはより効率的に動作することが出来ます。そのようにする方法の1つは、例外処理をまとめる事です。TApplicationには、開発者が例外処理を行う事が出来るイベント、OnExceptionイベントがあります。開発者は、このイベントを使って、それ以外では、アプリケーションから操作できない、全てのタイプの例外を処理する事が出来ます。またこのイベントを使って、例外のログをとったり、特定のタイプの例外に対する特定の操作を行うことが出来ます。
例外をトラップすべきなのは、アプリケーションを書く人
以下で論じるように、コンポーネントとライブラリのコードは、例外の主な発生源となるべきです。つまり、コンポーネントとライブラリのコードは、最も例外が生成され、発生する場所となるべきです。アプリケーションを記述する場合、開発者が例外を生成・発生させる必要は、ほとんどありません。アプリケーションを書く人は、主にコンポーネントやライブラリのコードによって発生する例外を処理する役割を果たすべきです。
特定の例外だけをトラップすべし
前述のとおり、開発者は例外を食べてしまうべきではありません。代わりに、開発者のコード内で発生すると理論的に予想される、特定の例外だけをトラップすべきです。開発者が大量の計算をしている場合は、EMathError例外をトラップしても良いです。大量の変換をしている場合は、EConvertError例外をトラップしても良いです。データベースを使用している場合は、EDatabaseError例外を監視しても良いです。が、それらのエラーでさえ、ちょっと一般的すぎるかもしれません。例えば、データベースのコードでは、特定のデータベース動作を起動する時だけ発生する、特定のEDatabaseError継承クラスがあるかもしれません。よって、クエリーを開いた時は、もしかすると、一般的なEDatabaseErrorより、DataSetを開いた時だけに発生する例外をトラップすべきかもしれません。前述の通り、私は例外を食べてしまうコードが追加されているのを見た事があります。その理由は、開発者(やマネージャー、もしくは頭の悪い誰か)がユーザーに、エラーを絶対見せたくないからです。ユーザーにエラーを見せない方法は、ユーザーが見てしまう特定の例外をトラップする事です。例えば
try EConvertErrorを発生する、いくつかのコード; except on E: EConvertError do begin // 特定の例外に、ここで対応 end; end;
開発者がその例外で何をするとしても、このコードは単純に全ての例外を食べてしまうよりはマシです。その理由は少なくとも、トラップするのは、1つの例外だけであり、一緒にやって来る他の例外をトラップしないからです。更に、データベース例外(そしてその他の、例えばCOMエラー)には一般的に、エラーコードがあります。開発者は、特定のエラーコードのエラーだけをトラップして、他は受け流したいかもしれません。これは以下のようにすれば可能です。
try EConvertErrorを発生する、いくつかのコード; except on E: EIBError do begin ifE.ErrorCode = iSomeCodeIWantToCatch then begin // 特定の例外に、ここで対応 end else begin raise; // 処理する例外でない場合は、例外を再生成 end; end; end;
例外を出来るだけ継承関係の下の方へ置く理由は今後、より一般的な例外クラスを継承した例外が作成されるかもしれないからです。例えば、このようなコードが有る場合
try SomeDataset.Open except on E: EDatabaseError do begin // 例外処理 end; end;
そしてその後、追加宣言します。
type ENxStrangeDatabaseError = class(EDatabaseError)
新しい見慣れない例外が、先ほどのコードでトラップされるでしょう。もしかすると、これは開発者が元々意図した事ではないかもしれません。誰かが既存の例外を継承した時、意図しないトラップが起きないようにする事は、明らかに出来ません。しかしクラス図の最下層の例外をトラップすることで、意図しないトラップが起きる頻度を減らす事は出来ます。結論:例外を出来るだけクラス階層の下でトラップする事。処理するつもりのある例外だけトラップする事。
例外を発生させるのは、コンポーネントを書く人とライブラリを書く人
例外は神秘的に発生するわけでは有りません。それらの大部分は、VCLの中で生成され、発生します(いくつかはDelphiのコード外で発生します)。そして開発者が自分の例外を発生する事もまた完全に認められています。一般的なルールとして、開発者はライブラリのコードやコンポーネント内で、特定の例外を発生させるべきです。そうすれば、アプリケーションを書く人は、上記のように自分のコード内で、それらの特定の例外をトラップできます。ライブラリのコードやコンポーネントのメソッドを記述する場合、開発者は以下の二者択一の処理方法を記述すべきです。つまりライブラリのコードやコンポーネントのメソッドが無事実行されて元に返るのか、例外を発生させるのかです。またアプリケーションを書く人も、そのような記述がある事を想定すべきです。つまりそのルーチン呼び出しの結果は、無事に戻ってくる(関数の場合は、妥当な結果と共に)のか、例外を発生するのかの二者択一です。
例外の正しい使い方(後編)
開発者独自のカスタム例外を発生すべし
例外を発生させる場合は、常に開発者独自のカスタム例外を発生しましょう。例えば、NixUtils.pasと呼ばれるファイル内で使用する、ライブラリ・コードがあるとします。このファイルで、以下のような宣言をします。
NixUtilsException = class(Exception);
NixUtils.pasのルーチン内で発生させる全ての例外は、NixUtilsException型かNixUtilsExceptionを継承したものです。こうすることによって、開発者のライブラリ・コードやコンポーネントの利用者は、先ほど紹介した事、つまり特定の例外だけをトラップする事が出来ます。開発者独自の例外クラスを宣言したり、それを継承する事を恐れないで下さい。特定のルーチンのために特定の例外を用意しても良いのです。これによりコードの利用者は、彼らが必要とするどんな粒度でも、例外をトラップできます。私はVCLを書く人が、もっとこうする事を願っています。VCLの例外階層は、まっ平らです。VCLを書く人が、もっと特定の例外を発生させれば、素晴らしいでしょう。特にデータベース関係は、色々な場所で例外を発生出来ますし、発生します。
ユーザーには例外メッセージを見せるべし
もし開発者が、全てのエラーを、ユーザーの思いやりのある目から隠そうとしている場合は、自分自身にこんな質問をして下さい。ユーザーにエラー・メッセージをみせる事と、何事もなかったかのように(たぶん途中で、マズイ計算や間違ったデータの跡を残しながら)、アプリケーションを進行させるのと、どちらがマズイのでしょうか?残念ながら、私の見てきた多くのコードの答えは、「みせる」より「みせない」でした。これが開発者のコードが、空の例外ハンドラのあるコードや、全ての例外を食べてしまうコードになってしまう仕組みです。例外は、何かがマズイから発生するのです。それを無視すると、意外な結果になってしまうでしょう。メモリ例外を食べてしまうと、悲惨な結果になるでしょう。その理由は、マズイことが起きた時に、何事も無かったかのように、アプリケーション・・・と利用者・・・が処理を続けてしまうからです。ユーザーは、ユーザー・インターフェースの達人の多くが気づいているように、ダイアログ・ボックスにびくびくしています。しかしダイアログ・ボックスで、ユーザーが何か出来るようになっていれば、ダイアログ・ボックスは、通常の物より有益な物になるでしょう。
遠慮せずに、十分な例外メッセージを用意すべし
VCLが発する、標準的なエラーメッセージは残念な事に、とても薄っぺらくて、悪名高い物です。私が個人的に「お気に入り」なのは、「List Index out of range」エラーです。このメッセージで、リストが範囲外である事について伝えている情報は、それこそゼロです。少なくとも、このメッセージでは、リストのクラス名ぐらいは知らせて、エラーの範囲をちょっとでも狭くするべきです。そう思いませんか?開発者のエラーは、簡単すぎ、情報量の少ないエラーであってはなりません。開発者が例外を発生する場合、遠慮なく、好きなだけ情報量の多いメッセージを、スタックに渡しましょう。こんな風にする必要はありません。
type ESomeException = class(Exception); procedure CauseAnException; begin raise ESomeException.Create('退屈なメッセージ'); end;
こんな風に出来る場合
type ESomeException = class(Exception); procedure CauseAnException; begin raise ESomeException.Create('「CauseAnException」プロシージャーは’ + '例外を発生すること以外何もしません。’ + '一体全体なんでこんな物を、あなたは’ + '呼んでいるのですか?’ ); end;
VCLの奴の真似をしないで下さい・・・十分で、説明力のあるエラーメッセージを書きましょう。プロシージャーの名前や、TObject.ClassNameなど、何でも好きな事をメッセージに含めましょう。それどころか、既存の例外エラー・メッセージを拡張する事も出来ます。例外の動作は通常のままです。
type EBoringMessageException = class(Exception); procedure CauseABoringException; begin raise EBoringMessageException.Create('退屈なメッセージ'); end; procedure TForm1.Button1Click(Sender: TObject); begin try CauseABoringException; except on E: EBoringMessageException do begin E.Message := 'これは、私がわざと発生したエラーです。' + '場所はTForm1 クラスのButton1Click イベントです。' + '本当のエラーメッセージは:' + E.Message; raise; end; end; end;
この例外ハンドラは、例外の呼び出しを含んでいます。よって、ここでは、例外を食べずに、どこでエラーが起きたのか等の情報を示す事が出来ます。メッセージには、ユーザーが助けを求めに行く場所や、エラーが続いた場合、どうすればよいのか等についての情報を入れましょう。
1つのライブラリ・ルーチンに2つのバージョンを用意すべし
時には人々が例外を返すルーチンを嫌う事もあります。いいでしょう、OK、彼らに必要なものを提供しましょう。VCLは、これをしています・・・VCLは時々、同じ事をする2つの関数を用意しています。つまり片方は失敗に対して、例外を発生させる物。他方は、nilやエラー値を返したり、結果次第の物。FindClassとGetClassのペアがこの例です。FindClassは名前によって、クラス型を検索し返します。もし見つけられない場合は、例外を発生します。一方、GetClassは、クラスが見つからない場合、nilを返すだけです。
結論
例外処理を不適切に使用する罠に落っこちるのは、本当に簡単です。エラーや問題の「姿を隠してしまう」能力は、本当に魅力的です。しかし例外の誤解や誤用は、いくつかの本物の問題の原因となってしまいます。つまり追跡不可能なクラッシュやデータの消失です。例外の適切な使用は、コードを読みやすく、保守しやすくします。例外を賢く使いましょう。そうすれば開発者は頑丈で、奇麗なコードを作ることが出来るでしょう。
謝辞
Craig Stuntz氏に深く感謝します。彼は、この文書の原稿を読み、素晴らしい、価値ある貢献をしてくれました。更に、例外や例外ハンドリングについての私の知識の多くは、Delphi Component Design http://www.amazon.com/exec/obidos/tg/detail/-/0201461366/103-9473483-5912602(Danny Thorpe著)を読んで得た物です。
|