web-dev-qa-db-fra.com

Modèle de conception des règles métier?

Je travaille sur une interface pour implémenter des règles métier afin d'améliorer SOLID-ity; donc je peux déplacer beaucoup de logique hors des contrôleurs d'API Web et dans une bibliothèque d'entreprise. Le problème commun étant qu'une action doit se produire si une ou plusieurs conditions sont remplies, et certaines de ces conditions sont susceptibles d'être requises dans tout le système avec différentes actions comme résultat final. J'ai fait quelques recherches et trouvé le code ci-dessous. Est-ce conforme à un modèle de conception existant? J'ai regardé dans la liste du GoF et n'y ai trouvé aucune correspondance.

/// <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

Vous pourriez probablement jeter un oeil à Pattern Design Pattern . Il y a aussi une bonne vidéo sur Pluralsight, voir Pattern Pattern (vous devrez vous connecter).

12
Dmitry Nogin

Pour développer mon commentaire je vais vous donner mon avis sur votre code:

Génériques: Je crois qu'il y a une mince ligne entre les cas où les génériques sont utiles et les cas où ils sont maltraités et ne font qu'engendrer plus de problèmes. Le vôtre est bien passé dans la zone des problèmes. Le simple fait de regarder la grande définition générique sonne une alarme pour moi. Ceci est augmenté du fait que vous avez à la fois des interfaces non génériques et génériques pour la même chose. Cela pourrait entraîner des problèmes de composition des règles métier. Je chercherais à le refactoriser en interfaces uniquement.

Gestion des erreurs: vous utilisez à la fois l'état "en panne" et les exceptions. C'est bizarre et déroutant. Utilisez l'un ou l'autre. J'irais pour RuleFaultedException et jetterais cela au lieu de définir un état défectueux. Cela simplifie à la fois la règle métier et le code appelant.

Séparation de la condition préalable et des actions: Pour moi, une action et une condition préalable si cette action peut être exécutée sont des parties cohérentes. Ils doivent être ensemble et inséparables. Si une règle métier a la même action et des conditions préalables différentes, cette règle doit être constituée de plusieurs règles. La rupture des deux, comme dans votre cas, rompt la SRP (contrairement à ce que la plupart des gens pensent que la SRP fonctionne dans les deux sens, elle sépare le comportement non cohésif et regroupe les comportements cohésifs).

Exposition de collections modifiables: votre classe de règles métier expose un ICollection, qui est modifiable. Cela signifie qu'après la création d'une règle concrète, la collection peut être ajoutée. Ce n'est peut-être pas ce qui est souhaitable avec des règles commerciales spécifiques. S'il est vrai que dans votre cas, si vous affectez un tableau à la propriété, cela entraînera une exception d'exécution. Il est toujours vrai que l'interface n'expose pas correctement ce qui est possible avec elle via des types. Vous pouvez le remplacer par IEnumerable sans trop de problèmes.

Actions et résultats multiples: À quelle fréquence voyez-vous une règle métier avec plusieurs actions qui ont plusieurs résultats? Si ce n'est pas souvent, le fait d'avoir plusieurs actions et résultats complique inutilement le code appelant, car le code appelant doit toujours supposer que vous obtenez plusieurs résultats. Cela rend le code plus compliqué qu'il ne devrait habituellement l'être. Votre conception rend également très difficile la mise en règle de plusieurs actions avec des actions ayant différents types de retour. Vous pouvez toujours retourner object, mais l'effacement de type n'est jamais une bonne chose lorsqu'il est exposé au code appelant.

13
Euphoric

Pour l'essentiel, la règle Preconditions est la validation. Avec l'approche de garder la validation en dehors de l'action de la règle et du flux d'exécution, vous obtiendrez:

  1. Vous pourrez réutiliser les conditions préalables. - Dans la plupart des cas, c'est une flexibilité inutile. De plus, vous n'aurez généralement des règles qu'avec une seule condition préalable et une seule action de règle.
  2. Il y aura un problème de partage des données entre les conditions préalables et les actions de règle. Habituellement, ils ont besoin d'un contexte d'exécution commun, comme vous validez disons l'âge, puis vous écrivez l'âge dans la base de données. Passer l'âge aux constructeurs de précondition et d'action serait trop de travail (et s'il y a 10 paramètres).

Ce que j'essaierais de réaliser:

  1. Restez simple pour les développeurs: facile à comprendre/facile à mettre en œuvre.
  2. Gardez-le testable.
  3. Granulez la logique métier et sortez-la des contrôleurs.

J'essaierais de créer une implémentation simple de CQRS . Où Q est l'implémentation du modèle de référentiel et C est quelque chose de trivial comme:

public interface ICommand 
{
    Task ExecuteAsync();
}

Et si besoin il implémente:

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

Toute la logique métier et la validation se trouvent dans ExecuteAsync. Certaines choses peuvent être déplacées dans des classes distinctes si elles deviennent trop importantes.

L'API Web doit généralement intercepter quelques types d'exceptions pour l'encapsuler dans les résultats HTTP NotFound ou BadRequest. Dans ce cas, vous devez introduire les exceptions Core et les intercepter dans le filtre de gestion des exceptions. Toutes les autres exceptions doivent être encapsulées dans le code HTTP InternalServerError.

2
Andrei