Чому я так і не використав патерн Interpreter — історія з проєкту Property Management

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Якось я консультував проєкт в галузі «Property Management» — це коли є житло, в якому за гроші і на якихось умовах живуть мешканці. Суть проєкту була в тому, щоб чесно розподілити рахунки на всіх.

Найскладніше в попередньому абзаці формалізувати одне слово — «чесно», тому що примітивно «порівну на всіх» виходить не завжди правильно: в одного є трикімнатна велика квартира, а в іншого — маленька комірка, тому рахунок за опалення має розподілятись не навпіл, а згідно з житловою площею. Або один мешканець живе цілий повний місяць, а інший — тільки вʼїхав сюди. Десь людина живе одна, і тому продукує мало сміття, а десь проживає велика багатолюдна родина. Таких параметрів було декілька, і їх всі треба врахувати.

Бізнес-команда проєкту дослідила всі кейси цього питання, і з часом формалізувала правила у вигляді декількох стратегій: «порівну на всі квартири», «по метражу», «за кількістю мешканців» і так далі. Таким чином, якого б виду не прийшов рахунок на будинок за місяць (household bill), він завжди розподіляється на мешканців за однією зі стратегій, наприклад:

  • за кабельне ТБ рахунок $250 на весь будинок з 25 квартир — стратегія «порівну на всі квартири», тобто з квартири по $10;
  • за опалення — стратегія «по метражу», наприклад, з великої квартири $15, а з маленької $5;
  • за вивіз сміття — «за кількістю мешканців», тобто беремо всіх мешканців будинку, (нехай буде 54) і рахуємо долю «три жильці вашої квартири від 54 загальних», з вас до оплати 3/54 рахунку.
  • і т.п.

СЕО запросив на проєкт нового американського СТО, дуже класного дядьку, який одразу поставив питання так: нам треба відокремити логіку розрахунків від решти коду — як він казав, «externalize the logic». Це треба було, щоб ми змогли показати і юристам, і користувачам сайту, як виходить так, що вони отримали сумарно до сплати саме $20.39, а не іншу суму.

Короче,

нам треба було мати код, який можна і інтерпретувати, і продемонструвати нетехнічній людині.

Ця стаття — про те, як ми це зробили.

0. Точка відліку

Код, який дістався від попередній команди з сонячного Пакистану, був складний та розгалужений. Але не буду приховувати — на нашій стороні він теж добре підтримувався в тому ж самому стані.

На бекенді ядро розрахунків — це метод на декілька тисяч стрічок. Тут не те що не було Фабрики Стратегій, тут все було побудовано на великих IF-ELSE гілках.


if (allocationType === "BY SQUARE METERS") {
    // 200 стрічок коду
}
else if (allocationType === "BY OCCUPANCY") {
    // 200 стрічок майже однакового коду
}
else if (allocationType === "BY SOMETHING ELSE") {
    // 200 стрічок майже однакового коду
}
else  {
    // 200 стрічок майже однакового коду
}

Плюс, ще була додаткова логіка показу результату на фронт-енді.

Тобто логіка розрахунків була максимально зашита у решту коду. Саме це і треба було «externalize».

Додатково треба зазначити, що проєкт дозрів до того, що його було легше переписати, ніж рефакторити. Проєкт був на Node.js. Наш СТО сказав, що раз буде нова версія, то нехай вона буде на .NET — він знав цю мову. Нових .NET-девелоперів набрали, а мені (архітектору) треба було запропонувати загальне рішення.

В першу чергу, я розібрався з логікою кожної з чотирьох стратегій та описав їх усі компактними блок-схемами, щоб залишити саму мякотку логіки і не губитись в тисячах рядків коду ініціалізації змінних та умовних переходів. На це пішло півтора тижня. Так в нас зʼявилась логіка розрахунків, для якої нам треба було підібрати форму відображення.

Підхід 1. Excel

