Условия, null и явные контракты
Структуры в коде — Структуры данных; persistence — ORM.
Три связанные темы — быстрый провал, операторы if и null — про одно: ветвления и "пустота" должны быть частью модели, а не размазаны по коду. Это продолжение MAPPER и дополнение к цикломатической сложности.
Принцип быстрого провала (Fail Fast)
Ошибочное состояние должно всплыть сразу, у границы системы, с понятным сообщением. Тихий return null или default в switch "на всякий случай" откладывает катастрофу.
public void Transfer(Money amount, Account target) {
if (amount.IsNegative)
throw new ArgumentException("Amount must be positive");
if (target is null)
throw new ArgumentNullException(nameof(target));
// ...
}
На практике:
- предусловия вместо глубоких проверок внутри;
- строгие типы параметров (не
stringтам, гдеEmail); - без
default, если все вариантыswitchдолжны быть исчерпывающими; - не менять коллекцию во время обхода;
- рефакторинг отдельно от новой функциональности (один PR — одна цель; см. легаси).
Зачем меньше if
Каждый if — дополнительный путь в графе потока управления. Цель не «запретить if», а убрать несущественные ветки:
| Запах | Лучше |
|---|---|
Флаги isX, hasY + цепочка if | Полиморфизм, strategy, state |
switch по типу/коду | Таблица обработчиков, объекты |
Булевы переменные только для if | Предикат с именем или объект правила |
| Вложенные "стрелы" | Guard clauses, ранний return |
Сравнение с true/false | Прямое условие или именованный предикат |
// До: жёстко запрограммированное правило
if (user.Age < 18 && movie.Rating == "AdultsOnly") { Deny(); }
// После: правило у объекта политики
movie.Rating.EnsureAllowedFor(user);
Подробный указатель приёмов для условий — Справочник тем чистого кода.
Связь с Фаулером: Replace Conditional with Polymorphism, Decompose Conditional — Методы рефакторинга программного кода.
Проблема null
Ссылки, допускающие null, часто смешивают три смысла — "нет значения", "неизвестно", "ошибка".
Альтернативы:
| Подход | Когда |
|---|---|
| Null Object | Пустое поведение вместо null (NullLogger, пустой список) |
| Optional / Maybe | Явно "может отсутствовать" в API |
| Коллекция | 0 или 1 элемент вместо null ссылки |
| Исключение | Невозможное состояние на границе |
| Result / Either | Ожидаемый отказ без исключения |
// Optional chaining маскирует дыру в модели
const city = user?.address?.city ?? "";
// Явная модель
const city = user.knownAddress().map(a => a.city()).orElse("");
Практики: Null Object; убрать лишние ?.; не хранить "неизвестное местоположение" как null GPS.
В C# 8+ — nullable reference types; в Kotlin — non-null по умолчанию. Всё равно нужна доменная дисциплина, а не только аннотации.
Декларативность условий
Разделяйте что проверяем и как исправляем — см. Декларативный стиль. Имя isEligibleForDiscount лучше, чем комментарий над if (a && !b || c).
Регулярные выражения и парсеры — с именованными шагами или готовыми типами (Email.parse), иначе условие становится нечитаемым "супом".
Метрики и ревью
- Рост цикломатической сложности на diff — повод открыть статью 2.
- Новые
switchпоenumбезdefault— хорошо, если enum закрыт; иначе — риск тихого пропуска. - Каждый новый
?в TypeScript/C# — вопрос: есть ли понятие в домене?
Куда дальше
- Связанность и глобалы — когда
ifразмазан из-за Singleton и god object. - Исключения — ошибки без
nullи безgoto. - Справочник тем.