Рефакторинг длинных цепочек if...else в JavaScript

Введение
Условные операторы — основа JavaScript. Они позволяют выполнить код в зависимости от условия true/false. Но при длинных цепочках if…else читаемость быстро страдает. В этой статье показаны приёмы для упрощения таких конструкций: guard clauses, ранние return, вынос логики в функции и альтернативные подходы.
Кратко: цель — уменьшить вложенность и сделать намерения очевидными.
Проблема: сложные вложенные цепочки
Рассмотрим исходный пример с вложенной логикой проверки возраста:
function canDrink(person) {
if (person?.age != null) {
if (person.age < 18) {
console.log("Still too young")
} else if (person.age < 21) {
console.log("Not in the US")
} else {
console.log("Allowed to drink")
}
} else {
console.log("You're not a person")
}
}
const person = { age: 22 }
canDrink(person)Логика понятна, но вложенность мешает быстрому чтению. Разработчику приходится читать несколько уровней, чтобы понять исключительные случаи. Дальше мы поэтапно улучшим этот код.
Охранные проверки (Guard clauses)
Если внешний if покровительствует всей функции, чаще всего его можно превратить в охранную проверку — то есть выход из функции при невыполнении условия. Это уменьшает уровень вложенности.
Пример с guard clause:
function canDrinkBetter(person) {
if (person?.age == null) return console.log("You're not a person")
if (person.age < 18) {
console.log("Still too young")
} else if (person.age < 21) {
console.log("Not in the US")
} else {
console.log("Allowed to drink")
}
}Теперь ключевой путь чтения расположен слева, а исключения — обработаны вверху. Код легче воспринимать.
Ранние return вместо единственного return
Строгое правило «один return в функции» часто заставляет писать лишнюю вложенность. Контрпример: несколько ранних return делают намерения ясными и избавляют от else-блоков.
function canDrinkBetter(person) {
if (person?.age == null) return console.log("You're not a person")
if (person.age < 18) {
console.log("Still too young")
return
}
if (person.age < 21) {
console.log("Not in the US")
return
}
console.log("Allowed to drink")
}Преимущество — каждый условный блок завершает выполнение, и вам не нужно читать скрытую логику в else.
Вынос логики в отдельные функции
Если функция выполняет несколько задач, вынос их в отдельные, узкие по ответственности функции улучшает тестируемость и читаемость.
Функция, которая возвращает строку-результат:
function canDrinkResult(age) {
if (age < 18) return "Still too young"
if (age < 21) return "Not in the US"
return "Allowed to drink"
}
function canDrinkBetter(person) {
if (person?.age == null) return console.log("You're not a person")
const result = canDrinkResult(person.age)
console.log(result)
}Разделив проверку и форматирование/логику вывода, вы делаете код модульным и удобным для тестов.
Альтернативы if…else
Иногда лучше использовать другие подходы:
- switch — удобно для дискретных значений;
- таблица соответствий (lookup table) — для сопоставления вход => действие без сложных условий;
- объектно-ориентированный подход / полиморфизм — для сложной поведенческой логики;
- функции высшего порядка — для композиции правил.
Пример таблицы соответствий для простых состояний:
const ageRules = [
{ check: age => age < 18, result: () => "Still too young" },
{ check: age => age < 21, result: () => "Not in the US" },
{ check: () => true, result: () => "Allowed to drink" }
]
function canDrinkByTable(person) {
if (person?.age == null) return console.log("You're not a person")
const rule = ageRules.find(r => r.check(person.age))
console.log(rule.result())
}Таблица упрощает добавление новых правил и делает поведение очевидным.
Когда рефакторинг может не подойти
Counterexamples / случаи, когда guard clauses и множественные return не лучшие:
- Нужна единая точка выхода для выполнения завершающих действий (очистка ресурсов, единый логинг) — тогда один return оправдан.
- Очень тонкая оптимизация стека вызовов или специфические требования к трассировке — редкие практические случаи.
В таких ситуациях используйте шаблон try/finally или гарантируйте выполнение завершающих операций в одном месте.
Ментальные модели и эвристики
- Читай сверху вниз: сначала обработай исключения, затем основной путь исполнения. Это уменьшает когнитивную нагрузку.
- Правило трёх: если внутри функции более трёх уровней вложенности, подумайте о рефакторинге.
- Single responsibility: функция должна решать одну задачу. Если есть несколько причин для изменения функции — разделяйте.
Мини‑методика рефакторинга условных цепочек
- Найдите внешние проверки, оборачивающие весь код. Превратите их в guard clauses.
- Замените вложенные else на ранние return там, где это безопасно.
- Вынесите длинную логику в вспомогательные функции с понятными именами.
- Рассмотрите таблицу правил или switch для дискретных случаев.
- Напишите юнит‑тесты для каждой ветки.
- Проверяйте: уменьшилась ли вложенность? стало ли понятнее сообщение об ошибке?
Чеклист для роли: разработчик / ревьюер
Разработчику:
- Убираю лишнюю вложенность через guard clause.
- Выношу логику в отдельную функцию при росте сложности.
- Добавляю тесты для каждой ветки.
Ревьюеру:
- Видна ли основная последовательность выполнения сразу после чтения кода?
- Нет ли скрытых побочных эффектов в ранних return?
- Соответствует ли поведение требованиям и нет ли регрессий?
Критерии приёмки
- Читаемость: основной путь выполнения должен быть очевиден.
- Тесты: покрыты все основные ветки и исключения.
- Поддержка: добавление нового правила занимает минимальное число изменений.
Словарь в одну строку
- Guard clause — ранняя проверка, которая сразу прекращает выполнение, если условие не выполнено.
- Lookup table — структура данных, связывающая условие с действием.
- Полиморфизм — замена условных разветвлений распределением поведения по типам объектов.
Риски и смягчение
- Несвоевременный выход из функции может пропустить очистку ресурсов. Решение: использовать finally или централизованную очистку.
- Множество return усложняет трассировку стека. Решение: логирование в начале и конце функции.
Примеры альтернатив — switch и обработка по объектам
Switch (иногда полезен для диапазонов с дополнительной логикой):
function canDrinkSwitch(person) {
if (person?.age == null) return console.log("You're not a person")
switch (true) {
case person.age < 18:
console.log("Still too young")
break
case person.age < 21:
console.log("Not in the US")
break
default:
console.log("Allowed to drink")
}
}Полиморфизм (пример для расширяемости):
class PersonChecker {
constructor(person) { this.person = person }
check() { return "Default" }
}
class AgeChecker extends PersonChecker {
check() {
const age = this.person?.age
if (age == null) return "You're not a person"
if (age < 18) return "Still too young"
if (age < 21) return "Not in the US"
return "Allowed to drink"
}
}
const checker = new AgeChecker({ age: 22 })
console.log(checker.check())Полиморфизм полезен, если правила зависят от типа объекта и часто растут.
Краткое резюме
Используйте guard clauses и ранние return, чтобы убрать вложенность. Вынесите проверочную логику в отдельные функции или таблицы правил. Рассмотрите switch, lookup table или полиморфизм при усложнении условий. Всегда покрывайте ветки тестами и проверяйте, что критические операции выполняются независимо от того, где происходит выход из функции.
Важно: нет универсального рецепта. Выберите подход, который делает код понятным и поддерживаемым для вашей команды.
Похожие материалы
Как увидеть время отправки сообщения на iPhone
Макрофотография дешево — практическое руководство
Изменить hostname в Ubuntu — краткое руководство
Как изменить поля в Google Docs — инструкция
Бюджет с нулевым остатком — как составить