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

Архитектура: декомпозиция, именование, связывание

middle-senior~80 мин

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

Архитектура: декомпозиция, именование, связывание

Перед тем как открыть редактор и завести первую папку, остановись и спроси: что именно ты собираешься делить, как назовёшь куски и как они будут коммуницировать между собой. Это и есть архитектура. Не папки, не фреймворк, не выбор между NestJS и Express. А три вопроса: как режу, как называю, как соединяю.

Главная формула урока:

Архитектура = декомпозиция + именование + связывание.

Простой пример, небольшой магазин. Декомпозиция, это где проводят стены: один зал под овощи, второй под хлеб, третий под кассу. Именование, это таблички над залами: если “хлеб” висит над овощами, ни покупатель, ни кассир не найдут товар, хотя зонирование верное. Связывание, это маршруты внутри: тележка катится по проходу мимо хлеба к кассе, а не петляет через подсобку.

В коде та же тройка. Декомпозиция, это где проходят границы между модулями. Именование, это словарь, на котором команда говорит о домене: одинаковые сущности с разными именами создают невидимую сложность. Связывание, это топология: какой узел знает каких соседей и через какие каналы. Всё остальное (layered, clean, hexagonal, microservices), это частные случаи этих трёх вопросов.

Архитектура начинается не с папок

Распространённая ошибка: открыть пустой репозиторий и сразу создать src/controllers, src/services, src/repositories. Это не архитектура, это раскладка папок. Архитектура начинается раньше, с характеристик кода.

Возьми конкретный модуль payment/: принимает заказ и списывает деньги через сторонний шлюз. Прежде чем рисовать его на бумаге, спроси про этот модуль семь вещей. Помнит ли он начатые транзакции и где, в памяти процесса или в Postgres? Спишутся деньги дважды или один раз, если клиент нажал “Оплатить” повторно? Что будет, когда шлюз упадёт: транзакция повиснет, заказ откатится, заденет ли это соседние модули? Какие данные payment/ принимает и возвращает, что обещает не делать? Считает ли он скидку и шлёт ли чек, или это уже не его дело? Кто его вызывает и куда стучит он сам? Через что течёт информация: аргумент функции, HTTP, событие в шине?

Это и есть характеристики кода. Применимы к любому модулю, список получается короткий:

  • Состояние: есть ли оно, где живёт, кто его владелец.
  • Идемпотентность: дважды вызвал, что произошло.
  • Изоляция: что валится вместе, а что переживает.
  • Контракт: что входит и что выходит.
  • Граница: за что модуль отвечает и за что не отвечает.
  • Топология: как модули соединены между собой.
  • Способ передачи данных: каналы, протоколы, формы.

Семь характеристик. Папка controllers/ ни на один из них не отвечает.

Декомпозиция: чем режем

Декомпозировать, это провести границу. Граница хороша, когда по обе её стороны живут разные люди, разные команды, разные циклы релиза, разные требования к надёжности или просто разные причины для изменения.

Плохая граница режет по техническому слою. Например, по принципу “вот тут все контроллеры, вот тут все репозитории”. В итоге фича создать заказ размазана по пяти папкам. Чтобы прочитать её целиком, надо открыть пять файлов.

Хорошая граница режет по причине изменения. Если изменение требований по оплате затрагивает один пакет payment/, граница проведена правильно. Если это правит десять файлов в разных папках, граница не там.

Полезный тест Парнаса (1972): представь, что ты меняешь решение в одном модуле. Сколько других модулей придётся переписать? Если много, декомпозиция плохая. Хороший модуль скрывает решение, а не отражает его в имени.

Закон Конвея добавляет важное: декомпозиция системы повторяет декомпозицию команд. Один сильный архитектор не сможет жить с микросервисами, если все программисты сидят в одном чате и правят чужой код. И наоборот, монолит из десяти команд развалится по швам.

Именование: одна сущность, одно имя

“There are only two hard things in Computer Science: cache invalidation and naming things.” Phil Karlton, 1996.

Шутка популярна не зря. Большая часть архитектурных болей идёт от того, что одну и ту же сущность зовут по-разному в разных местах, или разные сущности зовут одним словом.

Признаки того, что с именами проблема:

  • В трёх соседних модулях User означает три разных объекта (доменный, БД-модель, DTO API).
  • getUser, fetchUser, loadUser, findUser, retrieveUser живут рядом и делают одно и то же.
  • Слово Manager или Service в имени класса не сужает смысл (что именно делает OrderManager?).
  • Команда раз в неделю спорит, что такое “заказ”: корзина, оплаченный объект или собранный для доставки.

