QA@IT
この質問・回答は、@IT会議室からインポートされたものです。

戻り値と例外の仕分け(使い分け)

戻り値でのエラーと例外の仕分け(使い分け)の仕方に悩んでいます。

下記の書き込みをみて、とっかかりにはなりました。
http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=21003&forum=7

 - 正常応答          は 戻り値
 - エラー応答(業務エラー)   は 戻り値
 - 異常応答(システムエラー) は 例外

例えば、接続されているDBから、検索条件を元に、値をとってくるという場合

 - 正常応答 は 値が取得できた
 - エラー応答 は  値が取得できなかった
 - 異常応答 は  DBとの接続ができてない、接続を切断された

という風に仕分けるようにしたらよい、というのがわかりました。

この辺の仕分けたり、設計の仕方で参考になる書籍(サイト)は無いでしょうか?

元のトピックに出ていた
「.NETエンタープライズWebアプリケーション開発技術大全〈Vol.4〉」 
という本の該当箇所は(立ち読みですが)見たところ、さらっと書いてあって、
もうちょっと深く追いたいと考えてトピックを立てました。

一番は経験して学習を積むことだとは思いますが、出だしで参考になるものが
あれば使いたいです。

投稿者: rnsz

回答

なぜファイルI/Oライブラリーが戻り値ではなく例外を使うのかを、引き続き考えてみましたが、これは例外オブジェクトを上位層に引き渡せるというメリットがあるからですよね。

たとえば

public void Foo(StreamReader sr)
{
}

のようなメソッドを持ったライブラリー(A)を、それより上位層から呼び、それがまたラッパーとしてのライブラリー(B)になる場合を考えます。このBをさらに上位層のアプリケーション(C)から呼ぶとします。

A がエラーを戻り値ではなく例外で返す場合、B のライブラリーの設計者は大まかに分けてつぎの2とおりの設計のどちらかを選択することになります。
(1) エラーをBの中で吸収して、Bの上位へは戻り値でエラーを返す。
(2) エラーをBを素通りさせて、Bの上位へ例外をそのまま throw する。

この内 (1) はBで完結した作りになりますが、ファイルI/Oに伴うたくさんのエラーの種類をもてあまします。IOException の細かな違いごとにたくさんの戻り値の定義を決めるか、あるいは種類を限定してしまい、エラーの詳細を握りつぶしてしまうかです。戻り値の再定義は大変ですし、例外の種類を握りつぶしてしまえば、エラーの理由がディスクの容量不足だったのかファイル共有違反だったかなどが分からなくなるかもしれません。
一方、(2) は例外の情報をまったく縮小させることなく上位層にわたせます。コーディングも不要です。

どちらが良いか、となると私も(2)を選択したくなります。(2)は、Bの内部を上位層に見せていますが、それはBの下位層のAの状態を素通ししただけであり、かならずしもBのカプセル化がまずいということでもないでしょう。

ただ、C は B から投げられたものを catch することはあっても、C の中で throw する必要性はやはり感じられません。これはもしも B がそれより上位層がない場合(C がない場合)でも同様です。

投稿者: unibon

編集 履歴 (0)

このスレをお借りして、この機会に私が考えてみたことを書いておきます。(これはあくまでも私一人の考えを紹介しているだけです。)

私は、try/catch を使うかそれとも使わないで戻り値にするかの境目は、スクラッチプログラミングかそうでないかだと感じました。結局、境目を設定していますが、用途による分類ではなくコーディングスタイルによる分類なのではないか、と思います。

たとえば、ファイルI/Oのライブラリーのように、open も read も seek も close もエラーとして返すものが似ているような場合に、メソッドを呼ぶごとにいちいちエラーチェックをするのは面倒くさいし、API のリファレンスマニュアルにもメソッドごとに似たようなエラーコードを書くのは面倒くさい、といった場合には、ライブラリーを作る人が戻り値ではなく例外を throw するような設計にしてしまうわけです。そういう設計のライブラリーはスクラッチプログラミングのコードからは呼びやすいでしょう。

そして、.NET でも Java でも、なぜ特にファイルI/Oが例外を throw するような設計になっているかと言えば、C++ が登場したころの昔は、クラスを設計することが大掛かりであり、前述の Match クラスみたいなクラスを戻り値だけのために作るということが避けられたためではないかと思います。

