モデルがデータを検証している場合、不正な入力に対して例外をスローすべきではありませんか?


9

このSOの質問を読むと、ユーザー入力を検証するために例外をスローすることは不快に思われるようです。

しかし、誰がこのデータを検証する必要がありますか?私のアプリケーションでは、すべての検証はビジネスレイヤーで行われます。これは、クラス自体だけが、各プロパティの有効な値を実際に認識しているためです。プロパティを検証するためのルールをコントローラーにコピーすると、検証ルールが変更される可能性があり、変更を行う場所が2つあります。

ビジネス層で検証を行う必要があるという私の前提は間違っていますか?

私がやること

したがって、私のコードは通常、次のようになります。

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      throw new ValidationException("Name cannot be empty");
    }
    $this->name = $n;
  }

  public function setAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        throw new ValidationException("Age $a is not valid");
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      throw new ValidationException("Age $a is out of bounds");
    }
    $this->age = $a;
  }

  // other getters, setters and methods
}

コントローラーでは、入力データをモデルに渡し、スローされた例外をキャッチしてユーザーにエラーを表示します。

<?php
$person = new Person();
$errors = array();

// global try for all exceptions other than ValidationException
try {

  // validation and process (if everything ok)
  try {
    $person->setAge($_POST['age']);
  } catch (ValidationException $e) {
    $errors['age'] = $e->getMessage();
  }

  try {
    $person->setName($_POST['name']);
  } catch (ValidationException $e) {
    $errors['name'] = $e->getMessage();
  }

  ...
} catch (Exception $e) {
  // log the error, send 500 internal server error to the client
  // and finish the request
}

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

これは悪い方法ですか?

別の方法

多分私はisValidAge($a)true / falseを返すためのメソッドを作成してからコントローラからそれらを呼び出す必要がありますか?

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if ($this->isValidName($n)) {
      $this->name = $n;
    } else {
      throw new Exception("Invalid name");
    }
  }

  public function setAge($a) {
    if ($this->isValidAge($a)) {
      $this->age = $a;
    } else {
      throw new Exception("Invalid age");
    }
  }

  public function isValidName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      return false;
    }
    return true;
  }

  public function isValidAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        return false;
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      return false;
    }
    return true;
  }

  // other getters, setters and methods
}

そしてコントローラーは基本的に同じですが、try / catchの代わりにif / elseがあります:

<?php
$person = new Person();
$errors = array();
if ($person->isValidAge($age)) {
  $person->setAge($age);
} catch (Exception $e) {
  $errors['age'] = "Invalid age";
}

if ($person->isValidName($name)) {
  $person->setName($name);
} catch (Exception $e) {
  $errors['name'] = "Invalid name";
}

...

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

それで、私は何をすべきですか?

私は元の方法にかなり満足しており、一般にそれを示した同僚はそれを気に入っていました。それにもかかわらず、別の方法に変更する必要がありますか?それとも私はこれをひどく間違っているので別の方法を探すべきですか?


「元の」コードを少し処理ValidationExceptionしてその他の例外を修正しました
CarlosCampderrós

2
例外メッセージをエンドユーザーに表示する際の問題の1つは、モデルがユーザーの話す言語を突然知る必要があることですが、これは主にビューの問題です。
Bart van Ingen Schenau 2013年

@BartvanIngenSchenauグッドキャッチ。私のアプリケーションは常に単一言語でしたが、実装で発生する可能性のあるローカリゼーションの問題を考慮することは良いことです。
CarlosCampderrós2013年

検証の例外は、プロセスに型を注入するための優れた方法です。のような検証インターフェースを実装するオブジェクトを返すことで、同じ結果を得ることができますIValidateResults
Reactgular 2014年

回答:


7

過去に使用したアプローチは、すべての検証ロジックを専用の検証クラスに配置することです。

その後、これらのValidationクラスをプレゼンテーション層に挿入して、早期の入力検証を行うことができます。また、モデルクラスがまったく同じクラスを使用してデータ整合性を適用することを妨げるものはありません。

このアプローチに従って、検証エラーが発生するレイヤーに応じて、検証エラーを異なる方法で処理できます。

  • モデルでデータ整合性検証が失敗した場合は、例外をスローします。
  • プレゼンテーションレイヤーでユーザー入力検証が失敗した場合は、役立つヒントを表示し、モデルへの値のプッシュを遅らせます。

