Экспресс-курс · No. 03

Архитектуру почти никогда не изобретают. Её узнают: какая из известных форм подходит к задаче перед тобой — и какую цену эта форма за себя берёт.

Только суть · Один образ на паттерн · Примеры важнее теории

§ 01

Паттерн — это переиспользуемый ответ на проблему, которая возвращается снова и снова. Знать каталог — значит перестать изобретать заново и начать выбирать.

Паттерн — это имя для ответа, а не правило

В шахматах ты не изобретаешь дебют каждую партию. Ты узнаёшь: «это сицилианка» — и уже знаешь, куда она обычно ведёт.

Архитектурный паттерн — то же самое: проблема, которая повторяется в тысячах систем (как части находят друг друга? как система переживает нагрузку?), вместе с проверенной формой и известной ценой. Ты не гениален — ты стоишь на чужих шишках, набитых за десятилетия. Когда ты говоришь «сервисам нужно реагировать на то, что происходит в другом месте» — это событийная архитектура, уже решённая, названная и описанная.

Архитектурные паттерны — это не паттерны проектирования

Паттерн проектирования — это как расставить мебель в одной комнате. Архитектурный паттерн — это планировка всего дома.

Паттерны проектирования (Factory, Observer, Strategy) живут внутри одного модуля, в коде. Архитектурные паттерны решают, какие большие коробки есть и какие линии между ними — что разворачивается отдельно, что с чем разговаривает, где живут данные. Этот курс — про планировки, а не про мебель.

Каждый паттерн берёт свою цену

Спорткар и минивэн оба «правильные» — для разной жизни. Ни один не лучше в отрыве от контекста.

Лучшего паттерна не бывает — бывает подходящий. Микросервисы покупают тебе независимое масштабирование и берут за это распределённую систему, которую надо эксплуатировать. Слоистый монолит покупает простоту и берёт за это болезненное масштабирование потом. Мастерство — прочитать счёт до того, как подписал его.

Ты выбираешь не знаменитый паттерн. Ты выбираешь тот, чью цену можешь себе позволить.

§ 02

Большинство систем начинаются как один разворачиваемый кусок. Первые паттерны — про то, как устроить его изнутри, чтобы он не превратился в кашу.

Слоистая (n-tier): сложить ответственности этажами

Ресторан: зал принимает заказы, кухня готовит, кладовая хранит. Официант не готовит; повар не лезет в погреб — за продуктами идёт следующий слой вниз.

Самый распространённый паттерн вообще. Код делится на горизонтальные слои — представление, бизнес-логика, доступ к данным, база — и запрос падает сквозь них вниз. Пример: веб-приложение, где контроллер зовёт сервис, тот зовёт репозиторий, тот идёт в Postgres. Легко понять и легко тестировать (замокай слой ниже). Цена: всё разворачивается одним куском, и даже мелкое изменение может означать передеплой всего целиком.

Модульный монолит: один дом, квартиры с крепкими стенами

Многоквартирный дом — одна крыша, один вход, но у каждой квартиры свои стены и своя дверь. Ты не идёшь через спальню соседа к лестнице.

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

MVC / MVVM: держать экран подальше от мозгов

Театр: сценарий, актёры с режиссёром и сцена со светом — это разные работы разных людей. Декорации можно поменять, не переписывая пьесу.

Эти паттерны ставят стену между интерфейсом и бизнес-логикой с данными, чтобы изменение того, как что-то выглядит, не угрожало тому, что оно делает. Пример: Django и Rails используют MVC (Model — данные, View — страница, Controller — клей); богатые веб- и мобильные приложения тяготеют к MVVM, где «view model» держит состояние экрана, которое view просто отражает. Стена всегда одна: представление по одну сторону, смысл — по другую.

Монолит — это не бардак. Бардак — это монолит без внутренних стен.

§ 03

Одна и та же идея, нарисованная тремя способами: вытолкнуть фреймворк, базу и веб на края, а бизнес-правила оставить ничего о них не знающими.

Порты и адаптеры (гексагональная): стандартные разъёмы вокруг ядра

У игровой консоли стандартные порты. Игре всё равно, что ты воткнул — геймпад, руль или аркадный стик; она просто читает «ввод».