さらに、例外を使える背景にも理由があると考えます。私も Java の NullPointerException や .NET の NullReferenceException あるいは ArgumentException などの例外は必要に応じて積極的に throw すべきだと考えます。そのためには言語やフレームワークが try/catch の仕組みを持っていることが当然の前提として必要です。また finally(あるいは似たような機能を持ったそれ以外のステートメント等)も仕様としては必要だと思います。

こういったお膳立てがあると、さして使う必要がない場面でも、try/catch を使いたくなってしまうのではないかと思います。そのひとつがファイルI/Oのような気がします。名目はファイルI/Oだったら、finally でのリソース解放でどうせブロックを囲まなければならないのだから、ついでに catch もしてしまえば良いし、コードが短く書ける、などです。

ただ、私は、これらは try/catch を使う理由としては弱いと感じます。finally は finally で、catch は catch で独立して使えば良い話です。コードの短さもファイルI/O程度ならば、戻り値で検査することも十分に可能だと思います。今の技術ならば IDE で戻り値のエラー検査をテンプレートで一気に補完してくれるようなこともやろうと思えばできると思います。

int の parse が例外を throw する理由もファイルI/Oと似たようなものなのかなと感じます。

ちなみに、私は、コーディング時に型がガチガチに決まっている言語が好きでして、デフォルトや省略があってコードが短いのは嫌うタイプの人間です。メソッド呼び出しごとに似たような検査用のコーディングが長々と書いてあっても、見ればなにをやっているのかが一目で分かるほうが、スクラッチで書けるコードよりも好きです。

ただ、「スクラッチプログラム」と一言で言っても、上位層のアプリケーションレベルと、それより下位層のライブラリーでは意味合いが違ってくるとも思います。もしかしたら下位層ならば try/catch もメリットがあるかもしれません。このあたりは私も経験が少なくて良く分かりません。ただ、少なくとも上位層では try/catch は必要性を感じません。

投稿者: unibon

編集 履歴 (0)

>5.自分で「システムエラー」(例外)を出す場合は、独自の
>  例外クラスではなくて、ApplicationExceptionを使う

ApplicationException の使用は .NET Framework 1.x 時代の推奨で、.NET Framework 2.0 移行は推奨されていなかったはずです。

結局、「アプリケーション」と「ライブラリ」や「フレームワーク」の境界線も不動の客観的な基準があるわけではないので。

投稿者: 渋木宏明(ひどり)

編集 履歴 (0)

みなさんのコメントやリンク先のURLなんかを見ながら、まとめました。

今回は「システムエラー」、「業務エラー」の区分けは、単純に考えることにしました。

1.「システムエラー」はアプリケーションが継続できないエラーで、
  アプリケーションを終了させるものを指す。
 
   「システムエラー」が発生すると、ランタイムに補足
   (Application.ThreadException イベントハンドラ)させて、
   エラーダイアログやログを出して、アプリを終了させる。

2.上記以外は「業務エラー」として戻り値(またはref/out)で判定させる。

3.ライブラリやランタイムから飛んでくる例外は必要がある個別の
  例外だけ、try-catchで目的の例外だけ補足して
 「業務エラー」に変換する。
 「システムエラー」にする場合はそのままスルーさせる。(アプリを終了させる)

    自分が意図しないエラー(例外)は処理を継続できないだろうと
    考えました。

4.ただし、アプリケーションをどーしても終了させたくない場合にのみ
  最上位で、Exception型でcatchする。(※)
  ※:アプリケーションで対応できない例外とかの時に困るから、
    あんまりやらない方がいい気はしますが・・・

5.自分で「システムエラー」(例外)を出す場合は、独自の
  例外クラスではなくて、ApplicationExceptionを使う。

6.同じ処理内容だが、場合によって「システムエラー」や
  「業務エラー」を使い分ける場面があるならば、
  Parse/TryParseに相当する関数を作る。

共通処理を自作ライブラリ化で切り出しまで行くと、
システムエラーか業務エラーかの判断がつけられないので、
例外を自作ライブラリがthrowしないといけない局面はあります。

ですが、今回は泥団子化したアプリを直すというのが目的だったので、
今回は上記のようにまとめて、結論にしました。

全く知らなかったことが多く、コメントをいただけて、
とても勉強になりました。

投稿者: rnsz

編集 履歴 (0)

>べるさん

