Я не совсем уверен, какой дизайн будет наиболее подходящим, и вам непременно придется пройти несколько итераций модели, пока вы не будете удовлетворены, но я думаю, что суть проблемы, которую я предполагаю, состоит в составлении правил и нахождении противоречивых правил может быть решен с использованием шаблона спецификации . Шаблон спецификации в основном состоит из придания правилам первоклассных граждан модели, а не их выражения только через условные языковые конструкции.
Существует много способов реализации шаблона, но вот пример:
В одной из систем, которые я разработал 1 , мне удалось повторно использовать один и тот же набор спецификаций для обеспечения соблюдения правил авторизации команд и запросов, а также для применения и описания бизнес-правил.
Например, вы можете добавить в свои спецификации метод describe(): string
, который отвечает за описание его ограничений, или метод toSql(string mainPolicyTableAlias)
, который может перевести его на SQL.
например. (Псевдо-код)
someSpec = new SomeRule(...).and(new SomeOtherRule(...));
unsatisfiedSpec = someSpec.remainderUnsatisfiedBy(someCandidate);
errorMessage = unsatisfiedSpec.describe();
Однако выполнение таких операций непосредственно над спецификациями может привести к их загрязнению различными проблемами применения / инфраструктуры. Чтобы избежать такого загрязнения, вы можете использовать Visitor Pattern , который позволит вам смоделировать различные операции в правом слое. Недостаток этого подхода заключается в том, что вам придется менять всех посетителей каждый раз, когда добавляется новый тип конкретной спецификации.
# 1 Для этого мне пришлось реализовать другие операции спецификации, описанные в вышеупомянутой статье, такие как remainderUnsatisfiedBy
и т. Д.
Прошло много времени с тех пор, как я программировал на C #, но я думаю, что деревья выражений в C # могут оказаться очень полезными для реализации спецификаций и преобразования их в несколько представлений.
проверить соответствие между различными правилами в каждом разделе политики
Я не совсем уверен, что вы имели в виду, но, добавив в ваши спецификации операцию, такую как conflictsWith(Spec other): bool
, вы могли бы реализовать алгоритм обнаружения конфликтов, который бы сообщал вам, если одно или несколько правил конфликтуют.
Например, в приведенном ниже примере оба правила будут противоречивыми, поскольку невозможно, чтобы оба они когда-либо были истинными (псевдокод):
rule1 = new AttributeEquals('someAttribute', 'some value');
rule2 = new AttributeEquals('someAttribute', 'some other value');
rule1.conflictsWith(rule2); //true
В заключение, вся ваша модель, безусловно, будет более сложной, чем эта, и вам придется найти правильный способ описания правил и связать их с правильными компонентами. Возможно, вы даже захотите связать некоторые правила со спецификациями применимости, чтобы они применялись только в том случае, если выполняются некоторые конкретные условия, и у вас может быть много различных типов кандидатов в спецификации, таких как Policy
, Section
или SectionAttribute
, учитывая, что некоторые правила могут необходимо применять ко всему Policy
, в то время как правила другого типа должны интерпретироваться с учетом атрибута определенного раздела.
Надеюсь, мой ответ вызовет некоторые идеи, чтобы поставить вас на правильный путь. Я также рекомендовал бы вам взглянуть на существующие рамки валидации и механизмы правил для большего количества идей. Обратите также внимание, что если вы хотите, чтобы все правила и состояние Policy
всегда были согласованными, то вы, скорее всего, будете проектировать Policy
как большой агрегат, состоящий из всех разделов и правил. Если каким-то образом это невозможно или нежелательно из-за причин производительности или конфликтов параллелизма (например, многие пользователи редактируют разные разделы одной и той же политики), то, возможно, вам придется разбить ваш большой агрегат и использовать вместо этого возможную согласованность.
Вам также непременно придется подумать о том, что нужно делать, когда существующее состояние становится недействительным по новым правилам. Возможно, вы захотите принудительно изменить правила и состояние или можете внедрить индикаторы проверки состояния, чтобы пометить части текущего состояния как недействительные и т. Д.
1-Не могли бы вы объяснить подробнее о description (), toSql (строка mainPolicyTableAlias), я не понял цели этих функций.
Ну, describe
даст описание правила. Если вам нужна поддержка i18n
или больше контроля над сообщениями, которые вы можете использовать вместо посетителя, и, возможно, вам также понадобится функция, где вы можете переопределить автоматическое описание шаблонными сообщениями и т. д. Метод toSql
будет таким же, но генерировать то, что может например, использоваться внутри условия WHERE
.
new Required().describe() //required
new NumericRange(']0-9]').if(NotNullOrEmpty()).describe() //when provided, must be in ]0-9] range
Это существенный недостаток! Могу я спросить, как преодолеть эту проблему.
Поддержка поведения непосредственно на объектах облегчает добавление новых объектов, но сложнее добавлять новые поведения при использовании шаблона посетителя, облегчает добавление новых поведений, но сложнее добавлять новые типы. Это хорошо известная проблема выражения 1080 *.
Проблема может быть решена, если вы найдете общее абстрактное представление, которое вряд ли изменится для всех ваших конкретных типов. Например, если вы хотите нарисовать много типов Polygon
, таких как Triangle
, Square
и т. Д., Вы можете в конечном итоге представить их все как ряд упорядоченных точек. Спецификацию, конечно, можно разбить на Выражение (, исследуемое здесь ), но это не поможет волшебным образом решить все проблемы перевода.
Вот пример реализации в JavaScript и HTML. Обратите внимание, что реализация некоторых спецификаций очень наивна и не будет хорошо работать с неопределенными / пустыми / нулевыми значениями, но вы должны понять это.
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>