今でもこの記事にコメントを頂くことがあり大変有難いのですが、実は、現在はまたもや異なる見解、というか捉え方をしています。(何度もすみません…)
といっても、「業務エラーは戻り値で表すべき」という考えになったというわけではありません。
以前は業務エラーは「戻り値」で表すべきか「例外」で表すべきかについて悩んでいましたが、今の僕は、そもそも業務エラーを表すのはこのどちらでもないと考えています。
まず、
- 戻り値は、メソッドが結果を返すためのものです
- 例外は、メソッドが例外的事象を通知するためのものです
そして、これが全てです。
戻り値も例外も、業務エラーを表すためのものではないのです。
これだけじゃイマイチ伝わらないかもしれませんね。
ではここから、ドメイン駆動設計 (DDD) という設計思想を例に取ってみます。
書籍「ドメイン駆動」によると、DDD プロジェクトの典型的なレイヤ分割では、次の 4 つのレイヤに分割されるとしています。(直接的にはあまり深く語られていないので、以下は僕なりの解釈です。)
[ User Interface レイヤ ]
その名の通り UI に関するレイヤです。MVC における View に相当します。
テキストボックスの入力値を取得したり設定したり、ラベルの表示・非表示を切り替えたり、といった直接的な UI 操作のみを行います。
[ Application レイヤ ]
アプリケーションとしての動作に関するレイヤです。MVC における Controller に相当します。
User Interface レイヤ、Domain レイヤ、Infrastructure レイヤを操作します。アプリケーションとしての中核を担います。
例えば、図書館の貸出管理システムにおいて、貸出画面で複数の図書を一度に貸出できるようにするのか、一冊ずつしか貸出できないようにするのかは、アプリケーションの仕様に左右されます。一度に複数の貸出ができるようにするために、図書オブジェクトを一時的にコレクションに格納するといったロジックは、「アプリケーションとしての動作」のためのものです。(*1)
[ Domain レイヤ ]
ドメインモデルに関するレイヤです。MVC における Model に相当します。
例えば、貸出期限や貸出可能図書数などは図書館の貸出規定として決められていることです。貸出可能図書数を超えてしまう場合は貸出できないようにするといったロジックは、アプリケーションとしての動作とは無関係なドメインロジック (ビジネスロジック) です。
[ Infrastructure レイヤ ]
データアクセスや周辺機器の制御などに関するレイヤです。
DDD において、業務エラーを表すのは Application レイヤだと僕は考えます。なぜなら、業務エラーは「アプリケーションとしての動作」の一種だからです。
Application レイヤが色々な手段で「業務エラーにすべき状況の検出」をし、「業務エラーとしてユーザーに通知」するわけです。
業務エラーにすべき状況を検出する手段には、例えば次のような手段があります。(*2)
- UI への入力値の妥当性を検証しその結果を見て検出
- ドメインモデルの検証用メソッドを実行した結果を見て検出
- ドメインモデルの状態プロパティを見て検出
- ドメインモデルのメソッドを実行した結果例外が発生するかを見て検出
- データアクセス時に例外が発生するかを見て検出
繰り返しますが、これらはあくまでも検出するための手段 (*3) であり、業務エラーそのものではありません。
業務エラーの検出後は、User Interface レイヤに対して、業務エラーをユーザーに通知するように指示を出します。この指示はメソッド呼び出し等 (*4) で行います。
ということで、最初に書きました通り、例外も戻り値も業務エラーを表すために使うものではないと主張します。特に、「本来例外を投げるべきところ」で「業務エラーは戻り値で表さなきゃいけないから戻り値にする」という考えには賛同できません。
今回、例として DDD を選びましたが、必ず DDD を適用しろとか、必ず Application レイヤで業務エラーを表せと言っているわけではありませんので注意してください。
また、ここで僕が述べたことは、他の設計思想においても当てはまるかもしれませんし、当てはまらないかもしれません。
*1:これは場合によっては Domain レイヤで行った方が適切だったりするかもですが、良い例が浮かばなかったので…
*2:これは決してボトムアップ設計を示唆しているわけではありません。
*3:"検出" を "選定" と置き換えた方が伝わりやすいかもしれませんね。
*4:まだ触ったことないですが ASP.NET MVC なら ViewData プロパティを使用するんですかね。
僕はやはり、
「業務エラーは例外で表現するべき」
と考えます。
では、その理由について語っていきます。
【 目次 】
1. 例外の利点
2. 業務エラーとしての例外
3. 注意点
4. まとめ
5. 参考文献
【 用語 】
この記事で使用する用語について、以下のように定義します。
・例外
System.Exceptionから派生する全ての例外クラスを指します。
・エラー
正常系ではないフローを指します。エラーは業務エラーとシステムエラーに大別できます。
・業務エラー
エンドユーザーの操作次第で発生しうるエラーを指します。
業務を遂行する上で間違った操作をエンドユーザーが行った場合に発生するエラーや、エンドユーザーが操作を行っている間に、他のエンドユーザーが何らかの操作を行ったことによって、現在の操作が有効でなくなった場合に発生するエラーなどです。
・システムエラー
システム異常やネットワーク異常、プログラムの欠陥など、エンドユーザーに非がないエラーを指します。
【 1. 例外の利点 】
業務エラーとかシステムエラーとかはひとまず置いといて、まずは「エラー」を例外で表す場合の利点を挙げてみます。
- 例外時の処理コードを分離できる
- バグを検出しやすい
- 詳細な情報を保持している
それぞれ見ていきましょう。
[ 1-1. 例外時の処理コードを隔離できる ]
構造化例外処理は、その名が表すように、「例外処理コードを分離・隔離」することによって「構造化」します。つまり、可読性、保守性に優れるということです。
エラーを戻り値で表す場合はどうでしょうか?
これは、if文やswitch文を用いて処理コードを分離することができます。
しかし、確実に構造化例外処理より劣ります。
構造化例外処理なら、例えば例外をスローする可能性のあるメソッドを呼び出す時、例外が発生したとしても呼び出し元で特に処理する必要がなければ、呼び出し元には例外に関するコードを一切記述する必要がありません。例外はそのまま上位へと伝わり、適切な箇所で処理が行われます。
[ 1-2. バグを検出しやすい ]
例外はハンドルされなければCLRに検出されます。また、ハンドルし損ねた例外を最上位 ( ASP.NETで言えばGlobal.asaxのエラーハンドラ ) で全てハンドルし、ログを残すといったことも可能です。
僕の場合、この最上位のハンドラで例外を検出したら、「予期しない例外が発生しました。」というようなメッセージをエンドユーザーに見せて、裏ではログを残すという手法をよく使います。
エラーが戻り値で帰ってきた場合、そのエラーを適切に処理しなくても、CLRには当然検出されません。すると、どこかで不整合が生じます。その結果アプリケーショ ンがおかしな挙動を起こしたとしても、何が原因なのかは例外に比べて判別がつきづらくなります。また、アプリケーションが ( 一見すると ) 正常な動作を続けてしまうかもしれません。その場合、バグを検出することは困難になります。
[ 1-3. 詳細な情報を保持している ]
デバッグの最中にハンドルされない例外が発生した場合、その例外の各プロパティを参照すれば色々な情報を得ることができます。具体的には、どんな例外なのか ( 型やMessageプロパティ ) 、どのアセンブリで発生したのか ( Sourceプロパティ ) 、どのメソッドで発生したのか ( TargetSiteプロパティ ) 、どういう経緯で発生したのか ( StackTraceプロパティ ) 、例外の元となった例外について ( InnerExceptionプロパティ ) 、といった情報となります。
先ほど書いたように、ハンドルされなかった例外が発生したら例外の情報をログに残すようにすれば、運用時に発覚したバグも調べやすくなります。
エラーを戻り値で表した場合、これらの情報を取得するのは言うまでもなく大変困難です。
【 2. 業務エラーとしての例外 】
ここまでで、例外の利点について見てきました。これらは全て、業務エラーを例外で表した場合にも適用される利点です。
ここからは、業務エラーとしての例外について別の視点から考察していきます。
[ 2-1. 一つのメソッドは一つの機能を表す ]
例えば、「DBへの反映を行うメソッド」があったとします。
このメソッドは「DBへの反映を行うメソッド」であり、「DBへ反映できるようなら反映するメソッド」ではありません。
このメソッドは、楽観同時実行制御を行います。つまり、他者が先にDBを更新していた場合、DBへの反映は行えず、業務エラーとなります。
業務エラーは、一般的に想定されるエラーと見なされます。しかし、想定されるエラーだからと言って戻り値で返してはいけません。想定されていようがいなかろうが、DBへの反映を行うメソッドでDBへの反映ができないのであれば、それは紛れもない「例外的事象」なのです。
「じゃあメソッド名を変えれば良い」と思うかもしれません。例えば先ほど挙げた 「DBへ反映できるようなら反映するメソッド」 というメソッド名でしょうか?でもこれ、一つのメソッドで二つの機能を表してますね。可能であるかどうかを調べ ( 検証 ) 、可能であればHogeを反映 ( 実行 ) するわけです ( 内部のロジックがどうであれ使う側にしてみればそうなります ) 。構造化の観点からすると、一つのメソッドが複数の機能を表すのはよろしくありません。
では、実行用のメソッドの他に検証用のメソッドを用意すればいいかというと、一概にそうとは言えません。
基本的に検証用メソッドは、例外発生のコストによるパフォーマンスの低下が無視できない状況であったり、前もって検証を行いたい理由がある場合 ( これは業務エラーの表現方法に非依存 ) に用意します。
なお、検証メソッドは検証結果を戻り値で返します。検証メソッドは、言うまでもなく検証を行うためのメソッドなので、結果を戻り値で返すのは自然 ( 当然 ) なことです。
[ 2-2. コスト ]
例外にはコストがかかります。
でも、基本的には問題にならないはずです。
例えば、DBの更新を非接続型データアクセスにて行う場合楽観同時実行制御が行われます。この時、他者が既にDBの更新を行っていたために更新に失敗した場合、これは業務エラーとなります。でもこれ、そう頻繁に発生することではありません ( 頻繁に発生するのなら楽観同時実行制御では不適切です ) 。頻繁に発生しない業務エラーで多少レスポンスが悪くても、それは問題にはならないでしょう。
では頻繁に発生する業務エラーがあったとします。これも問題にはなりません。2-1にて前述したように、「例外発生のコストによるパフォーマンスの低下が無視できない状況」では、検証用のメソッドを用意するからです。
例えば、エンドユーザーからの入力値の妥当性検証は必ず行います。エンドユーザーからの入力値が不正な値であることは ( 故意であろうがなかろうが ) よくあることです。入力値の妥当性検証は、パフォーマンスの向上以外にも、可読性の大幅な向上にも繋がります。
ちなみに、入力値の妥当性を検証した後の処理では、入力値は必ず妥当であるはずですので、もし検証した後の処理で入力値が妥当でないことが検出された 場合、それは業務エラーではなくシステムエラーとなります。これを表す例外クラスは、大抵の場合 ArgumentException クラス、またはその派生クラスとなります。
【 3. 注意点 】
ここまでで、業務エラーを例外で表すことの素晴らしさを語ってきました。
次に、業務エラーを例外で表す際の注意点をいくつか挙げます。
[ 3-1. 適切な実装の上で必要となるのは判別情報のみ ]
判別情報、つまり例外の場合は型です。各業務エラーに応じた適切な例外クラスを用意し、それぞれをハンドルできるようにします。
Exception クラスの持つ各プロパティ ( Exception.Message プロパティや Exception.StackTrace プロパティ ) は、業務エラーが適切に処理されている限り利用することはありません。例えば、 Exception.Message プロパティをそのままエンドユーザーに表示したりしてはいけません。例外クラスの持つ各プロパティを利用するのは、例外の処理漏れ ( ハンドルされない例外の発生 ) があった場合となります ( デバッガやログファイルを通して参照します ) 。
なお、これらは業務エラー・システムエラー問わず言える事です。 ( システムエラーの場合、SQLException.Numberプロパティなど、型以外に判別情報を提供している場合もありますが。。。 )
[ 3-2. 業務エラーはExceptionクラスから派生 ]
Microsoftは以前、アプリケーション固有の例外はApplicationExceptionから派生させることを推奨していました。しかし、現在はこれを推奨していません。変わりにExceptionから派生させることを推奨しています。
なぜでしょうか?これは、Microsoftに原因があります。
Microsoftはこのようなルールを推奨しようとしていました。
- CLRがスローする例外は SystemException クラス (System) 派生の例外
- アプリケーションがスローする例外は ApplicationException クラス (System) 派生の例外
しかし、.NET Framework クラスライブラリ ( 以降、FCL ) 自身がこのルールを守れなかったのです。実は、FCLに用意されている例外の中には、ApplicationExceptionから派生しているクラスや、Exceptionクラスから直接派生しているクラスが紛れ込んでいます。FCLがルールを守れていない以上、このルールはもう台無しです。ApplicationExceptionから派生させる意味はなくなります。
【 4. まとめ 】
- 業務エラーは例外としてスローし、上位で適切な処置を行う必要があります。
- 業務エラーを例外で表すことにより、可読性・保守性の向上に繋がります。
- 業務エラーを例外で表しても、適切な設計を行えばパフォーマンスに問題はありません。
- 業務エラーを表す例外に対して適切な処理を行っている限り、型以外の例外情報は不要です。
- 業務エラーを表す例外がハンドルされなかった場合、例外情報がとても重宝します。
- 業務エラーを表す例外はException クラスから派生させます。
【 5. 参考文献 】
プログラミング Microsoft .NET Framework 第2版
この記事を書くに当たって、この書籍の例外に関するページが大変参考になりました。業務エラー・システムエラーという観点では特に書かれていませんが、是非一読してみることをお勧めします。
2007/02/25 追記
この記事に書いたことは、現在の私の意見とは異なります。
現在は、業務エラーは例外で表すべきと私は考えています。
詳細はこちらの記事を参照してください。
以前書いた業務エラーの表現方法ついての考察。
経験不足が否めない。。。
つっこみしてくれる方大募集!w
【 業務エラーの定義 】
まず、Application Architecture for .NET の第三章 ( 以下、AAfN ) と、書籍 「 Microsoft Visual Studio 2005 による Webアプリケーション構築技法 」 ( 以下、赤間本 ) での、業務エラーの定義に差異はないか?
[ AAfN ]
AAfNでは、例外を以下のように分類している。
データベース接続の失敗など。
業務上の例外
外部キー制約の違反など。
[ 赤間本 ]
赤間本では、例外を以下のように分類している。
( 例として、 ) データベース障害やネットワーク障害により、データベースの読み書きが行えなかった場合。
業務エラー
( 例として、 ) 希望された顧客IDがすでに他の顧客により利用されていたといった理由により、顧客情報が登録できなかった場合。
また、赤間本では、業務エラーとアプリケーション/システムエラーの切り分け方として、「業務設計の中で想定されていなければならないケースが業務エラー」というようなことも書かれている。
[ 比較 ]
AAfNは外部キー制約の違反、赤間本は一意キー制約の違反を例に挙げている。これらは、「他のユーザーが既に登録/編集済み」などが原因で発生する。
どちらもあまり詳細には書かれていないが、定義の差異はなさそうだ。
個人的には、「エンドユーザーに許容されている範囲内での、エンドユーザーの操作 ( 入力 ) 次第で発生しうる、正常系でないフローが業務エラー」で、「システム異常など、まさに例外的な状況がアプリケーション/システムエラー」だと解釈している。
【 戻り値と例外 】
業務エラー云々を抜きにして、戻り値と例外について分析してみる。
[ 戻り値 ]
与えられた入力 ( 引数 ) に操作を適用した結果生じる出力。
引数と違い、返せるオブジェクトは一つだけ。
固定の型。
戻り値は呼び出し元でしか受け取れない。
[ 例外 ]
操作の途中で、例外的な状況に陥った場合に、通常とは異なるフローで、その例外情報を上位に伝播する。
例外メッセージ、スタックトレース、例外の元となった例外など伝播される情報が多い。
例外そのものは、例外の分類に応じた型 ( Exception派生クラス ) となる。
例外は例外ハンドラで処理する。その際、捕らえる例外を限定することができる。
スローされた例外は、対応する例外ハンドラが現れるまで上位に伝播される。
特殊な情報伝播を行うため、コストが高い。
【 業務エラーの表現方法の分析 】
[ 業務エラーの内容 ]
どのような業務エラーが発生したのかを知る必要はあるか?
これは、「ある」と思う。
DBの制約に関する業務エラーを例に挙げると、「IDが重複 ( 一意キー制約に違反 ) しているのか?」、「削除されたアイテムを参照 ( 外部キー制約に違反 ) しているのか?」、「違反している場所はどこなのか?」といった情報は、どこを修正すればいいのかが明確になり、有益である。
・業務エラーを例外で表した場合
ExceptionのMessageプロパティに、どのような業務エラーが発生したのか記述することができる。
・業務エラーを戻り値で表した場合
戻り値の型がBoolean型だと、どのような業務エラーが発生したのかは不明となる。ただし、業務エラーが一つの理由でしか発生しない場合は、Boolean型でも明確となる。また、チェック系メソッドを用意して事前にチェックを行えるようにしておけば、業務エラーの内容を知ることができる。
また、戻り値の型を、Boolean型ではなくEnum型にすれば、業務エラーに応じた戻り値を返せる。
[ スタックトレース ]
業務エラーを例外で表した場合は、スタックトレースが利用できるが、スタックトレースは必要か?
これは、「不要」である。そもそも業務エラーは「設計時に想定されている状況」なのだから、スタックトレースなんて見る必要がない。
[ 上位での処理方法 ]
先に述べたように、戻り値は呼び出し元でしか受け取れない。例外は、対応する例外ハンドラが現れるまで上位に伝播される。
最上位に例外ハンドラを用意するだけで済むので、例外の方に分があるように思える。
【 考察 】
一見すると、戻り値の方が処理がめんどうで、例外に分があるように見える。
しかし、業務エラーの内容を伝えるのにExceptionのMessageプロパティを使うのは、あまり適 切ではないように思う。GUIに表示するメッセージを下位層で決めているようなものだ。業務エラーは文字通り「業務」上のエラーなので、表示・表示内容の 責任を持つのはGUIである。しかも、文字列なので、上位層の「プログラムコード」で業務エラーの詳細を判別できない。 ( 各業務エラーごとにExceptionクラスを定義したり、独自のプロパティなんかを用意してやればなんとかなりそうだけど。 )
そもそも、業務エラーは設計時に想定されている事象なので、例外的な状況ではない。 ( という話をどこかで聞いた。 )
【 結論 】
「業務エラーは戻り値で表現」です。
例えば、DBに業務情報を格納しておくWebアプリで、あるユーザーが業務情報を登録しようとした時に、その業務情報に関連する情報が他のユーザーによって削除されていて、外部キー制約違反が起こってしまう場合などが該当する。
[ 業務エラーはApplicationExceptionで? ]
オレは今まで、業務エラー時にはApplicationException派生の自作クラスをスローしてた。
だって、ApplicationExceptionの説明には
ApplicationExceptionは、共通言語ランタイムではなく、ユーザー プログラムによってスローされます。デザインしているアプリケーションで固有の例外を作成する必要がある場合は、ApplicationException クラスの派生クラスを生成します。ApplicationException クラスは Exception を拡張しますが、新しい機能は追加しません。この例外は、アプリケーションで定義された例外とシステムで定義された例外を区別する手段として提供されます。
って書いてあるし、
Application Architecture for .NET ( 通称AAfN ) の第三章にも、
例外クラスは、ApplicationException の派生クラスとして作成します。
とか、
例外には大きく分けて、業務上の例外と技術上の例外の 2 種類があります。例外をこのように分類することにより、アプリケーションのさまざまなコンポーネントで該当する種類の例外をキャッチし、発行する処理を簡単に実現できるようになります。
- 技術上の例外 (データベース接続の失敗など)
- 業務上の例外 (外部キー制約の違反など)
って書いてあるから。(あとここにも。)
[ 業務エラーは戻り値で? ]
でも、クラス ライブラリ開発のデザイン ガイドラインの中のカスタム例外のデザインでは、
「標準の例外の種類のキャッチとスロー」のガイドラインに示されているように、ApplicationException からカスタム例外を派生させることは推奨されていない点に注意します。
なんて書いてある。 ( 対象がクラス ライブラリ開発だからか・・・? )
書籍 「 Microsoft Visual Studio 2005 による Webアプリケーション構築技法 」 でも、 「 6.4 例外処理 」 で、DBに対して顧客情報を新規登録するメソッドを例に挙げ、終了パターンを以下のように3つに大別した上で、
・正常終了
受け渡された顧客情報を利用して、正常にデータベースに顧客情報が登録された場合。
・業務エラー
希望された顧客IDがすでに他の顧客により利用されていたといった理由により、顧客情報が登録できなかった場合。
・アプリケーション/システムエラー
データベース障害やネットワーク障害により、データベースの読み書きが行えなかった場合。
それぞれの表現方法を以下のように示している。 ( 実際は表形式 )
分類:
正常終了
対応するケース:
業務で期待された主たる処理が問題なく終了した場合
.NETでの表現方法:
戻り値の一部として表現
分類:
業務エラー
対応するケース:
業務設計の中で想定されている範囲内で、処理が分岐し、正常終了できなかった場合
.NETでの表現方法:
戻り値の一部として表現
分類:
アプリケーション/システムエラー
対応するケース:
業務設計の想定範囲外の異常事態が発生し、アプリケーション処理を正しく遂行できなくなった場合
.NETでの表現方法:
例外を用いて表現
[ ふむ・・・ ]
ということで、業務エラーについては、以下の2つの意見があるようだ。
・ApplicationExceptionの派生クラスを定義し、それをスローする。
・例外は用いず、戻り値 ( BooleanとかEnumってことかな ) で表す。
どっちがいいんだろ?
いや、単に人それぞれの好みで選んでいいのかな?
考察してみると面白そうだし、もしかしたらそのうち考察記事書くかも。