Реализация пользовательских бизнес-правил с DDD

Допустим, если у меня есть приложение, которое позволяет пользователю создавать бизнес-правила для применения к объекту домена. Правило может быть комбинацией условия и нескольких действий, где, если условие оценивается как истинное, выполняются соответствующие действия. Это правило создается пользователями в текстовом формате произвольной формы, который затем преобразуется в собственный формат, который механизм правил может понять и выполнить.

Например, для системы управления сотрудниками, если существует бизнес-правило, позволяющее проверить, работает ли сотрудник в текущей роли более года и работает ли он лучше, чем ожидалось, его можно повысить до следующей роли с приростом оклада 10%. Это бизнес-правило может быть введено пользователями, как показано ниже.

Условие: Employee.CurrentRoleLength > 1 && Employee.ExceededExpectations()
Действие: Employee.PromoteToNextRole () | Employee.GiveSalaryIncrement (10)

Обратите внимание, что несколько действий разделены с |, Кроме того, чтобы выполнить это правило, приложение использует отдельную библиотеку классов механизма правил для анализа этого условия и обоих действий в собственном формате, скажем, ExecutableScript, также определенный в библиотеке классов механизма правил.

Теперь, чтобы смоделировать это требование, используя DDD; Я придумал следующие объекты домена.

Правило (сущность)
Условие (объект значения)
Действие (объект значения)

где Rule - это сущность, которая содержит объект значения условия и список объектов значения действия, как показано ниже.

public class Rule : Entity
{
    public Condition Condition { get; private set; }
    public IList<Action> Actions { get; private set;}

    public Rule(Condition condition, IList<Action> actions)
    {
        Condition = condition;
        Actions = actions;
    }
}

public sealed class Condition : ValueObject<Condition>
{
    public string ConditionText { get; private set;}
    public ExecutableScript ExecutableCondition{ get; private set;}

    public Condition(string conditionText)
    {
        ConditionText = conditionText;            
    }     

    public Parse()
    {
        ExecutableCondition = // How to parse using external rule engine ??;            
    }

    public Execute()
    {
        // How to execute using external rule engine ??;            
    }
}      

public sealed class Action : ValueObject<Action>
{
    public string ActionText{ get; private set;}
    public ExecutableScript ExecutableAction{ get; private set;}

    public Action(string actionText)
    {
        ActionText = actionText;            
    }

    public Parse()
    {
        ExecutableAction = // How to parse using external rule engine ??;            
    }

    public Execute()
    {
        // How to execute using external rule engine ??;            
    }
}

Основываясь на вышеупомянутой модели предметной области, у меня есть следующие вопросы.

  1. Как я могу проанализировать и выполнить Условие и Действия, не имея зависимости от внешнего механизма правил. Я понимаю, что уровень домена не должен зависеть от внешних уровней и должен быть ограничен его собственным.

  2. Даже если я анализирую Условие и Действия вне их доменных объектов, все равно их проанализированное значение ExceutableScript должно присутствовать в них, что все еще будет нуждаться в зависимости от внешнего механизма правил.

  3. Просто DDD не подходит для этого сценария, и я иду в неверном направлении.

Простите за длинный пост. Любая помощь будет высоко оценен.

Благодарю.

2 ответа

Решение

Технические области могут выиграть от тактических шаблонов DDD, но стоимость создания правильных абстракций обычно выше, чем с другими доменами, потому что это часто требует абстрагирования сложных структур данных.

Хороший способ начать думать о необходимых абстракциях - спросить себя, какие абстракции понадобятся, если вы поменяете местами базовые технологии.

Здесь у вас есть сложное текстовое выражение, из которого ExecutableScript создается движком правил.

Если подумать, то здесь есть три основных элемента:

  1. Синтаксис текстового выражения, который является собственностью.
  2. ExecutableScript который является собственностью; Я предполагаю, что это абстрактное синтаксическое дерево (AST) со встроенным интерпретатором.
  3. Контекст оценки правила, который, вероятно, является частным.

Если бы вам пришлось поменять базовую технологию для выполнения правил, то синтаксис выражений другого механизма правил мог бы быть другим, и у него наверняка был бы совершенно другой механизм интерпретации правил.

На данный момент мы определили, что должно быть абстрагировано, но не то, что должно быть правильными абстракциями.

Вы можете решить реализовать свой собственный синтаксис выражений, свой собственный синтаксический анализатор, свой собственный AST, который будет представлять собой представление дерева в виде выражения в памяти и, наконец, собственный контекст оценки правил. Этот набор абстракций будет затем использоваться конкретными механизмами правил. Например, ваш текущий механизм правил должен будет преобразовать domain.Expression АСТ к ExecutableScript,

