
В последнее время часто использую ИИ для доработок своих программ (Cursor и DeepSeek) и замечаю любопытную вещь. По сути ИИ — это коллективный опыт человечества. Он обучался на миллиардах строк кода, почерпнутых из всех доступных источников. Так вот, судя по шаблонам кода, которые предлагают мне ИИ, большинство программистов увлекаются различными проверками. В основном входящих аргументов, но, бывает, и результатов работы процедур.
Для своих личных проектов я пришёл к выводу, что проверки входных аргументов в методах и функциях — зло. И вот почему.
Оговорки
Подход, о котором речь в статье, я использую в личных проектах. Не знаю, насколько он применим к корпоративной разработке. Возможно, учитывая непредсказуемый реальный уровень исполнителей, он не годится для больших коллективов программистов. Эта статья —не догма, а скорее приглашение к дискуссии. Добро пожаловать в комментарии после чтения статьи!
Истоки явления
В крупных организациях редко за код отвечает 1 человек. Задача фрагментируется и разбивается на отдельные процедуры и методы, которые поручаются разным людям.
Возможно, поэтому многие программисты, получающие задачу на разработку метода или процедуры, первым делом устраивают множество проверок входящих данных. Но иногда дело этим не оканчивается — делаются ещё и проверки выходных данных.
Помимо фрагментации ответственности, существует несколько других причин, заставляющих разработчиков добавлять избыточные проверки:
Защитное программирование (Defensive Programming): Это устоявшаяся парадигма, которая призывает программиста предполагать, что всё, что может пойти не так — пойдёт не так. В своей крайней форме она приводит к «параноидальному коду», где каж��ый аргумент считается враждебным.
Страх перед падением в продакшене: Падение программы — это серьёзный инцидент. Разработчики испытывают от этого страшный стресс и предпочитают «подстраховаться» в новом коде, даже там, где это не нужно.
Отсутствие явных контрактов: Когда спецификации размыты или существуют только в голове уже уволившегося автора, каждый следующий разработчик вынужден гадать, что же на самом деле ожидает метод. Проверки в таком случае — это попытка угадать и обезопасить себя от непредсказуемого поведения.
Идея «хрустального кода»
Проверки делаются только при первоначальном получении данных
Например, при чтении с диска (может быть 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) почти всегда укажет на место и причину сбоя. Тут оговорюсь, что для начального этапа разработки или поддержки спагетти-кода всовывать массу проверок полезно, чтобы получать ясные сообщения и человеческим языком. Дальше об этом подробнее.
Повышение качества в долгосрочной перспективе. Жёсткое и быстрое падение заставляет разработчиков немедленно исправлять корень проблемы, а не маскировать её. После того как все такие «хрустальные» ошибки будут исправлены, программа будет радовать вас стабильностью и скоростью.
Плюсы «хрустального кода»
Когда мы не корректируем входящие данные и не пытаемся быть «умнее» вызывающего метода, мы получаем несколько ключевых преимуществ.
Быстрота и недублирование проверок
Все проверки делаются в одном месте. Это ускоряет программу, проясняет её логику. А также убирает класс ошибок связанный с тем, что при наличии нескольких проверок в разных методах сами проверки могут делаться по разному. Одна из них может пропускать некорректный результат, а другая — нет.
Сама идея кода без дублирования — очень сильная. Логику при изменении входных данных нужно править только в одном месте.
Читаемость и простота
Код становится чище и понятнее. Он избавляется от слоёв «защитного» 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. Но нет, когда речь идёт о наносекундах или о методах/процедурах, которые участвуют в горячих циклах, код в которых выполняется триллионы раз, то мы получаем огромную разницу в скорости, вплоть до десятков процентов. А если проект нашпигован проверками, да ещё и с участием регулярных выражений, то производительность может упасть в разы.
Когда «хрустальный код» — не лучшая идея
Важно понимать, что этот подход — не серебряная пуля. Есть области, где он неприменим или должен быть сильно модифицирован.
Публичные 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
}
Несколько сложных объектов на входе. Когда метод принимает несколько сложных объектов на входе из разных источников, то это трудно отследить простыми тестами. Поэтому в подобных случаях приходится всё-таки делать проверку кодом.
Как внедрить «хрустальный код» на практике
Резко отказаться от всех проверок — плохая идея. Более того, при первоначальном написании программы, когда код ещё похож на спагетти, даже стоит устраивать всякие проверки входящих данных, которые потом можно убрать.
Вот несколько шагов для постепенного перехода на «хрустальный код»:
Пишите чёткие спецификации. Используйте системы автодокументирования (например, 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 ООО «МТ ФИНАНС»
Комментарии (9)

DustCn
14.11.2025 13:11Мне кажется что решение давно найдено. Есть отладочное построение, где куча ассертов, проверок данных и всевозможных результатов под дефайнами. И есть релизный вариант, где минимум проверок. Переключается в системе построения типом таржета.
Ну и не забывать,что все что попадает извне в программу, т.е. любой юзер-инпут должно проверяться независимо.

mixsture
14.11.2025 13:11Код становится чище и понятнее. Он избавляется от слоёв «защитного» boilerplate-кода, который затуманивает его основную бизнес-логику
соглашусь, некоторое затуманивание есть. Но часто эти проверки можно и нужно группировать и выносить в отдельные функции. Тогда их вызов - это одна строка. Это в принципе хорошо для любого проекта - разделять получение данных, проверку, обработку и вывод.

mixsture
14.11.2025 13:11Когда «хрустальный код» — не лучшая идея
я бы добавил еще, что типовыми методами вы покроете только совсем уж глупые ошибки: вроде засовывания не того типа. Но проверку граничных случаев приходится делать кодом, а именно с ними очень много проблем. Особенно остро это проявляется, когда граничными и недопустимыми становится комбинация вариантов объектов (т.е. когда нужно сравнить состояние нескольких поданных объектов).

devmargooo
14.11.2025 13:11Способ избавиться от лишних проверок и получить fail fast есть - нужно строго проверять тип входных данных и высокоуровнево работать с уже известными данными: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/.

wsf
14.11.2025 13:11Ознакомьтесь, например, с Erlang, там все эти механизмы вшиты в сам язык. Вообще по сути вы описали принципы pattern matching и концепцию let it fall

kotan-11
14.11.2025 13:11Этот подход не применим в embedded/robotics/desktop/mobile. Везде, где падения - это потери данных и долгая починка. Этот подход ограниченно применим только в сетевых решениях, только на бакенде и только если ваш продукт легко переносит roll-back к предыдущей версии, если этот roll-back автоматический и быстрый и если ваши клиенты готовы терперь outages. Даже в этом случае нет гарании, что предыдущая версия на этом же самом упавшем edge-case не упадет снова, потому что эта "логика в коде существовала всегда".
Я помню как в Google Drive тоже пытались делать "надежность через харакири". Кончилось все очень плохо - потерянной репутацией, потерянными корпоративными клиентами и потерей кучи денег. Потом был resilience/hardening проект длительностью в год чтобы заменить все падения восстановлениями.
mixsture
Ну наоборот же!
Если функция проверяет входные параметры - она сразу выругается о них и сделает это в человекочитаемом виде. Если не проверяет - где-то в глубинах других функций вы получите ошибку с малозначащим описанием: например "деление на ноль" - которое только с отладчиком можно превратить во что-то понятное, потратив час(ы): ага, вот тут подали отрицательное число на вход, а в функции этот параметр дальше перекладывается в беззнаковое число, что неявно приводит его к нулю и дальше в вызовы передается ноль и где-то там через 3 вызова получается деление на ноль.
Поэтому проверка входных параметров - это как раз fail-fast
inetstar Автор
Спасибо за найденную неточность!. Идея была в том, что какие-то проверки кроме самих проверок могут делать что-то с данными. Делать их «корректнее». Например nil преобразовать в 0. Потом куда-то ещё передать результат построенный на неверном входе. И поэтому потеряется изначальная ошибка. И найти её будет невероятно трудно.