Бизнес-логика сидит в центре и определяет порты — интерфейсы вроде «сохранить заказ» или «отправить уведомление». Адаптеры снаружи реализуют их под конкретную технологию: Postgres, Stripe, email. Пример: домен зовёт порт OrderRepository; один адаптер подкладывает под него Postgres в проде, другой — список в памяти в тестах. Внешний мир можно подменить, а ядро этого даже не заметит.

Clean / Onion: зависимости смотрят внутрь

Луковица: ядро в центре, слои вокруг. Внешнюю шелуху можно снять, не повредив сердцевину — а сердцевина от шелухи не зависит.

Тот же принцип, нарисованный концентрическими кругами: сущности и сценарии в центре, фреймворки и ввод-вывод по краю, и каждая стрелка зависимости смотрит к центру. Пример: правило «счёт больше 10k требует согласования» живёт в обычном классе, который не импортирует ничего из веб-фреймворка или ORM — поэтому переживёт оба. Можно сменить UI-библиотеку, поменять веб-фреймворк, мигрировать базу — правила останутся нетронутыми.

Фреймворки — это детали. Относись к ним как к обоям, а не как к фундаменту.

§ 04

На каком-то масштабе — нагрузки или команд — один кусок перестаёт работать. Эти паттерны режут систему на независимо отгружаемые части и взамен вручают тебе сеть.

Микросервисы: фуд-корт, а не одна большая кухня

Фуд-корт — отдельные точки, у каждой своя кухня, меню и касса. На пиццу налегли — берут двух поваров; салатной точке это не нужно.

Приложение разбито на маленькие сервисы, каждый владеет одной способностью и своими данными, каждый разворачивается и масштабируется сам по себе. Пример: отдельные сервисы заказов, склада и доставки, общающиеся по HTTP или через шину сообщений; в чёрную пятницу ты масштабируешь только «заказ» и «оплату». Это поджарый потомок SOA, которая пробовала тот же раскол, но гнала всё через одну тяжёлую корпоративную шину и захлебнулась. Цена крутая и реальная: сетевые задержки, отсутствие лёгких транзакций между сервисами и распределённая система в эксплуатации. Подходит большим командам и неровной нагрузке — избыточно для стартапа из пяти человек.

Serverless / функции: арендовать кухню поблюдно

Вместо машины, которую не получается загрузить, ты вызываешь такси на поездку и платишь только пока тикает счётчик.

Ты пишешь маленькие функции, а облако запускает их по требованию, масштабируя от нуля до тысяч и обратно, выставляя счёт за каждый запуск. Пример: загруженная в хранилище картинка триггерит функцию, которая делает превью — между загрузками сервер не простаивает. Отлично для рваной, событийной работы; неудобно для долгих задач и всего, что не терпит холодного старта.

Распределённые системы превращают вызовы функций в вызовы по сети. И каждый из них может упасть.

§ 05

Как только коробок становится больше одной, архитектура в основном в линиях между ними. То, как они разговаривают, решает, как они падают.

Запрос–ответ: спросить и ждать у стойки

Заказ у стойки — ты спрашиваешь, стоишь и получаешь свой кофе, прежде чем идти дальше.

Форма по умолчанию: клиент зовёт сервер и ждёт ответа (REST, gRPC, запрос в базу). Просто и прямо. Подвох — связанность во времени: если сервер тормозит или лежит, вызывающий тоже застрял в ожидании. Пример: оформление заказа зовёт платёжный API и блокируется, пока тот не ответит.

Событийная: объяви — пусть слушатели реагируют

Редакция новостей. Что-то случилось, объявление вышло, и кто заинтересован — спорт, погода, реклама — реагирует сам. Диктор их не ждёт.

Вместо прямых вызовов сервисов компонент издаёт событие — «OrderPlaced» — и реагирует всё, кому интересно. Два вкуса: broker (события сцепляются свободно, без дирижёра) и mediator (координатор гонит шаги по порядку). Пример: «OrderPlaced» расходится в почту, склад и аналитику — каждый независим, каждый масштабируется, никто не блокирует заказ. Цена: за потоком труднее следить и его труднее тестировать, плюс ты наследуешь дубли и проблемы порядка.

Очереди и pub/sub: почта между ними

