Модуль 2.2. Решения и правила: DMN, decision tables
Темы: дерево решений, hit policies, валидация правил. Скидки, лимиты, антифрод. Артефакты: таблицы решений. Практика: формализация скидочной политики.
Как системный аналитик (SA) вы отвечаете за то, чтобы «устные договорённости» превратились в формальные правила, которые:
- понятно читать бизнесу,
- однозначно исполнять разработке/движку правил,
- легко тестировать/версионировать и безопасно выкатывать.
В этом модуле мы разложим DMN (Decision Model & Notation): дерево решений, decision tables, hit policy, FEEL, валидацию (дыр/перекрытий), версионирование и практику на трёх живых доменах: скидки, лимиты, антифрод. На выходе у вас будут шаблоны и готовые фрагменты правил, плюс план практики «Скидочная политика».
Базовые понятия DMN (как сотруднику — к исполнению)
DMN-артефакты:
- Decision — что вычисляем (например, FinalDiscount, RiskAction).
- Input Data — входы (поля профиля клиента, корзина, транзакция).
- BKM (Business Knowledge Model) — переиспользуемые знания/функции (например, «нормализация адреса», «квантование суммы»).
- DRD (Decision Requirements Diagram) — граф зависимостей решений и входов.
FEEL (Friendly Enough Expression Language) — язык выражений DMN. Ключевые элементы:
- Типы: number, string, boolean, date, time, date and time, duration.
- Диапазоны: [1..10], (0..], [..100).
- Операторы: in, сравнения, if-then-else, списки {…}, some/every (кванторы), switch.
Когда DMN уместен: есть табличные правила, их много, они меняются, важна проверяемость и аудит. Когда правил мало и они «одноразовые», можно начать с простого if/else, но закладывайте миграцию в DMN по мере роста.
Паттерны моделирования: дерево vs таблица
|
Ситуация |
Используйте |
Почему |
|---|---|---|
|
Бинарные развилки, 2–4 уровня глубины |
Дерево решений |
Быстро читабельно, мало осей вариативности. |
|
Многоосевые условия (сегмент × канал × сумма × время) |
Decision table |
Таблица компактнее, покрытие видно сразу. |
|
Накопление баллов/рисков |
Scorecard (COLLECT/SUM) |
Прозрачно складывать вклад правил и порог действий. |
|
«Лучшая»/«приоритетная» скидка |
PRIORITY / FIRST |
Детерминирует выбор без ручной сортировки в коде. |
Часто удобна композиция: дерево определяет ветвь (например, тип клиента), а внутри — таблица правил по суммам/купонам.
Decision Table: анатомия, синтаксис, хиты
Структура строки: IF (Inputs match) → THEN (Outputs).
Поля-столбцы: входы (с типами/диапазонами), выход(ы) (значение/приоритет), Rule ID, Комментарий, Источник (ссылка на договор/политику).
Hit policy (как агрегировать совпавшие строки):
|
Код |
Название |
Семантика |
Когда применять |
|---|---|---|---|
|
U |
Unique |
В каждый момент совпадает не больше одной строки. Перекрытия — ошибка. |
Простые, взаимно исключающие правила. |
|
A |
Any |
Может совпасть несколько строк, все выдают одинаковый результат. |
Декларативные, без приоритета. |
|
F |
First |
Берём первую совпавшую строку по порядку таблицы. |
Нужен явный порядок (но помните о прозрачности!). |
|
P |
Priority |
Из совпавших берём наивысший приоритет значения (см. список приоритетов). |
«Лучшая скидка», «важнейший статус». |
|
R |
Rule order |
Возвращаем все совпавшие строки в порядке в таблице. |
Пошаговая интерпретация, редкий случай. |
|
C |
Collect |
Возвращаем все результаты. С аггрегатором: C+SUM, C+MIN, C+MAX, C+COUNT. |
Скоринг/суммирование бонусов, риск-баллы. |
|
O |
Output order |
Все совпавшие, отсортированы по значению выхода. |
Специфические отборы/рекомендации. |
Рекомендация: P (Priority) для выбора одной «лучшeй» скидки; C+SUM для суммирования независимых бонусов; U/Any — там, где пересечений быть не должно (включите проверку консистентности!).
Выражения FEEL в ячейках (часто используемое):
- Числа/диапазоны: >= 1000, [100..200), < 10.
- Списки/множества: in ("GOLD","VIP"), not("B2B").
- Даты/время: date("2025-11-28"), in [date("2025-11-28")..date("2025-11-29")].
- Проверка пустоты: = null, is not null.
- Текст: matches("(?i).*black.*") для купонов.
Качество правил: валидация, покрытие, конфликтность
Проверки, которые должны быть в вашем Definition of Done для любой таблицы:
- Completeness (полнота): нет «дыр» в диапазонах. Пример: 0..100, 100..200 — пересечение на границе, договоритесь [0..100) и [100..200).
- Consistency (согласованность): при U/Any нет перекрытий; при P/F — перекрытия осознанны.
- Subsumption: широкие правила не «маскируют» узкие (проверяйте порядок/приоритет).
- Boundary tests: «−1/край/ +1»: если правило [100..200), протестируйте 99, 100, 199, 200.
- Determinism: однотипный вход всегда даёт один и тот же выход (при одинаковом времени).
- Explainability: правило объяснимо для аудита (комментарий/ссылка на источник).
- Versioning & Effective dates: у правил есть effectiveFrom/To; смена версий не ломает прод.
Набор тестов:
- Happy/edge/negative по каждому диапазону.
- Overlaps & gaps — отдельные тесты, чтобы поймать ошибки моделирования.
- Golden dataset (набор регресса): 30–200 кейсов, которые гоняются при любом изменении.
Управление и версии: как не превратить правила в хаос
- SemVer правил/решений: Decision.FinalDiscount v2.1.0.
- Effective window: поля valid_from, valid_to на строках (особенно для акций).
- Governance: CR-процесс, ревью (бизнес+SA+QA), артефакты в Git (CSV/DMN-XML), рендер в Wiki.
- Тест-пакет обязателен к PR (не пройдёт — не мёржить).
- Observability: логируйте decisionId, вход/выход, ruleIds, причину выбора (без PII!), метрики decision_latency_p95, decision_error_rate.
- Прокатка: canary/FF, fallback (по умолчанию «консервативный» результат).
Интеграция и эксплуатация: от таблицы до сервиса
Архитектура решения:
- Статический сервис правил (DMN-engine) или «встроенный» в приложение.
-
Контракт API (пример):
- POST /decisions/final-discount
- Headers: X-Correlation-Id, X-Decision-Version: 2.1
- Body: профиль клиента, корзина, купоны, канал и дата.
- Response: { finalDiscountPct, appliedRules: [RuleID], reasons: [text], capsApplied: true }
- Наблюдаемость: логи (решение/правила/причины), метрики (latency, hit-rate по правилам), трассировка.
- Производительность: кэширование справочников/приоритетов, холодный старт, ожидания p95 < 50–100 мс.
Практические примеры (полные)
A) Скидки (e-commerce): «лучшая vs суммарная» + потолок
Бизнес-правила (кратко):
- Базовая скидка по сегменту и сумме корзины.
- Купон может заменить базовую, если выгоднее.
- В «Black Friday» действует наибольшая из (купон, базовая, BF-скидка).
- Дополнительный бонус не суммируется с купоном (кроме free shipping).
- Потолок итоговой скидки — 30%.
DRD (словами):
- BaseSegmentDiscount (таблица U) → BestOfBaseOrCoupon (P) → BFOverride (P) → ApplyCap (функция) → FinalDiscount.
Таблица 1. BaseSegmentDiscount (hit policy: U)
|
Rule |
Segment |
CartAmount |
Channel |
BaseDiscount% |
Note |
|---|---|---|---|---|---|
|
R1 |
"NEW" |
[0..100) |
any |
0 |
нет базовой |
|
R2 |
"NEW" |
[100..300) |
any |
5 |
5% новому клиенту |
|
R3 |
"REGULAR" |
[0..200) |
"ONLINE" |
3 |
онлайн лояльность |
|
R4 |
"REGULAR" |
[200..500) |
any |
7 |
7% |
|
R5 |
"GOLD","VIP" |
[0..] |
any |
10 |
минимум 10% |
Таблица 2. BestOfBaseOrCoupon (hit policy: P с приоритетами на выходе)
- Приоритет значений Chosen="COUPON" > "BASE" > "NONE". Выход: Chosen, Discount%.
|
Rule |
BaseDiscount% |
CouponType |
CouponValue |
Result:Chosen |
Result:Discount% |
Комментарий |
|---|---|---|---|---|---|---|
|
R1 |
> 0 |
= "PERCENT" |
any |
max("BASE","COUPON") по % |
max(BaseDiscount%, CouponValue) |
берём большее |
|
R2 |
> 0 |
= "AMOUNT" |
any |
вычислить эквивалент в % |
max(BaseDiscount%, (CouponValue / CartAmount)*100) |
купон сумма |
|
R3 |
= 0 |
any |
any |
"COUPON" |
купон в % |
если есть купон |
|
R4 |
any |
= null |
any |
"BASE" |
BaseDiscount% |
нет купона |
|
R5 |
any |
"FREESHIP" |
— |
"BASE" |
BaseDiscount% |
фришип не влияет на % |
Таблица 3. BFOverride (hit policy: P)
Приоритет на выходе: "BF" > "PREV".
|
Rule |
IsBlackFriday |
PrevDiscount% |
BFDiscount% |
Chosen |
Discount% |
|---|---|---|---|---|---|
|
R1 |
true |
any |
any |
"BF" если BFDiscount% > PrevDiscount% |
max(BF, Prev) |
|
R2 |
false |
any |
— |
"PREV" |
PrevDiscount% |
Каппинг: ApplyCap(final%) = min(final%, 30).
FEEL-проверки «дыр»/границ:
- CartAmount in [0..) покрыто?
- Для BF всегда присутствует BFDiscount%? иначе дефолт 0.
- На границах 100/200/300/500 — тесты: 99,100,199,200,299,300…
Тест-кейсы (выдержка):
- NEW, Cart=150, no coupon, not BF → Base=5%, Final=5%.
- REGULAR-ONLINE, Cart=180, coupon PERCENT=10%, not BF → Base=3%, Coupon=10% → Final=10%.
- VIP, Cart=800, coupon AMOUNT=150 → Base=10%, Coupon≈18.75% → Final=18.75% (cap не сработал).
- REGULAR, Cart=2000, any coupon 40% → cap → Final=30%.
- BF, NEW, Cart=250, coupon 5%, BF=20% → Final=20%.
Риски и меры:
- Перекрытия диапазонов — линтер/review, boundary tests.
- Непрозрачность выбора — логируйте appliedRules, Chosen, capApplied.
- Манипуляции купонами — антифрод на стороне купонов (раздел C).
Лимиты (финансовые): дневные/ежемесячные, KYC-уровни
Бизнес-правила (кратко):
- Лимиты зависят от KYC level (BASIC, ADVANCED, FULL) и канала (WEB, MOBILE, ATM).
- Если заявка превышает любом из лимитов (суточный/месячный), требуется MANUAL_REVIEW или REJECT.
Таблица 4. CheckLimits (hit policy: P; приоритет выхода REJECT > REVIEW > APPROVE)
|
Rule |
KYC |
Channel |
DailyRemain |
MonthlyRemain |
Amount |
Decision |
|---|---|---|---|---|---|---|
|
R1 |
BASIC |
any |
< Amount |
any |
any |
REJECT |
|
R2 |
any |
any |
any |
< Amount |
any |
REJECT |
|
R3 |
BASIC |
any |
>= Amount |
>= Amount |
> 50_000 |
REVIEW |
|
R4 |
ADVANCED |
ATM |
any |
any |
> 200_000 |
REVIEW |
|
R5 |
any |
any |
>= Amount |
>= Amount |
<= Limits[KYC,Channel] |
APPROVE |
Примечание: Limits — справочник (BKM). Тестируйте границы (=, +1, -1).
Антифрод: скоринг и пороги действий
Бизнес-правила (кратко):
- За каждый сигнал начисляется риск-балл.
- Итоговый балл → действие: < 30 → APPROVE, 30..60 → REVIEW, > 60 → DECLINE.
Таблица 5. RiskSignals (hit policy: C+SUM → riskScore)
|
Rule |
Сигнал |
Условие |
Баллы |
Комментарий |
|---|---|---|---|---|
|
R1 |
high_amount |
Amount > 100_000 |
30 |
крупная сумма |
|
R2 |
velocity |
TxnCountLast5m ≥ 5 |
25 |
высокая частота |
|
R3 |
geo_mismatch |
Country != CardCountry |
20 |
гео-рассинхрон |
|
R4 |
device_new |
DeviceAgeDays < 7 |
10 |
новый девайс |
|
R5 |
blacklist |
Card in Blacklist |
100 |
стоп-лист |
Таблица 6. ActionByScore (hit policy: P, приоритет DECLINE > REVIEW > APPROVE)
|
Rule |
riskScore |
Action |
|---|---|---|
|
R1 |
> 60 |
DECLINE |
|
R2 |
[30..60] |
REVIEW |
|
R3 |
< 30 |
APPROVE |
Observability: логируйте riskScore, signalsTriggered и конечное Action.
Риски: дрейф данных → пересмотр весов; конфликт с лимитами → в DRD сделайте Limits → Risk → финальное Decision (приоритет отказов).
Частые риски и как их гасить
-
Границы и перекрытия.
Симптом: 199 и 200 дают неожиданные результаты.
Мера: единый стиль диапазонов ([a..b)) и boundary-tests. -
Зависимость от порядка строк (F) без осознанности.
Мера: используйте P/Any/U там, где порядок не должен влиять; документируйте приоритеты. -
«Маскирование» узких правил широкими.
Мера: валидаторы subsumption, линтеры и визуализация покрытий. -
Даты/таймзоны.
Мера: FEEL-тип date and time + единая TZ; тесты для DST. -
Денежные округления.
Мера: decimal, явные правила округления (до копейки, HALF_UP), фиксация валюты/тарифа. -
Производительность.
Мера: кэш справочников, запрет тяжёлых вычислений в ячейках, p95 SLA и мониторинг. -
Аудит/объяснимость.
Мера: appliedRules, ruleVersion, sourceRef в логах; шаблон объяснений для саппорта.
Вопрос–Ответ
Q: Когда выбрать PRIORITY, а когда FIRST?
A: Если важен ранжированный список значений (скидки по приоритету) — P. Если порядок строк — часть дизайна и контролируется вручную — F (но осознанно, с тестами).
Q: Как представить «суммарную скидку, но не больше 30%»?
A: COLLECT+SUM для независимых бонусов → затем BKM ApplyCap(min(sum, 30)). Или делите на таблицы: «плюсуются» и «взаимоисключающие».
Q: Чем DMN лучше «if/else» в коде?
A: Прозрачность для бизнеса, валидация покрытий/конфликтов, аудит, быстрые изменения без перекомпиляции, единый тест-пакет.
Q: Как тестировать правила?
A: Набор golden cases (CSV), boundary-testing, тесты «дыры/перекрытия», контракт API решения, метаморфные тесты (монотонность: ↑суммы — не ↓скидка без причины).
Q: Как катить изменения?
A: Новая версия решения, canary на малой доле трафика, сравнение решений (A/B), лог appliedRules. При инциденте — быстрый rollback на предыдущую версию.
Артефакты модуля (шаблоны)
Шаблон decision table (Markdown/CSV)
Decision: <Имя> | Version: <X.Y.Z> | Hit policy: <U/A/F/P/C+SUM/...> Inputs: <имя:тип>, ... Outputs: <имя:тип> [+ priority values if P] Effective: <from>..<to> | Owner: <ФИО> | Source: <док/политика> | Rule | In1 | In2 | ... | Out1 | Out2 | Comment | SourceRef | | R1 | ... | ... | ... | ... | ... | ... | POL-2025-04 §3 |
аблон DRD (текстом)
[Input: Customer, Cart, Coupon, Channel, Date] ↓ Decision BaseSegmentDiscount (U) ↓ Decision BestOfBaseOrCoupon (P) ↓ Decision BFOverride (P) ↓ BKM ApplyCap ↓ Decision FinalDiscount
Чек-лист ревью правил
- Явная hit policy и приоритеты (для P).
- Отсутствуют дыры/перекрытия (или задокументированы).
- Тесты: границы, golden dataset, регресс.
- Версии/сроки действия/ссылки на источник.
- Логи «почему так решили» и SLO (latency).
Практика: формализация скидочной политики (2–4 часа)
Задание: превратить описание (ниже) в DRD + 3 decision tables + тесты.
Описание (бизнес):
- Сегменты клиентов: NEW, REGULAR, VIP.
- Базовые скидки: NEW [100..300) → 5%, REGULAR [200..500) → 7%, VIP → 10%.
- Купон: % или сумма, берём выгоднее, но FREESHIP не влияет на %.
- В «Black Friday» берём максимум из базовой/купона/BF-скидки.
- Потолок итоговой скидки — 30%.
Шаги выполнения:
- Нарисовать DRD (см. §7A).
- Создать 3 таблицы: BaseSegmentDiscount (U), BestOfBaseOrCoupon (P), BFOverride (P) и BKM ApplyCap.
- Задать приоритеты для P (например, BF > PREV, COUPON > BASE > NONE).
- Прописать диапазоны сумм без перекрытий ([100..300) и т.п.).
- Собрать 10–20 тестов: граничные значения и «интересные» сочетания (VIP+большая корзина+купон-amount, BF+купон).
-
Сформировать артефакты:
- docs/rules/discounts/base.csv
- docs/rules/discounts/best.csv
- docs/rules/discounts/bf.csv
- docs/rules/discounts/tests.csv (golden)
- страница Wiki «Discount Policy» (рендер + ссылки на CSV/DMN/XML).
- Прогнать тесты; приложить отчёт.
Критерии зачёта:
- Нет дыр/перекрытий, корректные границы.
- Тесты покрывают все диапазоны и особые случаи.
- Логику можно объяснить бизнесу по строкам таблиц.
- Каппинг 30% соблюдён во всех кейсах.
Шпаргалка (распечатайте)
- DMN = решения как данные: читаемо, тестируемо, версионируемо.
- Hit policy задаёт как «сливать» совпавшие строки: P для лучшего, C+SUM для суммирования, U/Any для уникальности.
- Диапазоны по одной конвенции ([a..b)).
- Обязательны: boundary-tests, golden dataset, лог причин (applied rules).
- Версии и effective dates — всегда.
- Сначала структура DRD, потом заполнение таблиц.
- «Если не можете объяснить правило бизнесу — оно слишком хитрое» → разбейте на более простые.



