Чому я так і не використав патерн 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 разів ліпше, ніж Екселька. Але в мене був сумнів, що це буде зрозуміло не-технарям, ну й виглядало воно якось монструозно: одна специфікація займала до
Підхід 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 — одна специфікація стала займати
Єдине, я виніс пару кастомних функцій-хелперів (типу Sum()
у 9 рядку) в окремий бібліотечний Python-скрипт та підключив його окремо, щоб той, хто буде міняти цю специфікацію, не міг зламати бібліотечну функцію.
Коли СТО побачив презентацію, він сказав «I like what I see! Go ahead!».
Ось так ми разом вирішили питання «externalize the logic», і продукт зараз завойовує ринок США — але я так і не використав патерн Interpreter.
40 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів