如何定义和执行约束层次实体的复杂规则

How to define & enforce complex rules constraining hierarchical entities

如果我有一个 Policy 并且这个 Policy 应该由 Sections(固定数字)组成。

我的部分是 4 个预定义部分:

每个 Section 都有固定的属性,不同于其他部分的属性。 如果我能打个比方:

例如:

Note: based on the domain expert explanation: He want a Save action with each section (as a draft) so that he can update the section unless the Policy not submitted yet, and a Submit action for the whole Policy so that after the Policy submission No one can update or delete this Policy or its sections. (any required update = Define new Policy)


现在我想设计PolicySectionits content。但我卡住了。

首先我想我可以将 Policy 设计为实体(聚合根)并创建 four 类,每个 Section 一个,并从 Section base class(Id,name),而 Policy 包含 Section.

的列表

其次,我将我的思路概括为以下方式的部分内容:

我将创建:

然后我将创建参考 Table SectionRules:

EX:

rule-key         |default-value|operators|section-type|value-type|rule-type

NumOfhoursInMonth| 5:00        |  =      |  2         | String      |Assignment 
AvailableExcuses |2:00,2:30    |  IN     |  2         | List<String>|Relational 

备注:

当用户启动时 Policy 我将遍历 ref table 以在表单中列出规则,以便他可以更改 default values 并将它们保存在 基于其类型的部分如下:

  Id   |Name       |Rule                   |section-type
  1    |Excuses    |NumOfhoursInMonth <= 6 |   2

我现在面临两个问题。

  1. 如果其中一些规则依赖于每个规则,如何关联不同的规则 其他?例如 NumOfExcuses'hoursInMonth Should be less than or equal 6:00 根据第一条规则,但是如何防止用户 在设置第二条规则时违反了这条规则 如果他设置了 AvailableExcuses IN(5:00,7:00)!现在我应该阻止用户 添加一个大于 6 的数字,因为第一条规则限制了 第二个 ?第二条规则与第一条规则不一致,因为列表包含 (07:00) 而第一条规则声明 totalExcuseshoursInMonth <= 06:00 hours
  2. 如何使规则更具表现力以允许条件规则和其他 规则?

我的方向正确吗?我可以得到一些建议吗?

您似乎需要一个对象模型,大概是用于自定义 CMS 的,它将用于呈现表单:

  • 政策就是形式
    • 提交后,表单被锁定
  • 部分是字段集
    • 可独立保存
  • 部分属性是字段
    • 可以用初始值填充字段
    • 字段受其他地方/动态定义的验证规则的约束

一些注意事项:

  • 应在 SectionAttributes 上捕获默认值
  • SectionAttributes 有 ValidationRules

从你的问题来看,至少有两个角色:

  • 可以锁定政策的人,管理员
  • 无法锁定策略的用户

设计注意事项

  • 节可以递归吗?
  • 谁是与系统交互的参与者、管理员、用户等?
  • 每个实体的 public 操作是什么?
  • 策略锁定后是否可以更新 SectionAttributeValidationRules?当新的/更新的规则使现有的 SectionAttributes 无效时会发生什么?
  • 部分可以跨策略重复使用吗?
  • 策略是否受访问控制?

我的建议

  • 坚持良好的软件原则
    • 开闭原则
    • SOLID、DRY、Demeter 法则等
  • 不用担心出错
  • 重构模式
  • 利用测试驱动设计(红色、绿色、重构)

这是一个良好的开端,真正尝试提前 100% 是浪费时间;希望这可以帮助您摆脱困境。

我不完全确定什么样的设计最合适,你肯定要经历多次模型迭代直到你满意,但我认为问题的核心,我认为是编写规则和发现冲突规则可以使用 Specification Pattern 来解决。规范模式基本上包括首先制定规则 - class 模型的公民,而不是仅通过条件语言结构来表达它们。

有很多方法可以实现该模式,但这里有一个例子:

在我设计的一个系统中1,我设法重用了一组相同的规范来执行命令和查询的授权规则,并执行和描述业务规则。

