Блоки else: почему стоит от них отказаться

2
751
views

Перевод статьи «Write better code and be a better programmer by NEVER USING else statements».

Photo by Remy_Loz on Unsplash

Я профессионально занимаюсь программированием последние несколько лет. За это время мое мастерство разработчика существенно возросло. Я начинал как стажер, а теперь стал ведущим разработчиком, ответственным за линейку продуктов, которыми пользуются больше 160 тысяч человек в 140 разных странах.

Недавно я проанализировал код, который писал за эти годы (конечно, только тот, к которому у меня все еще есть доступ). При написании всех этих программ я пользовался разными языками: Haskell, Scala, Go, Python, Java и JavaScript. Но во всех кодовых базах я заметил одну тенденцию: я практически не использовал блоки else.

Могу сказать, что моя нелюбовь к предложениям else имеет логическое обоснование. Я считаю, что эти предложения не следует использовать, а их наличие стоит рассматривать как запах кода. Для этого есть две причины. Во-первых, блоки else нарушают правило прямой видимости, а во-вторых, они всегда оторваны от контекста.

Правило прямой видимости

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

«Программы предназначены для того, чтобы их читали люди, и лишь попутно — для исполнения компьютерами».

Но проблема в том, что читаемость кода — понятие субъективное. Довольно сложно точно определить, что именно делает код читаемым. Но тут мы можем опереться на правило прямой видимости (англ. line-of-sight rule). Оно пользуется большой популярностью в сообществе Go. Мэт Райер хорошо объясняет это правило в своей речи и статье. Если говорить по-простому, идея в том, что «наиболее удачный путь» в коде должен писаться с минимальным отступом.

И наоборот, любая обработка ошибок или код, описывающий особый случай, должны иметь большие отступы.

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

И как же с этим связаны предложения else?

Проблема этих предложений в том, что они сдвигают код на один отступ дальше. И внезапно становится непонятно, какой код относится к «наиболее удачному пути», а какой — к особому случаю.

Этот недостаток ясности затрудняет беглое чтение кода и, следовательно, вредит читаемости.

Отсутствие контекста

Умение быстро и эффективно читать код очень важно. Ключевая часть этой задачи — умение улавливать суть каждого маленького раздела кода. В идеале для понимания отдельной части кодовой базы разработчик не должен вчитываться в каждую строчку.

Блоки else затрудняют эту задачу, поскольку они стоят на некотором расстоянии от if-условия и кода в этом условии. Лучше всего это показать на примерах. Посмотрите на этот код и скажите, можете ли вы сразу понять, что происходит при выполнении этих трех строк кода?

if myVariable == nil { 
    return “”
}

Надеюсь, для вас это очевидно. А теперь давайте для контраста рассмотрим второй пример:

} else { 
    return “”
}

Не видя блока if, мы не можем определить, что именно делает этот код. Почему он возвращает пустую строку? Это ошибка или «нормальное» поведение? Чтобы понять этот блок кода, мы должны прочесть и запомнить предыдущий контекст. Это не имеет большого значения, если предложения маленькие. Но если в блоке if { … } у нас сложная логика или если нам нужно просмотреть код быстро, отделение контекста от кода может существенно ухудшить читаемость. Ситуация становится еще хуже, если у вас есть вложенные предложения if/else или если их просто много в одной функции (к какому предложению if относится это предложение else?).

Как избавиться от предложений else?

Итак, надеюсь, я вас убедил, что предложения else это мусор. Но знание этого факта само по себе нам не слишком помогает. Главный фокус в том, как избежать написания этих блоков. Для этого у нас есть два пути:

  • инвертирование if-условий и ранние возвраты (return),
  • создание вспомогательный функций.

Инвертирование условия

Это самый распространенный вариант, который мне встречался. Он также может быть в двух разных видах: else может быть скрытым или явным. «Явная» версия выглядит следующим образом:

func doSomething() error {
  if something.OK() {
    err := something.Do()
    if err != nil {
      return err
    }
  } else {
    return nil, errors.New("something isn't ok")
  }
}

«Скрытая» версия похожа, но там нет предложения else как такового. Там просто отбрасывается конец функции, в результате чего else как бы подразумевается. Это чаще встречается в Python или JavaScript, где, если ничего не указано явно, возвращается None или undefined.

function doSomething() {
  if (something.OK()) {
    return something.Do()
  }
}

Опять же, при таком формате записи у нас все равно нет четкого понимания всего поведения целиком. Без прочтения всей функции возвращаемые значения не ясны.

Но если просто инвертировать условие if, мы избавляемся от этих проблем.

function doSomething() {
  if (!something.OK()) {
    // return or throw error
  }
  return something.Do()
}

Теперь мы можем бегло просмотреть функцию и ясно увидеть условие в блоке с отступом и нормальное поведение, запись которого соответствует правилу прямой видимости. Поведение функции понятно, к тому же мы не отрываем контекст. Так получается намного лучше.

Вспомогательные функции

Бывают также предложения else, не дающие прямого результата в return. Обычно это встречается в не изолированной должным образом логике особых случаев.

Например:

 let charities
  if (country != "") {
    if (tier != "") {
      charities = getCharitiesByCampaignCountryAndTier(campaign, country, tier)
    } else {
      charities = getCharitiesByCampaignAndCountry(campaign, country)
    }
  } else {
    charities = getCharitiesByCampaign(campaign)
  }

  // do something with charities

Читаемость этого кода можно улучшить, если выделить логику получения пожертвований в отдельную функцию. Тогда можно будет должным образом обрабатывать особые случаи и сделать ранний возврат. А если инвертировать некоторые if-предложения, то можно сделать этот код еще лучше.

Например:

function getCharities(campaign, country, tier) {
  if (country == "") {
    return getCharitiesByCampaign(campaign)
  }

  if (tier == "") {
    return getCharitiesByCampaignAndCountry(campaign, country)
  }

  return getCharitiesByCampaignCountryAndTier(campaign, country, tier)
}

Эта вспомогательная функция ловко инкапсулирует всю нужную нам логику, устраняет необходимость любых else-предложений и помогает смещать код «наиболее удачного пути» влево. Благодаря всему этому существенно облегчается беглый просмотр кода, то есть повышается читаемость.

Заключение

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

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

Несколько оговорок для педантов:

  • В SQL CASE WHEN … ELSE … обойти не удастся.
  • В Scala из-за неявных возвратов (избегания операторов return) вам придется использовать блоки else, потому что у вас попросту не будет возможности «раннего возврата».
  • С тернарными операторами нет никаких проблем.
  • В Python тернарный оператор использует else. Это тоже нормально.

2 КОММЕНТАРИИ

  1. На первом скриншоте как специально — условие через отрицание.
    Но, вообще согласен.

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here