Почта держит твоё письмо, пока получатель не готов. Ты бросил его и ушёл; он прочитает, когда проснётся.

Очередь сообщений или брокер pub/sub (RabbitMQ, Kafka) встаёт между отправителем и получателем, чтобы им больше не нужно было быть онлайн в один и тот же момент. Пример: медленный запрос отчёта бросают в очередь, воркер берёт его, когда свободен, и сайт остаётся шустрым. Это покупает устойчивость и сглаживает пики — ценой того, что результат становится отложенным, а не мгновенным.

Pipe-and-filter: конвейер для данных

Линия розлива — помыть, налить, закрыть, наклеить этикетку — каждая станция делает одно и передаёт бутылку дальше. Или вода сквозь стопку фильтров.

Данные текут через цепочку независимых шагов, каждый преобразует их и передаёт дальше. Пример: пайп в Unix (grep | sort | uniq), ETL-задача или компилятор (токенизация, разбор, оптимизация, генерация). Каждый фильтр прост, тестируем и переставляем. Лучше всего, когда работа — это ясная последовательность преобразований — плохо, когда шагам нужно возвращаться назад и переговариваться друг с другом.

Синхронно — это телефонный звонок. Асинхронно — это письмо. Выбирай по тому, насколько стороны должны зависеть от того, что другая не спит.

§ 06

Некоторые паттерны вообще не про коробки. Они про то, как ты пишешь, читаешь и помнишь правду.

CQRS: разделить то, как пишешь, и то, как читаешь

В библиотеке есть быстрый каталог, чтобы найти книгу, и задняя комната, где книги реально расставляют по полкам. Поиск оптимизирован одним способом, хранение — другим.

CQRS разделяет модель записи (команды, которые что-то меняют) и модель чтения (запросы, которые что-то достают), чтобы каждую можно было заточить и масштабировать под свою работу. Пример: дашборд читает из денормализованной, заранее посчитанной витрины, а записи идут в чистое транзакционное хранилище. Мощно, когда чтений кратно больше записей — но это две модели, которые надо держать в синхроне, так что не тянись за этим по умолчанию.

Event sourcing: хранить историю, выводить настоящее

Банк хранит не просто твой баланс — он хранит каждый приход и расход. Баланс — это лишь сумма. Потеряешь его — пересчитаешь из журнала.

Вместо того чтобы хранить текущее состояние и перезаписывать его, ты хранишь весь поток случившихся событий; текущее состояние — это их проигрывание. Пример: счёт, хранимый как «открыт, +100, −30», а не «баланс: 70» — это даёт идеальный аудит-след и возможность спросить «а как это выглядело в прошлый вторник?». Цена: запрос „сейчас“ становится сложнее (часто в паре с CQRS), а события навсегда — историю просто так не отредактируешь.

Saga: длинная сделка, которую можно отмотать

Бронируешь поездку — рейс, отель, машина. Сорвалась машина — отменяешь и отель, и рейс. Единого «отменить» нет; ты отшагиваешь каждый шаг назад.

Когда одно бизнес-действие охватывает несколько сервисов, у каждого из которых своя база, обернуть его в одну транзакцию нельзя. Saga сцепляет локальные транзакции и при сбое запускает компенсирующие действия, отменяющие предыдущие. Пример: создать заказ, зарезервировать товар, списать с карты; если списание упало — освободить товар и отменить заказ. Так держат распределённые данные согласованными без глобального замка.

Транзакция — это один атом намерения. Когда он охватывает сервисы, атом приходится собирать самому.

§ 07

Несколько паттернов существуют ради одной конкретной боли. Тянись за ними, когда у тебя именно эта боль — и ни секундой раньше.

Microkernel / плагины: крошечное ядро со сменными частями

Дрель: один мотор, дюжина сменных насадок. Дрель та же; ты прищёлкиваешь то, что нужно под задачу.

Минимальное ядро даёт основу, а всё остальное приходит как плагины, которые сами себя регистрируют, не завися друг от друга. Пример: VS Code, Eclipse и браузеры — это в основном хосты для плагинов; страховая система может держать правила по конкретным регионам как плагины, и новый регион — это новый модуль, а не переписывание. Идеально, когда фичи изменчивы или зависят от клиента.

