Как определить и применять сложные правила, ограничивающие иерархические объекты - PullRequest
0 голосов
/ 14 января 2019

Если у меня есть Policy и это Policy должно состоять из Sections (фиксированное число).

Мои разделы - это 4 предопределенных раздела:

  • Правила рабочего времени.
  • Оправдания.
  • Сдвиги.
  • Время-листы.

Каждый Section имеет фиксированные атрибуты, отличающиеся от атрибутов в других разделах. Если я могу проиллюстрировать это как аналогию:

  • Политика ---> Человеческое тело.
  • Секции ---> (Руки, Ноги, Голова)
  • Каждый раздел отличается друг от друга, как (голова содержит глаза, уши, и т. д., а не руки содержит две руки)

Например:

  • Правила рабочего времени раздел имеет name,list of work-times.
  • Оправдания раздел имеет numberofhours, reason, excuse_type.

Примечание: на основе объяснения эксперта по домену: он хочет Сохранить действие с каждым разделом (как черновик), чтобы он мог обновить раздел если политика еще не представлена, и действие Submit для всего Политика, так что после отправки политики никто не может обновить или удалить это Политика или ее разделы. (любое необходимое обновление = определение новой политики)


Теперь я хочу разработать Policy, Section и its content. но я застрял.

Сначала я подумал, что смогу спроектировать Policy как Entity (совокупный корень) и создать four классы, по одному для каждого Section, и унаследовать их от Section base class(Id,name), а Policy содержит список Section.


Во-вторых, я стараюсь обобщить содержание раздела следующим образом:

Я создам:

  • Интерфейс ISection: SectionType, SectionRule
  • Каждый раздел будет реализовывать этот интерфейс

Затем я создам Справочную таблицу 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 

Примечания:

  • тип сечения 1 is Excuses
  • Операторы Enum
  • Типы секций - Enum

Когда пользователь инициирует Policy, я переберу таблицу ссылок, чтобы вывести список правил в форме, чтобы он мог изменить default values и сохранить их в Раздел основан на его типе, как это:

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

У меня сейчас две проблемы.

  1. Как соотнести разные правила, если некоторые из них зависят от каждого Другой? Ex NumOfExcuses'hoursInMonth Should be less than or equal 6:00 по первому правилу, но как не дать пользователю нарушая это правило во время установки второго правила, если он установил AvailableExcuses IN(5:00,7:00)! Теперь я должен помешать пользователю добавьте число больше 6, потому что первое правило ограничивает второй ? Второе правило не согласуется с первым правилом, поскольку список содержит (07:00), а первое правило гласит, что totalExcuseshoursInMonth <= 06:00 hours
  2. Как сделать правила более выразительными, чтобы разрешить условные правила и другие правила

Я в правильном направлении? Могу ли я получить некоторые рекомендации в моем случае?

Ответы [ 2 ]

0 голосов
/ 20 января 2019

Я не совсем уверен, какой дизайн будет наиболее подходящим, и вам непременно придется пройти несколько итераций модели, пока вы не будете удовлетворены, но я думаю, что суть проблемы, которую я предполагаю, состоит в составлении правил и нахождении противоречивых правил может быть решен с использованием шаблона спецификации . Шаблон спецификации в основном состоит из придания правилам первоклассных граждан модели, а не их выражения только через условные языковые конструкции.

Существует много способов реализации шаблона, но вот пример:

Specification Pattern

В одной из систем, которые я разработал 1 , мне удалось повторно использовать один и тот же набор спецификаций для обеспечения соблюдения правил авторизации команд и запросов, а также для применения и описания бизнес-правил.

Например, вы можете добавить в свои спецификации метод describe(): string, который отвечает за описание его ограничений, или метод toSql(string mainPolicyTableAlias), который может перевести его на SQL.

например. (Псевдо-код)

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

Однако выполнение таких операций непосредственно над спецификациями может привести к их загрязнению различными проблемами применения / инфраструктуры. Чтобы избежать такого загрязнения, вы можете использовать Visitor Pattern , который позволит вам смоделировать различные операции в правом слое. Недостаток этого подхода заключается в том, что вам придется менять всех посетителей каждый раз, когда добавляется новый тип конкретной спецификации.

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>
0 голосов
/ 18 января 2019

Похоже, вам нужна объектная модель, предположительно для пользовательской CMS, которая будет использоваться для визуализации форм:

  • Политика - это форма
    • При отправке форма блокируется
  • Разделы - это наборы полей
    • Может быть сохранен независимо
  • Атрибуты раздела являются полями
    • Поля могут быть заполнены начальными значениями
    • Поля подчиняются правилам проверки, определенным в другом месте / динамически

er diagram

enter image description here

Несколько замечаний:

  • Значения по умолчанию должны быть зафиксированы в SectionAttributes
  • Атрибуты раздела имеют ValidationRules

От вашего вопроса звучит так, как минимум две роли:

  • те, кто может заблокировать политику, администраторы
  • те, кто не может заблокировать политики, пользователи

Особенности проектирования

  • Могут ли секции быть рекурсивными?
  • Кто такие актеры, администраторы, пользователи и т. Д., Которые взаимодействуют с системой?
  • Что такое публичные операции для каждого объекта?
  • Можно ли обновить SectionAttributeValidationRules после блокировки политики? Что происходит, когда новые / обновленные правила делают недействительными существующие атрибуты SectionAttributes?
  • Можно ли повторно использовать разделы в разных политиках?
  • Контролируются ли политики доступа?

Мой совет

  • придерживаться хороших принципов программного обеспечения
    • принцип «открыто-закрыто»
    • ТВЕРДЫЙ, СУХОЙ, Закон Деметры и остальные
  • не беспокойтесь об ошибках
  • рефакторинг к шаблонам
  • Конструкция с использованием тестового рычага (красный, зеленый, рефакторинг)

Это хорошее начало, и действительно попытка получить 100% авансом - пустая трата времени; надеюсь, это поможет вам отклеиться.

...