Главная » Статьи » Практическое руководство по написанию более функционального JavaScript

Практическое руководство по написанию более функционального JavaScript

Практическое руководство по написанию более функционального JavaScript

От автора: функциональное программирование великолепно. С появлением React все больше и больше внешнего кода JavaScript пишется с учетом принципов ФП. Но как нам начать использовать образ мышления ВП в повседневном кодировании? Рассмотрим подробно, как пишется функциональный JavaScript. Я попытаюсь использовать обычный блок кода и шаг за шагом пояснить его рефакторинг.

Проблема: Пользователь, который заходит на нашу страницу /login, может иметь параметр запроса redirect_to. Например, /login?redirect_to=%2Fmy-page. Обратите внимание, что %2Fmy-page — это на самом деле /my-page, когда она закодирована как часть URL. Нам нужно извлечь эту строку запроса и сохранить ее в локальном хранилище, чтобы после входа в систему пользователь мог быть перенаправлен к my-page.

Шаг № 0: Императивный подход

Если бы нам пришлось выразить решение в простейшей форме списка команд, как бы мы его написали? Нам нужно будет:

Разобрать строку запроса.

Получить значение redirect_to.

Декодировать это значение.

Сохранить декодированное значение в localStorage.

И нам также нужно добавить блоки try catch вокруг «небезопасных» функций. При этом наш блок кода будет выглядеть так:

