Рефакторинг платіжного процесу Я. Грошей — пробудження сили

<img src=«habrastorage.org/files/a32/bd2/d70/a32bd2d705ce4e9c914eb060191ae8b4.jpg» alt=«image» alt text"/>
Для будь-якого проекту з довгою історією одного разу наступає момент, коли код починає жити своїм життям — просто не залишається тих, хто добре орієнтується в логіці і зв'язках. Додавання нових функцій часом схоже на постріл навмання: може потрапити в ціль, а може — у глядачів.
І тоді приходить він, рефакторинг платіжного процесу. Але ми вирішили зробити процес ще цікавіше, додавши до рефакторінгу ідеї IDEF-0.
Це все тимчасово, потім поміняємо
Платіжний процес Яндекс.Грошей розвивався з 2002 року, і його фронтенд за ці роки обріс результатами праці багатьох поколінь розробників. Обріс до того, що навіть зміна алгоритму перевірки балансу користувача перед відправкою перекладу перетворювалося в подорож по галявині з капканами, — подорож, непомітне користувачеві, але захоплююче під капотом. У статті торкнемося саме серверної частини фронтенда.
Крім труднощів з підтримкою, було складно вводити в курс справи нових розробників, а це великий мінус для компанії, де інженери регулярно мігрують між проектами. Тому було вирішено провести глибокий рефакторинг коду. З урахуванням обсягів роботи це означало написати процес заново.
Якщо починати з нуля, то робити грунтовно, з використанням визнаних методологій теорії кінцевих автоматів і IDEF-0. Принципи опису бізнес-процесів за цим стандартом знайомі з університетської лави як інженерам, так і управлінцям — в цьому вони повинні були знайти спільну мову. Заодно збудеться блакитна мрія технаря про автоматичному побудові діаграм процесу, які так любить керівництво. Наприклад, така схема відображається на одному з дисплеїв зі статистикою, які удосталь розвішані в офісі Яндекс.Грошей.
Мало просто причесати код — потрібно зробити це з розумом
При перекладі всього старого коду на нові рейки з'явився набір модулів Node.js, в яких описано всі базові методи-процеси. Причому описані не просто набором процедур, а відповідно з ідеями IDEF-0: є функціональні блоки, вхідні і вихідні дані, згідно процесів.
Взагалі, в IDEF-0 описано багато всього, що при розробці можна спростити, тому кальку зі стандарту ми не робили і просто запозичили ідею і всі релевантні принципи.
Функціональні блоки
У IDEF-0 функціональний блок — це просто окрема функція системи, яку графічно зображують у вигляді прямокутника. У платіжному процесі Яндекс.Грошей функціональні блоки містять частинки бізнес-логіки того чи іншого процесу.
<img src=«habrastorage.org/files/56d/cc6/74f/56dcc674faa345dcb0fb23b04c7ff88c.jpg» alt=«image» alt text"/>
У кожної з чотирьох сторін функціонального блоку своя роль:
  1. Верхня сторона відповідає за управління;
  2. Ліва — вхідні дані (для кого операція, скільки перевести тощо);
  3. Права сторона виводить результат;
  4. Нижня — це "Механізм", який позначає використовуються в процесі ресурси.
У платіжному фронтенде Яндекс.Грошей використовуються тільки дві сторони функціонального блоку — вхід і вихід: на вхід передається набір даних для виконання бізнес-логіки, біля виходу система очікує результат виконання цієї логіки.
Ось як це виглядає в коді:
/**
* Функціональний блок для перевірки імені користувача
* @param {Object} $flow службовий об'єкт, примірник поточного процесу, що дозволяє управляти переходами від блоку до блоку
* @param {Object} inputData вхідні дані
* @param {Object} inputData.userName-ім'я користувача
*/
const checkUserName($flow, inputData) {
if (inputData.userName) {
// Переходимо до наступного функціональному блоку
const outputData = {
userName: inputData.userName
isUserNameValid: true
};
$flow.transition('doSomethingElse', outputData);
return;
}
$flow.transition('checkFailed', inputData);
}

Функція приймає в якості аргументів два параметри:
  1. $flow — службовий об'єкт, примірник поточного процесу;
  2. inputData — об'єкт з вхідними даними для функціонального блоку. Відмінність функціонального блоку від звичайної функції полягає в способі передачі управління зовнішнім кодом. Функціональний блок для цього використовує окремий метод transition.
