Совершенный код: дефолты в свитчах

Читать в полной версии →

Свитч — очень простая конструкция, которую изучают программисты в самом начале своего пути. Она ни у кого не вызывает вопросов, но с ней связана одна интересная деталь, которую очень часто упускают из виду и, в итоге, используют свитч неправильно. Это дефолтное поведение.

Представьте, что в приложении у нас есть заказ, который может находиться в определенных состояниях. Например, он может быть оплачен, доставлен, передан в службу доставки и так далее. И где-то есть логика, зависящая от этого состояния:

switch (order.state) {
  case 'delivered':
    // код
    break; 
  case 'paid':
    // код
    break;
  // Ниже все остальные кейсы
}

По логике приложения, в этом коде задействованы все возможные состояния, и на каждое из них есть свое поведение. При такой постановке switch не содержит дефолтного поведения.

Однако линтер с нами не согласится. Практически во всех языках он начнет ругаться на то, что дефолт не определен. С этого момента есть два пути.

Начнем с неправильного. Программист думает, что линтер ошибается, и либо заглушает его, либо добавляет туда код, который ничего не делает. Например, возвращает null или просто содержит один break.

Так ли ошибается линтер? Путь, описанный выше, чисто механический. Он не учитывает семантику кода (привет, ментальное программирование) и порождает отложенные проблемы.

Подписывайтесь на канал Кирилла Мокевнина в Telegram — чтобы узнать больше о программировании и профессиональном пути разработчика

Главный вопрос, который нужно задать себе в этом месте: «А в каком случае мы туда действительно попадем?» Таких вариантов несколько: программист ошибся в имени состояния либо добавилось новое состояние, на которое еще нет обработчика. Считаем ли мы такое поведение нормальным? В подавляющем большинстве случаев – нет. Это так называемая ошибка программирования. Если программист ошибся, значит программа работает неверно, и лучшим решением в данной ситуации будет максимально быстрое оповещение о проблеме. Это позволит локализовать проблему и отреагировать сразу, как только она появилась. В противном же случае программа может продолжать вести себя «почти» корректно. Иногда корректно, а иногда нет, и узнаем мы об этом, скорее всего, не сразу и, в худшем случае, от клиентов, когда уже возникнут более серьезные проблемы. То же самое касается добавления нового состояния. Наверняка оно потребует своей собственной обработки.

Перепишем switch с учетом вышесказанного:

switch (order.state) {
  case 'delivered':
    // код
    break; 
  case 'paid':
    // код
    break;
  // оставшиеся кейсы
  default:
    throw new Error('Unknown state!');
}

Уже значительно лучше, но все еще недостаточно. Любой код, который связан с ошибками, должен помогать отладке. Код выше этого не делает. Да, мы увидим ошибку сразу, но как мы узнаем, а что там было? Куда смотреть? Какое состояние неверное? Исследования потребуют дополнительного времени. Гораздо лучше помочь себе сразу:

switch (order.state) {
  case 'delivered':
    // код
    break; 
  case 'paid':
    // код
    break;
  // оставшиеся кейсы
  default:
    throw new Error(`Unknown order state: '${order.state}'!`);
}

Если кратко, то общее правило такое: если логика кода предполагает поведение по умолчанию, то реализуйте в дефолте необходимую логику, если нет, то это ошибочная ситуация, которую надо обработать, выбросив исключение.

Дополнительные материалы: