PHP
1/** 2* 自然数同士の足し算を行う 3* 4* @param integer $x 自然数であること 5* @param integer $y 自然数であること 6* @return integer $xと$yの合計値 7*/ 8function Add($x, $y) { 9 $sum = $x + $y; 10 return $sum; 11}
このようなコードがあるとします。
$x, $yが引数として妥当であること(型がintegerである、自然数である)の検査をする箇所について質問があります。
検査すべき場所として下記3パターンあると思うのですがどれが妥当なのですか?
(a)関数内で行うもの
(b)呼び出し側で行うもの
(c)a,b両方で行うもの
防衛的プログラミングや契約プログラミングというワードを「達人プログラマー」の書籍でしり混乱しています...
上記3パターン以外の手法も歓迎しております。どうかお知恵をお貸しください。
【追記】
情報の質と量が多くみなさんの全ての回答を読めていません。
いまだ「未解決」のままになっていますが週末利用して頂いた回答を理解できるように致します。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答6件
0
和田卓人さんの講演が参考になると思います。長い(45分)ですが、それだけの価値があるものです。私は本番も聴講していますが、加えて何度もビデオで見返しました。
PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 和田 卓人(ビデオ)
PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 和田 卓人(スライド)
投稿2017/11/06 06:08
総合スコア11705
0
これはとっても難しい問題で、賛否両論ある議題でもあるんですよね。
そんな中で、持論を書いてみます。
まず、その関数がどの業務にも依存しない共通のユーティリティー的な立ち位置なのであれば、
防衛的プログラミングをした方が、呼び出し側もシンプルになり使いやすいものになると思います。
(a)関数内で行うもの
こちらに該当します。
ただ、型の問題(自然数である前にまずは整数であること)は、
maisumakunさんがおっしゃっているタイプヒント一択ではないでしょうか。
次に、アプリケーション設計を各レイヤーに分けている場合です。
レイヤーの定義は以下のものを前提にします。(ググるとたまたま一番上にきただけの理由です)
アプリケーションのレイヤ化
アプリケーション層からドメイン層のサービスを呼ぶ場合の引数などは
関数内ですべきではありません。
(b)呼び出し側で行うもの
こちらに該当します。
なぜなら、ドメイン層への入力は決められた業務の情報であり、
それ以外での呼ばれ方を意識すべきではないからです。(あり得ない業務を意識するのはおかしい)
つまり、想定外の呼ばれ方をした場合は、エラーが起きるべきだということです。
本当に良く見るのはドメイン層でのnullチェックですね。
そもそも業務上nullであること自体がおかしいのだからエラーにならなければおかしいでしょう。
そしてこのエラーはドメイン層のバグではないということが重要です。
nullエラーになったからといって、関数内でnullチェックを入れるのはおかしいということです。
入力情報の検証などは、アプリケーション層の役割です。
そこで業務に合った検証を行い、不適切であればその時点でエラーにする。
「入力情報の検証」といっても、数字であること、日付であること、などの単純なものと
業務的な検証(例えば在庫のチェック)とかで、また話しは変わってきますが、
「自然数であること」などはアプリケーション層の役割でしょう。
(c)a,b両方で行うもの
こちらに関しては、全く必要性を感じません。
関数内で行っているのに、呼び出し側でも行っているのは、
そのプログラマーが、ただ単にその関数の仕様を分かっていないだけのことでしょう。
ただ関数を作成する側も、仕様が分かるように丁寧にコメントを残すなどの配慮は必要だと思います。
コメントの必要性や、粒度も色々と議論があるようですが、
最低限、クラス、インターフェース、関数などの仕様の説明は絶対に必要です。
みなさんそれらを見て、OSSなどを利用しているわけですから。
投稿2017/11/06 04:01
総合スコア4666
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
ベストアンサー
>契約プログラミング → (b)呼び出し側で行う
>防衛的プログラミング → (a)関数内で行う
ご質問の部分に対して、上記のような対応になります。
この防衛と契約で何が違ってくるのか、意味を少し考えてみます。
エラーチェックは何も考えずにやると、ものすごく負担になります。
想定外の想定はキリがないので、際限なく複雑になりうる。
単純に、関数が100個あったら、100回チェックすれば、何倍にもなります。
>(c)a,b両方で行う などと重複してたら、もっと増えてしまうでしょう。
そこで、事前条件の確保は呼び出し側の責務と考え、
違反していたときに例外で突き返して良いとすることで、
複雑になりがちなエラー処理をシンプルにできます。
防衛より契約の方がモダンな手法だと思います。
治安が良いと自衛の負担が少ない、とかそういうイメージです。
もう少し踏み込んだ内容も見てみます。「契約プログラミング」は難解なので、
以下は中途半端な解説になりますが、参考までに流し読みしてください。
メイヤーの契約概念は例外処理と(だけ)結びつけて語られやすいですが、
メイヤー流の見方では、そもそもオブジェクト指向そのものが、
抽象データ型を利用した契約の体系(公理的・代数的な体系)なのです。
紙幅が足りないので、極度に単純化したものとお断りしておきますが、
その考え方の一部だけ言うと、たとえば不変条件をクラスの単位だと考えます。
どういうことかというと、処理を書いてから検証を考えるというより、
処理の前提となる基準(不変条件)が共通するように、最初からクラスを作っておきます。
加算関数や減算関数などが各自バラバラに自然数の条件を検証するのではなく、
自然数クラスに加算メソッドや減算メソッドを付けていく感じです。
OOPだとコンストラクタやゲッター/セッター、例外/表明などの仕組みがあり、
データを検証しやすいのです。コンストラクタやセッターで
値を監視しておけば、負の値にならないことなどは保証できます。
関数ごとにチェックするコードを全部書いて回る必要がない。
たとえれば、取引所に関係者だけ集めて取引すると、
スムーズに話が進むみたいなイメージでしょうか。
そういう感じで、OOで上手く分析・設計できれば(これが難しいけど)、
重複を省くことでチェックするコードを劇的に減らせます。
ただ、高度なOOは入門書や入門サイトに全く書いてなく、非常に難しいです。
処理よりデータを中心に考えることに、違和感を覚えるかもしれませんが、
「DDD(ドメイン駆動開発)」なども、こうした発想の延長にあります。
投稿2017/11/08 23:48
総合スコア5592
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
それ以前に、条件にあわないときにどういった処理をしたいかによるのでは?
Addがエラーを返したり、想定外のデータを受けたときに別に置き換えてやるなら
Add内で完結してよいでしょう。
もちろん事前にデータをチェックしたとしても、Add側で二重にチェックしてやるほうが
より確実だと思います
追記
考え方
- パターン1
PHP
1function Add($x, $y) { 2 $sum = $x + $y; 3 return $sum; 4} 5function is_natural(){ 6 $args=func_get_args(); 7 $ret=true; 8 foreach($args as $num){ 9 if(!is_int($num) or $num<=0) $ret=false; 10 } 11 return $ret; 12} 13$x=10;$y=20; 14if(is_natural($x,$y)){ 15 print Add($x,$y); 16}else{ 17 print "wrong data!"; 18}; 19$x=-10;$y=20; 20if(is_natural($x,$y)){ 21 print Add($x,$y); 22}else{ 23 print "wrong data"; 24}; 25
※上記のように、外側でチェックをすればAddから戻るデータはかならず自然数です。
型が保証されるのでわかりやすいでしょう。
逆に例外処理を都度書かなくては行けないのは煩雑です。
- パターン2
PHP
1$x=10;$y=20; 2print Add($x,$y); 3$x=-10;$y=20; 4print Add($x,$y); 5function Add($x, $y) { 6 if(!is_natural($x,$y)) return "wrong data!"; 7 $sum = $x + $y; 8 return $sum; 9} 10function is_natural(){ 11 $args=func_get_args(); 12 $ret=true; 13 foreach($args as $num){ 14 if(!is_int($num) or $num<=0) $ret=false; 15 } 16 return $ret; 17}
メインパートは書きやすくなりますが、Addからの戻り値の型が曖昧になります
errorでthrowしたりすることも視野にいれれば
個人的にはパターン1の方がデータ管理がし易いとおもいます。
投稿2017/11/06 02:18
編集2017/11/06 05:04総合スコア117345
0
似たような質問がTeratail上にあるので、一度目を通しておくと良いでしょう。
実行制限のある関数の記述方法
PHP7ではプリミティブ型のタイプヒンティングが可能ですが、
これは関数のあり方を宣言しているだけなので、関数側での対策には入らないと考えています。
当然あった方が良いと思います。
基本は「プロジェクトの決定に従う」が正解です。
個々が協力せず、自分の思想を勝手にコードに反映すると反発しあった汚いコードになります。
当事者同士で納得行くまで話し合って決定しましょう。
ですので、その決定を作る為に議論している状況と仮定します。
その場合b > a >> c
で考えています。
下記、例を交えつつ解説します。
あなたが部下や同僚に仕事を依頼したとしましょう。
仕事を完遂させるには、前提条件となる資料(引数)が必要です。
もし資料不足で部下の仕事が失敗したら誰の責任になりますか?
aの設計は仕事を依頼された側に責任があるから、よしなに解決しろと言っています。
bの設計のみ仕事の依頼元の責任と言ってます。
私が依頼された側だったらイラッとするのでbにしてほしいです!
モダンな言語の多くでは、依頼先(関数の中身)は例外投げて死ねばいいという設計です。
これは仕事の依頼者(呼び出し元)の責任でハンドリングしろと言っています。
また、多くの言語のビルトイン関数も引数が不正な場合、エラーや例外を投げて死ぬ挙動になっています。
自分たちの作った関数だけ急に呼び出し先が頑張る思想にするとあべこべになりますから、
呼び出し元がハンドリングを頑張るのが自然な設計かと思います。
aは典型的な防衛的プログラミングですね。
これはルールを厳格に行い、用法・用量を守って適切に扱いましょう。
そうでなければプロジェクトが悲惨な目にあいます。
関数を呼ぶ時に「Stringが来たらどうすんだ?」や「合計するとInt型の上限値を超える値を渡したらどうなるんだ?」
…という重箱の隅をつつくようなケースもやり玉に挙げられる可能性があります。
それらの対応を全て行うときりがありません。
簡単な2つの値を足すだけの単純な処理が、例外処理だらけで20行くらいになってる関数を使いたいですか?
それが全ての関数に適用されている状況を想像してみてください。
もし私がそれをメンテし続けろと命じられたら、その日の内に退職願いを叩きつけるかもしれません。
なので匙加減のハンドリングをし続ける努力が必要になります。
私は面倒なので、あまりやりたくないのでb推奨派です。
一番危ないのはユーザーからの入力値ですので、
そこさえ死ぬ気で見張れば、他の処理はあまり神経質になる必要はないと思ってます。
投稿2017/11/06 06:34
総合スコア21291
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/11/10 02:56

0
人為的に検査しなくても、PHP 7であれば、スカラー型についても引数のタイプヒントを宣言できます(リファレンス)。自分で実装しなくて済むことは、処理系に任せてしまいましょう。
php
1// int以外が来たらTypeError例外 2function Add(int $x, int $y) { 3 $sum = $x + $y; 4 return $sum 5}
投稿2017/11/06 02:19
総合スコア146409
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/11/06 06:16
退会済みユーザー
2017/11/06 06:17
2017/11/09 05:12
2017/11/09 14:46