それでPersonValidator、のさまざまな属性を検証するためのすべてのロジックを備えたクラスと、これに依存PersonするPersonクラスをPersonValidator持っていますよね?あなたの提案が、質問で提案した別の方法よりも優れている点は何ですか?に異なるValidationクラスを挿入する能力しか見てPersonいませんが、これが必要になるケースは考えられません。
CarlosCampderrós2013年

検証のためにまったく新しいクラスを追加することは、少なくともこの比較的単純なケースではやり過ぎであることに同意します。より複雑な問題に役立ちます。

まあ、あなたが複数の人/会社に販売することを計画しているアプリケーションの場合、各会社が人の年齢の有効範囲を検証するための異なるルールを持っているかもしれないので、それは理にかなっているかもしれません。したがって、それは有用である可能性はありますが、私のニーズには本当に行き過ぎです。とにかく、あなたのためにも+1
CarlosCampderrós2013年

1
検証をモデルから分離することは、結合と結束の観点からも理にかなっています。この単純なシナリオではやり過ぎかもしれませんが、個別のValidatorクラスをより魅力的にするために、単一の「クロスフィールド」検証ルールのみを使用します。
Seth M.

8

私は元の方法にかなり満足しており、一般にそれを示した同僚はそれを気に入っていました。それにもかかわらず、別の方法に変更する必要がありますか?それとも私はこれをひどく間違っているので別の方法を探すべきですか?

あなたとあなたの同僚がそれに満足しているなら、私は変える必要はないでしょう。

実用的な観点から疑わしい唯一のことは、Exceptionより具体的なものではなく投げているということです。問題は、をキャッチするとException、ユーザー入力の検証とは関係のない例外をキャッチしてしまう可能性があることです。


現在、「例外は例外的なものにのみ使用されるべきであり、XYZは例外的ではない」と言う人はたくさんいます。(たとえば、@ dann1111のアンサー...ユーザーのエラーに「完全に正常」というラベルを付けます。)

これに対する私の反応は、何か( "XY Z")が例外的であるかどうかを決定するための客観的な基準がないということです。それは主観的な尺度です。(プログラムユーザー入力のエラーをチェックする必要があるという事実は、発生エラーを「正常」にするわけではありません。実際、「正常」は客観的な観点からはほとんど意味がありません。)

そのマントラには一粒の真実があります。一部の言語(より正確には、一部の言語実装)では、例外の作成、スロー、および/またはキャッチは、単純な条件文よりもはるかにコストがかかります。しかし、その観点から見ると、作成/スロー/キャッチのコストと、例外の使用を避けた場合に実行する必要がある可能性のある追加のテストのコストを比較する必要があります。また、「式」では、例外をスローする必要がある確率を考慮に入れる必要があります。

例外に対するもう1つの主張は、コードを理解しにくくする可能性があるということです。ただし、逆に言えば、適切に使用すると、コード理解しやすくなります。


簡単に言うと、例外を使用するか使用しないかの決定は、メリットを検討した後で行う必要があり、単純化した教義に基づいたものではありません。


ジェネリックExceptionがスロー/キャッチされることの良い点。私は本当にのいくつかの独自のサブクラスをスローしException、セッターのコードは通常、別の例外をスローすることができるものは何もしません。
CarlosCampderrós2013年

ValidationExceptionおよびその他の例外を処理するように「元の」コードを少し変更しました/ cc @ dan1111
CarlosCampderrósJun

1
+1、すべてのメソッド呼び出しの戻り値を確認する必要があるという暗い時代に戻るよりも、説明的なValidationExceptionを使用するほうがはるかに便利です。シンプルなコード=潜在的にエラーが少ない。
ハインツィ2013年

2
@ dan1111-私はあなたが意見を持つ権利を尊重しますが、あなたのコメントには意見以外のものはありません。検証の「正常性」と検証エラーを処理するメカニズムとの間には論理的な関連はありません。あなたがしているすべては、教義を暗唱することです。
スティーブンC

@StephenC、振り返って、私は自分のケースを強く主張しすぎたように感じます。私はそれが個人的な好みであることに同意します。

6

