让我们说如果我有一个应用程序让用户创建要应用于域实体的业务规则。规则可以是条件和多个动作的组合,其中如果条件评估为真,则执行相应的动作。此规则由用户以自由格式文本格式创建,然后转换为规则引擎可以理解和执行的专有格式。
E.g。对于员工管理系统,如果有业务规则来检查员工是否在当前角色中工作超过一年并且表现优于预期,那么可以将其提升为下一个角色,加薪率为10%。用户可以输入此业务规则,如下所示。
条件:Employee.CurrentRoleLength> 1&& Employee.ExceededExpectations()
操作:Employee.PromoteToNextRole() | Employee.GiveSalaryIncrement(10)
请注意,多个操作都使用 | 分隔。此外,为了执行此规则,应用程序使用单独的规则引擎类库来解析此条件,并将这两个操作解析为专有格式,例如, ExecutableScript 也在规则引擎类库中定义。
现在为了使用DDD建模这个要求;我想出了以下Domain对象。
规则(实体)
条件(价值对象)
行动(价值对象)
其中Rule是一个实体,它包含一个条件值对象和一个Action Value Objects列表,如下所示。
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 ??;
}
}
基于以上领域模型,我有以下问题。
如何在不依赖外部规则引擎的情况下解析和执行条件和操作。我理解Domain层不应该对外层有任何依赖性,应该仅限于它自己。
即使我在其域对象之外解析条件和操作,仍然需要在其中存在已解析的ExceutableScript值,这仍然需要依赖于外部规则引擎。
只是DDD不是这种情况的正确方法而且我的方向是错误的。
对不起,很长的帖子。任何帮助将受到高度赞赏。
感谢。
答案 0 :(得分:1)
如何在不依赖外部规则引擎的情况下解析和执行条件和操作。我理解Domain层不应该对外层有任何依赖性,应该仅限于它自己。
这部分很简单:依赖倒置。域定义了一个服务提供者接口,描述了它如何与某些外部服务进行通信。通常,域会将其某些内部状态的副本传递给服务,然后返回一个可以应用于自身的答案。
所以你可能会在你的模型中看到类似的东西
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;
}
}
答案 1 :(得分:1)
技术领域可能会受益于DDD战术模式,但创建正确抽象的成本通常高于其他领域,因为它通常需要抽象出复杂的数据结构。
开始考虑所需抽象的一个好方法是问自己,如果要交换底层技术,需要哪些抽象。
这里有一个复杂的基于文本的表达式,规则引擎从中创建ExecutableScript
。
如果你想一想,这里有三个主要元素:
ExecutableScript
;我将假设这是一个带有嵌入式解释器的抽象语法树(AST)。如果您要交换底层技术来执行规则,那么其他规则引擎的表达式语法可能会有所不同,它肯定会有完全不同的规则解释机制。
此时我们已经确定了什么必须被抽象,但不是什么是正确的抽象。
您可以决定实现自己的表达式语法,自己的解析器,自己的AST,它将是内存中表达式的基于树的表示形式,最后是您自己的规则评估上下文。然后,这组抽象将由特定规则引擎使用。例如,您当前的规则引擎必须将domain.Expression
AST转换为ExecutableScript
。
这样的事情(由于你没有提供有关它的任何信息,故意遗漏了评估背景。)
但是,创建一组抽象可能成本很高,尤其是如果您不希望交换规则引擎。如果当前规则引擎的语法符合您的需要,那么您可以将它用作基于文本的表达式的抽象。您可以这样做,因为它不需要专有的数据结构来表示内存中的文本;它只是一个String
。如果您将来交换规则引擎,那么您仍然可以使用旧引擎来解析表达式,然后依赖生成的AST为其他规则引擎生成新的引擎,或者您可以返回编写自己的抽象
此时,您可以决定只在您的域中保留该表达式String
,并在必须进行评估时将其传递给Executor
。如果您担心每次重新生成ExecutableScript
的性能成本,那么您应首先确保这确实是一个问题;过早优化是不可取的。
如果您发现它的开销太大,那么您可以在基础结构执行程序中实现memoization。 ExecutableScript
可以存储在内存中,也可以保存到磁盘中。您可以使用基于字符串的表达式的哈希值来识别它(注意冲突),整个字符串,域名指定的ID或任何其他策略。
最后但并非最不重要。请记住,如果聚合不处理规则操作,或者规则谓词跨越多个聚合,则用于评估表达式的数据可能已过时。我不是在扩展这个因为我不知道你打算如何生成规则评估上下文和处理操作,但我认为它仍然值得一提,因为不变执行是每个域的一个重要方面。
如果您确定所有规则最终都是一致的,或者对陈旧数据做出的决策是可接受的,那么我还会考虑为此创建一个完全独立的有界上下文,可能称为“规则管理和执行”。
修改强>
这是一个示例,显示如果表达式在域中存储为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.
}
}
}