例如,您可以在规范中添加一个 describe(): string 方法来负责描述它的约束,或者添加一个 toSql(string mainPolicyTableAlias) 方法来将其转换为 SQL.

例如(伪代码)

someSpec = new SomeRule(...).and(new SomeOtherRule(...));
unsatisfiedSpec = someSpec.remainderUnsatisfiedBy(someCandidate);
errorMessage = unsatisfiedSpec.describe();

但是,直接在规范上实施此类操作可能会因各种 application/infrastructure 问题而污染它们。为了避免这种污染,您可以使用 Visitor Pattern,这将允许您在正确的层中对各种操作进行建模。这种方法的缺点是每次添加新类型的具体规范时都必须更改所有访问者。

#1 为了做到这一点,我不得不实现上述论文中描述的其他规范操作,例如remainderUnsatisfiedBy

我已经有一段时间没有用 C# 编程了,但我认为 expression trees 在 C# 中可以非常方便地实现规范并将它们转换为多种表示形式。

validate the correlation between different rules in every section of the policy

我不完全确定您在这里的想法,但是通过在您的规范中添加诸如 conflictsWith(Spec other): bool 之类的操作,您可以实现一种冲突检测算法,该算法会告诉您一个或多个规则是否符合要求冲突中。

例如,在下面的示例中,两条规则会发生冲突,因为它们不可能都为真(伪代码):

rule1 = new AttributeEquals('someAttribute', 'some value');
rule2 = new AttributeEquals('someAttribute', 'some other value');
rule1.conflictsWith(rule2); //true

总而言之,您的整个模型肯定会比这更复杂,您将必须找到描述规则并将它们与正确组件相关联的正确方法。您甚至可能想要 link 一些具有适用性规范的规则,以便它们仅在满足某些特定条件时才适用,并且您可能有许多不同的规范候选类型,例如 PolicySectionSectionAttribute 鉴于某些规则可能需要应用于整个 Policy 而其他类型的规则必须根据特定部分的属性进行解释。

希望我的回答能激发一些想法,让您走上正轨。我还建议您查看现有的验证框架和规则引擎以获得更多想法。另请注意,如果您希望整个规则和 Policy 的状态始终保持一致,那么您很可能会将 Policy 设计为由所有部分和规则组成的大型聚合体。如果由于性能原因或并发冲突(例如,许多用户编辑同一策略的不同部分),这在某种程度上是不可能的或不可取的,那么您可能会被迫分解您的大型聚合并改为使用最终一致性。

你当然也必须考虑当现有状态被新规则无效时需要做什么。也许你会想要同时更改规则和状态,或者你可能会实现状态验证指示器以将当前状态的一部分标记为无效等。

1-Could You explain more about describe(),toSql(string mainPolicyTableAlias),I didn't understand the intent behind these functions.

嗯,describe 会给出规则的描述。如果你需要 i18n 支持 或者更多地控制您可能想要使用访问者的消息,也许您还想要一个功能,您可以在其中使用模板化消息等覆盖自动描述。 toSql 方法是相同的,但生成例如,可以在 WHERE 条件中使用什么。

new Required().describe() //required
new NumericRange(']0-9]').if(NotNullOrEmpty()).describe() //when provided, must be in ]0-9] range

This's a considerable drawback ! Could I ask how to overcome this problem.

直接在对象上支持行为可以很容易地添加新对象,但很难添加新行为,而使用访问者模式可以很容易地添加新行为,但很难添加新类型。那就是众所周知的Expression Problem

如果您能找到一种不太可能针对所有特定类型进行更改的通用抽象表示,则可以缓解该问题。例如,如果您想绘制多种类型的 Polygon,例如 TriangleSquare 等,您最终可以将它们全部表示为一系列有序点。规范当然可以分解为 Expression (explored here),但这并不能神奇地解决所有翻译问题。

这是 JavaScript 和 HTML 中的示例实现。请注意,某些规范的实现非常幼稚,不能很好地使用 undefined/blank/null 值,但你应该明白这一点。

