Обещания: Пять советов, чтобы работать умнее

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

Вот несколько советов, которые могут улучшить качество и читабельность кода в вашем проекте.

1. Вам не нужна* эта анонимная функция

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

getZero()
  .then((number) => addOne(number))
  .then((number) => addTwo(number))
  .then((number) => addThree(number))
  .then((number) => console.log(number));

// Let's clean it up. Now we can read it like a sentence.
getZero()
  .then(addOne)
  .then(addTwo)
  .then(addThree)
  .then(console.log);
Вход в полноэкранный режим Выход из полноэкранного режима

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

portUsed.check(port)
  .then(rejectIfUsed)
  .then(loadEnvironmentConfig)
  .then(startMonitor)
  .then(startServer)
  .then(logServerDetails)
  .catch(reportError);
Вход в полноэкранный режим Выход из полноэкранного режима

* Иногда нужна анонимная функция

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

const state = {
  color: 'blue',
  setColor(newColor) {
    this.color = newColor;
  },
};

// We need some information not in the promise chain
getSystemData()
  .then((config) => showColorConfig(state.color, config));
Вход в полноэкранный режим Выход из полноэкранного режима

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

// Bind example to access state outside the chain
getSystemData()
  .then(showColorConfig.bind(null, state.color));
Вход в полноэкранный режим Выход из полноэкранного режима

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

// Regular method on an instance or object.
state.setColor('yellow');

// BAD, "state" context gets lost when setColor is called.
// "this" inside setColor is not what we expect.
getColor()
  .then(state.setColor);

// Good, preserve method context
getColor()
  .then((color) => state.setColor(color));

// Also OK, bind method context
getColor()
  .then(state.setColor.bind(state));
Войти в полноэкранный режим Выход из полноэкранного режима

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

2. Обещать все

Одна из важнейших особенностей Promises — продолжение цепочки обещаний. Любой ответ функции, не относящейся к Promise, оборачивается в разрешенное обещание, поэтому цепочки обещаний автоматически позволяют любой функции в цепочке быть синхронной или асинхронной.

Все, что вам нужно сделать, чтобы получить это преимущество, — начать с Promise. Использование Promise.resolve() — это простой способ надежно открыть цепочку обещаний. После этого вам не нужно знать, является ли что-то асинхронным; оно просто работает.

Promise.resolve()
  .then(getFirstValue)
  .then(addOne)
  .then(addRandomValue)
  .then(saveValueToServer)
  .then(displayValue)
  .catch(displayError);
Вход в полноэкранный режим Выход из полноэкранного режима

С первого взгляда невозможно определить, какая из этих функций является асинхронной, и на таком высоком уровне это не имеет значения. Вы все равно можете прочитать поток кода, и он просто работает. По мере роста и изменения проекта очень просто изменить функции с sync на async, если они уже находятся в цепочке обещаний.

Запуск цепочки с Promise.resolve() означает, что даже самое первое действие в вашей цепочке обещаний — то, которое почти гарантированно будет асинхронным — может быть заменено на синхронную версию. Это может быть большим преимуществом при написании тестов.

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

3. Try/Catch бесплатно

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

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

В обработке ошибок есть много деталей и опций, поэтому давайте немного разберем их.

Основы

Когда код довольно прост, мы можем просто обернуть все в try/catch. На этом уровне разница между try/catch и promises незначительна.

// Everything in try/catch for simplicity
try {
  const settings = await getUserSettings();
  const data = JSON.parse(settings);
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}

Promise.resolve()
  .then(getUserSettings)
  .then(JSON.parse)
  .then(displayUserSettings)
  .catch(displayErrorPage);
Вход в полноэкранный режим Выход из полноэкранного режима

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

Восстанавливаемые ошибки

Но что если у нас больше требований? Что если программа может восстанавливаться после определенных ошибок? Может быть, мы хотим отображать настройки пользователя по умолчанию, если getUserSettings не работает.

С блоками try/catch мы должны предвидеть каждый возможный сбой и обработать его.

