name: title count: false background-image: url(title.png) ??? Привіт! Я Ігор і сьогодні я вам розкажу про те, як ми використовуємо Rules Engine в Kasta. --- # Хто, хто я, хто - 10+ років веб розробки - Python, потім Clojure - Лід сайту kasta.ua - 4300+ коммітів, окрім керування командою - Затягнув рулс енжин в Касту рік тому ??? Але спочатку трошки про мене Я працюю лідом команди сайту в компанії kasta.ua. Крім того, що керую командою, ще й пишу чимало коду. Загалом маю більше десяти років досвіду веб розробки. П'ять з яких моєю основною мовою був пайтон, а зараз це кложа, чому я ніяк не нарадуюсь :) --- # Що за Kasta? - Найбільший маркетплейс України в категорії fashion - 1-2k RPS - 10-20k замовлень в день ??? Що за Каста, спитаєте ви? Це такий сайт ) Найбільший маркетплейс України в категорії фешн. Якесь навантаження ми маємо. Дві тисячі RPS стабільно в звичайний день, 20 тисяч замолень на день. Не амазон, але і не блог твого сусіда. На блек фрайдей це все можна помножити там, в декілька разів. --- # Проблема - Ecommerce любить розпродажі - Розпродаж потребує різного роду знижки - Безкоштовна доставка на замовлення Adidas від 1000 грн - Кешбек 10% на всі вишиванки при оплаті карткою - Мінус 50 грн на замовлення з промокодом XXX в мобільному додатку - Знижки потребують ручної роботи ??? Яка в нас була проблема? Як і будь який єкомерс ми любимо розпродажі, а розпродаж, в свою чергу, потребує різного роду знижки. Ну наприклад: безкоштовна доставка на замовлення адідас від тисячі гривень, або кешбек в 10% на всі вишиванки при оплаті каркою або ж якась знижка по промокоду на замовлення в мобільному додатку. Але все це потребує ручної роботи. --- # Що ми робили до? - Отримуєш список вимог від маркетинга ??? Як це виглядало? Ну, отримуєш вимоги від маркетингу: період розпродажу, які саме мають бути знижки, промокоди, бонуси і так далі. І на які саме товари. Потім заводиш фіча свіч на потрібний період. Далі ідеш в код чекауту і намагаєшся знайти вдале місце, куди б додати потрібний код. Ну типу де вже є всі потрібні дані або де їх можна зручно отримати. Додаєш if-чик з умовами ну і релізиш? Після того як пройде код рев'ю, звісно. А потім приходить маркетинг і каже: ми тут передомовились, давайте от цю знижку тільки для замовлень із застосунку, ага? А отут дати поміняємо, а отут ще шось. Ну і шо робити? Ідеш перероблюєш і знову релізиш. І знову код рев'ю. Ну коротше, цілий процес. І таких ітерацій може бути декілька, тому що чим раніше ти підготуєшся, тим більше буде часу на те шоб шось змінилося :) Неприємно, короче. Але все ще гірше, тому що є підказки -- - Заводиш фіча перемикач зав'язаний на час розпродажу -- - Шукаєш в чекауті місце де було б зручно це додати -- - Додаєш if'чик з умовами -- - Релізиш -- - Вимоги змінюються -- - Перероблюєш (і, можливо) переміщуєш код -- - Все ще гірше, тому що підказки --- # Підказки - Показати користувачу, що буде промо при виконанні умов в корзині/товарі - "Додай товарів adidas ще на 30грн і отримай безкоштовну доставку!" - Код в іншому місці - Форма даних теж відрізняється - Інші стратегії кешування ??? Що я маю на увазі? Просто застосувати знижку в чекауті — замало. Треба ще якось сповістити користувача, що вона є. Або що вона може бути. Ну, наприклад, показати в корзині повідомлення типу: додай товарів adidas ще на 30 гривень і отримай безкоштовну доставку. Або на сторінці товару показати що на нього вже діє безкоштовна доставка, або підвищений кешбек. Саме зараз. Типу, звичайно нема, а зараз діє, не втрать свій шанс. Чому це все ускладнює? Ну, тому що цей код в іншому модулі. І працює трошки з іншими даними. А ще політики кешування дуже відрізняються. Якщо на чекауті у тебе фактично немає кеша (ну тому що на чекауті ти хочеш найактуальнішу інформацію про стоки, ціни, знижки і так далі), то в товарі, наприклад, все максимально закешоване, тому що це найнавантаженіша сторінка. А значить треба думати ще й про те як воно буде кешуватись і інвалідуватись, що взагалі-то не тривіально. І це кожного разу! --- class: center, middle #
БІЛЬ
??? Короче, БІЛЬ... і так жити не можна. --- # Що хочеться? - Ну, не робити цього :) - Не думати куди додавати умови, просто додати - Не придумувати кожен раз як різні умови взаємодіють між собою - Без релізу - В ідеалі адмінка, щоб маркетинг міг сам налаштувати - І щоб не тормозило! ??? А як хочеться? Ну, хочеться взагалі цього не робити! ) Не шукати, куди додавати умови, а просто шоб очевидно було куди піти і додати. Не паритись кожного разу як різні умови взаємодіють між собою. Ну і шоб без релізу працювало. Взагалі в ідеалі, хочеться адмінку, щоб маркетинг сам налаштував а ми про це і не знали. Ага, і шоб не тормозило! Перформанс — це взагалі одна із причин чому до цього ми це робили руками. --- # Що таке Rules Engine - Вхідні факти => Правила => Вихідні факти - Тобто, інша модель виконання - Тобто, інша ментальна модель - Спосіб писати if'чики краще - Просто бібліотека ??? Тож, що таке рул енжин? По-перше, це інший підхід для написання if'чиків. Коли ми думаємо про наш код, як набір незалежних правил які трансформуть вхідні факти у вихідні факти. По-друге, це просто бібліотека. В нашому випадку, оскільки ми пишемо на кложі, це бібліотека яка називається clara rules. Але аналоги існують в багатьох мовах. В світі джави це drools, наприклад. Якщо спробувати провести аналогію, то про рул енжин можна думати як про таку собі дивну базу даних в пам'яті, в якій сидять запити замість даних (правила, в термінології рулів). І при додаванні даних в цю базу відповідні цим даним запити виконуються і продукують нові дані. В термінології рул енжинів — це факти. Які потім можна окремо закверити, щоб дізнатися що відбулось. --- # Що таке Rules Engine .center[] ??? Якщо схематично це зобразити, то буде якось так. Є рул сесія в пам'яті аплікейшена де сидять заздалегідь підготовлені правила і в яку можна додати вхідні факти. Вони ніякої структури не мають, це просто купка фактів, які про один одного нічого не знають. Так само як і правила. Це просто набір фактів і правил якийсь, які висять десь в повітрі --- # Що таке Rules Engine .center[] ??? Потім цій сесії можна сказати: виконайся, що запустить виконання всіх правил на наявних в сесії фактах Що, в свою чергу, може зумовити появу нових фактів в сесії. --- # Що таке Rules Engine .center[] ??? Якщо дивитись на схему, то тут правило один взагалі не виконалось, тому що не було потрібних йому фактів. Правило два на основі фактів один і три продукувало факти чотири і п'ять. А правило три, на основі єдиного факту три додало в сесію факт 6. Факт 2 нікому не цікавий, тому залишився як є. Ну окей, це все абстрактно якось, як це в коді виглядає? --- # Як це виглядає (факти)? ``` (def product-fact {:fact/type :fact/product :price 666 :brand "Puma" :kind "Shorts" :color "Black" :size "L" :supplier 123}) (def payment-fact {:fact/type :selected-payment :value :card}) ``` ??? Отак. Кожен факт це мапка (або словник, дікшенарі) з якимись даними. В якій є спеціальний ключ :fact/type, яким розрізняються види цих фактів, і далі довільний набір якихось атрибутів, які вам треба для написання правил. Оці штуки, що починаються з двокрапки, це ківорди в кложі. Можна про них думати як про спосіб писати ключі для мапок. Це приблизно те ж саме що символи в Ruby, наприклад. Якщо їх на строки замінити просто, то вийде взагалі майже JSON, думаю всім зрозуміло. Якщо б це була якась ООП мова, то кожен тип факту був би об'єктом якогось класу, а дані би зберігались в його полях. Це було б теж саме приблизно що тут написано. Але оскільки у нас кложа, то у нас це просто голі дані. Просто мапка. Тут ми маємо два факти, перший про товар бренду пума за 666 грн. Другий, про вибраний користувачем спосіб оплати — оплата карткою. --- # Як це виглядає (правила)? ``` (defrule rule-example [:fact/product (= "Puma" (:brand this)) (= "Shorts" (:kind this)) (= ?price (:price this))] [:fact/selected-payment (= :card (:value this))] => (println "got puma shorts" ?price)) ``` ??? Правила оперують над фактами і мають дві частини, які розділяються стрілочкою в синтаксі клари --- # Як це виглядає (правила)? ``` (defrule rule-example * [:fact/product (= "Puma" (:brand this)) * (= "Shorts" (:kind this)) * (= ?price (:price this))] * [:fact/selected-payment (= :card (:value this))] => (println "got puma shorts" ?price)) ``` ??? Ліва частина. Або верхня. Ну, вона зверху пишеться, але зручно про неї думати як про ліву. Тому що вивід зліва направо. Ну типу якщо-то, if-then. Це умови, при яких це правило виконується. Кожна умова записується в квадратних дужках і (якщо не специфіковано інше) об'єднується через AND з іншими. OR і NOT теж можна дописати якщо треба. --- # Як це виглядає (правила)? ``` (defrule rule-example [:fact/product (= "Puma" (:brand this)) (= "Shorts" (:kind this)) (= ?price (:price this))] [:fact/selected-payment (= :card (:value this))] => * (println "got puma shorts" ?price)) ``` ??? Права частина — це довільний код, який треба виконати, коли правило спрацювало. Типу then частина if'у --- # Як це виглядає (правила)? ``` (defrule rule-example * [:fact/product (= "Puma" (:brand this)) * (= "Shorts" (:kind this)) * (= ?price (:price this))] [:fact/selected-payment (= :card (:value this))] => (println "got puma shorts" ?price)) ``` ??? Розберемо детальніше окрему умову --- # Як це виглядає (правила)? ``` (defrule rule-example [`:fact/product` (= "Puma" (:brand this)) (= "Shorts" (:kind this)) (= ?price (:price this))] [:fact/selected-payment (= :card (:value this))] => (println "got puma shorts" ?price)) ``` ??? Першим елементом завжди є тип факту, який потрібен цій умові, далі йдуть додаткові фільтри по цьому факту. Якщо такого факту в сесії не буде, правило не виконається. Якщо буде, то виконається для кожного факту, який підходить під фільтр. --- # Як це виглядає (правила)? ``` (defrule rule-example [:fact/product (= "Puma" `(:brand this)`) (= "Shorts" (:kind this)) (= ?price (:price this))] [:fact/selected-payment (= :card (:value this))] => (println "got puma shorts" ?price)) ``` ??? У фільтрах, конкретний факт, який зараз заматчений, доступний через змінну this. Тобто тут двокрапка brand від this — це спосіб дістати з факту значення атрибуту бренд у цього товару. Ну, це просто кложурний синтаксис такий. Можна про це думати як про this крапка brand в ООП мові. Тобто просто значення поля. --- # Як це виглядає (правила)? ``` (defrule rule-example [:fact/product `(= "Puma" (:brand this))` `(= "Shorts" (:kind this))` (= ?price (:price this))] [:fact/selected-payment (= :card (:value this))] => (println "got puma shorts" ?price)) ``` ??? Ну і далі ми його порівнюємо з тим що нам треба. В цьому прикладі нам треба шорти бренду пума. До речі, знак дорівнює тут, це не якийсь спеціальний синтаксис, це звичайне кложурівське порівняння, на його місці може бути довільна функція. Тобто, правила не обмежені тільки якимись своїми операторами, там може бути ваш будь який код. --- # Як це виглядає (правила)? ``` (defrule rule-example [:fact/product (= "Puma" (:brand this)) (= "Shorts" (:kind this)) (= `?price` (:price this))] [:fact/selected-payment (= :card (:value this))] => (println "got puma shorts" `?price`)) ``` ??? Третій фільтр більш цікавий. Тут у нас вводиться змінна (все що починається зі знаку питання — це змінна). Тут значення атрибуту прайс ми запам'ятовуємо в змінній знак питання прайс. Потім це значення можна використовувати в правій частині правила. Або в інших умовах лівої частини. А ще змінні — це спосіб зробити джоін між кількома фактами різних типів. Якщо змінна, наприклад ?price, зустрічається декілька разів в різних умовах, то у ВСІХ умовах вона повинна мати одне й те саме значення, щоб правило спрацювало. Це така фішка з мов логічного програмування типу пролог або даталог, називається уніфікація. Я тут заглиблюватись не буду, але якщо ви не бачили мов логічного програмування, то це цікава тема на подивитись. І якщо в цілому говорити про це правило, то воно воно спрацює, якщо в сесії є факт про товар бренду пума з видом шорти і факт що вибраний спосіб оплати це оплата карткою. --- # Як це виглядає (агрегація)? ``` (defn product-total [p] (* (:price p) (:quantity p))) (defrule rule-agg-example [?total <- (acc/sum product-total) :from [:fact/product (puma? this) (shorts? this)]] [:test (>= ?total 500)] => (println "puma shorts total price" ?total)) ``` ??? Фільтри по окремим фактам це окей, але нам же хочеться працювати з замовленнями, а замовлення це набір товарів, а не один товар. Тому нам потрібен якийсь спосіб оперувати колекціями фактів, тобто агрегаціями. --- # Як це виглядає (агрегація)? ``` (defn product-total [p] (* (:price p) (:quantity p))) (defrule rule-agg-example [?total <- (acc/sum product-total) `:from [:fact/product (puma? this) (shorts? this)]`] [:test (>= ?total 500)] => (println "puma shorts total price" ?total)) ``` ??? Щоб перетворити звичайний фільтр в агрегацію, ми пишемо перед ним ключове слово :from. Після нього йде такий самий фільтр по факту, який ми бачили на минулому слайді. Ну, трошки умови переписані для компактності, але по суті те ж саме. --- # Як це виглядає (агрегація)? ``` (defn product-total [p] (* (:price p) (:quantity p))) (defrule rule-agg-example [`?total <- (acc/sum product-total)` :from [:fact/product (puma? this) (shorts? this)]] [:test (>= ?total 500)] => (println "puma shorts total price" ?total)) ``` ??? Перед :from вказується агрегуюча функція, яка приймає колекцію фактів, які підходять під умову і щось з ними робить. В цьому прикладі — рахує суму цін всі товарів. acc/sum це стандартна функція агрегації, їх є якась кількість: count, max, sum, average, all, distinct. Але можна і писати свої, якщо треба. Результат агрегації тут присвоюється змінній ?total оцей запис зі стрілочкою зправа наліво це робить. --- # Як це виглядає (агрегація)? ``` (defn product-total [p] (* (:price p) (:quantity p))) (defrule rule-agg-example [?total <- (acc/sum product-total) :from [:fact/product (puma? this) (shorts? this)]] `[:test (>= ?total 500)]` => (println "puma shorts total price" ?total)) ``` ??? Далі ми маємо довільний тест. Тобто це не фільтр по якомусь конкретному правилу, це просто спосіб зробити якусь умову, яка може використовувати змінні, отримані від інших фільтрів, щоб зробити обмеження по ним. В даному випадку наше правило спрацює, якщо в сесії є шортів пуми на сумму більше 500 гривень. --- # Як це виглядає насправді? ```clojure (defrule puma+visa-cashback [:fact/now (rule-active? this)] [:fact/selected-payment (card? this)] [:fact/selected-delivery (novaposhta? this)] [?total <- (acc/sum product-total) :from [:fact/product (puma? this) (shorts? this)]] [:test (>= ?total 500)] => (clara/insert! {:fact/type :fact/cashback :percent 13})) ``` ??? Більш реалістичний приклад буде мати, звісно, більше умов. Наприклад тут показано правило, яке дає кешбек в 13%... Основне тут, це те що в правій частині у наc з'являється clara/insert!, це спосіб створити новий факт в поточній сесії, коли правило спрацювало. Тож замість виконання якогось довільного коду з сайд ефектом (як прінти на попередніх слайдах), ми просто створюємо новий факт, який можемо потім закверити, в потрібний нам момент і якось його обробити. Це важливо, тому що ми не контролюємо ні порядок, ні кількість виконнання правил кларою і тому писати сайд ефекти в правій частині — страшнувато. Ще з цікавого тут це :fact/now. Перед кожним запуском сесії ми додаємо в неї цей факт, який просто містить в собі поточний час. Це треба для того щоб правило могло перевірити чи настав (або закінчився) його час виконуватись. У нас базова сесія в пам'яті оновлюєтося або на релізі, або коли щось змінилось в адмінці, тому в ній можуть бути як актуальні на цей час правила, так і правила які будуть актуальні в майбутньому або вже які вже скінчилися --- # Що це дає? - Правила незалежні одне від одного - Кожне правило працює з потрібними йому фактами напряму - Перформанс ??? Правила незалежні одне від одного, тобто просто додаєш десь в коді і прив'язуєш до сесії, не треба думати куди. Те що і хочеться Кожне правило працює з потрібними йому фактами напряму. Це значить, що не треба прокидувати вхідні параметри в каскад функцій, воно само розбереться які дані йому треба. Головне звалити їх всі у сесію. Алгоритми рул енжинів дозволяють різні оптимізації, зокрема спільні частини різних правил виконаються тільки раз. --- # Ідея - Робимо рул сесію з підготовленими правилами для всіх знижок ??? Тож, що ми з цим всім робимо? -- - Кожне замовлення додає в неї свої факти -- - Товари -- - Вибраний спосіб доставки -- - Вибраний спосіб оплати -- - Чи ж у користувача карта лояльності -- - Будь-що потрібне -- - Запускаємо правила -- - Дивимось які факти на виході, приміняємо кожне (`apply-action`) -- - Пишемо це один раз, а потім тільки додаємо правила --- # Приблизно так ``` (defquery actions-q [] [:or [?action <- :fact/free-delivery] [?action <- :fact/cashback] [?action <- :fact/bonus]]) (defn apply-action [order action] (case action :fact/free-delivery (assoc order :delivery-cost 0))) (defn allocate [order] (let [facts `(make-facts order)` session (clara/insert (get-session) facts) actions (-> (clara/fire-rules session) `(clara/query res actions-q)`)] `(reduce action/apply-action order actions)`)) ``` ??? По-перше у нас є запит, яким нам витягне всі результуючі правила після запуску нашої сесії. actions-q. Запити в кларі виглядають так само як ліва частина правил, тобто це таке правило, яке нам треба _явно_ виконати, щоб отримати результат і результатом є не виконання якогось коду, а набір фактів, які під це правила підходять. Тут ми дістаємо всі факти типів кешбек, бонус, безкоштовна доставка, це те що нам наша сесія сгенерувала при запуску. Далі у нас є apply-action. Який приймає на вхід замовлення і один з результуючих фактів і змінює замовлення відповідним чином. Ну наприклад для безкоштовної доставки оновлює в замовлення вартість доставки в нуль. Ну і далі allocate, як основне API, яким користується чекаут. Воно приймає замовлення. З нього створює набір фактів і додає їх в сесію. Далі запускає її, виконує запит на результат, наш actions-q і отримує список фактів, які репрезентують наші знижки. І далі по ним ітерується і приміняє кожне через apply-action. Розумію, що кложу важкувато може бути прочитати без звички, але суть наче зрозуміла :) --- # Що з підказками? - Генеруємо часткові правила - Матчать тільки товари, без фільтрів по замовленню - Генерують підказки (просто строка) по заданому шаблону ??? З підказками така штука. Їх треба показати або на сторінці товару, або в корзині. Ні там ні там у нас ще немає ніяких опцій, вибраних користувачем. Ну типу він ще не вибирав ні доставку, ні оплату ні шось ще. Плюс, якщо це сторінка товару, то ми не знаємо чи тільки цей товар буде в замовленні, чи ще якісь інші. Тому що ми робимо, це створюємо крім основної сесії правил, ще і спеціальну сесію для хінтів. В яку пишемо часткові правила. Тут мається на увазі, що вони не мають інших фільтрів, крім фільтра по товару. Вони потрібні щоб зрозуміти, чи взагалі потенційно на цей товар можлива знижка, і якщо можлива, то показати користувачу якесь повідомлення. Це повідомлення це просто строка, яка генерується по заданому шаблону. І яка каже щось на кшталт: на цей товар діє безкоштовна доставка при якихось умовах. --- # Писати правила руками? No way - треба адмінка, але згенерувати таке з пітона складно - тому свій конфіг, з нього генеруємо правила - простіше читати очима - очевидно як згенерувати ??? Це все прикольно, але шо треба писати правила руками? Ну не хочеться. Хочеться адмінку, в якій їх налаштовувати, а вона шоб генерувала ці правила. Але ви бачили, синтаксис такий, що піди спробуй згенеруй. Та ще й з адмінки, яка на пайтоні написана. Тому ідея така, ми робимо свій конфіг, який буде просто згенерувати з адмінки, а потім, вже в кложі, цей конфіг перетворюємо в правила. Ну по суті такий собі міні-компілятор з нашого DSL із адмінки в правила на клара рулс. Як сайд побічний ефект, цей конфіг ще і очима читати простіше, ніж правила. Синтаксис правил потрібний тільки коли працюєш над компілятором. Як це виглядає? --- # Конфіг ```json {"start": "2022-08-24", "finish": "2022-08-25", "product_filters": {"brand": "Puma", "kind": "Shorts"}, "order_filters": [{"type": "selected_delivery", "value": "novaposhta"}, {"type": "selected_payment", "value": "card"}, {"type": "total_value", "value": 500}], "actions": [{"type": "cashback", "value": 13]} ``` ??? Ну типу просто JSON, будь-хто прочитає і навіть напише руками. Як з адмінки генерувати теж зрозуміло. Воно напряму мапиться на інтерфейс. Є три секції: фільтри по товарам, фільтри по замовленню і дії або знижки, які треба --- .center.adminui[] ??? Ті ж три секції бачимо на інтерфейсі адмінки: товари, замовлення, знижка По товарам можна налаштувати будь-який фільтр по будь-яких характеристикам товару (яких сотні). По замовленню теж є там з десяток доступних фільтрів і інколи ми додаємо нові. Ну і декілька типів знижок є --- # Адмінка - Версіонність через append-only таблицю в постгресі - Чітка схема, щоб мінімізувати можливість помилки - Тестові рули ??? Із цікавого про адмінку. Вона зберігає рули в постгресі, прям json-ом як на попередньому слайді, плюс метадані, типу коли це було створено, чи тестове це правило і так далі. На кожне збереження ми робимо новий запис в таблицю, тобто вся історія змін у нас є. Це треба для того щоб можна було при потребі подивитись хто шо і коли змінював. Також, адмінка має чітку схему і валідує конфіг перед збереженням таким чином, щоб неможливо було налаштувати щось, що зламає систему або не буде мати сенсу. Ще ж можливість помічати конфіги тестовими. Тоді вони видимі тільки для юзерів у яких є на це права (типу тестувальники). Ми це використовуємо, щоб, наприклад, потестувати як якийсь новий тип факту, який ми додали в код, поводиться на проді, при цьому не змінивши нічого для звичайних клієнтів. --- # Система в зборі .center.systemdia[] ??? Якось так все це виглядає в зборі. Аплікейшен зберігає в пам'яті дві сесії рулів, одну для застосування в чекауті з повними правилами, іншу для підказок, з частковими правилами. Тут намальована одна апка, але насправді в проді їх декілька, кожен зі своєю копією правил. Правила зберігаються в постгресі. На старті апки вичитують всі правила які акуальні на зараз, або можуть бути актуально в майбутньому, компілюють їх в рули і підписуються на топік в кафці, в якому чекають на нотифікацію, якщо в адмінці щось змінили, щоб перечитати і перекомпілювати сесію. Відповідно, в адмінці, коли щось змінюється, то в кафку публікується повідомлення типу "щось змінилось", на яке апка реагує. --- # Що вийшло? - Самостійно працює - Очевидно як додавати нові факти для нових хитрих знижок - Чекаут не тормозить - Як і бажалось ??? Що ж у нас вийшло? Ну все так як і хотілось! Самостійно працює, нічого робити не треба руками, все налаштовує спеціальна людина в адмінці. Маркетинг дуже щасливий, розробники дуже щасливі, що ще треба. Очевидно як додавати нові типи знижок, фільтрів і так далі. Просто додаєш підтримку нових фактів в компілятор. З моменту релізу ми вже декілька разів це робили і це завжди доволі зрозуміла задача. Чекаут не тормозить, хоча ми і запускаємо правила на кожен запит в чекаут. А це _кожного разу_ коли користувач змінює якусь опцію вибору в чекауті. Ну, тому що якщо користувач змінив вибір, то і доступні йому знижки могли змінитися — значить треба перерахувати. Я тут не приводжу цифр, просто тому що воно настільки мало займає відносно всього іншого що відбувається в чекауті, що ми навіть це не трекаємо :) --- # Що не вийшло? - Проблем не було взагалі? O_o - Хінти не дуже - Складно сформулювати складні умови зрозуміло - Незалежні, тому легко заплутати користувача, коли їх декілька: - "додай товарів ще на 30 грн і отримай шось" - "додай товарів ще на 50 грн і отримай шось" - чому їх дві? незрозуміло якщо не уточнено в тексті що це різні бренди, наприклад ??? Тут хотілось якийсь супер веселий фейл розповісти, але нічого такого не було. Все працює! Ми з самого початку зробили, що кожна функція API системи знижок всередині обернена в трай-кетч, з репортінгом помилок і все таке, тому що боялися що незнайома система може непередбачувано себе вести. А все ж краще просто не дати знижку, ніж взагалі не оформити замовлення. Але воно жодного разу не спрацювало. Ну тупо нереально взагалі :) Єдине на що можна звернути увагу, це те шо з хінтами вийшло не ідеально. Черер те що вони незалежні інколи можна нагенерувати такого, що буде збивати користувача з пантелику. Плюс шаблон для хінта пишеться людиною в адмінці і від неї дуже залежить наскільки це буде зрозуміло. Наприклад, якщо у тебе в замовленні товари двох різних брендів і на кожен є якась своя знижка від конкретної суми, то ти можеш побачити дві підказки: додай товарів ще на 30 грн і отримай шось + додай товарів ще на 50 грн і отримай шось. І типу не зрозуміло чому на 30 чи на 50 додавати, а це просто товари різних брендів, про що в підказку забули вписати. З цим можна жити, але акуратно, таке коротше. --- # Чому вийшло? - Вузький юзкейс, замість намагання вирішити все - Обмеження на сайд ефекти (правила генерують нові факти, а не виконують код) - Без каскадних правил - Якщо не так, то дуже швидко може стати плутаниною ??? Чому ж вийшло? Тому що ми взяли дуже вузький і зрозумілий нам юзкейс, а не почали робити рулами все що приходить в голову. А воно інколи дуже хочеться, наприклад розрахунки вартості доставки ними зробити, чи ще шось. Явно зробили обмеження на сайд ефекти. Тобто у нас правила можуть тульки генерувати нові факти, а не виконувати якийсь код. Також вдалося побудувати правила так, щоб не було каскаднних правил. Це коли одне правило продукує якийсь новий факт, а інше — від нього залежить. Таке можна робити, але це може ускладнювати розуміння того шо відбувається і, відповідно, дебаг. --- # Посилання - Clara Rules https://github.com/cerner/clara-rules - Clarifying Rules Engines with Clara Rules https://youtu.be/Q_k5MkZmd-o ??? У мене все. Тут кілька посилань. Перше - це документація бібліотеки, якою ми користуємось, а друге - доповідь про неї, де є більше деталей про те як писати факти, як працює Rete алгоритм, який використовує клара рулс і таке інше. Дякую за увагу!