
В последнее время часто использую ИИ для доработок своих программ (Cursor и DeepSeek) и замечаю любопытную вещь. По сути ИИ — это коллективный опыт человечества. Он обучался на миллиардах строк кода, почерпнутых из всех доступных источников. Так вот, судя по шаблонам кода, которые предлагают мне ИИ, большинство программистов увлекаются различными проверками. В основном входящих аргументов, но, бывает, и результатов работы процедур.
Для своих личных проектов я пришёл к выводу, что проверки входных аргументов в методах и функциях — зло. И вот почему.
Оговорки
Подход, о котором речь в статье, я использую в личных проектах. Не знаю, насколько он применим к корпоративной разработке. Возможно, учитывая непредсказуемый реальный уровень исполнителей, он не годится для больших коллективов программистов.
Также нужно сказать, что этот подход мало применим к фронтэнду, где у каждого пользователя разный браузер, разных версий или Android Web View произвольных версий. Он больше применим к компилируемому коду или к интерпретируемому коду, который выполняется централизованно на сервере.
Эта статья — не догма, а скорее приглашение к дискуссии. Добро пожаловать в комментарии после чтения статьи!
Истоки явления
В крупных организациях редко за код отвечает 1 человек. Задача фрагментируется и разбивается на отдельные процедуры и методы, которые поручаются разным людям.
Возможно, поэтому многие программисты, получающие задачу на разработку метода или процедуры, первым делом устраивают множество проверок входящих данных. Но иногда дело этим не оканчивается — делаются ещё и проверки выходных данных.
Помимо фрагментации ответственности, существует несколько других причин, заставляющих разработчиков добавлять избыточные проверки:
Защитное программирование (Defensive Programming): Это устоявшаяся парадигма, которая призывает программиста предполагать, что всё, что может пойти не так — пойдёт не так. В своей крайней форме она приводит к «параноидальному коду», где каждый аргумент считается враждебным. (Хабр1, Хабр2, Blogspot, перевод Blogspot)
Страх перед падением в продакшене: Падение программы — это серьёзный инцидент. Разработчики испытывают от этого страшный стресс и предпочитают «подстраховаться» в новом коде, даже там, где это не нужно.
Отсутствие явных контрактов: Когда спецификации размыты или существуют только в голове уже уволившегося автора, каждый следующий разработчик вынужден гадать, что же на самом деле ожидает метод. Проверки в таком случае — это попытка угадать и обезопасить себя от непредсказуемого поведения.
Идея «хрустального кода»
Проверки делаются только при первоначальном получении данных
Например, при чтении с диска (может быть bad sector), при пользовательском вводе, открытом API, сетевом запросе. Иными словами все источники, которые характеризуются ошибочным вводом извне, мы проверяем. Если корректные данные уже попали в программу, то мы следим, чтобы на выходе каждого метода и процедуры они оставались корректными.
Следуем спецификациям
Если метод должен принимать дату, то мы не проверяем, что пришла именно дата. Раз в спецификации сказано, что придёт определённый тип данных, то мы слепо верим, что именно этот тип данных и придёт.
На каждый метод у нас есть чёткая спецификация, что он принимает и выдаёт. Этот подход напрямую соотносится с принципом Программирования по Контракту (Design by Contract, DbC).
Контракт метода состоит из:
Предусловий: Что клиент обязан гарантировать перед вызовом метода (например,
amount > 0). В «хрустальном коде» мы не проверяем предусловия внутри метода, мы просто объявляем их.Постусловий: Что метод гарантирует в результате своего выполнения (например, «баланс не становится отрицательным»).
Инвариантов: Условия, которые остаются истинными на протяжении всей жизни объекта (например, «баланс счёта никогда не отрицательный»).
В нашем подходе мы слепо верим, что клиент выполнил все предусловия. Наша ответственность — выполнить свою часть контракта и обеспечить постусловия.
Пример инварианта в Go:
type Account struct {
balance uint64
// Инвариант: balance >= 0
}
func (a *Account) Transfer(amount uint64, to *Account) {
// НЕТ проверок — вся ответственность на вызывающей стороне
a.balance -= amount
to.balance += amount
// Если инвариант нарушен — это фатальная ошибка проектирования
}
Кто-то может возразить — бывают и ошибки в процессорах, в банках памяти, в космосе радиация может сбросить отдельный бит памяти. Процессор может быть разогнан и иногда выдавать неверные значения.
Все подобные случаи мы игнорируем. Если по техническому заданию программа должна работать в таких жёстких условиях, то этот подход не годится. Поскольку он был создан с приоритетом на получение высокоскоростных программ.
Ссылки для любознательных:
Концепция была популяризована Бертраном Мейером в языке Eiffel.
Позволяем ошибкам случаться (Fail-Fast)
Само собой, при таком подходе результирующая программа будет чаще крашиться, падать и сыпаться. Но это только вначале. И это — благо! Такой подход является воплощением принципа Fail Fast! (Упади быстро!) (Вики, Хабр).
Некоторые предусмотрительные программисты помимо проверок могут восстанавливать корректность входящих данных, например, преобразовывать nil в 0. Или делать из некорректного формата корректный. Но поскольку со 100% эффективностью это сделать в принимающем методе нельзя, то ошибка просто маскируется и становится настолько трудноуловимой, что может потом десятилетиями жить в коде и стать «фичей».
«Хрустальный код» же даёт такие плюшки при падениях:
Повышение качества в долгосрочной перспективе. Жёсткое и быстрое падение заставляет разработчиков немедленно исправлять корень проблемы, а не маскировать её. После того как все такие «хрустальные» ошибки будут исправлены, программа будет радовать вас стабильностью и скоростью.
Ошибка быстрее обнаруживается Если функция ожидает положительное число, а получает отрицательное, она немедленно выйдет за границы массива или уйдёт в бесконечный цикл. Гораздо сложнее отлаживать ситуацию, когда некорректное значение молча «проглатывается», передаётся по цепочке вызовов и вызывает сбой в совершенно другом месте системы, где уже нет контекста первоначальной ошибки.
Упрощение отладки (но не всегда). Цепочка вызовов процедур (stack trace) почти всегда укажет на место и причину сбоя. Тут оговорюсь, что для начального этапа разработки или поддержки спагетти-кода всовывать массу проверок полезно, чтобы получать ясные сообщения и человеческим языком. Дальше об этом подробнее.
«Хрустальный код» не равно «Fail fast!», так как падение без соответствующих логов хоть и ускорит понимание, что ошибка есть, но сам процесс поиска может быть долгим. Поэтому упасть желательно правильно, чтобы в логах осталась информация о месте падения, стек вызовов. (из комментария)
Области применения «хрустального кода»
Сама идея появилась при программировании на интерпретируемых языках, где в функцию, буквально, может придти всё что угодно. И типичный выход из ситуации добавить кучу проверок.
Типизированные языки программирования по своей природе в какой-то степени являются и как воплощением «защитного» подхода — проверяется тип аргумента при попадании в функцию, так и «хрустального» — она падают при несоответствии (в процессе компиляции или при исполнении в случае интерпретируемых языков). То что сказано о типизируемых языках так же относится к языкам со строгим совпадением с шаблоном а-ля Erlang.
Однако принцип «хрустального кода» применим и к ним. Да, тип uint32 не допустит отрицательного возраста и даже может вернуть панику при присвоении отрицательного значения, но он проглотит возраст равный 1 для банковского клиента, где он должен быть минимум 14.
В случае типизированных языков «хрустальность» проявляется не только в том, что сами типы верные (это происходит автоматически), но и в том, что проверки на валидность диапазонов делаются только один раз. Например, проверка на минимальный возраст должна делаться 1 раз при попадании данных в систему, а потом возраст уже везде считается валидным.
Плюсы «хрустального кода»
Когда мы не корректируем входящие данные и не пытаемся быть «умнее» вызывающего метода, мы получаем несколько ключевых преимуществ.
Быстрота и недублирование проверок
Все проверки делаются в одном месте — там, где данные появляются из подверженного ошибкам источника. Это ускоряет программу, проясняет её логику. А также убирает класс ошибок связанный с тем, что при наличии нескольких проверок в разных методах сами проверки могут делаться по разному. Одна из них может пропускать некорректный результат, а другая — нет.
Сама идея кода без дублирования — очень сильная. Логику при изменении входных данных нужно править только в одном месте.
Читаемость и простота
Код становится чище и понятнее. Он избавляется от слоёв «защитного» boilerplate-кода, который затуманивает его основную бизнес-логику. Вместо:
func ProcessUser(input interface{}) error {
// Традиционный подход с проверками
if input == nil {
return errors.New("input cannot be nil")
}
user, ok := input.(*User)
if !ok {
return errors.New("input must be *User")
}
if user.Name == "" {
return errors.New("user name cannot be empty")
}
if user.Age <= 0 {
return errors.New("user age must be positive")
}
// ... настоящая логика где-то тут ...
}
Мы сразу переходим к сути:
func ProcessUser(user *User) {
// "Хрустальный" подход — сразу к делу!
// Предполагаем, что user валиден
fmt.Printf("Processing user: %s, age: %d\n", user.Name, user.Age)
}
Это также упрощает рефакторинг и поддержку.
Производительность
Казалось бы, какая разница — ещё один дополнительный IF. Но нет, когда речь идёт о наносекундах или о методах/процедурах, которые участвуют в горячих циклах, код в которых выполняется триллионы раз, то мы получаем огромную разницу в скорости, вплоть до десятков процентов. А если проект нашпигован проверками, да ещё и с участием регулярных выражений, то производительность может упасть в разы.
Статья на Хабре, про то, чем assert лучше IF.
Когда «хрустальный код» — не лучшая идея
Важно понимать, что этот подход — не серебряная пуля. Есть области, где он неприменим или должен быть сильно модифицирован.
Публичные API и библиотеки. Когда вы создаёте код, которым будут пользоваться миллионы неизвестных вам разработчиков, вы не можете полагаться на то, что они прочтут вашу спецификацию. Здесь защитное программирование и тщательная валидация входных данных — must-have.
Критические системы. Управление космическим аппаратом, самолётом, медицинское оборудование, АЭС. В таких системах падение недопустимо. Здесь применяется глубокоэшелонированная защита и обработка всех возможных сбоев, даже тех, что кажутся невероятными.
Ввод данных от пользователя. Всё, что приходит извне (из форм на сайте, API-запросов), должно быть тщательно проверено и преобразовано в правильную валидную форму. «Хрустальный» подход заканчивается там, где начинается неподконтрольный вам внешний мир.
Многопоточные системы. В конкурентных средах состояние может измениться между проверкой и действием. Здесь блокировки и атомарные операции становятся частью контракта:
func (a *Account) SafeWithdraw(amount float64) error {
a.mu.Lock()
defer a.mu.Unlock()
// В многопоточной среде эта проверка — часть бизнес-логики,
// а не "паранойя", поскольку состояние могло измениться
if a.balance < amount {
return ErrInsufficientFunds
}
a.balance -= amount
return nil
}
Несколько сложных объектов на входе. Когда метод принимает несколько сложных объектов на входе из разных источников, то это трудно отследить простыми тестами. Поэтому в подобных случаях приходится всё-таки делать проверку кодом.
Цена выпуска новой версии высока. Условно, если новая версия отправляется на CD или необходима отправка инженера для её установки, то, возможно, что программа с глюками, но не падающая, будет лучше, чем падающая, но которая способна быстро выявить глюки. (из комментария)
Как внедрить «хрустальный код» на практике
Резко отказаться от всех проверок — плохая идея. Более того, при первоначальном написании программы, когда код ещё похож на спагетти, даже стоит устраивать всякие проверки входящих данных, которые потом можно убрать.
Вот несколько шагов для постепенного перехода на «хрустальный код»:
Пишите чёткие спецификации. Используйте системы автодокументирования (например, GoDoc для Go или другие), строгую типизацию, явные контракты. Чем лучше вы опишете контракт, тем меньше потребность в его ручной проверке.
Используйте средства языка. Система типов — ваша первая и лучшая линия обороны. В Go вместо
interface{}используйте конкретные типы, если это возможно. Компилятор Go не позволит передать не тот тип, и это уже огромный пласт проверок, которые не нужно писать вручную.Полагайтесь на панику в действительно критических ситуациях. В Go паника (
panic) — это не исключение, а фатальная ошибка, которая должна возникать только при нарушении основных инвариантов системы:
func (a *Account) Withdraw(amount uint64) {
// ВСЯ ответственность за валидность аргументов — на вызывающей стороне
a.balance -= amount
// Если баланс ушёл в минус — это фатальная ошибка логики,
// которую нужно исправить на этапе разработки
}
Пишите модульные тесты. Они являются исполняемой спецификацией вашего кода. Тесты гарантируют, что и вы, и ваши коллеги понимают контракт одинаково и не нарушат его в будущем:
func TestAccount_Withdraw(t *testing.T) {
account := &Account{balance: 100}
// Проверяем основную логику
account.Withdraw(50)
if account.balance != 50 {
t.Errorf("Expected balance 50, got %v", account.balance)
}
}
Релизный билд без проверок. С помощью условной компиляции и систем сборки можно исключить проверки из релизного или скомпилированного кода. Это хороший компромисс между удобством отладки и быстротой конечного продукта.
Заключение
Подход «хрустального кода» — это не призыв к халяве и безответственности. Напротив, это призыв к высокой ответственности и чёткому проектированию.
Это философия, которая ставит во главу угла простоту, производительность и быструю диагностику ошибок, а не их маскировку. Она заставляет нас думать о контрактах и спецификациях, а не о том, как бы пережить любой безумный ввод.
Этот подход идеально ложится на такие современные практики, как TDD и чистая архитектура, где код изначально проектируется с чёткими границами ответственности.
Что такое TDD
Test-Driven Development — это методология разработки программного обеспечения, в которой сначала пишутся тесты, а затем код, который должен успешно проходить эти тесты.
Да, он требует зрелости от команды и дисциплины, но в долгосрочной перспективе он окупается созданием простого, быстрого и надёжного программного обеспечения.
А что думаете вы? Готовы ли вы пожертвовать «защищённостью» своего кода ради простоты и скорости?
© 2025 ООО «МТ ФИНАНС»