class AttrRule {
  isSatisfiedBy(value) { return true; }
  and(otherRule) { return new AndAttrRule(this, otherRule); }
  or(otherRule) { return new OrAttrRule(this, otherRule); }
  not() { return new NotAttrRule(this); }
  describe() { return ''; }
}

class BinaryCompositeAttrRule extends AttrRule {
  constructor(leftRule, rightRule) {
    super();
    this.leftRule = leftRule;
    this.rightRule = rightRule;
  }
  
  isSatisfiedBy(value) {
    const leftSatisfied = this.leftRule.isSatisfiedBy(value);
    const rightSatisfied = this.rightRule.isSatisfiedBy(value);
    return this._combineSatisfactions(leftSatisfied, rightSatisfied);
  }
  
  describe() {
    const leftDesc = this.leftRule.describe();
    const rightDesc = this.rightRule.describe();
    return `(${leftDesc}) ${this._descCombinationOperator()} (${rightDesc})`;
  }
}

class AndAttrRule extends BinaryCompositeAttrRule {
  _combineSatisfactions(leftSatisfied, rightSatisfied) { return !!(leftSatisfied && rightSatisfied); }
  _descCombinationOperator() { return 'and'; }
}

class OrAttrRule extends BinaryCompositeAttrRule {
  _combineSatisfactions(leftSatisfied, rightSatisfied) { return !!(leftSatisfied || rightSatisfied); }
  _descCombinationOperator() { return 'or'; }
}

class NotAttrRule extends AttrRule {
  constructor(innerRule) {
    super();
    this.innerRule = innerRule;
  }
  isSatisfiedBy(value) {
    return !this.innerRule;
  }
  describe() { return 'not (${this.innerRule.describe()})'}
}

class ValueInAttrRule extends AttrRule {
  constructor(values) {
    super();
    this.values = values;
  }
  
  isSatisfiedBy(value) {
    return ~this.values.indexOf(value);
  }
  
  describe() { return `must be in ${JSON.stringify(this.values)}`; }
}

class CompareAttrRule extends AttrRule {
  constructor(operator, value) {
    super();
    this.value = value;
    this.operator = operator;
  }
  
  isSatisfiedBy(value) {
    //Unsafe implementation
    return eval(`value ${this.operator} this.value`);
  }
  
  describe() { return `must be ${this.operator} ${this.value}`; }
}

const rules = {
  numOfHoursInMonth: new CompareAttrRule('<=', 6),
  excuseType: new ValueInAttrRule(['some_excuse_type', 'some_other_excuse_type']),
  otherForFun: new CompareAttrRule('>=', 0).and(new CompareAttrRule('<=', 5))
};

displayRules();
initFormValidation();

function displayRules() {
  const frag = document.createDocumentFragment();
  Object.keys(rules).forEach(k => {
    const ruleEl = frag.appendChild(document.createElement('li'));
    ruleEl.innerHTML = `${k}: ${rules[k].describe()}`;
  });
  document.getElementById('rules').appendChild(frag);
}

function initFormValidation() {
  const form = document.querySelector('form');
  form.addEventListener('submit', e => {
    e.preventDefault();
  });
  form.addEventListener('input', e => {
    validateInput(e.target);
  });
  Array.from(form.querySelectorAll('input')).forEach(validateInput);
}

function validateInput(input) {
    const rule = rules[input.name];
    const satisfied = rule.isSatisfiedBy(input.value);
    const errorMsg = satisfied? '' : rule.describe();
    input.setCustomValidity(errorMsg);
}
form > label {
  display: block;
  margin-bottom: 5px;
}

input:invalid {
  color: red;
}
<h3>Rules:</h3>
<ul id="rules"></ul>

<form>
  <label>numOfHoursInMonth: <input name="numOfHoursInMonth" type="number" value="0"></label>
  <label>excuseType: <input name="excuseType" type="text" value="some_excuse_type"></label>
  <label>otherForFun: <input name="otherForFun" type="number" value="-1"></label>
</form>