Кажуть, що робота академіків — ставити правильні питання. Наприклад: «чи можуть люди передавати думки?». Далі доктора наук, керуючись цим питанням, розробляють гіпотези, наприклад: «а що, якщо через людину пропускати струм, в неї зʼявляться здібності до телепатії?». Так доктора наук ставлять дослідницьку задачу кандидатам наук — які вже ставлять експерименти на людях та пишуть стопіцот наукових робіт.

Бути академіком не так легко, як може здатись — до правильного питання ще треба дійти, середовище академіка ефемерне — це ноосфера. Тому було б дивно, якби академік взяв в руки вольтметр та почав би робити роботу замість кандидата наук.

Але то в науці. В айті все інакше — СТО завжди теж хоче щось закодити.

Так, наш СТО, як справжній академік, поставив дуже правильне питання: «чи можна якусь логіку відокремити від решти коду та показувати людям?». Але, як зазвичай і буває, СТО на цьому не зупинився і не віддав питання на розробку варіантів рішення архітекту, а одразу запропонував своє рішення: «а що, якщо для розрахунків використовувати Excel-таблицю?», а потім ще й набросав прототип.

Тобто чому б не загнати дані з коду у спеціальну ексельку — формулки там все порахують, і ми витягнемо готові дані. А ексельку можна показувати живим людям!

Я зроблю тут велику паузу.

Дуже велику паузу.

Щоб ви відчули цей біль.

Перед моїми очима пропливало все, що пішло прахом, — улюблені фреймворки, які мають тисячі різних класних бібліотек. Всі ті патерни проєктування та ООП, і особливо буква «О» в слові SOLID — вона символізує принцип «програмне забезпечення має бути відкритим для розширення, але закритим для модифікацій».

Біль в тому, що мастер-екселька, яка лежить на диску, — це максимально відкрита для модифікацій інформація... Як тільки вони наймуть свіжих менеджерів та скажуть поміняти там одну цифру... І весь ваш high-load помножиться на нуль. А ми навіть не будемо знати, хто це зробив та коли.

Гірше того, воно ще й платне!

Але ми почали пропрацьовувати цю гіпотезу.

СТО розробив мастер-ексельку, в яку треба було вставляти масив даних:

А я почав ганяти тести навантаження та дивитись, як воно себе поведе. Якщо всі розрахунки робити звичайним кодом, то навантаження майже не зростало: що 100 ітерацій, що мільйон — все рахувалось за долі секунд.

А екселька, інтегрована з .NET-кодом, жрала ресурси майже експоненційно: 100 ітерацій — це пара секунд, 10 000 ітерацій — вже треба чекати, а 1 мільйон я так і не дочекався. Нагадую, що воно само собою платне, а ще й витрати на AWS-потужності треба буде враховувати.

Але головна задача була поставлена з самого початку — «externalize the logic», і це було робоче рішення, хоч воно мені і не подобалось. Я жартома почав називати СТО «екселенц».

Я пішов понити про це все другові, який працює біг-босом в IT в Англії. Друг мені сказав: «Ми працюємо в аутсорсі. Ми не кажемо „ні“ — навпаки, ми описуємо ризики. Опиши ризики цього рішення, запропонуй інші — та дай клієнту обрати».

Ми поговорили про це з СТО, і він сказав, що насправді не наполягає саме на рішенні з Екселем. Просто треба знайти інші варіанти.

Підхід 2. JSON-структура процесу розрахунку

Поки я пропрацьовував свої інші гіпотези, СТО запропонував таку ідею: що, якщо ми опишемо логіку обробки у вигляді ієрархічної структури даних? Наприклад, XML або JSON. Ці структури даних можна назвати «специфікації» — їх можна зберігати в репозиторії. І вони навіть можуть мати версійність.


