web-dev-qa-db-ja.com

検証のためのif-elseステートメントが多すぎるのは悪いですか?

本から Professional Enterprise .Net 、これはAmazonで5つ星の評価があり、読み終えた後で疑っています。以下は、作成者が作成した住宅ローンアプリケーションの一部であるBorrowerクラスです(C#では基本的なものです。だれでも理解できます)。

    public List<BrokenBusinessRule> GetBrokenRules()
    {
        List<BrokenBusinessRule> brokenRules = new List<BrokenBusinessRule>();

        if (Age < 18)
            brokenRules.Add(new BrokenBusinessRule("Age", "A borrower must be over 18 years of age"));

        if (String.IsNullOrEmpty(FirstName))
            brokenRules.Add(new BrokenBusinessRule("FirstName", "A borrower must have a first name"));

        if (String.IsNullOrEmpty(LastName))
            brokenRules.Add(new BrokenBusinessRule("LastName", "A borrower must have a last name"));

        if (CreditScore == null)
            brokenRules.Add(new BrokenBusinessRule("CreditScore", "A borrower must have a credit score"));
        else if (CreditScore.GetBrokenRules().Count > 0)
        {
            AddToBrokenRulesList(brokenRules, CreditScore.GetBrokenRules());
        }

        if (BankAccount == null)
            brokenRules.Add(new BrokenBusinessRule("BankAccount", "A borrower must have a bank account defined"));
        else if (BankAccount.GetBrokenRules().Count > 0)
        {
            AddToBrokenRulesList(brokenRules, BankAccount.GetBrokenRules());
        }
        // ... more rules here ...
        return brokenRules;
    }

snipt.orgの完全なコードリスト

私を混乱させているのは、この本はプロのエンタープライズデザインに関するものであることになっています。著者が第1章でデカップリングが何であるか、またはSOLIDがプログラミングキャリアの8年目まで意味することを真に知らなかったと告白したため、 8.1年に本を書いた)

私は専門家ではありませんが、次のことに慣れていません。

  1. If elseステートメントが多すぎます。

  2. クラスはエンティティとして機能し検証されます。臭いデザインじゃないですか? (いくつかのコンテキストを取得するには、 クラス全体を表示する が必要になる場合があります)

たぶん私は間違っているかもしれませんが、エンタープライズデザインを教えることになっている本から悪い習慣を取り上げたくありません。この本には同様のコードスニペットがたくさんあり、本当に私を悩ませています。それがis悪い設計である場合、if elseステートメントが多すぎるのを避けるにはどうすればよいでしょうか。

私は明らかにあなたがクラスを書き直すことを期待していません、何ができるかについての一般的な考えを提供するだけです。

私は.NET開発者なので、メモ帳でC#構文(Javaと同じかもしれません)を使用してこれを書きました。

public class BorrowerValidator
{    
    private readonly Borrower _borrower;    

    public BorrowerValidator(Borrower borrower)
    {
        _borrower = borrower;
        Validate();
    }

    private readonly IList<ValidationResult> _validationResults;

    public IList<ValidationResult> Results
    {
        get
        {
            return _validationResults;
        }
    }

    private void Validate()
    {
        ValidateAge();
        ValidateFirstName();
        // Carry on
    }