私の意見では、アプリケーションエラーユーザーエラーを区別し、前者にのみ例外を使用すると便利です。

  • 例外は、プログラムの適切な実行を妨げるものを対象としています

    これらは、続行を妨げる予期しない出来事であり、それらの設計はこれを反映しています。通常の実行を中断し、エラー処理が可能な場所にジャンプします。

  • 無効な入力などのユーザーエラーは完全に正常であり(プログラムの観点から)、アプリケーションでは予期しないものとして扱われるべきではありません

    ユーザーが間違った値を入力してエラーメッセージを表示した場合、プログラムは「失敗」したり、何らかのエラーが発生したりしましたか?いいえ。アプリケーションは成功しました。ある種の入力が与えられた場合、その状況では正しい出力が生成されました。

    ユーザーエラーの処理は、通常の実行の一部であるため、例外を出してジャンプアウトするのではなく、通常のプログラムフローの一部にする必要があります。

もちろん、本来の目的以外に例外を使用することは可能ですが、そうすることでパラダイムが混乱し、これらのエラーが発生したときに不正な動作が発生する危険があります。

元のコードには問題があります:

  • setAge()メソッドの呼び出し元は、メソッドの内部エラー処理について多くのことを知っている必要があります。呼び出し元は、経過時間が無効な場合に例外がスローされ、メソッド内で他の例外がスローされないことを知る必要があります。この仮定は、内に追加の機能を追加した場合、後で破られる可能性がありますsetAge()
  • 呼び出し側が例外をキャッチしない場合、無効な経過時間の例外は後で他の、おそらくは不透明な方法で処理されます。または、未処理の例外クラッシュを引き起こします。無効なデータが入力された場合の動作は適切ではありません。

代替コードにも問題があります:

  • 追加の、おそらく不要なメソッドisValidAge()が導入されました。
  • ここで、setAge()メソッドは、呼び出し元が既にチェックした(ひどい仮定)か、年齢を再度検証すると仮定する必要がありますisValidAge()。それが再び経過時間を検証する場合、setAge() それでも何らかのエラー処理を提供する必要があり、再び1に戻す必要があります。

提案されたデザイン

  • setAge()成功した場合はtrue を返し、失敗した場合はfalse を返します。

  • の戻り値を確認し、setAge()失敗した場合は、例外ではなく、ユーザーにエラーを表示する通常の関数を使用して、年齢が無効であることをユーザーに通知します。


では、どうすればよいですか。私が提案した別の方法で、または私が考えていなかったまったく異なるもので?また、「検証はビジネス層で行われるべきである」という私の前提は間違っていますか?
CarlosCampderrós2013年

@CarlosCampderrós、更新を参照してください。あなたがコメントしたように私はその情報を追加していました。元のデザインの正しい場所で検証が行われていましたが、その検証を実行するために例外を使用するのは誤りでした。

別の方法ではがsetAge再度検証されますが、ロジックは基本的に「有効な場合は年齢を設定し、それ以外の場合は例外をスローする」ので、正方形に戻りません。
CarlosCampderrós2013年

2
代替方法と推奨される設計の両方で私が目にする1つの問題は、年齢が無効であった理由を区別できなくなることです。trueまたはエラー文字列を返すようにすることもできますが(そうです、phpはすごく汚いです)、"The entered age is out of bounds" == true人々は常にを使用する必要があるため===、これは多くの問題を引き起こす可能性があるため、このアプローチは、試行する問題よりも問題が多くなります解決
CarlosCampderrós2013年

2
ただし、アプリケーションのコーディングは非常に面倒です。なぜなら、setAge()どこで作成したものでも、それが実際に機能しているかどうかを確認する必要があるからです。例外をスローするということは、すべてが正常に行われたことを確認することを覚えておく必要がないということです。私が見ているように、無効な値を属性/プロパティに設定しようとすることは例外的なことなので、をスローする価値がありExceptionます。モデルは、データベースまたはユーザーから入力を取得しているかどうかを気にする必要はありません。それは悪い入力を受け取るべきではないので、例外をそこにスローすることは正当であると私は思います。
CarlosCampderrós2013年

4

私の観点からは(私はJavaの人です)、最初の方法で実装した方法は完全に有効です。