{
    "ForEach":
        [
            {"AllocationFactor":[ {"Operand":"AllocationFactors"}, {"Operation": "ArrayFind", ...} ]},
            {"ProRataFactor":[ {"Operand":"ServiceDays"}, {"Operation": "Divide", ...} ]},
            {"ProratedFactor":[ {"Operand":"OccupancyFactor"}, {"Operation": "Multiply", ...} ]}
        ...
        ]
}

Цей підхід був в 100 разів ліпше, ніж Екселька. Але в мене був сумнів, що це буде зрозуміло не-технарям, ну й виглядало воно якось монструозно: одна специфікація займала до 50-100 стрічок JSON-коду.

Підхід 3. Патерн Interpreter

В мене був свій задум — я хотів справжні формулки.

Я згадав, що завжди хотів використати патерн Інтерпретатор. Хтось вчиться водити автобус, трактор і трамвай, щоб отримати всі галочки на правах водія, — а я завжди хотів хоч раз використати всі патерни з класичної книги банди чотирьох.

Патерн Interpreter — це така простенька штучна мова або діалект, яку можна інтерпретувати вашим кодом. Ось приклад з «Вікіпедії», який реалізує формулу ”u + v - w + z” на РНР:

Це виглядало ідеальним рішенням — тобто, в нас будуть справжні формули, які можна показати людям, і за якими можна рахувати. Ура!

Але недовго музика грала — виявилось, що нам потрібно реалізовувати цикли. Річ у тім, що деякі параметри треба було рахувати на кожного мешканця квартири, що означало цикл по масиву мешканців квартири. Якщо подивитесь на JSON-приклад з попереднього пункту, там у стрічці 9 є ForEach — це так там зроблено цикл, і мені теж довелось би його реалізовувати у патерні Interpreter. А це занадто складно.

Я почав думати в сторону математичних операторів типу ∑ (сума ряду) та ∏ (добуток), але це було занадто: так можна було догратись і до інтегралів, а інтеграл — це стоп-слово у гуманітаріїв.

Але все одно я не хотів так просто відмовлятись від красивих формул.

Підхід 4. Діалект шаблонізатора

І тут я подумав — є ж багато існуючих штучних діалектів, які реалізовані, наприклад, для шаблонізаторів: якщо верстальник/ фронтендник буде писати фронтенд-код на РНР, є можливість додати помилку, а якщо він буде верстати на штучному діалекті, спеціально розробленому саме для цієї задачі, то він зможе зробити те, що треба без змоги «прострелити собі ногу» серверною мовою.

Я уважно подивився на код шаблонізатора:

Це вже ліпше — якщо патерн Interpreter ще треба самому реалізовувати, то тут вже майже все готове. Головне, є цикли. Але є одне незручне «але» — змінні в шаблонізаторі оголошувати дуже незручно, і це нівелює всю красу цього підходу.

Я почав думати далі. Щось мені нагадував цей простий та приємний синтаксис циклу:

for user in users

Треба щось, що легко читається і що можна інтерпретувати...

Підхід 5. Зручна мова

І тут до мене доперло: .NET це не мова, а платформа. Ця платформа підтримує багато різних мов. Треба просто подивитись на них на всіх і знайти саму просту для «звичайних» людей.

Вибач, C# — але ти не підходиш: я подумав, що ми не зможемо пояснити умовному бухгалтеру, що таке char[] та інші типи.

Ок, йдемо далі. Я відкрив список всіх мов, які підтримує .NET. Чомусь очікував, що переможе якась Lua.

Але серце моє зупинилось, коли я згадав про Python — це ж те, що треба! (Тут стало зрозуміло, що мені нагадував той приємний синтаксис циклів в попередньому пункті).

По-перше, немає строгої типізації.

По-друге, цикли виглядають дуже сексі.

По-третє, дужки такі самі, як в JSON.

Четверте, Пайтон підтримується в .NET через лібу IronPython і швидко працює.

Ну, і пʼяте, розрахунки дійсно виглядають як прості формули:

Тоді я взяв нашу логіку і переписав її на Python — одна специфікація стала займати 16-18 стрічок чистих формул.

