Описание против интерпретации: одна идея от монад до event sourcing
В прошлом уроке мы свели архитектуру к трём вопросам: как режу, как называю, как соединяю. И отдельно разобрали hexagonal: домен в центре, порты на границе, адаптеры снаружи. Этот урок про то, что вся машинерия, которую ты встретишь дальше (монады, алгебраические эффекты, Effect-TS, functional DDD, event sourcing, CQRS), это один и тот же архитектурный приём, применённый на разных уровнях абстракции.
Приём звучит так:
Сначала строим значение, которое описывает „что надо сделать“. Потом отдельно решаем „как это исполнить“.
Описание не делает ничего само по себе. Это инертные данные: дерево, список, структура. Исполнение живёт в отдельном месте и называется по-разному: интерпретатор, хендлер, runtime, адаптер, проекция. Один и тот же приём, разные имена в разных книгах.
Когда ты это увидишь один раз, ты перестанешь учить пять отдельных тем и начнёшь узнавать одну тему в пяти костюмах.
Минимальный пример без всякого FP
Возьмём задачу из прошлого урока: OrderService принимает заказ, списывает деньги, шлёт письмо. Наивно мы пишем так:
function placeOrder(order) {
chargeCard(order.card, order.total); // сразу лезет в Stripe
sendEmail(order.email, 'Заказ принят'); // сразу лезет в Resend
}
Эта функция намертво сшита с исполнением. Её нельзя протестировать без сети, нельзя посмотреть, что она собиралась сделать, нельзя поменять порядок или добавить шаг между списанием и письмом без правки тела.
Теперь то же самое, но функция возвращает описание, а не исполняет:
function placeOrder(order) {
return [
{ type: 'ChargeCard', card: order.card, amount: order.total },
{ type: 'SendEmail', to: order.email, subject: 'Заказ принят' },
];
}
placeOrder стала чистой функцией. Она ничего не списывает и не отправляет. Она возвращает список намерений. Дальше отдельный интерпретатор решает, что с ними делать:
function runReal(steps) {
for (const step of steps) {
if (step.type === 'ChargeCard') chargeCard(step.card, step.amount);
if (step.type === 'SendEmail') sendEmail(step.to, step.subject);
}
}
function runDry(steps) {
for (const step of steps) console.log('would do:', step.type);
}
Один и тот же placeOrder, два интерпретатора. runReal идёт в прод, runDry печатает план в тесте или в dev. Появился ещё и третий бесплатный бонус: список шагов можно проверить, не исполняя. Это и есть весь приём в миниатюре.
описание (данные) интерпретатор (исполнение)
+-----------------------+ +------------------------+
| ChargeCard $120 | ---> | runReal: Stripe |
| SendEmail to=... | | runDry: console.log |
+-----------------------+ | runTest: asserts |
чистое +------------------------+
(что надо сделать) грязное (как)
Запомни эту картинку. Всё остальное в уроке это она же, только описание становится богаче, а интерпретатор умнее.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.