Разделение ответственности
Разделение ответственности (Separation of Concerns, SoC) означает, что часть системы отвечает за одну согласованную область решений и меняется по своим причинам. Граница не обязана совпадать с файлом, пакетом или «слоем»: её определяют правила, владение данными и сценарии изменения.
Зачем это на интервью
Принцип проверяют задачами вида «handler содержит валидацию, расчёт цены и SQL — что делать?». Важно показать не механическое разбиение на папки, а умение отделить HTTP-детали от прикладного сценария и инфраструктурную реализацию от правила бизнеса, сохранив простой путь выполнения.
Минимум для E4
- Формулировать ответственность как причину изменения, а не как название технологии.
- Отделять decoding, HTTP-коды и аутентификационные детали от use case.
- Держать доменное правило рядом с данными и инвариантами, которые оно защищает.
- Передавать зависимости явно и тестировать прикладной сценарий без сети и БД.
Рассмотрим создание заказа. Handler преобразует HTTP-запрос в команду, проверяет формат и выбирает ответ. Use case проверяет бизнес-инварианты: доступность товара, права клиента, переход статуса. Repository скрывает SQL и транзакционные детали. Это не запрет на вызовы между слоями, а способ не позволять изменению URL, драйвера БД или бизнес-правила затронуть весь код одновременно.
import "errors"
type CreateOrder struct {
products ProductCatalog
orders OrderRepository
}
func (uc CreateOrder) Execute(ctx context.Context, customerID string, items []Item) (Order, error) {
if len(items) == 0 {
return Order{}, ErrEmptyOrder
}
if err := uc.products.Reserve(ctx, items); err != nil {
return Order{}, err
}
order := NewOrder(customerID, items)
if err := uc.orders.Save(ctx, order); err != nil {
// Release должна быть идемпотентной; при ошибке её нужно повторить
// через надёжную компенсацию и поднять алерт.
return Order{}, errors.Join(err, uc.products.Release(ctx, items))
}
return order, nil
}HTTP-handler может вызвать Execute, но CreateOrder не должен знать об http.Request, JSON или конкретном драйвере PostgreSQL. Здесь резервирование и сохранение не являются одной транзакцией, поэтому при ошибке Save требуется компенсация Release; для более строгой гарантии нужны общая транзакционная граница или durable workflow/outbox. Если цена ошибки вызова важна, бизнесовый тип ошибки переводится в HTTP-ответ на границе transport.
Углубление для E5/Senior
SoC полезно применять на нескольких масштабах. Внутри функции отделяйте вычисление от I/O. В сервисе выделяйте доменный сценарий, transport и адаптеры. Между сервисами границу проводите по владению данными, консистентности и автономности развёртывания, а не по каждой сущности ER-диаграммы.
Не создавайте «слой ради слоя». Тонкий CRUD-сервис с одним хранилищем может оставаться простым пакетом, пока не появляется независимая бизнес-вариативность. Напротив, если один пакет одновременно меняется из-за требований платежей, SQL-оптимизации и HTTP-версии API, формально маленький код уже имеет несколько ответственностей.
Граница требует контракта ошибок и транзакций. Use case должен определить, что атомарно и какие ошибки видит вызывающий код; repository не должен незаметно навязывать HTTP-статусы или retry-политику. Для распределённого процесса границы также определяют, где допустимы eventual consistency, outbox и компенсация.
Ключевые понятия
- Причина изменения — независимое требование, из-за которого код нужно менять.
- Transport — представление входа/выхода протокола: HTTP, gRPC, CLI, очередь.
- Use case — прикладной сценарий, координирующий правила и зависимости.
- Доменный инвариант — условие, которое должно оставаться истинным для корректного состояния.
- Адаптер — код, переводящий внешний контракт в внутренний или обратно.
- Граница транзакции — место, где определяется атомарность изменения и обработка сбоя.
Типовые вопросы
- Как отличить SoC от SRP?
- SoC — широкое разделение разных областей решений; SRP обычно формулируют для компонента с одной причиной изменения. Они дополняют друг друга.
- Нужно ли handler всегда делать тонким?
- Он должен владеть протоколом. Для простого endpoint допустима небольшая логика преобразования, но бизнес-правило, нужное другим входам, лучше вынести.
- Где должна жить валидация?
- Синтаксис и ограничения транспорта — на границе; инварианты предметной области — в домене/use case. Одно правило может проверяться в обоих местах с разными целями.
- Почему repository не должен возвращать HTTP-ошибку?
- Хранилище не знает протокол потребителя. Оно возвращает техническую или предметную ошибку, а transport выбирает её представление.
- Когда отдельный микросервис — плохая SoC?
- Когда у частей нет независимого владения данными, жизненного цикла и потребности в отдельном масштабировании: сеть добавит отказов и контрактов без реальной автономности.
- Как проверить границу тестом?
- Unit-тест use case подменяет порт in-memory fake и не поднимает HTTP или БД; отдельно тестируется адаптер и маппинг ошибок.
Практика
- Рефакторите handler, в котором есть JSON, SQL и расчёт скидки. Критерий: прикладной сценарий принимает типизированную команду, а его unit-тест не импортирует
net/http. - Опишите контракт
OrderRepository: операции, ошибки и границу транзакции. Критерий: PostgreSQL-адаптер и in-memory fake проходят один набор сценариев use case. - Добавьте gRPC-вход к существующему сценарию. Критерий: бизнес-правила не дублируются, а HTTP и gRPC по-разному отображают ошибки только на своих границах.
Частые ошибки и ловушки
- Называть пакет
serviceи помещать туда всё, что не подошло остальным пакетам. - Выносить каждую строку в отдельный слой и усложнять чтение без независимой причины изменения.
- Дублировать инвариант между handler и use case так, что каналы расходятся в поведении.
- Давать инфраструктуре принимать доменные решения, например выбирать скидку по тексту ошибки драйвера.
- Делить микросервисы по таблицам вместо бизнес-владения и требований к консистентности.
Связанные темы
Принципы проектирования · Связность и зацепление · Проектирование Go-приложений · System Design · Тестирование · Delivery
Источники
- Edsger W. Dijkstra, On the role of scientific thought, о separation of concerns.
- Robert C. Martin, Clean Architecture, главы о границах и зависимостях.
- Eric Evans, Domain-Driven Design, главы о bounded context и модулях.
- Документация Go: Organizing a Go module и Code Review Comments.