Єдине, я виніс пару кастомних функцій-хелперів (типу Sum() у 9 рядку) в окремий бібліотечний Python-скрипт та підключив його окремо, щоб той, хто буде міняти цю специфікацію, не міг зламати бібліотечну функцію.

Коли СТО побачив презентацію, він сказав «I like what I see! Go ahead!».

Ось так ми разом вирішили питання «externalize the logic», і продукт зараз завойовує ринок США — але я так і не використав патерн Interpreter.

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Как решалась проблема секурити ?
По сути из-за лени реализовать простейший интерпретатор, выглядит так, что в систему затянули бекдор, на котором теперь можно постить и выполнять любой код на питоне под правами сервера. Не только формулы.

не зовсім. Код на Python не могли додавати люди «зі сторони». Це такий самий source code, як і решта — просто конкретно ці стратегії на Python грали особливу роль.

Та ніяк. Зараз тобі скажуть, що доступ давали тіко бухгалтеру, котрий підписав документ, про те, що він не знає ніякого пайтона, тому не зможе нічого поганого написати.

Хай! вже на дотнет все переписали :D І видалили цей IronPython
що на пітоні, що на дотнеті код з 4 арифметичними операціями виглядає однаково за виключенням кількох ключових слів
Який сенс з дотнету запускати інтерпретатор пітону. Щоб бухгалтер сам формули редагував?

Вибач, C# — але ти не підходиш: я подумав, що ми не зможемо пояснити умовному бухгалтеру, що таке char[] та інші типи.

є таке слово var — почитайте як юзати. Називається виведення типів

PS а це точно ок публікувати скріншоти коду з проекту замовника?

вже на дотнет все переписали :D І видалили цей IronPython

це припущення чи ви працюєте на цьому проекті?

Сам власноручно випиляв геть. Це було рішення неіснуючої проблеми

невдобно получилось, коли повірив аноніму в коментарях

ну як би ви були б на тому проекті, ви б знали, цей код з того проекту чи рандомний скріншот )

СТО звати Грейді
І так, скріншоти коду з проекту allocation engine. Так точно можна робити? НДА підписували?

гм, нащо тоді бути анонімом на ДОУ, щоб так легко розкриватись )

Ми не знайомі ;D не перетиналися на проекті
Читаю доу і тут бац скріни знайомого коду. Думаю WTF?

Володимире, заспокойтесь — ви самі написали, що вже на дотнет все переписали і видалили цей код на IronPython. В чому у вас до мене претензія?

. В чому у вас до мене претензія?

Ну хоч код з проекту замініть на вигаданий псевдокод. Формули досі є, на дотнеті вони на 99% такі самі. Тільки обгорнуті дужками {} та цикл foreach замість for

шо, таки да? код красный! нужна евакуацыя! ))

Щоб бухгалтер сам формули редагував?

Так задача й ставилась — щоб нетехнічна людина могла відрегадувати це, і згодом воно стало новою версію розрахунків.

На практиці нетехнічна людина швидше покладе систему своїми правками, ніж принесе користі. Кодер набагато швидше внесе зміну ніж умовний бухгалтер. Хіба що розробляти середовище аналог 1С.
Як часто треба нові принципи розрахунку оренди додавати? Тай взагалі хіба там так багато можливих варіантів щоб таку гнучкість робити?

я згоден.
Але задача ставилась так:
а) треба показувати юристам код розрахунків (я не знаю, звідки цей вітер дув, може були якісь випадки),
б) якісь менеджери (у майбутньому) мали мати змогу міняти або додавати стратегії.
Треба — зробили.

Ми проговорювали, що це все одно буде через PR — поміняти пару стрічок формул та зробити PR можна навчити будь кого.

1. Для аудиту нічого не заважає просто алгоритм тримати за інтерфейсом який приймає щось на вхід і видає результат. Інкапсуляція та single responsibility. Головне не розмазати код по всьому проекту і все
2. Менеджери і юніт тести напишуть на ці формули?

