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:

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.

#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>