Примерно так (я намеренно пропустил контекст оценки, так как вы не предоставили никакой информации о нем).

Однако создание набора абстракций может быть дорогостоящим, особенно если вы не планируете менять свой механизм правил. Если синтаксис вашего текущего механизма правил соответствует вашим потребностям, вы можете использовать его в качестве абстракции для текстовых выражений. Вы можете сделать это, потому что для представления текста в памяти не требуется собственная структура данных; это просто String, Если бы в будущем вам пришлось поменять механизм правил, вы все равно могли бы использовать старый механизм для синтаксического анализа выражения, а затем полагаться на сгенерированный AST для генерации нового для другого механизма правил, или вы могли бы вернуться к написанию собственных абстракций.,

На данный момент, вы можете решить просто держать это выражение String в вашем домене и передать его Executor когда это должно быть оценено. Если вы обеспокоены производительными затратами на воссоздание ExecutableScript затем каждый раз вы должны сначала убедиться, что это действительно проблема; преждевременная оптимизация нежелательна.

Если вы обнаружите, что это слишком много накладных расходов, то вы можете реализовать памятку в исполнителе инфраструктуры. ExecutableScript может быть сохранен в памяти или сохранен на диск. Вы могли бы потенциально использовать хеш строкового выражения для его идентификации (остерегаться коллизий), всю строку, идентификатор, назначенный доменом, или любую другую стратегию.

Последнее, но не менее важное. Помните, что если действия правила не обрабатываются агрегатами или если предикат правила охватывает несколько агрегатов, то данные, использованные для оценки выражения, могли устареть. Я не буду подробно останавливаться на этом, потому что понятия не имею, как вы планируете генерировать контекст оценки правил и действия процесса, но я подумал, что все же стоит упомянуть, потому что принудительное применение инвариантов является важным аспектом каждого домена.

Если вы решите, что все правила могут быть в конечном итоге непротиворечивыми или что решения, принятые по устаревшим данным, являются приемлемыми, тогда я также рассмотрю возможность создания совершенно отдельного ограниченного контекста для этого, возможно, называемого "Управление и выполнение правил".

РЕДАКТИРОВАТЬ:

Вот пример, который показывает, как создание правила может выглядеть с точки зрения службы приложения, учитывая, что выражения хранятся как Stringс в домене.

//Domain
public interface RuleValidator {
    boolean isValid(Rule rule);
}

public class RuleFactory {
    private RuleValidator validator;

    //...

    public Rule create(RuleId id, Condition condition, List<Action> actions) {
        Rule rule = new Rule(id, condition, actions);

        if (!validator.isValid(rule)) {
            throw new InvalidRuleException();
        }

        return rule;
    }
}

//App
public class RuleApplicationService {
    private RuleFactory ruleFactory;
    private RuleRepository ruleRepository;

    //...
    public void createRule(String id, String conditionExpression, List<String> actionExpressions) {
        transaction {
            List<Action> actions = createActionsFromExpressions(actionExpressions);

            Rule rule = ruleFactory.create(new RuleId(id), new Condition(conditionExpression), actions);


            ruleRepository.add(rule); //this may also create and persist an `ExecutableScript` object transparently in the infrastructure, associated with the rule id.
        }
    }
}

Как я могу проанализировать и выполнить Условие и Действия, не имея зависимости от внешнего механизма правил. Я понимаю, что уровень домена не должен зависеть от внешних уровней и должен быть ограничен его собственным.

Эта часть проста: инверсия зависимостей. Домен определяет интерфейс поставщика услуг, который описывает, как он хочет общаться с какой-либо внешней службой. Обычно домен передает копию некоторого своего внутреннего состояния службе и возвращает ответ, который он затем может применить к себе.

Так что вы можете увидеть что-то подобное в вашей модели

Supervisor.reviewSubordinates(EvaluationService es) {
    for ( Employee e : this.subbordinates ) {
        // Note: state is an immutable value type; you can't
        // change the employee entity by mutating the state.
        Employee.State currentState = e.currentState;


        Actions<Employee.State> actions = es.evaluate(currentState);            
        for (Action<Employee.State> a : actions ) {
            currentState = a.apply(currentState);
        }

        // replacing the state of the entity does change the
        // entity, but notice that the model didn't delegate that.
        e.currentState = currentState;
    }
}
Другие вопросы по тегам