При розробці функціональних блоків важливо пам'ятати про принципі єдиної відповідальності, інакше не буде належної гнучкості при додаванні нової бізнес-логіки.
Інтерфейсні дуги
Інтерфейсна дуга — просто стрілка функціонального блоку, яка очікувано позначає передачу даних або вплив на функціональний блок.
В новому платіжному процесі роль інтерфейсної дуги виконує функція Transition об'єкта $flow, який є екземпляром відповідального за надання API процесу.
Декомпозиція
Добре всім відомий принцип розбиття великого і складного багато простих і зрозумілих частин. У коді це означає спрощення та уніфікацію функцій.
У IDEF-0 декомпозиція виглядає наступним чином:
<img src=«habrastorage.org/files/fec/e1d/5eb/fece1d5eb18d4d949972c2fe7b3f3159.jpg» alt=«image» alt text"/>
Декомпозиція застосовувалася в платіжному процесі повсюдно, але розглянемо на прикладі процесу перевірки властивостей.
<img src=«habrastorage.org/files/889/ee9/cc7/889ee9cc7ea5424183ea5eeb0d42b744.jpg» alt=«image» alt text"/>
Перевірка властивостей користувача складається з 5 функціональних блоків і двох виходів з процесу (зазначено синім), які можна декомпозировать. Наприклад, перевірка номери телефону не відноситься тільки до користувача і може стати в нагоді в інших процесах. Якщо виділити цю дію в окремий процес, то код стане простіше і зрозуміліше:
<img src=«habrastorage.org/files/5fa/eaa/0be/5faeaa0bec9540b19cf65962617b066c.jpg» alt=«image» alt text"/>
Після декомпозиції перевірки властивостей користувача частина функціональних блоків переміщається в новий процес, який перевіряє номер телефону. За допомогою BitBucket різниця видна більш наочно — за перевірку телефону користувача відповідають три функціональних блоки:
  1. prepareToCheckPhone —підготовка даних;
  2. requestBackendForCheckPhone — запит в бекенд;
  3. checkUserPhone — аналіз результатів.
До перенесення зовні всі ці блоки перевантажували логіку перевірки властивостей користувача, а тепер процес став значно простіше і зрозуміліше навіть дуже молодому розробнику.
Для допитливих залишу вихідний код під спойлером, щоб ви могли самостійно оцінити перевантаженість логіки.
// check-phone.js
module.exports = new ProcessFlow({
initialStage: 'prepareInputData',
finalStages: [
'phoneValid',
'phoneInvalid'
],

stages: {
/**
* Функціональний блок для підготовки даних до перевірки телефону
* @param {Object} $flow службовий об'єкт, примірник поточного процесу
* @param {Object} inputData вхідні дані
*/
prepareInputData($flow, inputData) {
/**
* Формат даних необхідний модулю провеки номера телефону, може відрізнятися від формату,
* яким оперує кінцевий процес, з цього дані потрібно підготувати.
* Так само зав'язуватися на структуру даних модуля перевірки телефону в кінцевому процесі не варто,
* модуль може помінятися, що може призвести до серйозних змін всього процесу
*/
$flow.transition('checkPhone', {
phone: inputData
});
},

/**
* Функціональний блок перевірки номера телефону
* @param {Object} $flow службовий об'єкт, примірник поточного процесу
* @param {Object} inputData вхідні дані
*/
checkPhone($flow, inputData){
const someBackend = require('some-backend-module');
someBackend.checkPhone(inputData.phone)
.then((result) => {
$flow.transition('processCheckResult', result);
})
.catch((err) => {
$flow.transition('phoneInvalid', {
err: err
});
});
},

/**
* Функціональний блок аналізу результатів перевірки
* @param {Object} $flow службовий об'єкт, примірник поточного процесу
* @param {Object} inputData вхідні дані
*/
processCheckResult($flow, inputData) {
if (inputData.isPhoneValid) {
$flow.transition('phoneValid');
return;
}
$flow.transition('phoneInvalid');
}
}
});

// check-user.js
const checkPhoneProcess = require('./check-phone');

