DanLevy.net

Мастер конвейеров: передача состояния

Привет, замыкание, мой старый друг.

Hero image for Мастер конвейеров: передача состояния

Мастер конвейеров: передача состояния

Сталкивались ли вы с трудностями при передаче состояния с помощью функциональных конвейеров?

Организация (или её отсутствие) вашего кода напрямую влияет на простоту передачи состояния.

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

В центре внимания этой статьи — следующий «реальный» фрагмент: функция оформления заказа, которая принимает userId и массив products. Она возвращает цепочку промисов, последовательно выполняющую 4 функции.

const checkout = (userId: number, products: number[]) => {
return getProductsSubtotal(userId, products)
.then(subTotal => applyTaxes(userId, subTotal))
.then(total => purchaseProducts(userId, total))
.then(result => sendReceipt(userId, result));
};

Подождите-ка, этот код на самом деле довольно неплох, насколько это возможно для конвейеров в JS!

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

Одна из проблем — мы повторно передаём userId в каждую (логически связанную) функцию. Теперь добавьте к этому другую проблему, которую легко упускают разработчики и TypeScript: перепутанные числовые аргументы легко создают скрытую ошибку. (См. applyTaxes и purchaseProducts. Что идёт первым — userId или amount?)

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

Плюсы и минусы

Плюсы

Минусы

Решение, часть 1: создайте модуль!

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

Фабрика CartHelpers

Пример модуля CartHelpers, где userId передаётся один раз, а все методы принимают один аргумент.

const CartHelpers = (userId: number) => {
return {
getProductsSubtotal: products => getProductsSubtotal(userId, products),
applyTaxes: subTotal => applyTaxes(userId, subTotal),
purchaseProducts: total => purchaseProducts(userId, total),
sendReceipt: invoice => sendReceipt(userId, invoice)
};
};

Класс CartHelpers

Если классы — ваша тема, адаптировать несложно:

class CartHelpers {
constructor(userId) {
this.userId = userId;
}
getProductsSubtotal = products => getProductsSubtotal(this.userId, products);
applyTaxes = subTotal => applyTaxes(this.userId, subTotal);
purchaseProducts = total => purchaseProducts(this.userId, total);
sendReceipt = invoice => sendReceipt(this.userId, invoice);
}

Некоторые немедленные преимущества:

Группируя связанные функции, мы создаём возможность сократить открытую поверхность (например, checkout(), «публичные» методы CartHelpers.)

Меньше поверхности === меньше когнитивной нагрузки, лучше тестируемость и поддерживаемость. Проектируйте системы с намерением и фокусом. ✨

Использование Checkout и CartHelpers

Посмотрим, как теперь выглядит функция checkout():

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
return Promise.resolve(products)
.then(products => cart.getProductsSubtotal(products))
.then(subTotal => cart.applyTaxes(subTotal))
.then(total => cart.purchaseProducts(total))
.then(result => cart.sendReceipt(result));
};
Checkout с дальнейшими улучшениями

Можно ли улучшить ещё? Да! Нам вообще не нужно повторять аргументы!

Когда аргументы функции предоставляются выводом предыдущих функций, код можно упростить ещё сильнее.

export const checkout = ({ userId, products }) => {
const cart = CartHelpers(userId);
// 🌈 Функции складываются как Lego и читаются как обычные «человеческие слова»! 💅
return Promise.resolve(products)
.then(cart.getProductsSubtotal)
.then(cart.applyTaxes)
.then(cart.purchaseProducts)
.then(cart.sendReceipt);
};

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

С чего начать?

Найдите связанные функции и сгруппируйте их вместе. (Например, CartHelpers.)

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

Что делает функции связанными?

Один интересный приём: найдите повторение в параметрах функций. Спросите, есть ли взаимосвязь? Или скрытая ответственность?

Хотя не существует единственного «правильного ответа» на проектирование модулей, полезно определить 2-3 варианта организации — набросайте план, напишите «фантазийный» код, спросите «вызывает ли это радость?»

Вам может казаться, что cart.sendReceipt() не принадлежит к методам, связанным с платежами. Возможно, customerNotifications.sendReceipt() — более подходящее место для клиентских уведомлений. Если CartHelper достаточно важен, он может выступать в роли контроллера, внутренне вызывая все необходимые сервисы, такие как customerNotifications.

Как узнать, помогаете ли вы?

Если читаемость не страдает по мере устранения случайных аргументов, ПОЗДРАВЛЯЮ!!! Вы, вероятно, создали модуль с ясной и устойчивой областью ответственности!

Итак, это подводит к вопросу: куда добавлять функциональность?

По моему опыту, есть две основные стратегии для оценки при добавлении функциональности:

  1. Расширить/отрефакторить существующий метод. (Когда новый код достаточно близок к существующему.)
  2. Создать новую (пятую) функцию в нужном месте цепочки. (Если новый код не связан с существующими функциями.)

В конечном итоге это упрощает решение, куда принадлежит новая функциональность. (Например, cart.applyDiscounts(), cart.applyTaxes(), rewards.getBalance().)

Заключение

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

Вопросы? Комментарии? Замечания? Не стесняйтесь обращаться @justsml или email.

Следите за следующей частью серии

Мы изучим вынесение состояния наружу и расширение функциональности нашего модуля!

Дополнительное чтение