Часто бывает необходимо выполнить определенную функцию только один раз в программе. Это простая и распространенная ситуация, с которой часто приходится сталкиваться при разработке программного обеспечения. Примером может служить приветственное сообщение, отображаемое на веб-сайте при первой регистрации пользователя. Или это может быть кнопка покупки на кассе, которая должна списывать деньги только один раз (даже при многократном нажатии). Другим примером может быть метрика аналитики, которая должна сообщаться один раз на протяжении всего пути пользователя.

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

Решение 1. Используйте глобальное состояние:

Это может быть наиболее очевидным и простым способом убедиться, что функция выполняется только один раз. глобальную переменную можно использовать для отслеживания функции, чтобы указать, была ли она уже выполнена или нет. флаг может быть инициализирован как false внутри функции, он будет переключаться, чтобы предотвратить последующие выполнения. Ниже приведена демонстрация такого решения:

let isUserWelcomed = false
const welcomeUserOnce = (user) => {
 if (!isUserWelcomed) {
   isUserWelcomed = true
   console.log("welcome to home", user)
 }
}

Это правильное решение, наша функция приветствия будет запущена только один раз, но есть возможности для улучшения:

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

Решение 2. Используйте локальное состояние:

Ранее мы видели, что первое решение плохо из-за использования глобального состояния. Мы можем улучшить его, сделав переменную состояния isUserWelcomed приватной переменной. Кроме того, мы можем использовать замыкание, чтобы скрыть переменную состояния, чтобы она была недоступна снаружи функции.

const welcomeUserOnce = ((isUserWelcomed) => {
 return (user) => {
 if (!isUserWelcomed) {
   isUserWelcomed = true
   console.log("welcome to home", user)
   }
 }
})(false)

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

Я считаю, что это можно улучшить, сделав расширяемой логику «выполнить один раз». На данный момент, если какой-либо другой фрагмент кода необходимо запустить один раз, мы должны повторить эту логику. Но, будучи хорошим программистом, мы не должны повторяться или, другими словами, наш код должен быть СУХИМ (советуем не повторяться).

Решение 3. Функциональный подход:

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

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

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

const once = fn => {
  let done = false
  return (…args) => {
    if (!done) {
      done = true
      fn(…args)
    }
  }
}

Давайте внимательно посмотрим, почему и как это работает:

  • Сигнатура функции once говорит о том, что она принимает функцию в качестве параметра и возвращает новую функцию.
  • Существует локальная переменная done для отслеживания выполнения переданной функции или нет.
  • Наконец, возвращается новая функция с нулем или более параметрами. Для этого мы использовали синтаксис spread operator.

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

  • Функцию once можно повторно использовать, чтобы превратить любую функцию любого типа в функцию "выполнить один раз". Нам не нужно ничего менять внутри функций. В следующем примере показано использование нашей функции once:
const greet = (name) => {
	console.log('Hello ' + name)
}

const greetOnce = once(greet)

greetOnce('Alice')
// Output: Hello Alice

greetOnce('Bob')
// Output:
  • Мы не полагаемся на глобальное или внешнее состояние. Все необходимое содержится в функции once. Это сделает тестирование проще и легче.
  • На самом деле все вышеперечисленные преимущества и качества можно свести к одному слову: Чистота. Функция once лучше, потому что она чиста.

Чистая функция всегда возвращает один и тот же результат, если заданы одни и те же параметры. И чистая функция не имеет побочных эффектов (без изменения локальных статических переменных, нелокальных переменных, изменяемых ссылочных аргументов или потоков ввода/вывода).

Вывод:

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

Я надеюсь, что вы нашли этот пост полезным и интересным. Делитесь, хлопайте и комментируйте. Очень жду ваших отзывов и мыслей.