Раздел 16 · Архитектура и решения

Описание против интерпретации: одна идея от монад до event sourcing

senior~60 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Описание против интерпретации: одна идея от монад до 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.