Оригинальная статья: Richard Tong — Practical Functional Programming in JavaScript — Techniques for Composing Data
Всем привет. Добро пожаловать на мою серию статей о практике функционального программирования на JavaScript. Сегодня мы рассмотрим методы составления данных (Composing Data), то есть лучшие практики, которые облегчают жизнь при работе со структурированными данными. Составление данных связано с формой и структурой данных и является таким же фундаментальным, как и преобразование, когда речь идет о функциональном программировании в JavaScript. Если все преобразования A => B, то при составлении данных рассматривается, как именно A становится B, когда и A, и B являются структурированными данными. From Geeks
Структурированные данные — это данные, которые соответствуют модели данных, имеют четко определенную структуру, следуют последовательному порядку и могут быть легко доступны и использоваться человеком или компьютерной программой.
Структурированные данные могут представлять что угодно, от профиля пользователя до списка книг и транзакций на банковском счете. Если вы когда-либо работали с записями в базе данных, вы работали со структурированными данными.
Существует множество способов составления данных, поскольку эта территория все еще относительно не развита. Хорошая компоновка данных означает разницу между простотой чтения / сложностью работой с кодом / кодом который вызывает только раздражение. Давайте рассмотрим это на практике, выполнив структурированное преобразование данных. Возьмем к примеру следующие структурированные данные о пользователях
const users = [ { _id: '1', name: 'George Curious', birthday: '1988-03-08', location: { lat: 34.0522, lon: -118.2437, }, }, { _id: '2', name: 'Jane Doe', birthday: '1985-05-25', location: { lat: 25.2048, lon: 55.2708, }, }, { _id: '3', name: 'John Smith', birthday: '1979-01-10', location: { lat: 37.7749, lon: -122.4194, }, }, ]
Допустим, нам нужно будет превратить эти пользовательские данные в данные, которые будут отображаться, например, на панели администратора. Согласно следующим требованиям:
Окончательный результат должен выглядеть примерно так.
const displayUsers = [ { _id: '1', firstName: 'George', age: 32, city: 'Los Angeles', }, { _id: '2', firstName: 'Jane', age: 35, city: 'Trade Center Second', }, { _id: '3', firstName: 'John', age: 41, city: 'San Francisco', }, ]
На верхнем уровне представления users структурированы как массив объектов. Поскольку displayUsers также является массивом объектов, это хороший случай для использования функции map. Из документов MDN docs,
Метод map() создает новый массив, заполненный результатами вызова предоставленной функции для каждого элемента в вызывающем массиве.
Давайте попробуем решить проблему одним махом, не составляя никаких данных, кроме отображения верхнего уровня.
Promise.all(users.map(async user => ({ _id: user._id, firstName: user.name.split(' ')[0], age: (Date.now() - new Date(user.birthday).getTime()) / 365 / 24 / 60 / 60 / 1000, city: await fetch( `https://geocode.xyz/${user.location.lat},${user.location.lon}?json=1`, ).then(res => res.json()).then(({ city }) => city), }))).then(console.log) /* [ { _id: '1', firstName: 'George', age: 32, city: 'Los Angeles' }, { _id: '2', firstName: 'Jane', age: 35, city: 'Trade Center Second' }, { _id: '3', firstName: 'John', age: 41, city: 'San Francisco' }, ] */
Это работает, но код выглядит сложным и неразборчивым. Нам и будущим читателям нашего кода может быть полезно разделить некоторые функции там, где это имеет смысл. Вот рефакторинг некоторых из перечисленных выше функций уменьшенного размера.
// user { // name: string, // } => firstName string const getFirstName = ({ name }) => name.split(' ')[0] // ms number => years number const msToYears = ms => Math.floor(ms / 365 / 24 / 60 / 60 / 1000) // user { // birthday: string, // } => age number const getAge = ({ birthday }) => msToYears( Date.now() - new Date(birthday).getTime(), ) // user { // location: { lat: number, lon: number }, // } => Promise { city string } const getCityName = ({ location: { lat, lon } }) => fetch( `https://geocode.xyz/${lat},${lon}?json=1`, ).then(res => res.json()).then(({ city }) => city)
Для получения переменных из свойств объекта эти функции используют присвоение деструктуризацию (destructuring assignment). Здесь мы видим композицию данных в силу разбивки нашей проблемы на более мелкие проблемы. Когда вы разбиваете одни объекты на более мелкие объекты (меньшие по размеру функции), вам нужно указать больше входных и выходных параметров. Таким образом, вы создаете больше данных в результате написания более понятного кода. Из документации ясно, что getFirstName, getAge и getCityName ожидают пользовательский объект user в качестве входного параметра. getAge дополнительно преобразует миллисекунды в года, msToYears.
getFirstName
— берет user с атрибутом name и возвращает только первое слово имени для firstName getAge
— берет user с атрибутом birthday, например 1992-02-22 и возвращает соответствующий age в годахgetCityName
— берет user с объектом location {lat, lon} и возвращает название ближайшего города в качестве Promise. Забыли, что такое Promise? Посмотрите в MDN документацию MDN docs
Объект Promise представляет возможное успешное завершение (или ошибочное завершение) асинхронной операции и ее итоговое значение.
Здесь я не буду углубляться в Promises. По сути, если возвращаемого значения еще нет, вы получаете Promises (Обещание) вместо него. В getCityName мы делаем запрос к внешнему API через fetch и получаем Promise, потому что отправка запроса и ожидание его ответа является асинхронной операцией. Получение значения названия города займет некоторое время.
Собрав все воедино, вот один из способов выполнить полное преобразование. Благодаря нашей новой структуре данных мы теперь можем ясно видеть новые поля firstName, age и city, вычисляемые из объекта user.
Promise.all(users.map(async user => ({ _id: user._id, firstName: getFirstName(user), age: getAge(user), city: await getCityName(user), }))).then(console.log) /* [ { _id: '1', firstName: 'George', age: 32, city: 'Los Angeles' }, { _id: '2', firstName: 'Jane', age: 35, city: 'Trade Center Second' }, { _id: '3', firstName: 'John', age: 41, city: 'San Francisco' }, ] */
Этот код довольно хорош, но можно сделать еще лучше. У нас есть некоторый шаблонный код Promise, и я не самый большой поклонник того, как мы выражаем асинхронное преобразование user => ({…}). Что касается ванильного JavaScript, этот код великолепен, однако, улучшения могут быть сделаны с помощью библиотечных функций. В частности, мы можем улучшить этот пример, используя fork и map из моей библиотеки асинхронного функционального программирования rubico. И нет, я не верю, что мы могли бы улучшить этот пример, используя другую библиотеку.
Мы можем выразить предыдущее преобразование с помощью функций fork и map следующим образом
// users [{ // _id: string, // name: string, // birthday: string, // location: { lat: number, lon: number }, // }] => displayUsers [{ // _id: string, // firstName: string, // age: number, // city: string, // }] map(fork({ _id: user => user._id, firstName: getFirstName, age: getAge, city: getCityName, // fork and map will handle the Promise resolution }))(users).then(console.log) /* [ { _id: '1', firstName: 'George', age: 32, city: 'Los Angeles' }, { _id: '2', firstName: 'Jane', age: 35, city: 'Trade Center Second' }, { _id: '3', firstName: 'John', age: 41, city: 'San Francisco' }, ] */
Больше нет Promise, и мы таким образом сократили трансформацию. Здесь мы одновременно указываем выходной массив объектов [{_id, firstname, age, city}] и способы, которыми мы вычисляем эти значения из объекта user: getFirstName, getAge и getCityName. Мы также прошли полный цикл; Теперь мы декларативно составляем массив пользовательских объектов в массив отображаемых пользовательских объектов.
Конечно, мы только коснулись данной темы. Опять же, есть много способов, которые вы можете использовать в вашем код, когда дело доходит до составления данных. Абсолютно лучший способ составления данных придет из вашего собственного опыта составления данных в вашем собственном коде — и я могу говорить только о своих собственных подводных камнях.
Спасибо за прочтение!
Остальные статьи этой серии вы можете найти на ресурсе Rubico github. Увидимся в следующий раз Practical Functional Programming in JavaScript — Control Flow
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…