Anyname Donotcare
Anyname Donotcare

Reputation: 11403

How to define & enforce complex rules constraining hierarchical entities

If I have a Policy and this Policy should consist of Sections(fixed number).

My Sections are 4 predefined sections:

Each Section has fixed attributes differ from the attributes in other sections . If I can illustrate it as an analogy:

For example:

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)


Now I want to design the Policy, Section and its content. but I'm Stuck.

Firstly I thought I could design Policy as Entity (the aggregate root) and create four classes, one for each Section and inherit them all from Section base class(Id,name), and The Policy contains List of Section.


Secondly I direct my thinking to generalize the section content in the following way:

I will create:

Then I will create Reference 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 

Notes :

When The user initiate Policy I will loop through the ref table to list the Rules in a form so that he could change the default values and save them in Section based on its type like this:

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

I face two problems right now.

  1. How to correlate different rules if some of them dependent on each other? Ex NumOfExcuses'hoursInMonth Should be less than or equal 6:00 according to the first rule, but how to prevent the user from violating this rule during setting the second rule If he set the AvailableExcuses IN(5:00,7:00)! Now I should prevent the user from add a number greater than 6 because the first rule restricts the second one ? The second rule is inconsistent with the first rule because the list contains (07:00) and the first rule states that totalExcuseshoursInMonth <= 06:00 hours
  2. How to make the rules more expressive to allow conditional rules and other rules?

Am I in the right direction? Could I get some recommendations in my case?

Upvotes: 3

Views: 708

Answers (2)

plalx
plalx

Reputation: 43728

I'm not entirely sure what design would be most suitable and you will certainly have to go through multiple model iterations until you are satisfied, but I think the core of the problem, which I assume is composing rules and finding conflicting rules could be solved using the Specification Pattern.The Specification Pattern basically consists of making the rules first-class citizens of the model, rather than having them only expressed through conditional language constructs.

There are many ways to implement the pattern, but here's an example:

Specification Pattern

In one of the systems I have designed1, I have manage to reuse set same set of specifications to enforce authorization rules of commands & queries and enforce & describe business rules.

For instance, you could add a describe(): string method on your specifications that is responsible to describe it's constraints or a toSql(string mainPolicyTableAlias) method which can translate it to SQL.

e.g. (pseudo-code)

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

However, implementing such operations directly on the specifications might pollute them with various application/infrastructure concerns. In order to avoid such pollution you may use the Visitor Pattern, which would allow you to model the various operations in the right layer. The drawback of this approach though is that you will have to change all visitors every time a new type of concrete specification is added.

Visitor pattern

#1 In order to do so I had to implement other specification operations described in the above-mentioned paper, such as remainderUnsatisfiedBy, etc.

It's been a while since I've programmed in C#, but I think that expression trees in C# could come very handy to implement specifications and transform them into multiple representations.

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

I'm not entirely sure what you had in mind here, but by adding an operation such as conflictsWith(Spec other): bool on your specifications you could implement a conflict detection algorithm that would tell you if one or more rules are in conflict.

For instance in the sample below both rules would be conflicting because it's impossible for both of them to ever be true (pseudo-code):

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

In conclusion, your entire model will certainly be more complex than this and you will have to find the right way of describing the rules and associating them with the right components. You may even want to link some rules with applicability specifications so that they only apply if some specific conditions are met and you may have many various specification candidate types, such as Policy, Section or SectionAttribute given that some rules may need to apply to the whole Policy while other kind of rules must be interpreted given a specific section's attribute.

Hopefully, my answer will have sparked some ideas to put you on the right track. I would also recommend you to have a look at existing validation frameworks & rule engines for more ideas. Please also note that if you want the entire rules and the state of the Policy to be consistent at all times then you will most likely to design the Policy as a large aggregate formed of all sections & rules. If somehow that is not possible nor desirable because of performance reasons or concurrency conflicts (e.g. many users editing different section of same policy) then perhaps you will be forced to break down your large aggregate and use eventual consistency instead.

You will also certainly have to consider what needs to be done when existing state is invalidated by new rules. Perhaps you will want to force the rules & state to be changed at the same time or you may implement state validation indicators to mark parts of the current state as invalid, etc.

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

Well, describe would give a description of the rule. If you need i18n support or more control over the messages you may want to use a visitor instead and perhaps you'd also want a feature where you may override the automated description with templated messages, etc. The toSql method would be the same, but generate what could be used inside a WHERE condition for instance.

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.

Supporting behaviors directly on objects makes it easy to add new objects, but harder to add new behaviors while using the visitor pattern makes it easy to add new behaviors, but harder to add new types. That's the well-known Expression Problem.

The problem can be alleviated if you can find a common abstract representation that is unlikely to change for all your specific types. For instance, if you want to draw many types of Polygon, such as Triangle, Square, etc. you could ultimately represent all of them as a series of ordered points. A specification can certainly be broken down as an Expression (explored here), but that's not going to magically solve all translation issues.

Here's a sample implementation in JavaScript & HTML. Please note that the implementation of some specifications is very naive and will not play well with undefined/blank/null values, but you should get the idea.

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>

Upvotes: 4

Rafael
Rafael

Reputation: 7746

It seems that you want an object model, presumably for a custom CMS, that will be used to render forms:

  • The Policy is the Form
    • When Submitted, Form is locked
  • Sections are the Fieldsets
    • May be saved independently
  • Section Attributes are the Fields
    • Fields may be populated with initial values
    • Fields are subject to Validation Rules defined elsewhere / dynamically

er diagram

enter image description here

Some things to note:

  • Default values should be captured on SectionAttributes
  • SectionAttributes have ValidationRules

From your question it sounds like there are at least two roles:

  • those who can lock a policy, admins
  • those who cannot lock policies, users

Design Considerations

  • Can Sections be recursive?
  • Who are the actors, admins, users, etc., that are interacting with the system?
  • What are the public operations on each entity?
  • Can SectionAttributeValidationRules be updated after a Policy is locked? What happens when new / updated rules invalidate existing SectionAttributes?
  • Can Sections be reused across Policies?
  • Are Policies access-controlled?

My Advice

  • adhere to good software principles
    • open–closed principle
    • SOLID, DRY, Law of Demeter, and the rest
  • don't worry about making mistakes
  • refactor to patterns
  • leverage test driven design (red, green, refactor)

This is a good start, and really trying to get this 100% upfront is a waste of time; hopefully this helps you get unstuck.

Upvotes: 1

Related Questions