使用 DDD 实现用户定义的业务规则

Implementing user-defined business rules with DDD

假设我有一个应用程序,它让用户创建要应用于域实体的业务规则。规则可以是条件和多个操作的组合,如果条件评估为真,则执行相应的操作。此规则由用户以自由格式的文本格式创建,然后转换为规则引擎可以理解和执行的专有格式。

例如对于员工管理系统,如果有业务规则检查员工是否在当前角色工作超过一年并且表现好于预期,则可以晋升到下一个角色,加薪 10%。此业务规则可由用户输入,如下所示。

条件: Employee.CurrentRoleLength > 1 && Employee.ExceededExpectations()
操作:Employee.PromoteToNextRole() | Employee.GiveSalaryIncrement(10)

请注意,多个操作由 | 分隔。此外,为了执行此规则,应用程序使用单独的规则引擎 class 库将此条件和两个操作解析为专有格式,例如 ExecutableScript 也在规则中定义引擎 class 库。

现在为了使用 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 不是这种情况下的正确方法,我走错了方向。

抱歉这么久 post。任何帮助将不胜感激。

谢谢。

How can I parse and execute Condition and Actions without having a dependency on external rule engine. I understand Domain layer should not have any dependency on outer layers and should be confined to it's own.

这部分很简单:依赖倒置。该域定义了一个服务提供者接口,该接口描述了它希望如何与某些外部服务对话。通常,域会将其某些内部状态的副本传递给服务,然后返回一个可以应用于自身的答案。

所以你可能会在你的模型中看到类似的东西

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;
    }
}

技术领域可能受益于 DDD 战术模式,但创建正确抽象的成本通常高于其他领域,因为它通常需要抽象出复杂的数据结构。

开始考虑所需抽象的一个好方法是问问自己,如果要交换底层技术,需要什么抽象。

这里有一个复杂的基于文本的表达式,规则引擎从中创建 ExecutableScript

如果你仔细想想,这里有三个主要元素:

  1. 专有的基于文本的表达式语法。
  2. 专有的ExecutableScript;我假设这是一个带有嵌入式解释器的抽象语法树 (AST)。
  3. 可能是专有的规则评估上下文。

如果你要交换底层技术来执行规则,那么另一个规则引擎的表达式语法可能会不同,它肯定会有一个完全不同的规则解释机制。

此时我们已经确定了必须抽象的内容,但还没有确定什么是正确的抽象。

您可以决定实现您自己的表达式语法、您自己的解析器、您自己的 AST,这将是内存中基于树的表达式表示,最后是您自己的规则评估上下文。然后,这组抽象将被特定的规则引擎使用。例如,您当前的规则引擎必须将 domain.Expression AST 转换为 ExecutableScript.

类似这样的事情(我故意省略了评估上下文,因为您没有提供任何相关信息)。

但是,创建抽象集的成本可能很高,尤其是在您不打算更换规则引擎的情况下。如果您当前规则引擎的语法符合您的需要,那么您可以将其用作基于文本的表达式的抽象。您可以这样做,因为它不需要专有数据结构来表示内存中的文本;这只是一个 String。如果您将来要更换规则引擎,那么您仍然可以使用旧引擎来解析表达式,然后依靠生成的 AST 为另一个规则引擎生成新引擎,或者您可以返回编写自己的抽象.

在这一点上,您可以决定简单地在您的域中保留该表达式 String 并在必须对其进行评估时将其传递给 Executor。如果您担心每次重新生成 ExecutableScript 的性能成本,那么您应该首先确保这确实是一个问题;过早的优化是不可取的。

如果您发现它的开销太大,那么您可以在基础设施执行器中实现 memoizationExecutableScript 可以存储在内存中或持久保存到磁盘。您可能会使用基于字符串的表达式的散列来识别它(当心冲突)、整个字符串、域分配的 ID 或任何其他策略。

最后但并非最不重要的。请记住,如果规则操作未由聚合处理,或者如果规则谓词跨越多个聚合,则用于评估表达式的数据可能已经过时。我不会对此进行扩展,因为我不知道您打算如何生成规则评估上下文和流程操作,但我认为它仍然值得一提,因为不变的执行是每个域的一个重要方面。

如果您确定所有规则最终可能是一致的,或者对陈旧数据做出的决定是可以接受的,那么我也会考虑为此创建一个完全独立的有界上下文,也许称为 "Rule Management & Execution"。

编辑:

这是一个示例,它显示了从应用程序服务的角度来看创建规则的方式,假设表达式在域中存储为 Strings。

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