Хорошее именование делает три вещи: даёт читателю предсказание (“если файл называется parser.ts, я ожидаю там парсер”), фиксирует словарь домена (один термин = одна сущность во всей системе) и подсказывает границу (если ты не можешь придумать имя короче трёх слов, скорее всего, у модуля больше одной ответственности).

Хорошее правило, чтобы выбраться: если в коде одну сущность зовут по-разному, открой единый язык домена с заказчиком и зафиксируй один вариант. Старые имена убирай rename-ом, не оставляй “для совместимости”.

Связывание: топология

Связывание, это вопрос “кто кого знает”. Граф: вершины это модули, рёбра это связи. Свойства графа определяют свойства системы. На картинках в материалах лекции этот язык уже введён: для каждой топологии есть число рёбер |E|, степень вершины deg(v) и диаметр d (максимальное число хопов между любыми двумя узлами).

Шесть базовых топологий, которые ты увидишь в коде на любом уровне (от React-компонентов до сервисов в проде).

Bus · шина

Линейная цепочка. Bus: каждое ребро добавляет один хоп между крайними. Удобно для конвейеров: read | parse | transform | validate | write. Это пайплайн в shell, middleware-chain в Express, поток в RxJS. Один сломавшийся узел рвёт всю цепочку.

Ring · кольцо

Замкнутая цепочка, каждый знает соседей. Ring: нет привилегированного центра. Token Ring (передаёшь только держа токен, нет коллизий). Chord DHT (ключ к следующему узлу по часовой стрелке, finger table даёт O(log n) прыжков). Ring buffer (write/read указатели крутятся, O(1) push/pop без аллокаций). Event loop JS: stack, microtasks, render, macrotask и снова по кольцу.

Кольцо выбирают, когда нужен порядок, отсутствие центра, равномерная нагрузка и O(1) операции без аллокации. Минус: разрыв одного ребра ломает порядок, латентность растёт линейно.

Star · звезда

Один хаб, n листьев. Star: коммутатор в LAN, CDN с origin и PoP, API gateway над сервисами, Redux store с подписчиками, React Context с потребителями, git с центральным origin. Диаметр всегда 2: лист, хаб, лист. Листья друг друга не знают, контракт держит центр.

Звезда хороша, когда листья не должны знать друг о друге. Звезда плоха, когда хаб становится узким местом или единой точкой отказа: gateway упал, всё упало.

Tree · дерево

Один корень, у каждого узла один родитель, циклов нет. Tree: DOM, React component tree, файловая система, DNS, AST, org chart. Дерево даёт O(log n) поиск на каждом уровне, scope chain (область видимости = путь до корня), изоляцию сбоев через Error Boundary (ограждение поддерева), универсальный обход DFS/BFS.

Дерево ломается, когда узлу нужно жить в двух местах одновременно. В React это решается через Context/Portal, в файловой системе через symlink или monorepo. Это переход к DAG, следующему уровню сложности.

Full mesh · полная связность

Full mesh: полный граф K_n. Каждый узел знает каждого, диаметр 1. Появляется там, где нет информации о структуре: FC-слой нейросети (каждый нейрон связан с каждым следующим), attention в LLM (каждый токен зависит от каждого), микросервисы без gateway, граф друзей в маленькой соцсети.

Цена квадратичная: n² памяти, n² трафика, n² каналов поддержки. Любую систему, которая дошла до full mesh, начинают резать обратно: иерархия (API gateway), локальность (Conv-слои вместо FC), порядок (sparse attention), общность (lockfile в монорепо).

Главный инсайт по full mesh: он появляется там, где нет информации о структуре. Как только появляется структура (локальность, иерархия, порядок), граф разрежается. Это правило одинаково работает для нейросети, для микросервисов, для зависимостей пакетов и для команд в большой компании.

Сводная таблица

Топология|E|deg(v)Диаметр dГде встретишь
Busn − 11 или 2n − 1pipeline, middleware chain
Ringn2⌊n/2⌋event loop, ring buffer, Chord DHT
Starn − 1hub: n − 1, leaf: 12API gateway, Redux store, switch
Treen − 1>= 1O(log n)DOM, FS, DNS, org chart
Full meshn(n − 1) / 2n − 11attention, FC-слой, dependency hell

Хороший архитектор смотрит на любую систему и видит топологию. Когда ты говоришь “наш фронт это Redux”, ты подписываешь архитектуру: звезда, store это хаб, листья (компоненты) не знают друг друга. Когда говоришь “у нас 12 микросервисов без gateway”, ты подписываешь full mesh с диаметром 1 и квадратичной болью.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.