Элегантные шаблоны в современном JavaScript: RORO

elegantnye shablony v sovremennom javascript roro

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

Что ж, за 20 лет с тех пор многое изменилось. Теперь я вижу в JavaScript то, что видел Дуглас Крокфорд, когда писал JavaScript: хорошие стороны: «Выдающийся динамический язык программирования… с огромной выразительной силой».

Итак, без лишних слов, вот отличный маленький шаблон, который я использовал в своем коде в последнее время. Надеюсь, вы насладитесь этим так же, как и я.

Пожалуйста, запиши: Я почти уверен, что ничего из этого не придумал. Скорее всего, я наткнулся на это в коде других людей и в конце концов принял его сам.

Получить предмет, вернуть предмет (РОРО).

Большинство моих функций теперь принимают один параметр типа object и многие из них возвращают или решают значение типа object так же.

Частично благодаря деструктуризация функцию, представленную в ES2015, я нашел, что это массивный шаблон. Я даже дал ему дурное название «РОРО», потому что… бренд? ¯\_(ツ)_/¯

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

Вот несколько причин, почему вам понравится этот узор:

  • Именуемые параметры
  • Более чистые параметры по умолчанию
  • Более богатые значения возврата
  • Легкая функциональная композиция

Давайте рассмотрим каждый из них.

Именуемые параметры

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

function findUsersByRole (  role,   withContactInfo,   includeInactive) {...}

Тогда вызов этой функции может выглядеть следующим образом:

findUsersByRole(  'admin',   true,   true)

Обратите внимание, насколько неоднозначны последние два параметра. Что означает «правда, правда»?

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

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

Давайте посмотрим, что произойдет, если вместо этого мы получим один объект:

function findUsersByRole ({  role,  withContactInfo,   includeInactive}) {...}

Обратите внимание, что наша функция выглядит почти идентично, за исключением того мы поставили наши параметры в скобки. Это указывает на то, что вместо получения трех различных параметров, наша функция теперь ожидает один объект с названными свойствами. role, withContactInfoи includeInactive.

Это работает благодаря функции JavaScript, представленной в ES2015 Деструктуризация.

Теперь мы можем вызвать нашу функцию так:

findUsersByRole({  role: 'admin',   withContactInfo: true,   includeInactive: true})

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

К примеру, это работает:

findUsersByRole({  withContactInfo: true,  role: 'admin',   includeInactive: true})

И так же:

findUsersByRole({  role: 'admin',   includeInactive: true})

Это также позволяет добавлять новые параметры, не нарушая старый код.

Одно важное замечание: если мы хотим, чтобы все параметры были необязательными, то есть, если следующий вызов является действительным…

findUsersByRole()

… нам нужно установить значение по умолчанию для нашего объекта параметра, например:

function findUsersByRole ({  role,  withContactInfo,   includeInactive} = {}) {...}

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

Обратите внимание на следующее:

