オブジェクト指向(C#)における関数の戻り値について
- 評価
- クリップ 2
- VIEW 4,215
主題の件ですが、関数の返り値として、「結果」ともし失敗した場合は「理由」も返すというような場合に、どのように実装すればよいのか悩んでいます。
説明が難しいので具体例を挙げて質問させていただきます。
具体例
- 社員データベースのようなものがあって、
名前
と生年月日
を引数で与えると、その人の社員番号
を返す関数があったとします - その関数は、データベースに登録されている
名前
と生年月日
があれば、正常に社員番号
を返しますが、データベースに登録されていない場合、失敗を返します。 - その失敗の理由として、例えば「該当の名前なし」とか「該当の生年月日なし」とかが考えられます。
- 上記の理由で関数が失敗した場合は、呼び出し元にその失敗理由も返します。
(そもそもそんな関数作らないという話もあるでしょうが、あくまで例ですのでご容赦ください。)
質問
このような条件の場合、例えば C# であれば、どのような実装がスマートなのでしょうか。
今まで業務では基本的にC言語ばかりを扱ってきたので、モダンなやり方がわからなくて困っています。
C言語的にやるならば、「成功可否のBOOL値を関数の返り値として、失敗理由をポインタ渡しして・・・」とかがありそうですが。
自分で考えてみた選択肢
① out
修飾子を使う
例えば以下のような感じです。なんか手続き型チックで違和感がありますが、一番直?な気もします。
class EmployeeNumber
{
int Value {get;}
public EmployeeNumber(int value)
{
Value = value;
}
}
// 成功した場合は返り値として社員番号を返す。失敗した場合はnullを返して、FailedReasonに失敗理由を設定して返す。
EmployeeNumber GetEmployeeNumberBy(string name, DateTime birthDay, out FailedReason failedReason);
// 本当にやるなら EmployeeNumber をヌルオブジェクトパターン適用すべきですが省略
② タプルで返す
例えば以下のような感じです。これも手続き型脳的には素直な気がします。
class EmployeeNumber
{
int Value {get;}
public EmployeeNumber(int value)
{
Value = value;
}
}
// 成功した場合は返り値として (社員番号, null) を返す。失敗した場合は (null, 失敗理由) を返す。
(EmployeeNumber employeeNumber, FailedReason? failedReason) GetEmployeeNumberBy(string name, DateTime birthDay);
// 本当にやるなら EmployeeNumber をヌルオブジェクトパターン適用すべきですが省略
③ 社員番号クラスの中に失敗理由を埋め込む
例えば以下のような感じです。
class EmployeeNumber
{
int Value {get;}
FailedReason? FailedReason {get;}
public EmployeeNumber(int value, FailedReason? failedReason=null)
{
Value = value;
FailedReason = failedReason;
}
}
EmployeeNumber GetEmployeeNumberBy(string name, DateTime birthDay);
④ ダブルディスパッチパターンを使用する
個人的にはこの方法が一番マシなのかなぁと思っています。
class EmployeeNumber
{
int Value {get;}
public EmployeeNumber(int value)
{
Value = value;
}
}
interface IGetEmployeeNumberFailed
{
void OnFailedBecauseNotMatchedName();
void OnFailedBecauseNotMatchedBirthDay();
}
// 成功した場合は返り値として社員番号を返す。失敗した場合は null を返して失敗理由を与えられたインターフェイスを使用してコールバックする。
EmployeeNumber GetEmployeeNumberBy(string name, DateTime birthDay, IGetEmployeeNumberFailed onFailed);
⑤ ドメインイベントを発行する
DDD でやるならこれなんでしょうが、個人的にはメリットがあまりピンとこないです。
class EmployeeNumber
{
int Value {get;}
public EmployeeNumber(int value)
{
Value = value;
}
}
// 成功した場合は返り値として社員番号を返す。失敗した場合はドメインイベントを発行する。
EmployeeNumber GetEmployeeNumberBy(string name, DateTime birthDay)
{
// ~~ 省略 ~~
// 詳細には書ききれないので、かなり省略します
if(名前が見つからなかった)
domainEvent.Publisher(new DomainEvent<FailedReason>(FailedReason.NotMatchedName));
if(誕生日が見つからなかった)
domainEvent.Publisher(new DomainEvent<FailedReason>(FailedReason.NotMatchedBirthDay));
return 社員番号
}
長くなりましたが
長文となり申し訳ありませんが、「①~⑤のうち●番が良い」とか、「こういう方法が良い」等、ご教授のほど、宜しくお願い致します。
また、これが良いとは言わないまでも、普段こうしているというのがあれば教えていただけると幸いです。
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
+10
私なら例外をスローします。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
+9
本当に「失敗」であればZuishinさんの回答のように私も「例外」にします。しかし、示された具体例であれば、そもそも「条件にあう社員番号が見つからない場合は『失敗』になるのか?」という疑問があります。つまり、検索すること自体は「成功」しているからです。回答者さんは条件にあう社員番号がみつからないことを通常の業務フローであると考えているようですが、そうであれば、それはそもそも「失敗」とは言えません。
考えるべきは検索結果として何を得たいのか、得るべきなのかです。
- 検索した結果として、0個以上の社員番号または社員オブジェクトのリスト
まず、必要なのは上のことでしょう。該当者がないことを特別扱いすべきではありません。0個以上のリストですので、0個の場合もあれば、1個、はたまた2個以上もありえます。同姓同名同生年月日がいないなんて保証はどこにもないからです。それらをどう解釈するのかはそれを呼び出す側の責任です。
ただこれだけでは足りないと考えているのでしょう。
- 検索に対する詳細な情報
一致した氏名の数、一致した生年月日の数、検索対象の社員数、データベース検索にかかった時間、そんなものが一緒にわかれば有意義だと考えたのかも知れません。「一致した氏名の数」が0であれば、呼び出し側は「ああ、同じ氏名の人がいないから、該当者リストの空なのか」とわかるでしょう。考えるべきなのは失敗した理由ではなく、検索に対する情報です。そもそも失敗していないのですから。
しかし、具体的な実装をするにあたって、「一致した氏名の数」を出すのはデータベースが通常のSQLであった場合、パフォーマンス的にいい実装にはなりません。なぜなら、二つの条件を一緒にしてデータベースを検索した方が効率的であり、そこには片方だけ一致したのがどれだけの数だったのかというのをデータベースは返さないからです。別途、全く別のSQLを発行する必要になります。本当に、「一致した氏名があるかどうか」が必要であれば別のメソッドとして実装すべきと考えています。それが必要になることは少なく、今回のメソッドに持たせるべきではありません。
では、その他の情報もどうでしょうか?先ほど挙げた例では、たぶん、デバッグ情報として必要なのは「データベース検索にかかった時間」ぐらいでしょう。それ以外は違うメソッドで必要な時だけ呼び出せば良いのであって、このメソッド返す必要がある情報とは思えません。そして、ユーザーにとって有意義なのは「データベース検索にかかった時間」よりも「検索開始を押してから表示までかかった時間」です。つまり、ユーザーにはデバッグ情報など必要が無い、言ってしまえば、メソッドの呼び出し側にはそんな情報は必要は無いということです。デバッグ情報については全く別の手段で(それはアプリ全体で統一された手段で)持たせるべきでしょう。
何を言いたいのかというと、今回の具体例であれば、そもそも「検索した結果として、0個以上の社員番号または社員オブジェクトのリスト」以外は返す必要も無いし、それら以外の情報をそのメソッドから得る必要は無いと考えています。
正直、例があまりよくない、フェアじゃない、と感じます。ぱっと見たとき、私はRailsのfind
のようなものを想定しました。ですので、そもそも「該当者がいなかった理由を返す」ということ自体に必要性の無さを感じました。データーベース側に発行するSQL文を想定しても、そのようなものを返すようにはならないと言うこともあったと思います。
結局の所、欲しい結果が何であるのかと言うことです。これがHTTP接続のレスポンスであれば、そのbodyの内容だけではなく、404などのコードも欲しいとなるでしょう。ですので、ほとんどの実装ではそれらを含めたレスポンスオブジェクトを返すようになっています。しかし、今回はただの検索であるため、検索結果が0であるということは、単に検索に一致するものがなかったということが自明であり、理由をそえる必要は無いと感じられます。どうしても、その他の情報を含めたいのであれば、Resultオブジェクトのようなものを作って、その中に結果とその他の情報を含めるのが良いのかも知れません。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
checkベストアンサー
+5
関数型からEither<L,R>型を輸入する
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
+2
今どきは、タプルがいいんじゃないですかね。①~⑤中で、他は論外。
例外もいいのですが、例外は、遅いですし、Try文を書かないとだめですし(サボり症)。
タプルは比較的新しい構文なので、場所によっては使うなとかそういうことになるのでしょうが。
とはいえ、タプルを使うようなやり方がイレギュラーというわけでもなくて、
Result用のクラスを作ってやるという方法は昔からあって、(通信周りはそう)
例えば、HTTPClientのGet
その返り値
そのResult用のクラスを作る手間が省けるから、タプルがいいと言っているだけですけど。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
+1
参考情報
...
対処すべきエラーは、その発生が的確に検出されなくてはならない。古典的なC言語用ライブラリにおいては、関数の戻り値でエラーを表していた。関数の戻り値をチェックしないコーディングがしばしば行われたため、エラーの発生が見逃されがちであるという問題があった。
C++においては「例外(exception)」が導入された。ランタイムライブラリ等はエラーを例外の発生によって通知するようになり、プログラマは例外の発生を無視できなくなった。
...
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
0
成功なら社員番号を返すなら、失敗のときはマイナスの数値を返せばよろしい
で、どっかで、Defineで、-1 は存在しない社員 -2 は定年退職 -3 は懲戒免職 とか定義しておく
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
0
個人的にはタプルで全然いいと思うんですが、業務エラーはまぁだいたい例外でやるのが慣例的な感じありますね。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
0
C からの習慣でしょうか、 out または、 ref です。
戻り値を boolにして判定します。
例外は、使いません。 まあ、結果が失敗するのが例外的なら別ですが、むちゃ遅いです。 (一つの処理で数か所も使うと、体感的にも分かります)
戻り値を boolで、メソッドそのまま、 if文に含める。
タプルは未だ、メリットが実感できてない。 (そういう場面に出会ってない?)
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
0
最近の言語はタプルを使えることが多く、タプルが使えるなら
結果とエラーをタプルにして戻すのがわかりやすく、書きやすい、に一票です。
次点で例外。IOやDB関連などのライブラリがエラーを例外として処理していることが多く、
自前のロジックも合わせたほうがスッキリすると判断すれば例外だと思います。
タプル、例外以外はあんまり考えたくないです。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.10%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
2018/07/26 22:34
例があまり良くなかったのですが、これらは一応業務ロジックとさせてください。いわゆるドメイン層でのロジックという感じです。
後出しになって申し訳ありませんが、「この処理が失敗した場合、システムにおいて致命的な状態になるわけではなく、業務ロジックとして失敗がありえる」という位置づけとさせてください。
2018/07/26 22:35
2018/07/27 12:07
これならば特定のところからは起きるはずのない事態なのでシステムエラーにし、それ以外のところからは後処理を行ってエラー画面に飛ばさないといったことも可能です
2018/07/27 12:40
ずっと C 言語の環境だと例外になじみがないかもしれませんね。
2018/07/27 17:58
2018/07/27 18:20
2018/07/27 18:24 編集
2018/07/27 21:42
len_souko 様
javaであれば検査例外(業務エラー)と実行時例外(システムエラー)は明確に区別されるので、この場合は検査例外でよいかと思いますが、C#の場合は区別がつかないので、C#では業務エラーにおいては例外は使用すべきでないという認識でした。
何か有効な切り分け方等ありましたら、ご教授いただけると幸いです。
2018/07/27 21:46
検査例外を派生させ、そこから更に個々の例外を派生させればいいでしょう。
例外は catch でフィルタリングできます。
2018/07/27 21:57
ありがとうございます。
yuya_tn様のコメントにも書きましたが、C#においては業務エラーで例外を使用すべきでないと勉強していたので、変な回答になってしまい申し訳ありませんでした。
例外も候補に入れて考えてみたいと思います。
2018/07/27 22:12
すぐに忘れてください。
フレームワーク自体に多く例外が使われています。
2018/07/28 12:57
となると、質問者さんの教えられた前提だと共通処理やフレームワーク関係は軒並み例外を出せなくなってしまうので、戻り値には常に正常に処理できた場合の本当の意味での戻り値を格納するクラスとエラー情報を載せるためのクラスをセットにしたクラスを定義しないといけなくなります
常にどちらか一方しか使いません。非常に面倒かつ分かりにくいものになると思われます
なので、処理一つ一つで問題があって続行できないものは例外を起こして内容は型(Exceptionから派生した自作クラス)で判別し、詳細情報を例外クラス内部に持たせます
で、使う側で~してくださいといったメッセージを表示するだけの業務エラーであるならばそのような処理を行い、システムに異常が起きないと起こりえない場面であるならばcatchせずにそのまま例外を上に丸投げしていけばいいだけなので、何ら問題はありません
ひょっとして、業務エラーは例外にするなというのは、「入力項目のうち、必須項目に入力がないまま処理を続行しようとして例外を起こすなんてことはするな、先に入力チェックを行え」という程度の事ではないでしょうか?
2018/07/28 13:16
1.社員データベースから検索するメソッド=>0件の場合は0件のデータを返す(正常終了)
2.社員番号を返し、レコードがない場合は失敗を返すメソッド=>0件の場合は失敗なのだから該当データなしの例外を発報する
3a.呼び出し側が例えば手入力の検索画面だったら該当データなしの例外をcatchして該当データがありませんとか条件を変更してくださいと言ったメッセージを出力する
3b.一覧で表示した中から選択したデータの取得処理だった場合は他の人がデータを変更したかデータが欠損した恐れがあるのでcatchせずに該当データなしのエラー画面へ飛ばすかcatchして他の人がデータを変更したかもしれない油脂のメッセージを出力させるかする
といった感じですかね
2.社員番号を返すメソッドがデータなしでも失敗を返さないのであればstring.Emptyあたりを返すだけにしますが
2018/07/28 15:48
N個のうちM個はOKで,N-M個はNGのとき,どのような形になるのでしょうか.その例外に正常に取得できたM個の情報も持たせるのですか?
2018/07/28 17:01
ナンセンスなのですが、それでもそのようなことがある場合、複数の間違いがあったという例外だけで十分かと思います。
それ以上の詳細な情報が欲しい場合はそれにあわせて作ればいいでしょう。
例外にはプロパティを持たせることができますので、正しい戻り値のリストと間違いの詳細のリストを持たせておけばいいのではないでしょうか。
2018/07/28 22:34 編集
要は、「契約による設計」でも取り上げられていますが、「そのメソッドの責任ではない異常」(不変条件を維持できないなんらかの外因的な異常)が発生した場合は例外を投げるべきということは理解しているつもりです。
しかし、メソッドの想定内の異常(つまりここでいう業務エラー)は例外を投げるべきではないとも書かれています。
また、「達人プログラマー」にも、「例外のキャッチが存在しなくても業務フローは成立してなければならない」という旨が記載されています。
名著がともに業務エラーでは例外を投げるべきではないという旨の記載があるのですが、この点についての見解を教えていただければ幸いです。
また、例外としての定義の問題だけでなく、個人的には業務エラーを例外としてしまうと、キャッチし忘れを懸念しなければならないことに不安があります。
プログラムを保守するのが自分だけなら問題ないのですが、大体の場合保守は別の人がやることになることが多いと思います。そのような場合、この「メソッドを使用するときは、この例外を捕捉しないといけない」ということを使い手に意識させるのは難しいと思います。(やるとしてもコメントで知らせるくらいしか思いつきません)
システムエラーならば呼び出し元がキャッチするべきではないので問題ないのですが、業務エラーは呼び出し元にキャッチを強制させる必要がでてきます(いわゆる検査例外)。そういう意味で、言語仕様としてシステムエラーと業務エラーの例外の扱い方に切り分けがない以上、むやみに業務エラーを例外として扱うのは危険であるという認識です。
この点については、そのような懸念は杞憂なのでしょうか・・・
それとも、C#においては慣習的に例外とは業務エラーにも使用するので、そんなことを気にすることがナンセンスなのでしょうか。
本題とは話はそれるのですが、せっかくなのでご教授いただければ幸いです。
宜しくお願い致します。
2018/07/29 05:03 編集
そこに書いてあるのがこの案件に相当するかどうかは自分でよく読み込んでください。
又聞きの理解が正しいかどうか議論するつもりはありません。
また自分で答えが出ているなら質問する必要はないでしょう。
例外をどうしても使いたくないなら使わず済ませる方法は他の方が書いてくれています。
私は業務エラーで例外を使ってはならない理由はないと思いますし、使っています。
名著の理解について知りたければ別に質問を立てて読んだ人に聞いてみれば良いのではないでしょうか?
2018/07/29 09:26
ご回答いただき、どうもありがとうございます。
2018/07/29 11:27
尤も、具体例のパターンだと普通は失敗と設計しないような内容だと思うので正直この場合は自分なら空文字列を返すようにして、呼び出し側で失敗と設計したから例外を発行するような作りになりそうですが