Це питання до бізнесу.
P.S. Ви коли ставите такі питання, забуваєте, що ТАМ МОГЛА БУТИ ЕКСЕЛЬКА!!1 ))

Розробник теж має вгамовувати ірраціональні бажання бізнесу. Це суто питання технічної доцільності, а не бізнесу. Екселька це звісно жесть

В мене враження, що ви не уважно читали статтю.
На марні уговори СТО пішло реально багато часу — до тих пір, поки мені не розказали як це працює (частина про пораду товарища із Англії, почитайте якщо цікаво).

Просто пояснюється що на формули треба юніт тести написати, пройти QA, зарелізити, а не просто так змінювати формули ключових розрахунків, які пов’язані з розподілом грошей.
Після цього було закрито питання доцільності даної кастомізації

Дуже люблю, коли кажуть «Просто треба було ...» ))))
Вам не приходило в голову, що інші люди теж розумні? Що вони теж могли казати про тестування? Що старе рішення взагалі не мало тестів на ту частину коду? Що з вами можливо так легко погодився СТО саме тому, що інші люди півроку йому про це казали? )))

Мені просто цікаво, що б робили саме ви, якщо вам кажуть «робимо на Ексельці», а на вашу пропозицію використовувати код на .NET однозначно кажуть «ні» — ваші дії? (це тру-сторі ситуація). Як треба було діяти? ))

Давайте будемо чесними, ви реалізували велосипедом доволі дивні забаганки замовника. Це ок — за ваші гроші зробимо як бажаєте. Але ж ви написали статтю на тему «гляньте як я технічно вирішив проблему», замість «гляньте в якій неординарній дичині меня довелося мати досвід».
Тому що технічно ви виконали завдання на трієчку.

PS дотнет код теж можна в рантаймі підгрузити з текстового файлика і запустити. І не треба пітон інтерпретатор тягнути. Звісно для такого тех рішення мають бути дуже вагомі причини

...і вони описані в статті як requirements.

Та нормально ви його так використали.
Там і Contex був, і abstract expression, ще й ціла папочка/неймспейся з AST

На бекенді ядро розрахунків — це метод на декілька тисяч стрічок. Тут не те що не було Фабрики Стратегій, тут все було побудовано на великих IF-ELSE гілках.

Это был AI. Вот так делаешь искуственный интелект, нейросети всякие, а потом приходит очередной архитектор, обзывает тебя пакистанцем и добавляет формулы на python.

Шаблон «интерпретатор» это использование интерпретатора. Поэтому ты его использовал. Ваш к.о.

чекаємо на паттерн проектування «компілятор».

Я даже стесняюсь спросить, но «Логика во внешних скриптовых файлах, которые грузятся и исполняются нашим приложением» — это случайно не тот самый паттерн Интерпретатор, только без прикладного велосипедостроения?

А то получается логика уровня «В ASP.NET Core своих приложениях я не испрользую паттерн Dependency Injection, потому что контейнер написан не мной»

это случайно не тот самый паттерн Интерпретатор

Чи можна змінювати граматику у моєму прикладі? Не можна. Тому й ні.

IronPython опенсорсный, так что там что угодно можно менять.
А еще можно вынести хелпер-функции в отдельную библиотеку или скрипт, и создать фактически свой DSL, который по крайней мере будет бросать понятные пользователю ошибки если, например, кто-то умудрится задать tenant.DaysOccupied = 0

IronPython опенсорсный, так что там что угодно можно менять.

Міняти граматику — це міняти синтаксис Python, тоді це був би патернг Interpreter.

который по крайней мере будет бросать понятные пользователю ошибки

Можна, просто такої задачи не було. Пайтон-скрипт користувач напряму не запусає — це робить біллінг-скрипт на .NET.

Підписатись на коментарі