SOLID и GRASP — про проектирование классов. Принципы из этой статьи старше и шире: они про инженерные решения вообще — от строчки кода до выбора, нужна ли вам ещё одна служба. Их часто цитируют как заклинания; здесь — что каждый значит на самом деле, где у него границы и как он выглядит в коде.
DRY — не повторяй знание
Представьте: правило «заказ дороже 10 000 ₽ — бесплатная доставка» записано в трёх местах — в обработчике, в SQL-отчёте и в форме на фронтенде. Порог повысили до 15 000 ₽ — сделали три правки, и одну забыли. Пользователи получают бесплатную доставку, когда не должны.
DRY (Don't Repeat Yourself) — каждая единица знания должна иметь одно авторитетное место в системе.
Ключевое слово — знание, не текст. DRY не нарушен, когда два куска кода совпадают текстуально, но несут разные знания. Лимит длины имени покупателя и лимит длины названия товара могут оба равняться 255 — но это два независимых решения, и склеивать их в одну константу MaxNameLength вредно: изменение одного потащит другое.
// пакет customer
const NameMaxLength = 255 // независимое решение
// пакет product
const TitleMaxLength = 255 // тоже независимое
Склейка случайно похожих вещей в одну абстракцию — самая дорогая форма нарушения DRY. Потом каждое изменение требует if-параметров, потому что «общая» константа оказывается не такой уж общей.
Практическое эмпирическое правило — правило трёх: терпите дублирование до третьего повторения. К третьему разу станет видно, что в нём общее знание, а что просто совпадение.
KISS — из работающих решений выбирай простейшее
Каждая дополнительная концепция в коде — налог на каждого будущего читателя. Реактивное программирование добавляет нетривиальные типы и другой стиль отладки — и оправдано только тогда, когда профиль нагрузки этого требует. Очередь задач на таблице PostgreSQL со SKIP LOCKED проще брокера сообщений — если событий тысяча в час, брокер «на вырост» означает отдельный кластер, мониторинг и команду, которая в нём разбирается.
KISS (Keep It Simple) — простота измеряется числом концепций, которые нужно держать в голове, чтобы понять код.
Тест при ревью: можно ли объяснить решение коллеге за минуту? Generic-фабрика стратегий с рефлексией, спрятанная за тремя интерфейсами, проигрывает обычному switch — даже если она «расширяемее».
YAGNI — не строй до потребности
Разработчик делает конфигурируемость «на будущее», хотя второго потребителя пока нет. Добавляет поле version в API, которым никто не версионирует. Разворачивает сложную многослойную архитектуру для прототипа, у которого ещё нет ни одного реального пользователя.
YAGNI (You Aren't Gonna Need It) — не делай сейчас то, что нужно потом. Всё это — ставки на будущее, которые обычно не сбываются, а обслуживания требуют уже сегодня.
KISS — про форму решения, YAGNI — про время: не «сделай проще», а «не делай сейчас».
Важная граница: YAGNI — про функциональность, не про качество. Тесты, понятные имена и миграции со стратегией отката «понадобятся потом» всегда — на них принцип не распространяется.
Separation of Concerns — разные аспекты в разных модулях
Код без разделения обязанностей выглядит так: метод в бизнес-логике сам разбирает HTTP-заголовки, сам открывает транзакцию, сам форматирует ответ. Поменяли формат ответа — трогаем бизнес-логику. Поменяли базу данных — трогаем HTTP-слой.
Separation of Concerns — разные аспекты системы должны жить в разных модулях: смешение делает каждый из них неизменяемым без риска сломать другой.
Транзакции, безопасность, кеширование, метрики отделяются от бизнес-логики через middleware и обёртки. HTTP-разбор отделён от обработки хендлером и кодеками. Конфигурация — от кода переменными окружения и конфиг-файлами.
Признак смешения — импорт через границу: HTTP-типы в бизнес-логике, объекты базы данных в контроллере.
Law of Demeter — не лезь во внутренности чужих объектов
Распространённая ситуация: метод получает заказ, потом залезает в адрес покупателя, потом в город адреса — и так несколько уровней глубины.
// нарушение — лезем во внутренности через несколько уровней
city := order.Customer().Address().City()
Чем глубже цепочка, тем сильнее связанность: OrderService теперь знает о внутреннем устройстве Customer и Address. Стоит переименовать поле в Address — ломается код в OrderService.
Law of Demeter — метод вызывает методы своих полей, параметров и локальных объектов, но не «друзей друзей».
// лечение — спрашиваем у ближайшего объекта
city := order.ShippingCity()
Метод ShippingCity() на объекте Order знает про внутренности — это его ответственность. OrderService больше не знает ничего лишнего.
Важная оговорка: fluent API (builder.WithTimeout(...).Build()) и конвейеры преобразований — не нарушение. Там каждый вызов возвращает тот же builder или трансформирует значения, а не лезет в чужие внутренности. Закон — про структурную связанность, а не про количество точек в строке.
Composition over Inheritance — делегируй, не наследуй
Представьте базовый тип BaseOrderService с методом Process, который вызывает Validate. Каждый сервис встраивает базу, рассчитывая «переопределить» Validate. Потом нужно переиспользовать валидацию в другом сервисе — и выясняется, что она прибита к базовому типу. А в Go встраивание даже не даёт переопределения: Process всё равно вызовет базовый Validate.
Composition over Inheritance — переиспользуй поведение через делегирование, а не через имитацию наследования встраиванием.
// имитация наследования через встраивание — хрупко
type BaseOrderService struct{}
func (s *BaseOrderService) Validate(order *Order) error { /* ... */ }
func (s *BaseOrderService) Process(order *Order) error {
return s.Validate(order) // всегда вызовет базовый Validate — перекрыть его нельзя
}
// композиция — гибко
type OrderProcessor struct {
validator OrderValidator
repository OrderRepository
}
func (p *OrderProcessor) Process(order *Order) error {
if err := p.validator.Validate(order); err != nil {
return err
}
return p.repository.Save(order)
}
Встраивание, имитирующее наследование, — самая сильная связь: внешний тип зависит от внутренностей встроенного, и контракт легко нарушить случайно. Композиция даёт то же переиспользование через делегирование без хрупкой иерархии.
Когда встраивание уместно: точки расширения, которые библиотека сама спроектировала под встраивание (например, UnimplementedGreeterServer в gRPC). Своё прикладное поведение — всегда через композицию.
Fail Fast — ошибка должна проявляться рано
nil прокрался через пять слоёв и всплыл как panic: nil pointer dereference в чужом модуле в пятницу ночью на проде. На поиск причины ушло три часа, потому что ошибка возникла далеко от того места, где сломалось.
Fail Fast — ошибка должна проявиться как можно раньше и как можно ближе к своей причине.
Три рубежа:
- Старт приложения — некорректная конфигурация должна валить сервис при запуске, а не в рантайме на проде.
- Граница запроса — входные данные проверяются до бизнес-логики.
- Конструктор объекта — объект не должен существовать в некорректном состоянии.
type Money struct {
amount decimal.Decimal
currency Currency
}
func NewMoney(amount decimal.Decimal, currency Currency) (Money, error) {
if currency == "" {
return Money{}, errors.New("currency обязателен")
}
if int(-amount.Exponent()) > currency.FractionDigits() {
return Money{}, fmt.Errorf("scale %d превышает допустимый для %s",
-amount.Exponent(), currency)
}
return Money{amount: amount, currency: currency}, nil
}
Антипод — «оборонительное» глотание: if err != nil { log.Error(err); return nil, nil }. Такой код превращает раннюю ошибку в позднюю и уничтожает контекст, который помог бы её диагностировать.
Principle of Least Astonishment — код делает то, что обещает
Метод называется GetOrder(), но заодно меняет статус заказа. GET-эндпоинт с побочным эффектом. Метод Equal, сравнивающий по указателю у типа-значения. Всё это работает, но врёт читателю.
Principle of Least Astonishment — код должен делать то, что ожидает читатель; удивление — признак дефекта дизайна.
Цена — не эстетика: код, которому нельзя верить по сигнатуре, приходится читать целиком, каждому, каждый раз.
Конвенции — прикладная форма принципа: query-методы не должны изменять состояние; GET-запрос должен быть безопасным, а PUT — идемпотентным. Нарушение этих конвенций ломает кеши и логику повторных запросов, которые на них рассчитывают.
Самый дешёвый способ соблюдать принцип — следовать конвенциям стека, а не изобретать свои: каждое «у нас принято иначе» — будущее удивление.
Бритва Оккама — не плоди сущности сверх необходимого
Второй микросервис, когда один домен и одна команда. Redis при ста запросах в секунду, «потому что масштабирование». Отдельный общий модуль ради трёх утилит. Брокер сообщений для трёх событий в день.
Бритва Оккама в проектировании: каждая сущность — сервис, брокер, база, библиотека, слой, паттерн — должна оправдывать своё существование.
В отладке работает так же: из гипотез «баг в рантайме», «баг во фреймворке», «баг в моём вчерашнем коммите» начинать стоит с последней — простейшее объяснение чаще оказывается верным.
KISS — про форму кода, бритва Оккама — про состав системы. Оба про экономию, на разных уровнях.
Коротко
- DRY — одно знание в одном месте; случайное совпадение текста — не нарушение DRY.
- KISS — из работающих решений выбирай то, что проще объяснить; каждая концепция — налог на читателя.
- YAGNI — не строй до реальной потребности; на тесты и понятные имена не распространяется.
- Separation of Concerns — разные аспекты в разных модулях; импорт через границу — признак смешения.
- Law of Demeter — метод работает с ближайшими объектами, не лезет через цепочку во внутренности; fluent API — не нарушение.
- Composition over Inheritance — переиспользуй через делегирование; встраивание уместно только там, где библиотека явно под него спроектирована.
- Fail Fast — ошибка дешевле ранняя; конструктор, граница запроса, старт приложения — три рубежа.
- Least Astonishment — код делает то, что обещает по сигнатуре и конвенциям стека.
- Бритва Оккама — каждая сущность в системе должна оправдывать своё существование.
Что почитать дальше
- SOLID на примерах — те же идеи на уровне устройства класса.
- GRASP на примерах — кому отдать ответственность в объектной системе.
- Монолит, модульный монолит или микросервисы — бритва Оккама на архитектурном уровне.