web-dev-qa-db-ja.com

ビジネスルールのデザインパターン?

SOLID性を向上させるために、ビジネスルールを実装するためのインターフェイスに取り組んでいます。そのため、多くのロジックをWeb APIコントローラーからビジネスライブラリに移動できます。一般的な問題は、1つまたは複数の条件が満たされた場合にアクションが発生することであり、これらの条件のいくつかは、最終的に異なるアクションでシステム全体に必要になる可能性があります。私はいくつかの調査を行い、以下のコードを思いつきました。これは既存の設計パターンに準拠していますか? GoFリストを調べたところ、何も見つかりませんでした。

/// <summary>
/// Use for designing a business rule where conditions are evaluated and the actions are executed based on the evaluation.
/// Rules can be chained by setting the "Action" as another business rule.
/// </summary>
/// <typeparam name="TCondition">The type of the condition or conditions.</typeparam>
/// <typeparam name="TAction">The type of the action or actions to be executed.</typeparam>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <seealso cref="Core.Interfaces.IBusinessRule" />
internal interface IBusinessRule<TCondition, TAction, TResult> : IBusinessRule
    where TCondition : IRulePredicate where TAction : IRuleAction<TResult>
{
    ICollection<TAction> Actions { get; set; }

    ICollection<TCondition> Preconditions { get; set; }
}


internal interface IBusinessRule
{
    IEnumerable Results { get; }

    RuleState State { get; }

    Task Execute();
}

public enum RuleState
{
    None,
    Initialized,
    InProgress,
    Faulted,
    FailedConditions,
    Completed
}

public interface IRulePredicate
{
    bool Evaluate();
}

public interface IRuleAction<TResult>
{
    Task<TResult> Execute();
}


public abstract class RuleBase<TCondition, TAction, TResult> :
    IBusinessRule<TCondition, TAction, TResult> where TCondition : IRulePredicate
    where TAction : IRuleAction<TResult>
{
    public ICollection<TResult> Results { get; } = new List<TResult>();

    public ICollection<TCondition> Preconditions { get; set; } = new List<TCondition>();

    public ICollection<TAction> Actions { get; set; } = new List<TAction>();

    IEnumerable IBusinessRule.Results => Results;

    public RuleState State { get; private set; } = RuleState.Initialized;

    public async Task Execute()
    {
        State = RuleState.InProgress;
        try
        {
            var isValid = true;
            foreach (var item in Preconditions)
            {
                isValid &= item.Evaluate();
                if (!isValid)
                {
                    State = RuleState.FailedConditions;
                    return;
                }
            }

            foreach (var item in Actions)
            {
                var result = await item.Execute();
                Results.Add(result);
            }
        }
        catch (Exception)
        {
            State = RuleState.Faulted;
            throw;
        }

        State = RuleState.Completed;
    }
}

public class TestRule1 : RuleBase<FakePredicateAlwaysReturnsTrue, WriteHelloAction, string>
{
    public TestRule1()
    {
        Preconditions = new[] { new FakePredicateAlwaysReturnsTrue() };
        Actions = new[] { new WriteHelloAction() };
    }
}

public class FakePredicateAlwaysReturnsTrue : IRulePredicate
{
    public bool Evaluate()
    {
        return true;
    }
}

public class WriteHelloAction : IRuleAction<string>
{
    public async Task<string> Execute()
    {
        return await Task.Run(() => "hello world!");
    }
}


public static class Program
{
    public static async Task Main()
    {
        IBusinessRule rule = null;

        try
        {
            rule = new TestRule1();
            await rule.Execute();

            foreach (string item in rule.Results)
            {
                // Prints "hello world!"
                Console.WriteLine(item);
            }
        }
        catch (Exception ex)
        {
            if (rule != null && rule.State == RuleState.Faulted)
            {
                throw new Exception("Error in rule execution", ex);
            }

            throw;
        }
    }
}
6
lorddev

あなたはおそらく Rules Design Pattern を見ることができます。 Pluralsightにも優れたビデオがあります。 ルールパターン を参照してください(サインインする必要があります)。

12
Dmitry Nogin