ポイントは各々のプロジェクトでの業務エラーとシステムエラーの
切り分けでしょうね。DBとの接続ができてない場合でも
「DBに接続できませんでした」と表示させたい場合は
業務エラーのように扱わなければいけないでしょうし。

 エラーの取り扱いを自分の中で考えると、

 例えば、仕様が変わって「DBに接続できませんでした」が「異常応答」から
 「エラー応答」に変更になった場合等の対応がでると、使い分けが難しいと感じました。

 2でunibonさんが書かれたように、

>unibonさん

私はそもそも、「区別ははっきりしなくてもこの区別はどこかにあることは確かだ。
だからその区別の境界で、戻り値か例外かを使い分けるべきだ。」という分け方は
あまりしたくないと考えます。それほど単純な分け方はできないと思います。

 単純な分け方ができないというのは全くその通りで、
経験者の方々はどうしてるのかと思うところもあり、トピックスを立てました。

 TryParse パターンは初めて知ったので参考になりました。
 「異常応答」と「エラー応答」が切り替わるような可能性がある場合は
Parse/TryParseパターンを使い分ければいいなかと考えています。

>WebSurfer さん

 URL教えていただいてありがとうございます。

 リンク先で出てた、ずばりな状態でした。「システムエラー」と
「エラー応答」以前に例外処理の使い方をロクにわかってなくて、
やたらと使って、try-catchの多用で、gotoみたいになってました。

