Schema di progettazione delle regole aziendali?

4

Sto lavorando su un'interfaccia per l'implementazione delle regole di business al fine di migliorare SOLID-ity; quindi posso spostare molta logica dai controller API Web e in una libreria aziendale. Il problema comune è che un'azione dovrebbe verificarsi se una o più condizioni sono soddisfatte e alcune di queste condizioni sono probabilmente richieste in tutto il sistema con diverse azioni come risultato finale. Ho fatto qualche ricerca e ho trovato il codice qui sotto. Questo è conforme a un modello di design esistente? Ho cercato nell'elenco dei GoF e non ho trovato nessuna corrispondenza lì.

/// <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;
        }
    }
}
    
posta lorddev 22.06.2016 - 22:44
fonte

3 risposte

9

Probabilmente potresti dare un'occhiata a Pattern design regole . C'è un buon video anche in Pluralsight, vedi Pattern delle regole (ti servirà per accedere).

    
risposta data 23.06.2016 - 05:03
fonte
10

Per espandere il mio commento ti darò la mia opinione sul tuo codice:

Generici: credo che ci sia una linea sottile tra i casi in cui i farmaci generici sono utili e i casi in cui sono abusati e portano solo più problemi. La tua è passata nella zona dei problemi. Solo guardando la grande definizione generica suona un allarme per me. Ciò è aumentato dal fatto che si hanno sia interfacce generiche che non generiche per la stessa cosa. Questo potrebbe portare i tuoi problemi con la composizione delle regole aziendali. Vorrei esaminare il refactoring in sole interfacce.

Gestione degli errori: si stanno utilizzando sia lo stato "errato" sia le eccezioni. Questo è strano e confuso. O usi l'uno o l'altro. Vorrei andare per RuleFaultedException e lanciarlo invece di impostare uno stato di errore. Ciò semplifica sia la regola aziendale che il codice chiamante.

Separazione delle precondizioni e delle azioni: per me, un'azione e una precondizione se quell'azione può essere eseguita sono parti coesive. Dovrebbero essere insieme e inseparabili. Se alcune regole aziendali hanno la stessa azione e precondizioni diverse, allora quella regola dovrebbe essere più regole. Rompendo entrambi, come nel tuo caso, rompe SRP (contrariamente a quanto la maggior parte della gente crede che SRP funzioni in entrambi i modi, separa il comportamento non coeso e raggruppa il comportamento coesivo).

Esposizione di raccolte modificabili: la classe della regola aziendale espone un ICollection , che è modificabile. Ciò significa che dopo aver creato una regola concreta, la raccolta può essere aggiunta. Potrebbe non essere ciò che è desiderabile con regole aziendali specifiche. Se è vero che nel tuo caso, se assegni un array alla proprietà, ciò provocherà un'eccezione di runtime. È ancora vero che l'interfaccia non espone correttamente ciò che è possibile con i tipi. Puoi sostituirlo con IEnumerable senza molti problemi.

Più azioni e risultati: con quale frequenza vedi la regola aziendale con più azioni con più risultati? Se non così spesso, avere più azioni e risultati complica inutilmente il codice chiamante, perché il codice chiamante deve sempre presupporre che tu ottenga più risultati. Questo rende il codice più complicato che di solito deve essere. Inoltre, il tuo design rende davvero difficile mettere più azioni in regola con azioni con tipi di ritorno diversi. Puoi sempre restituire object , ma digitare la cancellazione non è mai una buona cosa se esposto al codice chiamante.

    
risposta data 23.06.2016 - 07:43
fonte
1

Per la maggior parte la regola Preconditions è la convalida. Con l'approccio di mantenere la convalida al di fuori dell'azione della regola e del flusso di esecuzione otterrai:

  1. Sarai in grado di riutilizzare le precondizioni. - Nella maggior parte dei casi è una flessibilità inutile. Inoltre di solito avrai regole solo con una condizione preliminare e una azione della regola.
  2. Si verificherà un problema di condivisione dei dati tra condizioni preliminari e azioni delle regole. Di solito hanno bisogno di un contesto comune di esecuzione, come tu convalidi diciamo età, poi scrivi età al database. Passare l'età al precondizionatore e ai costruttori di azioni sarebbe troppo lavoro (e se ci fossero 10 parametri).

Quello che cercherò di ottenere:

  1. Semplifica gli sviluppatori: facile da comprendere / facile da implementare.
  2. Mantieni la versione testabile
  3. Granula la logica di business e spostala dai controller.

Vorrei provare a creare un'implementazione semplice di CQRS . Dove Q è l'implementazione del modello di repository e C è qualcosa di banale come:

public interface ICommand 
{
    Task ExecuteAsync();
}

E se necessario implementa:

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

Tutta la logica aziendale e la convalida si trovano in ExecuteAsync . Alcune cose possono essere spostate in classi separate se diventano grandi.

L'API Web di solito ha bisogno di catturare alcuni tipi di eccezioni per includerla nei risultati HTTP di NotFound o BadRequest. In questo caso è necessario introdurre eccezioni di base e catturarle nel filtro di gestione delle eccezioni. Tutte le altre eccezioni devono essere racchiuse nel codice HTTP InternalServerError.

    
risposta data 23.06.2016 - 16:17
fonte

Leggi altre domande sui tag