module.exports = new ProcessFlow({
// Вказуємо, який функціональний блок відповідає за вхід в процес
initialStage: 'checkUserName',
// Описуємо виходи з процесу
finalStages: [
'userCheckedSuccessful',
'userCheckFailed'
],
stages: {
/**
* Функціональний блок для перевірки імені користувача
* @param {Object} $flow службовий об'єкт, примірник поточного процесу
* @param {Object} inputData вхідні дані
*/
checkUserName($flow, inputData) {
if (inputData.userName) {
$flow.transition('checkUserBalance', inputData);
return;
}
$flow.transition('userCheckFailed', {
reason: 'invalid-user-name'
});
},

/**
* Функціональний блок для перевірки балансу користувача
* @param {Object} $flow службовий об'єкт, примірник поточного процесу
* @param {Object} inputData вхідні дані
*/
checkUserBalance($flow, inputData) {
if (inputData.balance > 0) {
$flow.transition('checkUserPhone', inputData);
return;
}
$flow.transition('userCheckFailed', {
reason: 'invalid-user-balance'
});
},

/**
* Функціональний блок перевірки номера телефону
* @param {Object} $flow службовий об'єкт, примірник поточного процесу
* @param {Object} inputData вхідні дані
*/
checkUserPhone($flow, inputData) {
const phone = inputData.operatorCode + inputData.number;
checkPhoneProcess.start(phone, {
// описуємо поведінку в точках виходу процесу перевірки телефону
phoneValid() {
$flow.transition('userCheckedSuccessful');
},
phoneInvalid() {
$flow.transition('userCheckFailed', {
reason: 'invalid-user-phone'
});
}
});
}
}
});

Кожен процес платежу Яндекс.Грошей є екземпляром класу ProcessFlow, який надає API керування процесом. У нього є метод start, який викликає функціональний блок, описаний initialStage. В якості аргументів метод start приймає вхідні дані і обробники виходів процесу.
Принципи обмеження складності
Процеси зазвичай містять у собі складну бізнес-логіку, тому в коді доводиться обмежувати їх складність у відповідності з рекомендаціями IDEF-0:
  • Не більше 6 функціональних блоків на кожному рівні. Це обмеження підштовхує розробника до використання ієрархії при описі складної логіки;
  • Нижня межа в 3 блоки гарантує, що створення процесу виправдано;
  • Кількість виходять з одного блоку інтерфейсних дуг обмежено.
На вже знайомій ілюстрації процесу "було" видно 7 функціональних блоків, що збільшує спокуса написати все плоско, не заморочуючись з ієрархією.
<img src=«habrastorage.org/files/889/ee9/cc7/889ee9cc7ea5424183ea5eeb0d42b744.jpg» alt=«image» alt text"/>
У наступному розділі покажу, як виглядає доопрацьований процес після спрощення логіки.
Пасхалка: автоматична побудова схем
У великих компаніях бізнес-процеси часом застарівають швидше, ніж їх встигають намалювати аналітики. На жаль, ми в цьому плані не виняток, тому довелося навчитися малювати швидше.
Завдяки IDEF-0 і суворим правилам опису процесів в коді, ми можемо з допомогою статичного аналізу коду побудувати діаграму зв'язків як функціональних блоків, так і процесів між собою. Наприклад, підійде продукт Esprima. В результаті аналізу коду цим інструментом формується об'єкт з усіма функціональними блоками і переходами, а візуалізація відбувається у браузері за допомогою бібліотеки GoJS:
<img src=«habrastorage.org/files/e8a/c8d/82c/e8ac8d82c65d444cb50761e4d1aa8121.jpg» alt=«image» alt text"/>
На схемі зображені процеси check-user і check-phone з зазначенням залежності. Якщо їх розгорнути, вийде наступне:
<img src=«habrastorage.org/files/bd3/58e/705/bd358e7053bb4206aba67aa812e056de.jpg» alt=«image» alt text"/>
На схемі відмінно видно початкові функціональні блоки, кольором позначені виходи процесу. Наприклад, з цієї схеми очевидно, що результат userCheckFailed може бути отриманий не тільки на етапі перевірки номера телефону, але і на момент перевірки імені. Раніше це було до смішного не очевидно.
Так чи варта шкурка вичинки
Результатом рефакторінгу платіжного процесу стала ціла платформа для опису процесів підготовки даних. Основний плюс від витраченого на рефакторинг часу — це правильний хід думок розробників, які тепер дотримуються жорстких правил при формуванні логіки нових процесів. Отже, в майбутньому рефакторінгу буде менше.
Крім того, в суть процесу тепер може швидко вникнути будь-який новачок. Це економить масу часу на брифінгах і дозволяє впроваджувати нові фішки без побоювань, що все розвалиться.
Є і побічний ефект — бізнес-аналітикам більше не доводиться малювати статичні діаграми, тому споживання кави і чаю з какао різко зросла.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.