let data;
try {
  const settings = await getUserSettings();
  // JSON.parse can throw an error, too.
  data = JSON.parse(settings);
} catch {
  data = useDefaultSettings();
}

try {
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Ошибки умножения

Если функции в нашем «восстанавливаемом» блоке catch могут ошибаться, нам нужно вложить блоки try/catch. Для этого мы можем сделать различные функции, чтобы добавить уровни обработки ошибок без глубоких отступов, которые усложняют процесс.

// Separate logic to avoid nesting
const tryUserSettings = async () => {
  try {
    const settings = await getUserSettings();
    return JSON.parse(settings);
  } catch {
    return useDefaultSettings();
  }
};

try {
  // Handling more possibilities.
  const data = await tryUserSettings();
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть обработка ошибок вокруг всего, но следить за ходом программы стало сложнее. Но мы можем сделать лучше. Поскольку await работает на любом обещании, мы можем объединить цепочки обещаний и async/await.

// Simple await.catch() pattern
const data = await mainAction().catch(backupAction);
Вход в полноэкранный режим Выход из полноэкранного режима

Этот стиль может помочь сгруппировать определенные действия и обработчики ошибок более аккуратно, чем несколько блоков try/catch, сохраняя при этом всю нашу логику вместе.

// A bit of both - await a promise chain
// All of "get data" is now one block
//  and we catch errors from useDefaultSettings

try {
  const data = await getUserSettings()
    .then(JSON.parse)
    .catch(useDefaultSettings);
  displayUserSettings(data);
} catch(e) {
  displayErrorPage(e);
}
Войти в полноэкранный режим Выход из полноэкранного режима

Это заняло немного времени, но мы обработали все ошибки, даже от useDefaultSettings.

Способ обещания

Давайте реализуем ту же логику в цепочке обещаний. С помощью обещаний мы можем использовать .catch() по пути, чтобы поддерживать обратные действия или общие настройки, когда предполагаемое поведение не срабатывает. Мы также получаем преимущество от совета №2, поскольку нам не нужно знать, какие функции являются асинхронными.

Promise.resolve()
  .then(getUserSettings)
  .then(JSON.parse)
  .catch(useDefaultSettings)
  .then(displayUserSettings)
  .catch(displayErrorPage);
Вход в полноэкранный режим Выход из полноэкранного режима

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

4. Избегайте случайной сериализации

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

// Accidental Serialization
const getProductDetails = async () => {
  // Await stops the code, so getProductList
  //  won't start until getUserSettings is done.
  const user = await getUserSettings();
  const products = await getProductList();

  // Did products need to wait for user?
  console.log(user, products);
};

// Prevent Serialization
const getProductDetails = async () => {
  // These variables are promises.
  // Both requests start immediately.
  const user = getUserSettings();
  const products = getProductList();

  // No waiting, but we must remember they are promises.
  console.log(await user, await products);
};

// Hybrid solution
const getProductDetails = async () => {
  // No serialization thanks to Promise.all.
  // await gets us out of the promise chain.
  const [user, products] = await Promise.all([
    getUserSettings(),
    getProductList(),
  ]);
  console.log(user, products);
};

// Only Promises - No async needed
const getProductDetails = () => {
  Promise.all([
    getUserSettings(),
    getProductList(),
  ]).then(([user, products]) => {
    console.log(user, products);
  });
};
Вход в полноэкранный режим Выход из полноэкранного режима

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

5. Обещания не являются панацеей

Хотя обещания отлично справляются с инкапсуляцией сложных деталей, чтобы вы могли прочитать высокоуровневый поток программы, есть места, где попытка вместить все в обещания может привести к беспорядку. Сложные или повторяющиеся модели поведения, такие как повторные запросы и опросы, может быть проще читать и рассуждать о них вне цепочки обещаний.

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

const eureka = () => Promise.resolve()
  .then(connectToHomeServer)
  .then(loadSmartDeviceList)
  .then(selectOnlyLights)
  .then(turnOnDevices);
Войти в полноэкранный режим Выйти из полноэкранного режима

…и затем воплотить это в жизнь.

Оцените статью
Procodings.ru
Добавить комментарий