私のコメントを拡張するために、あなたのコードに関する私の意見をお伝えします:

ジェネリック:ジェネリックが有用であるケースと、ジェネリックが乱用されて問題が増えるケースの間には細い線があると思います。あなたのものは問題領域にずっと過去です。大きな一般的な定義を見るだけで、私には警告が鳴ります。これは、同じものに対して非ジェネリックインターフェイスとジェネリックインターフェイスの両方を持っているという事実によって補強されています。これは、ビジネスルールの構成に問題をもたらす可能性があります。インターフェイスだけにリファクタリングすることを検討します。

エラー処理:「障害」状態と例外の両方を使用しています。これは奇妙で紛らわしいです。どちらかを使用してください。私はRuleFaultedExceptionに行き、フォールト状態を設定する代わりにそれを投げます。これにより、ビジネスルールと呼び出しコードの両方が単純になります。

前提条件とアクションの分離:私にとって、アクションとそのアクションを実行できる場合の前提条件はまとまりのある部分です。彼らは一緒に、切り離せないはずです。一部のビジネスルールに同じアクションと異なる前提条件がある場合、そのルールは複数のルールである必要があります。あなたの場合のように、両方を分割すると、SRPが破損します(SRPが両方の方法で機能するとほとんどの人が信じているのとは対照的に、それは非凝集動作とグループ凝集動作を分離します)。

変更可能なコレクションの公開:ビジネスルールクラスは、変更可能なICollectionを公開します。つまり、具象ルールが作成された後、コレクションを追加できます。特定のビジネスルールでは望ましくない場合があります。あなたの場合、配列をプロパティに割り当てると、実行時例外が発生することは事実です。インターフェースが型を介してそれで可能なことを正しく公開していないことは依然として真実です。それほど問題なくIEnumerableに置き換えることができます。

複数のアクションと結果:複数の結果を持つ複数のアクションを含むビジネスルールをどのくらいの頻度で目にしますか?それほど頻繁ではない場合、呼び出しコードは常に複数の結果が得られると想定する必要があるため、複数のアクションと結果があると呼び出しコードが不必要に複雑になります。これにより、コードを通常よりも複雑にすることができます。また、設計によって、複数のアクションを異なる戻り値の型を持つアクションでルール化することが非常に難しくなっています。いつでもobjectを返すことができますが、呼び出し元のコードにさらされる場合、型の消去は決して良いことではありません。

13
Euphoric

ほとんどの場合、ルールPreconditionsは検証です。ルールアクションと実行フローの外で検証を維持するアプローチを使用すると、次の結果が得られます。

  1. 前提条件を再利用できます。 -ほとんどの場合、それは役に立たない柔軟性です。さらに、通常、ルールは1つの前提条件と1つのルールアクションのみで構成されます。
  2. 前提条件とルールアクションの間でデータを共有する問題が発生します。通常、それらは実行の一般的なコンテキストを必要とします。たとえば、年齢を確認してから、データベースに年齢を書き込みます。前提条件とアクションコンストラクターに年齢を渡すのは大変です(10個のパラメーターがある場合はどうでしょう)。

私が達成しようとすること:

  1. 開発者にとってはシンプルにしてください。理解しやすく、実装も簡単です。
  2. テスト可能にしてください。
  3. ビジネスロジックを細かくし、コントローラーの外に移動します。

[〜#〜] cqrs [〜#〜] の簡単な実装を作成しようと思います。 Qはリポジトリパターンの実装で、Cは次のような簡単なものです。

public interface ICommand 
{
    Task ExecuteAsync();
}

必要に応じて、以下を実装します。

public interface IResultable<TResult>
{
    TResult Result { get; set; }
}

すべてのビジネスロジックと検証はExecuteAsync内にあります。一部が大きくなった場合は、別のクラスに移動できます。

Web APIは通常、NotFoundまたはBadRequest HTTP結果にラップするために、いくつかのタイプの例外をキャッチする必要があります。この場合、コア例外を導入し、それらを例外処理フィルターでキャッチする必要があります。他のすべての例外は、InternalServerError HTTPコードでラップする必要があります。

2
Andrei