const options = {  role: 'Admin',  includeInactive: true}
findUsersByRole(options)
function findUsersByRole ({  role,  withContactInfo,   includeInactive} = {}) {  role = role.toLowerCase()  console.log(role) // 'admin'  ...}
console.log(options.role) // 'Admin'

Несмотря на то, что мы изменяем значение role значение options.role остается без изменений.

Редактировать: Следует отметить, что деструктуризация производит a неглубокий скопировать, чтобы любое из свойств нашего объекта параметра было сложного типа (например, array или object) их изменение действительно повлияет на оригинал.

(Совет Юрию Хомякову за то, что он обратил внимание на это)

Пока все хорошо, да?

Параметры очистки по умолчанию

С ES2015 функции JavaScript получили возможность определять параметры по умолчанию. На самом деле, недавно мы использовали параметр по умолчанию, когда добавляли ={} к объекту параметра на нашем findUsersByRole функция выше.

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

function findUsersByRole (  role,   withContactInfo = true,   includeInactive) {...}

Если мы хотим установить includeInactive к true мы должны явно передать undefined как значение для withContactInfo чтобы сохранить значение по умолчанию, например:

findUsersByRole(  'Admin',   undefined,   true)

Как это противно?

Сравните это с использованием объекта параметра, например:

function findUsersByRole ({  role,  withContactInfo = true,   includeInactive} = {}) {...}

Теперь мы можем написать…

findUsersByRole({  role: ‘Admin’,  includeInactive: true})

… и наше значение по умолчанию для withContactInfo сохраняется.

БОНУС: обязательные параметры

Как часто вы писали что-нибудь подобное?

function findUsersByRole ({  role,   withContactInfo,   includeInactive} = {}) {  if (role == null) {      throw Error(...)  }  ...}

Примечание: Мы используем == (двойное равно) выше, чтобы проверить для обоих null и undefined с одним утверждением.

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

Во-первых, нам нужно определить a requiredParam() функция, выдающая ошибку.

Нравится это:

function requiredParam (param) {  const requiredParamError = new Error(   `Required parameter, "${param}" is missing.`  )
  // preserve original stack trace  if (typeof Error.captureStackTrace === ‘function’) {    Error.captureStackTrace(      requiredParamError,       requiredParam    )  }
  throw requiredParamError}

Я знаю, знаю. requiredParam не RORO. Вот почему я сказал много из моих функций — нет все.

Теперь мы можем установить вызов requiredParam как значение по умолчанию для roleвот так:

function findUsersByRole ({  role = requiredParam('role'),  withContactInfo,   includeInactive} = {}) {...}

С вышеприведенным кодом, если кто-то звонит findUsersByRole без предоставления a role они получат Error это говорит Required parameter, “role” is missing.

Технически мы можем использовать эту технику с обычными параметрами по умолчанию; нам не обязательно нужен объект. Но этот трюк был слишком полезен, чтобы о нем не упоминать.

Больше возвращенных значений

Функции JavaScript могут возвращать только одно значение. Если это значение an object он может содержать гораздо больше информации.

Рассмотрим функцию, сохраняющую a User в базу данных. Когда эта функция возвращает объект, она может предоставить много информации абоненту.

К примеру, распространенным шаблоном является «перевернуть» или «объединить» данные в функции сохранения. Это означает, что мы вставляем строки в таблицу базы данных (если они не существуют) или обновляем их (если они существуют).

В таких случаях было бы удобно знать, была ли операция, выполненная нашей функцией хранения INSERT или an UPDATE. Также было бы хорошо получить точное представление о том, что именно хранилось в базе данных и было бы хорошо знать статус операции; удалось ли это сделать, ожидает ли оно как часть большей транзакции, или прошло время ожидания?

При возвращении объекта легко сообщить всю эту информацию одновременно.

Что-то вроде:

async saveUser({  upsert = true,  transaction,  ...userInfo}) {  // save to the DB  return {    operation, // e.g 'INSERT'    status, // e.g. 'Success'    saved: userInfo  }}

Технически вышеперечисленное возвращает a Promise что решает до object но вы поняли.

Простая функциональная композиция

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

Мы можем составлять функции вместе, используя a pipe функция, которая выглядит примерно так:

function pipe(...fns) {   return param => fns.reduce(    (result, fn) => fn(result),     param  )}

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

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

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

Вот пример, когда мы имеем a saveUser функция, передающая a userInfo с помощью 3 отдельных функций, последовательно проверяющих, нормализующих и сохраняющих информацию пользователя.

function saveUser(userInfo) {  return pipe(    validate,    normalize,    persist  )(userInfo)}

Мы можем использовать параметр rest в нашем validate, normalizeи persist функции, чтобы деструктурировать только те значения, которые требуются каждой функции, и все равно передавать обратно абоненту.

Вот немного кода, чтобы понять суть:

function validate({  id,  firstName,  lastName,  email = requiredParam(),  username = requiredParam(),  pass = requiredParam(),  address,  ...rest}) {  // do some validation  return {    id,    firstName,    lastName,    email,    username,    pass,    address,    ...rest  }}
function normalize({  email,  username,  ...rest}) {  // do some normalizing  return {    email,    username,    ...rest  }}
async function persist({  upsert = true,  ...info}) {  // save userInfo to the DB  return {    operation,    status,    saved: info  }}

RO или нет RO, вот в чем вопрос.

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

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

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

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *