
В последнее время часто использую ИИ для доработок своих программ (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 ООО «МТ ФИНАНС»
Комментарии (62)

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

tenzink
14.11.2025 13:11Отладочная сборка с ассертами малополезна. На продовых данных она все равно не запускается.

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

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

Dhwtj
14.11.2025 13:11типовыми методами вы покроете только совсем уж глупые ошибки: вроде засовывания не того типа. Но проверку граничных случаев приходится делать кодом
Не согласен. Видимо, вы используете убогий язык программирования, который не может это сделать красиво.
Rust - красиво
pub struct Email(String); impl Email { pub fn parse(s: &str) -> Result<Self, EmailError> { if /* regex проверка */ { Ok(Email(s.to_owned())) } else { Err(EmailError::Invalid) } } }C# - почти красиво
public sealed class Email // class, не struct { private readonly string _value; private Email(string value) => _value = value; public static Result<Email, string> Parse(string s) { if (Regex.IsMatch(s, pattern)) return new Email(s); return Error("Invalid email"); } }Остальные из популярных делают это хуже.
То есть вместо конструкторов использовать функцию с возвратом Result, Rust так умеет уже в конструкторе.
Го умеет
func NewEmail(s string) (*Email, error) { if !isValid(s) { return nil, errors.New("invalid email") } return &Email{value: s}, nil }Но в Go нет приватных полей структуры на уровне пакета (только экспортируемые/неэкспортируемые), поэтому сложно физически запретить создание невалидного Email{Value: "bad"} напрямую внутри того же пакета. паттерн работает, но только за счёт соглашений команды, а не гарантий компилятора. Если кто-то напишет Email{Value: x} — компилятор не остановит

mixsture
14.11.2025 13:11Не согласен. Видимо, вы используете убогий язык программирования, который не может это сделать красиво.
Да, но в других языках тоже это не особо решается. То, что вы описали - это и есть проверки кодом, не так важно, через что конкретно вы их сделали: через конструктор, класс, фабричный метод или просто вызовом функции-проверяльщика.
Я же говорю о том, что без кода это не получается: т.е. невозможно сделать такую сигнатуру функции (а именно она и является контрактом общения), чтобы она фильтровала граничные случаи.И это мы еще рассматриваем крайне упрощенные примеры: у вас на входе только 1 параметр и это строка. Давайте на входе у вас будет 3 сложных объекта с взаимным ограничением:
объект1.Поле1 + объект2.Поле2 < объект3.Поле3
Я не вижу никаких способов это сделать, кроме кода.
Dhwtj
14.11.2025 13:11В двух словах:
Создание может быть с ошибкой (данные не валидны)
Поэтому, при создании либо мы кидаем исключение (плохо) либо вернём Result<T, Err>
В данном случае
use std::fmt; #[derive(Debug)] struct A(i32); #[derive(Debug)] struct B(i32); #[derive(Debug)] struct C(i32); // Единственный валидный "комбо"-тип #[derive(Debug)] struct Triple { a: A, b: B, c: C, } // Ошибка валидации #[derive(Debug)] struct TripleError; impl fmt::Display for TripleError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "invalid combination of A, B, C") } } // Умный конструктор: снаружи нельзя сделать Triple минуя эту функцию impl Triple { fn new(a: A, b: B, c: C) -> Result<Self, TripleError> { // пример ограничения: a.0 < b.0 < c.0 if a.0 < b.0 && b.0 < c.0 { Ok(Triple { a, b, c }) } else { Err(TripleError) } } } // Использование: только Triple гарантированно валиден fn process(triple: Triple) { println!("valid triple: {:?}", triple); } fn main() { let a = A(1); let b = B(5); let c = C(10); // ОК let t1 = Triple::new(a, b, c).unwrap(); process(t1); // Невалидно — Triple даже не создаётся let bad = Triple::new(A(10), B(5), C(1)); assert!(bad.is_err()); }Или проще
struct Triple(i32, i32, i32); impl Triple { fn new(a: i32, b: i32, c: i32) -> Option<Self> { if a < b && b < c { Some(Triple(a, b, c)) } else { None } } } fn use_triple(t: Triple) { println!("valid triple: {}, {}, {}", t.0, t.1, t.2); } fn main() { let ok = Triple::new(1, 5, 10);// лучше без .unwrap(); use_triple(ok); let bad = Triple::new(10, 5, 1); assert!(bad.is_none()); }Более длинный пример вводил A/B/C как отдельные типы, чтобы показать ещё один уровень защиты

mixsture
14.11.2025 13:11Давайте я напишу, как бы я видел идеальный конечный результат:
fn use_triple(a: i32, b: i32, c: i32) //это блок в сигнатуре функции с ограничениями. В очень кратком виде. [ (a + b < c) => { ERR_LESS }, (a + b > 2*c) => { ERR_MORE } ] { //это основная логика и тут нет места ограничениям println!("valid triple: {}, {}, {}", t.0, t.1, t.2); } {[ //это блок формирования красивых сообщений пользователю о нарушении ограничения. Тут куча функций форматирования, подстановки в шаблоны и мультиязыковой поддержки. ERR_LESS => { write!(f, "invalid combination of A, B, C! A + B need to be less, than C, but got %a% + %b% < %c%") }, ERR_MORE => { write!(f, "invalid combination of A, B, C! A + B cannot be more, than 2*C, but got %a% + %b% > 2 * %c%"") } }]Почему так:
Я вижу сигнатуру функции и сразу понимаю, с какими ограничениями она принимает данные и это все в очень сжатом и кратком виде. Мне не нужно ползать по хитросплетениям кода, разгадывать вспомогательные классы Triple и TrippleError, которые раскиданы по модулю и не несут смысловой нагрузки, кроме хранения ограничения. Ограничения при этом отделены от основной логики и не засоряют ее.
В сущности это даже может быть синтаксическим сахаром, наподобие декоратора.
Dhwtj
14.11.2025 13:11fn validate(a: i32, b: i32, c: i32) -> Result<(), &'static str> { match (a, b, c) { (a, b, c) if a + b < c => Err("ERRLESS"), (a, b, c) if a + b > 2 * c => Err("ERRMORE"), _ => Ok(()), } }Так хотели?
fn use_triple(a: i32, b: i32, c: i32) -> Result<(),ErrCode> { // Блок ограничений match (a, b, c) { (a, b, c) if a + b < c => Err(ERR_LESS), (a, b, c) if a + b > 2 * c => Err(ERR_MORE), _ => Ok(()) }?; // Основная логика println!("valid triple: {}, {}, {}", a, b, c); Ok(()) } // Блок форматирования ошибок fn format_error(err: ErrCode, a: i32, b: i32, c: i32) -> String { match err { ERR_LESS => format!( "invalid combination of A, B, C! A + B need to be less, than C, but got {} + {} < {}", a, b, c ), ERR_MORE => format!( "invalid combination of A, B, C! A + B cannot be more, than 2*C, but got {} + {} > 2 * {}", a, b, c ), } } enum ErrCode { ERR_LESS, ERR_MORE } use_triple(a, b, c) .map_err(|err| format_error(err, a, b, c))Длиннее, но логика отделена от обработки ошибок

Siemargl
14.11.2025 13:11Я думаю, нужно вернуть Вам ваше же высказывание...=)
Видимо, вы используете убогий язык программирования, который не может это сделать красиво (C)
Можно посмотреть, как это сделано в языках с Dbc, например в Аде, или в Dlang, или в Eiffel.

Dhwtj
14.11.2025 13:11Предисловие и постусловия плохо влияют на композицию
- При композиции функций контракты не комбинируются автоматически
- Нарушение инкапсуляции (precondition раскрывает детали)
Result<T, E> лучше для композиции:
- map, and_then и так далее это контейнер для цепочек
- Явная обработка на нужном уровне
- Не привязан к конкретной функции

mixsture
14.11.2025 13:11Так хотели?
Нет.
Еще раз: я хочу в сигнатуре функции видеть ее ограничения, не прибегая к анализу тела функции. И уж точно не хочу кучу разных функций и перечислений, потому что в реальном проекте они будут раскиданы по огромному модулю - прыгать между ними ужасно неудобно.
Dhwtj
14.11.2025 13:11readonly class SearchParams { public function __construct( #[Assert\InArray(RequestRepository::SORT_COLUMNS)] public string $sort = 'name', #[Assert\Regex('/^ASC|DESC$/')] public string $order = 'ASC', #[Assert\Range(min: 1, max: 1000)] public int $limit = 25, #[Assert\PositiveOrZero] public int $offset = 0, #[Assert\Regex('/^\d*$/')] public ?string $inn = null, ) {} }Ваш идеал PHP 8.3+?
нашел
use validator::Validate; use lazy_static::lazy_static; use regex::Regex; lazy_static! { static ref ORDER_RE: Regex = Regex::new(r"^(ASC|DESC)$").unwrap(); static ref INN_RE: Regex = Regex::new(r"^\d*$").unwrap(); } #[derive(Debug, Validate)] pub struct SearchParams { // #[Assert\InArray(RequestRepository::SORT_COLUMNS)] #[validate(custom = "validate_sort")] pub sort: String, // #[Assert\Regex('/^ASC|DESC$/')] #[validate(regex(path = "ORDER_RE"))] pub order: String, // #[Assert\Range(min: 1, max: 1000)] #[validate(range(min = 1, max = 1000))] pub limit: i32, // #[Assert\PositiveOrZero] #[validate(range(min = 0))] pub offset: i32, // #[Assert\Regex('/^\d*$/')] #[validate(regex(path = "INN_RE"))] pub inn: Option<String>, } // аналог Assert\InArray(RequestRepository::SORT_COLUMNS) fn validate_sort(sort: &str) -> Result<(), validator::ValidationError> { const SORT_COLUMNS: &[&str] = &["name", "created_at", "updated_at"]; if SORT_COLUMNS.contains(&sort) { Ok(()) } else { let mut err = validator::ValidationError::new("in_array"); err.add_param("value".into(), &sort.to_string()); Err(err) } }

Dhwtj
14.11.2025 13:11ну ок, вот через DSL, даже в IDE контракт будет виден (Requires...)
contract! { /// Requires: a + b >= c && a + b <= 2*c fn use_triple(a: i32, b: i32, c: i32) // Блок ограничений — краткий, рядом с сигнатурой [ (a + b < c) => ERR_LESS, (a + b > 2*c) => ERR_MORE ] { // Основная логика println!("valid triple: {}, {}, {}", a, b, c); } { // Блок форматирования ошибок ERR_LESS(a, b, c) => { write!( f, "invalid combination of A, B, C! A + B need to be less than C, \ but got {} + {} < {}", a, b, c ) }, ERR_MORE(a, b, c) => { write!( f, "invalid combination of A, B, C! A + B cannot be more than 2*C, \ but got {} + {} > 2 * {}", a, b, c ) }, } } fn main() { match use_triple(1, 2, 10) { Ok(()) => println!("ok"), Err(e) => eprintln!("error: {}", e), } }DSL:
use std::fmt; // Мини-DSL для контракта macro_rules! contract { ( fn $name:ident ( $($arg:ident : $ty:ty),* $(,)? ) [ $( ($cond:expr) => $err:ident ),* $(,)? ] $body:block { $( $err2:ident ( $($p:ident),* ) => { $fmt_body:expr } ),* $(,)? } ) => { #[derive(Debug)] pub enum ErrCode { $( $err( $( $ty ),* ) ),* } impl fmt::Display for ErrCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { $( ErrCode::$err2( $( $p ),* ) => { $fmt_body } ),* } } } pub fn $name( $( $arg : $ty ),* ) -> Result<(), ErrCode> { $( if $cond { return Err(ErrCode::$err( $( $arg ),* )); } )* $body Ok(()) } }; }еще можно
// main.rs / lib.rs use contract_macros::contract; #[contract( (a + b < c) => ERR_LESS, (a + b > 2*c) => ERR_MORE, )] fn use_triple(a: i32, b: i32, c: i32) { println!("valid triple: {}, {}, {}", a, b, c); }через процедурные макросы, но реализовать сами макросы сложнее
эй, я так глядишь макросы изучу! )
ну хорошо, вот реализация процедурного макроса (хоть под кат убирай)
use proc_macro::TokenStream; use quote::quote; use syn::{ parse::{Parse, ParseStream}, parse_macro_input, parenthesized, punctuated::Punctuated, Expr, FnArg, Ident, ItemFn, Pat, Token, }; /// Одна запись: (условие) => ERR_NAME struct Rule { cond: Expr, err_ident: Ident, } impl Parse for Rule { fn parse(input: ParseStream) -> syn::Result<Self> { let content; parenthesized!(content in input); let cond: Expr = content.parse()?; let _arrow: Token![=>] = input.parse()?; let err_ident: Ident = input.parse()?; Ok(Rule { cond, err_ident }) } } /// Весь атрибут: #[contract( rule, rule, ... )] struct ContractArgs { rules: Punctuated<Rule, Token![,]>, } impl Parse for ContractArgs { fn parse(input: ParseStream) -> syn::Result<Self> { let rules = Punctuated::<Rule, Token![,]>::parse_terminated(input)?; Ok(ContractArgs { rules }) } } #[proc_macro_attribute] pub fn contract(attr: TokenStream, item: TokenStream) -> TokenStream { let args = parse_macro_input!(attr as ContractArgs); let func = parse_macro_input!(item as ItemFn); // Собираем аргументы функции: имена и типы let mut arg_idents = Vec::<Ident>::new(); let mut arg_types = Vec::<Box<syn::Type>>::new(); for arg in &func.sig.inputs { match arg { FnArg::Typed(pat_type) => { if let Pat::Ident(pat_ident) = &*pat_type.pat { arg_idents.push(pat_ident.ident.clone()); arg_types.push(pat_type.ty.clone()); } else { return syn::Error::new_spanned( &pat_type.pat, "contract: пока поддерживаются только простые идентификаторы как аргументы", ) .to_compile_error() .into(); } } FnArg::Receiver(_) => { return syn::Error::new_spanned( arg, "contract: методы с self не поддерживаются, используйте free-функцию", ) .to_compile_error() .into(); } } } let fn_name = &func.sig.ident; let vis = &func.vis; let inputs = &func.sig.inputs; let body = &func.block; // Имя enum'а с ошибками, просто <имя_функции>Error let err_enum_ident = Ident::new(&format!("{}Error", fn_name), fn_name.span()); // Поля вариантов: a: i32, b: i32, ... let field_defs: Vec<_> = arg_idents .iter() .zip(arg_types.iter()) .map(|(id, ty)| quote! { #id: #ty }) .collect(); let rule_conds = args.rules.iter().map(|r| &r.cond); let rule_err_idents = args.rules.iter().map(|r| &r.err_ident); let arg_names_for_init = &arg_idents; let expanded = quote! { #[derive(Debug)] pub enum #err_enum_ident { #( #rule_err_idents { #(#field_defs),* } ),* } impl std::fmt::Display for #err_enum_ident { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { #( #err_enum_ident::#rule_err_idents { .. } => write!(f, stringify!(#rule_err_idents)) ),* } } } #vis fn #fn_name(#inputs) -> Result<(), #err_enum_ident> { #( if #rule_conds { return Err(#err_enum_ident::#rule_err_idents { #(#arg_names_for_init),* }); } )* #body Ok(()) } }; TokenStream::from(expanded) }(вызов написал выше)
который сгенерит из вызова что-то такое
pub enum use_tripleError { ERR_LESS { a: i32, b: i32, c: i32 }, ERR_MORE { a: i32, b: i32, c: i32 }, } fn use_triple(a: i32, b: i32, c: i32) -> Result<(), use_tripleError> { if a + b < c { return Err(use_tripleError::ERR_LESS { a, b, c }); } if a + b > 2 * c { return Err(use_tripleError::ERR_MORE { a, b, c }); } println!("valid triple: {}, {}, {}", a, b, c); Ok(()) }

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 проект длительностью в год чтобы заменить все падения восстановлениями.

pravosleva
14.11.2025 13:11Ошибка быстрее обнаруживается
Не соглашусь с Вами. Я фронт, пишу в духе Защитного программирования, для меня важно иметь возможность быстро (asap) разобраться, что пошло не так (чтоб не ждать круговорот тикета через тестеров и искать причину по тексту ошибки из бандла). Я понимаю, у каждого своя практика, но исходя из моей практики, ошибку мало обнаружить, надо обработать - так ещё и корректно сообщить интерфейсу (а ещё лучше отправить лог в виде сформулированной проблемы) для возможности быстро разобраться в чем дело (не просто бросить в консоль). Не обязательно делать проверки в каждой функции, достаточно проверить входные данные (к примеру, получив ответ по апишке, можно сразу провалидировать ответ, получив на выходе интуитивно понятное `{ ok: boolean; message?: string; }` - и в случае
!okмы уже не работаем с "хрупкими функциями", а с человекочитаемым сообщением: `"Есть проблема! Некорректный формат ответа на запрос '/me' -> user.policyId (получено: undefined, ожидается: string)"`.А если такой контракт ошибок принят во всем нашем коде как единый стандарт - то, если где-либо что-то пошло не так по итогам стандартной валидации (которая написана один раз и ожидает конфиг с описанием правил на входе), мы уже всегда имеем возможность отправить соответствующие логи, зная что в строке
event.messageвсе уже грамотно сформулировано. При этом проверки будут описаны максимально декларативно и без паранойи )В общем, вкусовщина и полезные привычки (у каждого свои).

inetstar Автор
14.11.2025 13:11Фронт — это вообще отдельное царство, где у каждого пользователя свой браузер, разных версий. Не говоря о том, что пользователи могут любой бред вводить.

pravosleva
14.11.2025 13:11А здесь уже про разное. Ошибки должны быть обработаны независимо от браузера. Я про Защитное программирование - думаю за ним будущее ) вы когда-нибудь бывали на каком-нибудь военном командном пункте какой-нибудь абстрактной установке? Представьте что, для каждой неисправности есть красная лампочка и табличка (это удобно, потому что это стандарт вывода ошибки) - и это модульная система, где каждый модуль легко заменяем и быстро диагностируется

Случайное фото из Гугла (для явной аналогии) 
inetstar Автор
14.11.2025 13:11Браузер — это просто среда исполнения.
Смотрите, в скомпилированной программе код практически всегда исполняется предсказазуемо, а вот на клиенте, да ещё и в интерпретируемом языке, который на разных версиях интерпретаторов исполняется, конечно, нужно обрабатывать как можно большее число ошибок.
При компилированном коде уже один лишний if в «горячем цикле» портит производительность. А во фронте это не так важно.
pravosleva
14.11.2025 13:11Мы про исходный код говорим (про стиль его написания). На клиенте мы увидим продукт сборки в действии (про него мы не говорим)
И не про браузер речь, разумеется, а про "хрупкий" и "надёжный" код (как написанный человеком исходник). Если нужны такие вводные, пожалуйста - мы исходим из того, что транспайлер отработал как надо, код везде выполняется одинаково, говорим только об обработке ошибок входных данных и приведена аналогия "красных лампочек в интерфейсе" как хороший пример ориентира, какая часть системы отказала в данный момент (защитное программирование в действии, "хрупкий" код не даст такого эффекта).
Хотя, допускаю, Вы могли не понять аналогию, ну да ладно.

inetstar Автор
14.11.2025 13:11То что компилятор, транспайлер, обфускатор и т.п. отработали как надо — это даже не обсуждается. Конечно, для разработчика обременённого корпоративными правилами с кругами тестировщиков может быть удобнее в каждую функцию насовать по 100 проверок, только в конечном итоге это приводит к раздутым и тормозящим приложениям. Которые, зачастую, и глючат в придачу.
Подход «Хрустальный код» можно пприменять только тогда, когда ему следуют все участники разработки. И при абсолютно идеентичной исполнимой среде. И когда при падении ничего критичного для бизнеса не происходит.
pravosleva
14.11.2025 13:11Я как раз не сторонник раздувать код.
Описать один раз и переиспользовать много раз декларативно.
Если все делать правильно, не повторяя свой код, он и будет выглядеть и весить соответственно.
Не делайте 100 проверок, делайте одну стандартно.

viordash
14.11.2025 13:11:) только недавно я про подобное писал. Почти согласен с вашим подходом, но все таки инварианты нужно проверять в дебаге. Иначе больше времени уйдет на анализ последствий.

Kahelman
14.11.2025 13:11Идея контрактоноготпрограммирования и в частности язык Eifel, о котором упомянул автор, активно продвигалась после того как у французов ракета Ariane не помню какая не туда улетела. Приколка была в том, что примеры которые приводились в пользу Eifel, не спасали бы ракету от ошибки. Так что контрактное программирование хорошо в теории но не работает не практике

Kahelman
14.11.2025 13:11Туда же, Эрданг, который тут упомянули в комментариях, и который построен на идее fail-fast, (при этом использовался в реальных системах - телекоммуникационных свичах) это не пример «хрустального кода», это пример самого что ни есть защитного программирования- благодаря pattern matching.
Фактически у вас в каждой функции происходит проверка параметров: они либо соответствуют ожиданиям и обрабатываются, или передаются следующей функции. И если подходящего обработчика нет, то вызывается «глобальное исключение» и процесс/программа падает.

inetstar Автор
14.11.2025 13:11Вообще, сильная типизация дополняет идею хрустального кода тем, что выполнение контракта (формальное) отслеживается автоматически компилятором. Но по сути своей она, действительно, защитное программирование.
То есть компилятор отследит, что аозраст >=0, но не отсследит, что больше или равен 18.
Сама идея выдумывалась для малотипизированных языков. В рамках хрустального программироаания возраст проверяется 1 раз при заведении пользователя в систему. Везде далее считается, что он более 18 и не проверяется.

Dhwtj
14.11.2025 13:11Не смешивайте термин "защитное программирование" с термином гарантии в типах

pravosleva
14.11.2025 13:11Ого, тут свои комменты удалять нельзя
[Комментарий удален]
(решил не комментировать обсуждение кода на языке на котором не пишу))

Dhwtj
14.11.2025 13:11type Account struct { balance uint64 // Инвариант: balance >= 0 } func (a *Account) Transfer(amount uint64, to *Account) { // НЕТ проверок — вся ответственность на вызывающей стороне a.balance -= amount to.balance += amount // Если инвариант нарушен — это фатальная ошибка проектирования }Взоржал и упал под лавку! Лежу и ржу.
Никаких ошибок не будет если вычесть больше чем есть. Будет большое число.
Если типы, как здесь, не могут гарантировать инвариант, то код проверки и обработки ошибок долен быть рядом, близко к использованию. Видимо, это пытается опровергнуть автор, зря.
func ProcessUser(user *User) { // "Хрустальный" подход — сразу к делу! // Предполагаем, что user валиден fmt.Printf("Processing user: %s, age: %d\n", user.Name, user.Age) }Это подход "сделать невалидные данные непредставимыми/ невозможными", make illegal states unrepresentable. Если тип User гарантирует валидность. Но го плохой язык для этого. Айда к нам на Раст!
Вот полезная статья
Проектирование типами: Как сделать некорректные состояния невыразимыми

Tim7456
14.11.2025 13:11Хрустальный код не равно fail fast. Падать тоже можно по разному. Хрустальный код в драйвере легко приведет к тому, что после падения вы этот сервер вообще больше не загрузите. Просто будет падать в цикле при загрузке. Т.е. правильно падать тоже надо уметь! Бесконечные циклы и SegFault - зло независимо от парадигмы. А вот отрыгнуть в лог стек вызовов, значения параметров, версии модулей и другую диагностику - это отлично сочетается с fail fast.
Прежде чем даже думать о Хрустальном коде стоит определиться что в своем проекте вы можете гарантировать. В условиях любого нетривиального проекта в будущем вас ожидают изменения в требованиях и добавления новой функциональности. Тут уже неважно 1 разоаботчик код пишет, или несколько. Через полгода вы половину соглашений о значениях аргументов просто забудете и начнете неявно нарушать.
Ну и в принципе, идеологию типа fail fast можно использовать ТОЛЬКО если у вас цена выпуска новой версии и обновления работающих инсталяций равна нулю. А вот если вы свой софт отправляете заказчику на CD и ждете когда он там обновиться - тут с идеологией надо по другому определяться.

inetstar Автор
14.11.2025 13:11Да. В начале статьи я написал, что для собственных проектов использую.
Если соглашения и контракты забываются, то подход не применим.
Спасибо за коммент. Добавил в статью.

Wesha
14.11.2025 13:11Хрустальный код в драйвере легко приведет к тому, что после падения вы этот сервер вообще больше не загрузите.

BakaVaka
14.11.2025 13:11Ну не знаю.
Сейчас будет история из жизни.
В Avalonia примерно так поступили.
И знаете, очень весело, когда из-за их реализации Flyout'ов для TextBox'ов, пользователь нажимает ПКМ-вставить в MaskedTextBox, а у тебя приложение падает. И в логе у тебя просто IndexOutOfRangeException, потому что в релизе отключен вывод трассировки стека.
А веселья этому всему добавляет то, что у Avalonia изначально не было механизма перехвата исключения которое произошло в UI при отрисовке.
Вот. Упало у пользователя. Он объяснить что делал не может. Встроить трейс в то, что там Avalonia делает - вы тоже просто так не можете.
Документация Avalonia при этом довольно скудна.
Как говорится: "Удачной отладки".
И решение ты в результате вообще находишь когда изучаешь как реализованы стили компонентов и видишь что по какой-то, только богу известной причине, Flyout который используется для меню вставки - дергает метод базового TextBox'а, который не проверяет маску, и соответственно, если пользователь после вставки попробует изменить вставленный текст - получаешь тот самый IndexOutOfRangeException.
Я это все к чему написал.
К тому что fail-fast - это все конечно хорошо. Но, по моему мнению, только в случае, если есть:
1. Хороший механизм трассировки, позволяющий быстро локализовать проблему.
2. Абсолютное большинство кода который есть в приложении контролируешь ты.
inetstar Автор
14.11.2025 13:11В целом ко фронту всё это менее применимо. Об этом сказано вначале статьи.
Там где может быть неадекватный ввод, то, конечно, нужно всё тщательно проверять.

Dhwtj
14.11.2025 13:11fail-fast только при наличии супервайзера. В эрланг он есть. Там где его нет fail-fast нинада

Siemargl
14.11.2025 13:11Собственно бред по ряду причин.
Тема достаточно проработана, чтобы не изобретать хрустальные велосипеды.
Контракты, например, изобрёл ещё Мейер 40 лет назад.

inetstar Автор
14.11.2025 13:11В статье больше не про контракты, а про минимизацию проверок, которыми часто злоупотребляют в ущерб скорости.

Siemargl
14.11.2025 13:11Ох уж эти сторонники экономии на спичках.
Правильное проектирование позволит исключить избыточные проверки. Все остальное - надумано "по мотивам"

Ndochp
14.11.2025 13:11Не позволяет в языках, где может влетать что угодно. Ты сколько угодно можешь рассказывать тимлиду, что есть публичные функции, в них может что угодно прилететь, а есть приватные, которые только мы сами вызываем. А он тебе ответит, что не только мы сами, но и наш стажер. Так что проверяем все на каждом уровне стэка и не выёживаемся.

Siemargl
14.11.2025 13:11Для этого поможет правильная модульность и/или объектная архитектура. Все одно - проектирование.
Но есть защита от дурака, но только от неизобретательного (с)
Да и самый простой вариант - не дробить лишний раз функции. Меньше функций - меньше инвариантов.
Но я тоже придерживаюсь идеи, что лишний ассерт не тянет.

Wesha
14.11.2025 13:11есть публичные функции, в них может что угодно прилететь, а есть приватные, которые только мы сами вызываем
...а потом вторые медленно превращаются в первые.

RS6
14.11.2025 13:11Все проверки делаются в одном месте — там, где данные появляются из подверженного ошибкам источника.
У вас принципиальная, фундаментальная ошибка в умозаключениях состоит в том, что ошибочные данные якобы могут быть получены только в результате IO операции. На самом деле невалидные даные могут появляться в совершенно произвольные моменты времени в ходе обработки валидных входных данных. Потому что хм.. в програмах возможны ошибки. Валидация параметров функций как раз таки позволяет следовать fail fast идеологии для ошибок любого рода, а ваш подход fail fast только для невалидного ввода, в остальных случаях - отложенная и накопленная ошибка, которая стрельнет неизвестно где.
Я согласен с тем, что ручная проверка аргументов в начале каждого метода - это муторный и скучный код с высокой степенью дупликации и так же высокой вероятностью ошибок. Поэтому проверки должны обеспечиваться системой типов. Если вы не полененитесь и напишите специфичные типы для всех своих данных и перестанете передавать всюду примитивные типы, то это обеспечит автоматическую проверку большинства инвариантов.

inetstar Автор
14.11.2025 13:11Ну а как тогда предлагаете делать быстрые программы? Если всё проверять, то будут тормоза.
Да и выглядеть будет такой раздутый код отвратительно.

RS6
14.11.2025 13:11Как обычно - здравый смысл. И профилирование. Если вы оптимизируете "горячий цикл", то конечно вырежете все проверки кроме необходимых. Но в большинстве случаев это просто не нужно, достаточно помнить, что проверка в памяти это на много порядков быстрее любого IO, особенно сетевого. Быстрые программы это все-таки прежде всего правильная архитектура и алгоритмы, а не экономия на проверках.
Да и выглядеть будет такой раздутый код отвратительно.
"Раздутый" - это вместо примитивных типов использующий специфические? Может это дело вкуса, но я отвратительного в этом не вижу.

Siemargl
14.11.2025 13:11Любую идею можно довести до абсурда.
Пробегала серия статей, когда для каждой сущности создавался тип с констрейнтами на содержимое и взаимодействие с другими типами.
В результирующем коде получилась в 10 или 20 раз больше Loc.

Dr_Faksov
14.11.2025 13:11Ну а как тогда предлагаете делать быстрые программы?
А тут как раз надо ловить баланс между ущербом от тормозов, и ущербом от ошибки в результате. В этом и состоит искусство. А вообще говоря, хорошо-бы понимать "физику процесса". Чего у вас не может быть на выходе.

panzerfaust
14.11.2025 13:11Вот демонстрация того, зачем нужно знакомиться со старыми книжками и статьями по best practices, даже если все кричат, что они устарели. Чтобы велосипед не изобретать раз за разом. Да, многие тезисы из Мартина, Фаулера, Нюгарда, Клеппмана и т.д. или неприменимы, или вообще полная ересь - но именно почитать и поразмыслить не повредит. А то глядишь через пару лет
зумерыальфы начнут изобретать джаваскрипт с аджайлом по второму кругу.

Dr_Faksov
14.11.2025 13:11Если метод должен принимать дату, то мы не проверяем, что пришла именно дата. Раз в спецификации сказано, что придёт определённый тип данных, то мы слепо верим, что именно этот тип данных и придёт.
Ура! Я теперь знаю как назывался метод по которому писалось ПО для системы стабилизации первого японского орбитального телескопа ( полностью) и европейской миссии на Марс - частично. Это "хрустальный код". И да, он был высокоскоростным.
Система наведения и стабилизации раскрутила телескоп до скоростей, когда его разорвало центробежными силами. Там всё было прекрасно. И использования в расчётах данных выбега конвейера, (какие блокировки, все и так знают, что данные можно брать только с пятого выданного значения после каждого сброса) и использование значений без знака как значения со знаком при передаче из одного модуля в другой (но мы же знаем, что все знают, что куда передаётся, зачем проверять) . И много другого. Принцип Fail Fast! был выполнен полностью. Но как быстро оно его крутило!
Высотомер большой дальности для посадки на Марс считался нерабочим ( и переключался на резервный) только если он не выдавал вообще никаких отсчётов высоты. Какие-то отсчёты свидетельствовали о полной исправности, даже отрицательные. А высокоточный посадочный дальномер должен был включался на высоте 20 метров. В коде так и написали "если высота равна 20 метров, включить дальномер".
Говоря проще, вы никогда не можете быть уверены, что взятые для обработки данные верны по умолчанию.
Автор, к вам никаких вопросов, вы честно написали - "не для космоса". Просто вспомнилось.

viordash
14.11.2025 13:11я не знаком хорошо с указанными вами ситуациями, но судя по характеру проблем налицо отсутствие достаточного юнит-тестирования.
mixsture
Ну наоборот же!
Если функция проверяет входные параметры - она сразу выругается о них и сделает это в человекочитаемом виде. Если не проверяет - где-то в глубинах других функций вы получите ошибку с малозначащим описанием: например "деление на ноль" - которое только с отладчиком можно превратить во что-то понятное, потратив час(ы): ага, вот тут подали отрицательное число на вход, а в функции этот параметр дальше перекладывается в беззнаковое число, что неявно приводит его к нулю и дальше в вызовы передается ноль и где-то там через 3 вызова получается деление на ноль.
Поэтому проверка входных параметров - это как раз fail-fast
inetstar Автор
Спасибо за найденную неточность!. Идея была в том, что какие-то проверки кроме самих проверок могут делать что-то с данными. Делать их «корректнее». Например nil преобразовать в 0. Потом куда-то ещё передать результат построенный на неверном входе. И поэтому потеряется изначальная ошибка. И найти её будет невероятно трудно.
Neka_D
Но в статье говорится именно про guard ifs... А подгонка данных под обработку называется нормализацией
Wesha
ChatGPT, изыди из этого аффтара!