Space-based: скопировать данные в память и убить бутылочное горло

Стадион с двадцатью кассами, у каждой своя копия схемы зала на руках — вместо одной кассы, в которую все стоят в очереди.

Когда единственная база становится узким местом под экстремальной нагрузкой, этот паттерн разводит много обрабатывающих узлов, каждый держит данные в памяти, синхронизируемой между ними — и никакой центральной базы, в которую упирается очередь. Пример: флеш-распродажа или аукцион, принимающий вал одновременных ставок. Это мощно и дорого и оправдывает себя только при по-настоящему огромной, рваной конкурентности.

Peer-to-peer: вообще без центра

Ужин в складчину — каждый и повар, и гость. Ни ресторана, ни официанта; группа кормит себя сама.

Узлы общаются напрямую друг с другом без центрального сервера, каждый одновременно и клиент, и поставщик. Пример: BitTorrent, где каждый, кто скачивает, ещё и раздаёт — поэтому система становится сильнее, когда людей больше. Прекрасно для устойчивости и масштаба без центрального хоста — тяжело для согласованности и контроля.

Экзотические паттерны — это лекарство, а не витамины. Принимай их под поставленный диагноз.

§ 08

Паттерны — это словарь, а не меню, из которого берут одно блюдо. Реальные системы наслаивают несколько — и самый мудрый ход часто самый простой.

Паттерны комбинируют; редко берут лишь один

Дом использует много паттернов сразу — несущие стены, водопровод, проводку, утепление — а не «один истинный паттерн строительства».

Реальная система может быть модульным монолитом внутри, чистой архитектурой в каждом модуле, запрос-ответом для интерфейса, событием для медленных побочных эффектов и очередью перед тяжёлой задачей. Вопрос никогда не «какой паттерн», а «какие паттерны и где». Пример: большинство успешных продуктов — это скучный слоистый монолит, который оброс горсткой событий и одним-двумя вынесенными сервисами ровно там, где была боль.

Начинай с самой простой формы, которая работает

Ты не заливаешь фундамент под стадион, чтобы построить домик.

Почти любая система должна начинаться как модульный монолит и заслуживать свою сложность. Микросервисы, CQRS, event sourcing, space-based сетки — это ответы на проблемы масштаба и размера команды, которых у тебя может никогда не быть. Бери их, когда боль реальна и названа, а не на упреждение. Самая дешёвая распределённая система — та, которую ты не построил.

Прежде чем выбрать паттерн
  • Какую повторяющуюся проблему решает эта форма? Назови одним предложением. - Есть ли у меня эта проблема сейчас — или я гадаю про будущее? - Чего это стоит? В задержках, эксплуатации, сложности, деньгах. - Какой самый простой паттерн ещё сработал бы? Начни с него. - Могу ли я взять это позже, когда боль реально проявится? - С какими паттернами этому придётся уживаться? Они должны стыковаться.
Признаки, что ты переусложнил
  • У тебя сервисов больше, чем инженеров. - Одно действие пользователя задевает пять сервисов и две очереди ради того, что мог бы сделать вызов функции. - Ты добавил CQRS или event sourcing до того, как появилась проблема чтения или аудита. - Никто в команде не нарисует всю систему по памяти. - Ты выбрал паттерн потому что так делают большие компании.
Признаки, что ты выбрал хорошо
  • Новый инженер за минуты находит, куда вносить изменение. - То, что меняется вместе, лежит вместе; то, что масштабируется по-разному, разделено. - Когда что-то ломается, радиус взрыва — одна коробка, а не все. - Ты можешь объяснить работу и цену каждого паттерна одним предложением. - Архитектура подходит под нагрузку и команду, которые у тебя есть, а не под воображаемые.

Лучшая архитектура — самая простая, что переживает твои реальные требования, и ни одним паттерном больше.

Конец экспресс-курса · 8 глав · паттерны важнее теории

Дальше — практика. Каждый паттерн отсюда разворачивается в книги и набитые шишки: «Software Architecture Patterns» Марка Ричардса, «Fundamentals of Software Architecture», «Designing Data-Intensive Applications», «Building Microservices». Но прежде чем брать любой из них — назови свою проблему. Паттерн — это ответ; проблема — вопрос, который его заслуживает.