よほどのことがない限り、アプリケーションで try-catch を書いてはいけません。
もう一度(ry、すみませんしつこいですね^^。でもこれ、めちゃめちゃ重要なのです。
# 実際、私もいくつかの開発現場で、アプリケーションコード中に書かれた
try-catch 文を片っぱしから除去してもらったことがあります。それぐらい、
初学者は「なんとなく try-catch」を書いてしまうことが多いんですよね。
この点は初学者が陥りがちな典型的なミスの一つなので注意してください。

> 渋木宏明(ひどり)さん

値が取得できなかった結果を、どう扱いたいのかで変わってくると思います。

>よねKENさん

しかし、実際には、DBに接続できないのは例外的ケースだが、
値が取得できないのはままあるケースだとすると、上記の説明通りの
実装は、必ずしも適切ではありません。

 お二方が言われるとおり、戻り値、例外だけだと、上記の通りの
灰色っぽいエラーケースの扱いに困っていました。
Parse/TryParseは初めて目にしたのですが、確かに必要です。

xxパターンと言うと処理の動きが明確になるので、便利ですね。
不勉強だったのですが、デザインパターンの本に出てくるパターン以外は
初めて聞いたので、勉強になりました。

投稿者: rnsz

編集 履歴 (0)

今までに、例外を使う例として int の Parse などが登場していますが、私はこの用途に例外を使う必要はないと思います。

この根拠として私が考えることは、たとえば正規表現の Regex クラスを使って文字列を解析する場合を考えます。たとえばつぎのコードです。

Regex r = new Regex(@"(^\d+$)");
Match m = r.Match("123a456");
if (m.Success)
{
    string s = m.Groups[1].Value;
    Console.WriteLine(s);
}
else
{
    Console.WriteLine("not");
}

このコードでは、数字以外のものが入力されたとしても例外ではなく戻り値としてエラーを返します。

ちょっと言い訳しておきますと、int の Parse の代わりとしての例としてはピッタリではなく、数値に変換するというところまで行かず、数値としてパースできるかというところまでの例になっていますが、雰囲気は伝わると思って書きました。(あと、私は正規表現のパターン文字列にあまり詳しくないので、パターン文字列が多少適切ではないかもしれませんが。)

この例のように、文字列をパースして期待した文字列かどうかを調べる用途に、別に例外は要らないのです。戻り値でエラーを返すことができるのです。欠点としては、エラーを返すためのクラス等の定義をあらかじめしっかりとしておかないということはあります。この例の場合だと Match クラスです。逆にいえば、そのようなクラス設計をやれば例外を扱う必要性は減ると思います。

投稿者: unibon

編集 履歴 (0)

私の先の投稿に書いた内容をもっと具体的にわかりやすくまとめてあるページがあったのでご紹介しておきます。
[雑記] 例外の使い方

投稿者: よねKEN

編集 履歴 (0)

戻り値でのエラーと例外の仕分け(使い分け)の仕方に悩んでいます。

そのメンバの目的に応じた結果を返すために使うのが戻り値で、
その目的を果たせない場合に使うのが例外ですね。

そのメンバの目的に合致しないものを戻り値として返すのはお勧めしません。

例えば、接続されているDBから、検索条件を元に、値をとってくるという場合

 - 正常応答 は 値が取得できた
 - エラー応答 は  値が取得できなかった
 - 異常応答 は  DBとの接続ができてない、接続を切断された

という風に仕分けるようにしたらよい、というのがわかりました。

この例の場合、「値の取得」が行いたいことなので、取得した値を戻り値として返し、それ以外の状況、つまり、値を取得できなかった場合、DBと接続できない場合などのすべての値を取得できない状況では、例外が発生するようにするというのが基本的な考え方になると思います。

しかし、実際には、DBに接続できないのは例外的ケースだが、値が取得できないのはままあるケースだとすると、上記の説明通りの実装は、必ずしも適切ではありません。パフォーマンス上の問題が発生する可能性があります。そういうときには、Try-Parseパターンを使う手があります。渋木さんのご指摘にもあるInt32.Parseメソッドに対するInt32.TryParseメソッドを用意するパターンのことです。(上記の基本的考え方に沿ったメンバがParseで、Try-Parseパターンの考え方に従ったものが、TryParseメソッドです)

もう一つの設計方法としては、Tester-Doerパターンというのもあります。TesterはDoerを実行可能かどうかを検査するものでBooleanを返します。Doerは実際に実行したい処理です。

例えば、File.ExistsメソッドはTesterで、そのファイルをオープンする処理がDoerの一例となります。File.Existsで事前にファイルの存在確認を行うことで、その後のファイルオープンを例外を発生させずに行える確率を高めます。Tester実行時にはファイルが存在しても、Doer実行時にはファイルが存在しないということがありえるので、絶対に例外が発生しないというわけではありませんが、無闇に例外が発生することを防ぐことができます。

投稿者: よねKEN

編集 履歴 (0)

>例えば、接続されているDBから、検索条件を元に、値をとってくるという場合
>
> - 正常応答 は 値が取得できた
> - エラー応答 は  値が取得できなかった
> - 異常応答 は  DBとの接続ができてない、接続を切断された
>
>という風に仕分けるようにしたらよい、というのがわかりました。

んー、場合によりけりじゃないかな?

値が取得できなかった結果を、どう扱いたいのかで変わってくると思います。

Int32.Parse() と Int32.TryParse() の2つが存在しているのも、同じように「どういう結果を期待しているか」による使い分けと考えられるのではないでしょうか。

ただ、その判断をなるべく遅延というか、より上位の層に押し付けたいという欲求は感じます。

投稿者: 渋木宏明(ひどり)

編集 履歴 (0)

rnszさんがあげられたリンク先でも、最後のほうでは「業務」かそうでないかの区別はなにかということが話題になっていたと思います。

私はそもそも、「区別ははっきりしなくてもこの区別はどこかにあることは確かだ。だからその区別の境界で、戻り値か例外かを使い分けるべきだ。」という分け方はあまりしたくないと考えます。それほど単純な分け方はできないと思います。
ご質問の観点が「どこで分けるべきか?」だと、どこかに境界があるはずだ、いくらかは例外は使っても良い、ということが前提になってしまうように思います。そうすると、どういったケースでは例外は使っても良い、という考えを否定できなくなってしまうように思います。

私のこの考えはかなり極端なものです。
ただ、私が、通常の上位層(高いレイヤー)のアプリケーションを書いている限り、例外を使う必要は感じられません。戻り値でいくらでも書けると考えています。
だから、どこで分けるべきか、や、どこで例外を使うべきか、という前提で考えるとどうしても例外は使っても良いという向きになってしまい、まったく例外を使わないという選択肢が消えてしまうことを懸念しています。

(もちろん、自分が呼ぼうとする既成のライブラリーが例外を throw しているのならばそれは catch せざるを得ません。)

投稿者: unibon

編集 履歴 (0)

ご提示のスレッドではmsのページはリンク切れになっていますが
こちらはお読みになりましたか?
http://msdn.microsoft.com/ja-jp/library/ms229014.aspx

「戻り値でのエラーと例外の仕分け」というのは
何らかの値をreturnするのか、例外(Exception)をthrowするのか、
の仕分けという意味です?

私は、基本は
・最上位以外でcatchしない
・個別処理をしたい箇所だけcatch
だと思ってます。

ポイントは各々のプロジェクトでの業務エラーとシステムエラーの
切り分けでしょうね。DBとの接続ができてない場合でも
「DBに接続できませんでした」と表示させたい場合は
業務エラーのように扱わなければいけないでしょうし。

投稿者: べる

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。