function persistRedirectToParam() { let parsedQueryParam; try { parsedQueryParam = qs.parse(window.location.search); // https://www.npmjs.com/package/qs } catch (e) { console.log(e); return null; } const redirectToParam = parsedQueryParam.redirect_to; if (redirectToParam) { const decodedPath = decodeURIComponent(redirectToParam); try { localStorage.setItem("REDIRECT_TO", decodedPath); } catch (e) { console.log(e); return null; } return decodedPath; } return null;
}

Шаг № 1: Запись каждого шага как функции

На мгновение давайте забудем о блоках try catch и попробуем выразить все, как функцию.

// давайте объявим все функции, которые нам нужны const parseQueryParams = (query) => qs.parse(query); const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to; const decodeString = (string) => decodeURIComponent(string); const storeRedirectToQuery = (redirectTo) => localStorage.setItem("REDIRECT_TO", redirectTo); function persistRedirectToParam() { // и давайте вызовем их const parsed = parseQueryParams(window.location.search); const redirectTo = getRedirectToParam(parsed); const decoded = decodeString(redirectTo); storeRedirectToQuery(decoded); return decoded; }

Когда мы начинаем выражать все наши «результаты» как результаты функций, мы видим, что мы можем выйти из нашего основного тела функции. Когда это происходит, наша функция становится намного проще, а тестирование — намного легче осуществлять.

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

const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to; const storeRedirectToQuery = (redirectTo) => localStorage.setItem("REDIRECT_TO", redirectTo); function persistRedirectToParam() { const parsed = qs.parse(window.location.search); const redirectTo = getRedirectToParam(parsed); const decoded = decodeURIComponent(redirectTo); storeRedirectToQuery(decoded); return decoded;
}

Шаг № 2: Составление функций

Хорошо. Теперь кажется, что функция persistRedirectToParams представляет собой «композицию» из 4 других функций. Давайте посмотрим, сможем ли мы написать эту функцию как композицию, тем самым исключив промежуточные результаты, которые мы храним как consts.

const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to; // нам нужно немного переписать это, чтобы вернуть результат. const storeRedirectToQuery = (redirectTo) => { localStorage.setItem("REDIRECT_TO", redirectTo) return redirectTo; }; function persistRedirectToParam() { const decoded = storeRedirectToQuery( decodeURIComponent( getRedirectToParam( qs.parse ) ) )(window.location.search) return decoded; }

Но я уже представляю человека, который читает этот вложенный вызов функции. Если бы был способ распутать этот беспорядок, было бы здорово.

Шаг № 3: Более читаемая композиция

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

const compose = require("lodash/fp/compose");
const qs = require("qs"); const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to; const storeRedirectToQuery = (redirectTo) => { localStorage.setItem("REDIRECT_TO", redirectTo) return redirectTo;
}; function persistRedirectToParam() { const op = compose( storeRedirectToQuery, decodeURIComponent, getRedirectToParam, qs.parse ); return op(window.location.search);
}

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

Это не проблема, если вы математик и знакомы с концепцией, вы, естественно, будете читать это справа налево. Но остальные из нас хотели бы прочитать это слева направо.

Шаг № 4: Пайпинг и уплощение

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

Кроме того, кажется, наша функция persistRedirectToParams стала оболочкой для другой функции, которую мы вызываем — op. Другими словами, все, что она делает, это выполняет op. Мы можем избавиться от оболочки и «сгладить» нашу функцию.

const pipe = require("lodash/fp/pipe"); const qs = require("qs"); const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to; const storeRedirectToQuery = (redirectTo) => { localStorage.setItem("REDIRECT_TO", redirectTo) return redirectTo; }; const persistRedirectToParam = fp.pipe( qs.parse, getRedirectToParam, decodeURIComponent, storeRedirectToQuery ) // чтобы вызвать persistRedirectToParam(window.location.search);

Все почти готово. Помните, что мы оставили блок try-catch? Ну, нам нужен какой-то способ представить его обратно. qs.parse небезопасно, как и storeRedirectToQuery. Один из вариантов — сделать их функциями-оболочками и поместить их в блоки try-catch. Другой, функциональный способ — выразить try-catch, как функцию.

Шаг № 5: Обработка исключений в виде функции

Есть некоторые утилиты, которые делают это, но давайте попробуем написать что-нибудь сами.

function tryCatch(opts) { return (args) => { try { return opts.tryer(args); } catch (e) { return opts.catcher(args, e); } };
}

Наша функция ожидает объект opts, который будет содержать функции tryer и catcher. Он вернет функцию, которая при вызове с аргументами вызывает tryer с указанными аргументами, а при ошибке вызывает catcher. Теперь, если у нас есть небезопасные операции, мы можем поместить их в раздел tryer и, если они не проходят, выдать безопасный результат из раздела catcher (и даже зарегистрировать ошибку).

Шаг № 6: Собираем все вместе

Итак, наш окончательный код выглядит так:

const pipe = require("lodash/fp/pipe"); const qs = require("qs"); const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to; const storeRedirectToQuery = (redirectTo) => { localStorage.setItem("REDIRECT_TO", redirectTo) return redirectTo; }; const persistRedirectToParam = fp.pipe( tryCatch({ tryer: qs.parse, catcher: () => { return { redirect_to: null, // мы должны всегда передавать согласованный результат в последующую функцию } } }), getRedirectToParam, decodeURIComponent, tryCatch({ tryer: storeRedirectToQuery, catcher: () => null, // если localstorage не проходит, мы получаем ноль }), ) // чтобы вызвать, persistRedirectToParam(window.location.search);

Это более или менее то, что нам нужно. Но чтобы улучшить читаемость и тестируемость кода, мы можем выделить и «безопасные» функции.

const pipe = require("lodash/fp/pipe"); const qs = require("qs"); const getRedirectToParam = (parsedQuery) => parsedQuery.redirect_to; const storeRedirectToQuery = (redirectTo) => { localStorage.setItem("REDIRECT_TO", redirectTo); return redirectTo; }; const safeParse = tryCatch({ tryer: qs.parse, catcher: () => { return { redirect_to: null, // мы должны всегда передавать согласованный результат в последующую функцию } } }); const safeStore = tryCatch({ tryer: storeRedirectToQuery, catcher: () => null, // если localstorage не проходит, мы получаем ноль }); const persistRedirectToParam = fp.pipe( safeParse, getRedirectToParam, decodeURIComponent, safeStore, ) // чтобы вызвать, persistRedirectToParam(window.location.search);

Теперь у нас есть реализация гораздо более крупной функции, состоящей из 4 отдельных функций, которые в тесно связны, могут тестироваться независимо, могут повторно использоваться независимо, учитывают сценарии исключений и имеют высокую степень декларативности. (И ИМХО, они немного лучше читаемы.)

Есть еще синтаксический сахар ФП, который делает все еще лучше, но это на другой раз.

Автор: Nadeesha Cabral

Источник: https://medium.freecodecamp.org/

Редакция: Команда webformyself.