いくつかの前提条件が満たされない場合(たとえば、空の文字列)、オブジェクトが例外をスローすることは有効です。Javaでは、チェック例外の概念はそのような目的に組み込まれています-適切にスローされるようにシグニチャーで宣言されなければならない例外であり、呼び出し側は明示的にそれらをキャッチする必要があります。対照的に、チェックされていない例外(別名RuntimeExceptions)は、コードでcatch-clauseを定義する必要なしにいつでも発生する可能性があります。前者は回復可能な場合に使用されますが(たとえば、誤ったユーザー入力、ファイル名が存在しないなど)、後者はユーザー/プログラマーが何もできない場合(たとえば、メモリ不足)に使用されます。

ただし、@ Stephen Cですでに述べたように、独自の例外を定義し、他の例外を意図的にキャッチしないように具体的にキャッチする必要があります。

ただし、もう1つの方法は、ロジックのない単純なデータコンテナーであるデータ転送オブジェクトを使用することです。次に、そのようなDTOを検証のためにバリデーターまたはModel-Object自体に引き渡し、成功した場合にのみ、Model-Objectで更新を行います。このアプローチは、プレゼンテーションロジックとアプリケーションロジックが分離された層である場合によく使用されます(プレゼンテーションはWebページであり、アプリケーションはWebサービスです)。このように物理的に分離されますが、両方が1つの層にある場合(例のように)、検証なしで値を設定するための回避策がないことを確認する必要があります。


4

私のHaskell帽子をかぶった状態では、どちらのアプローチも間違っています。

概念的に何が起こるかというと、最初に大量のバイトがあり、解析と検証の後でPersonを作成できます。

Personには、名前のプリセンスや年齢など、特定の不変条件があります。

名前のみを持ち、年齢を持たない人物を表すことができることは、何としても避けたいものです。厳密な不変条件とは、たとえば、後で年齢の存在を確認する必要がないことを意味します。

したがって、私の世界では、Personは単一のコンストラクターまたは関数を使用してアトミックに作成されます。そのコンストラクターまたは関数は、パラメーターの有効性を再度確認できますが、ハーフパーソンを作成する必要はありません。

残念ながら、Java、PHP、その他のオブジェクト指向言語では、正しいオプションがかなり冗長になります。適切なJava APIでは、ビルダーオブジェクトがよく使用されます。このようなAPIでは、人物を作成すると次のようになります。

Person p = new Person.Builder().setName(name).setAge(age).build();

またはより詳細:

Person.Builder builder = new Person.Builder();
builder.setName(name);
builder.setAge(age);
Person p = builder.build();
// Person object must have name and age here

これらの場合、例外がスローされる場所や検証が発生する場所に関係なく、無効なPersonインスタンスを受け取ることは不可能です。


ここで行ったことは、問題をBuilderクラスに移動することだけです。Builderクラスには、まだ答えていません。
サイファー、2014

2
アトミックに実行されるbuilder.build()関数に問題を限定しました。その関数は、すべての検証ステップのリストです。このアプローチとその場限りのアプローチには大きな違いがあります。Builderクラスには単純型以外の不変条件はありませんが、Personクラスには強い不変条件があります。正しいプログラムを構築するには、データに強い不変式を適用する必要があります。
user239558 14

それはまだ質問に答えません(少なくとも完全にではありません)。個々のエラーメッセージがビルダークラスからコールスタックを介してビューに渡される方法について詳しく教えてください。
Cypher

3つの可能性:OPの最初の例のように、build()は特定の例外をスローできます。人間が読めるエラーのセットを返すパブリックSet <String> validate()が存在する可能性があります。国際化対応のエラーには、パブリックSet <Error> validate()があります。ポイントは、これがPersonオブジェクトへの変換中に発生することです。
user239558

2

素人の言葉で:

最初のアプローチは正しいものです。

2番目のアプローチは、それらのビジネスクラスがそれらのコントローラーによってのみ呼び出され、他のコンテキストから呼び出されることは決してないと想定しています。

ビジネスクラスは、ビジネスルールに違反するたびに例外をスローする必要があります。

コントローラーまたはプレゼンテーション層は、例外を発生させないようにするために、それらをスローするか、独自の検証を行うかを決定する必要があります。

覚えておいてください:クラスは、異なるコンテキストで、異なるインテグレーターによって使用される可能性があります。したがって、不正な入力に対して例外をスローするのに十分なほどスマートでなければなりません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.