    private void ValidateAge()
    {
        if (_borrower.Age < 18)
            _validationResults.Add(new ValidationResult("Age", "Borrower must be over 18 years of age");
    }

    private void ValidateFirstname()
    {
        if (String.IsNUllOrEmpty(_borrower.Firstname))
            _validationResults.Add(new ValidationResult("Firstname", "A borrower must have a first name");
    }   
}

Usage:

var borrower = new Borrower
{
    Age = 17,
    Firstname = String.Empty        
};

var validationResults = new BorrowerValidator(borrower).Results;

borrowerオブジェクトをValidateAgeValidateFirstNameに渡しても意味がありません。代わりに、フィールドとして保存してください。

さらに物事を進めるために、インターフェイスを抽出できます:IBorrowerValidator。次に、借り手が彼らの財政状況に応じて検証するさまざまな方法があるとします。たとえば、誰かが元破産した場合、次のような状況が考えられます。

public class ExBankruptBorrowerValidator : IBorrowerValidator
{
   // Much stricter validation criteria
}
15
CodeART

私は専門家ではありませんが、私は慣れていません

1- if elseステートメントが多すぎます。

2-クラスはエンティティとして機能し、検証されます。臭いデザインじゃないですか?

あなたの直感はここにあります。これは弱い設計です。

コードは、ビジネスルールバリデーターであることが意図されています。銀行で働いており、ポリシーが18歳から21歳に変更されたとします。ただし、承認された共同署名者がいる場合は、18歳であっても許可されます。メリーランド、その場合...

私がここに行くところがわかります。ルールは銀行で常に変化し、ルールは互いに複雑な関係を持っています。したがって、ポリシーが変更されるたびにプログラマがコードを書き直す必要があるため、このコードは悪い設計です。指定されたコードの意図は、ポリシーに違反した理由を生成することであり、それは素晴らしいことですが、その場合はポリシーはオブジェクトである必要がありますではなく、ifステートメントであり、ポリシーは、データベースからロードする必要があります。オブジェクトの構築の検証にハードコーディングしないでください。

35
Eric Lippert

ええ通常、そのようなことはコードのにおいだと考える必要がありますが、この特定のインスタンスでは問題ないと思います。はい、注釈を付けることを検討することはできますが、それらはデバッグするのに苦痛と半分です。はい、検証を別のクラスに移動することを検討することもできますが、この種の検証は変化したり、他のタイプで再利用されたりすることはほとんどありません。

この実装は効果的で、テストが簡単で、保守も簡単です。多くのアプリケーションで、この設計は設計のトレードオフの適切なバランスをとります。他の人にとっては、より多くのデカップリングが追加のエンジニアリングの価値があるかもしれません。

15
Telastyn

質問のポイント2に関するもう1つのこと:そのクラスから検証ロジック(またはエンティティーであるクラスの目的を果たさないその他のロジック)を削除すると、ビジネスロジックが完全に削除されます。これで 貧血ドメインモデル が残りますが、これはほとんどアンチパターンと見なされています。

編集:もちろん、私はEric Lippertに完全に同意します。これは、頻繁に変更される可能性のある特定のビジネスロジックは、オブジェクトにハードコードされるべきではないということです。それでも、ビジネスオブジェクトからanyビジネスロジックを削除するのが適切な設計であることを意味するわけではありませんが、この特定の検証コードはbeeingの抽出に適した候補です外から交換できる場所へ。

8
Doc Brown

はい、これは悪臭ですが、解決策は簡単です。 BrokenRulesリストがありますが、BrokenBusinessRuleは、私が見る限り、名前と説明の文字列です。これは逆さまです。これを行う1つの方法を示します(注意してください。これは頭の頂上から外れているため、デザインの考慮事項はあまりありませんでした。もっと考えれば、もっと良いことができるはずです)。

  1. BusinessRuleクラスとRuleCheckResultクラスを作成します。
  2. RuleCheckResultクラスを返す「checkBorrower」メソッドの名前プロパティを指定します。
  3. BusinessRuleがBorrowerのすべてのチェック可能なプロパティを確認できるようにするために必要なことをすべて実行します(たとえば、それを内部クラスにします)。
  4. ルールの特定のタイプごとにサブクラスを作成します(または、より良いのは、匿名の内部クラス、ルールファクトリ、またはクロージャーと高次関数をサポートする適切な言語を使用することです)。
  5. 実際に使用したいすべてのルールをコレクションに入力します

次に、それらすべてのif-then-elseステートメントを、次のような単純なループで置き換えることができます。

for (BusinessRule rule: businessRules) {
  RuleCheckResult result = rule.check(borrower);
  if (!result.passed) {
    for (RuleFailureReason reason: result.getReasons()) {
      BrokenRules.add(rule.getName(), reason.getExplanation())
    }
}

または、もっと簡単にして、これだけにすることもできます

for (BusinessRule rule: businessRules) {
  RuleCheckResult result = rule.check(borrower);
  if (!result.passed) {
      BrokenRules.add(rule.getName(), result)
    }
}

RuleCheckResultとRuleFailureReasonがどのように構成されるかは明らかです。

これで、複数の基準を持つ複雑なルール、または何もない単純なルールを持つことができます。しかし、ifとif-elsesの1つの長いセットでそれらすべてを表現しようとしているのではなく、新しいルールを思いついたり、古いルールを削除したりするたびにBorrowerオブジェクトを書き換える必要はありません。

3
itsbruce

これらのタイプのルールシナリオでは、適用するのに適したパターンは 仕様パターン であると思います。個々のビジネスルールをカプセル化するためのより洗練された方法を作成するだけでなく、個々のルールを他のルールと組み合わせてより複雑なルールを構築できるようにします。過度に設計されたインフラストラクチャを必要とせずに(IMHO)。ウィキペディアのページには、すぐに使えるすばらしいC#実装も用意されています(ただし、盲目的に貼り付けるだけではなく、理解しておくことをお勧めします)。

Wikipediaページの変更された例を使用するには:

_ValidAgeSpecification IsOverEighteen = new ValidAgeSpecification();
HasBankAccountSpecification HasBankAccount = new HasBankAccountSpecification();
HasValidCreditScoreSpecification HasValidCreditScore = new HasValidCreditScoreSpecification();

// example of specification pattern logic chaining
ISpecification IsValidBorrower = IsValidAge.And(HasBankAccount).And(HasValidCreditScore);

BorrowerCollection = Service.GetBorrowers();

foreach (Borrower borrower in BorrowerCollection ) {
    if (!IsValidBorrower.IsSatisfiedBy(borrower))  {
        validationMessages.Add("This Borrower is not Valid!");
    }
}
_

「この特定の例には、違反している個々のルールはリストされていません!」と言うかもしれません。ここでのポイントは、ルールを構成する方法を示すことです。 4つのルールすべて(複合ルールを含む)をISpecificationのコレクションに簡単に移動し、そのブルースが実証したようにそれらを反復することができます。

その後、上級者が連帯保証人のいるティーンエイジャーのみが利用できる新しいローンプログラムを作成したい場合、そのようなルールを作成するのは簡単で宣言的です。さらに、既存の年齢ルールを再利用できます!

ISpecification ValidUnderageBorrower = IsOverEighteen.Not().And(HasCosigner);

2
mclark1129

それはまったく再利用できません。私は_Data Annotations_または同様の属性ベースの検証フレームワークを使用します。チェック http://odetocode.com/blogs/scott/archive/2011/06/29/manual-validation-with-data-annotations.aspx たとえば。

編集:私自身の例

_using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace ValidationTest
{
    public interface ICategorized
    {
        [Required]
        string Category { get; set; }
    }
    public abstract class Page
    {
        [Required]
        [MaxLength(50)]
        public string Slug { get; set; }
        [Required]
        public string Title { get; set; }
        public string Text { get; set; }

        public virtual ICollection<ValidationResult> Validate()
        {
            var results = new List<ValidationResult>();
            ValidatePropertiesWithAttributes(results);
            ValidatePropertiesFromInterfaces(results);
            return results;
        }
        protected void ValidatePropertiesWithAttributes(ICollection<ValidationResult> results)
        {
            Validator.TryValidateObject(this, new ValidationContext(this,null,null), results);
        }
        protected void ValidatePropertiesFromInterfaces(ICollection<ValidationResult> results)
        {
            foreach(var iType in this.GetType().GetInterfaces())
            {
                foreach(var property in iType.GetProperties())
                {
                    foreach (var attr in property.GetCustomAttributes(false).OfType<ValidationAttribute>())
                    {
                        var value = property.GetValue(this);
                        var context = new ValidationContext(this, null, null) { MemberName = property.Name };
                        var result = attr.GetValidationResult(value, context);
                        if(result != null)
                        {
                            results.Add(result);
                        }
                    }
                }
            }
        }
    }
    public class Blog : Page
    {

    }
    public class Post : Page, ICategorized
    {
        [Required]
        public Blog Blog { get; set; }
        public string Category { get; set; }
        public override ICollection<ValidationResult> Validate()
        {
            var results = base.Validate();
            // do some custom validation
            return results;
        }
    }
}
_

プロパティValidatePropertiesWithAttributes(string[] include, string[] exclude)を含めたり除外したり、db Validate(IRuleProvider provider)などからルールを取得するルールプロバイダーを追加したりするのは簡単です。

1
Mike Koder

ここでの重要な質問は、「これはビジネスロジックを実装するための最も読みやすく、明確で簡潔な方法ですか "です。

答えはイエスだと思う場合。

コードを分割するすべての試みは、減少した数の「if」以外を追加することなく、意図のみをマスクします。

これはテストの順序付けられたリストであり、一連の「if」ステートメントが最良の実装であると思われます。何か賢いことをしようとすると、読みにくくなり、さらに悪いことに、「きれいな」より抽象的な実装で新しいルールを実装できない場合があります。

ビジネスルールは非常に複雑になる可能性があり、多くの場合、それらを実装するための「クリーン」な方法はありません。

0
James Anderson