Этот пост был впервые опубликован в моем блоге: Создание абстракции для сообщений интернационализации React.

Я натолкнулся на функцию, которую хотел создать, и часть ее включала рендеринг интернационализированного текста на основе типа данных из API. Этот API может возвращать три типа: common, password или biometry. И мы используем его для создания нашего EntryInfo компонента.

Для типа common ответ API выглядит так:

{
  type: 'common',
  ownerName: 'TK',
  password: null
}

Тип - common, password - null, а ownerName представлен в виде строки.

Для типа password:

{
  type: 'password',
  ownerName: null,
  password: 'lololol'
}

Тип - password, ownerName - null, но password присутствует в виде строки.

А для типа biometry:

{
  type: 'biometry',
  ownerName: null,
  password: null
}

Тип - biometry, без ownerName и password.

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

Логика построения текста сообщения на основе типа и других значений:

  • когда type равно 'Common', он отображает 'Owner {ownerName} will be there'
  • когда type равно 'Password', он отображает 'Password: {password}'
  • когда type равно 'Biometry', он отображает 'Type: biometry'
  • когда type равно null, он отображает 'Call the owner'

Итак, первое, что я сделал, - это построил определения сообщений:

import { defineMessages } from 'react-intl';
export const messages = defineMessages({
  common: {
    id: 'app.containers.entryInfo.owner',
    defaultMessage: 'Owner {ownerName} will be there',
  },
  password: {
    id: 'app.containers.entryInfo.password',
    defaultMessage: 'Password: {password}',
  },
  biometry: {
    id: 'app.containers.entryInfo.biometry',
    defaultMessage: 'Type: biometry',
  },
  defaultMessage: {
    id: 'app.containers.entryInfo.defaultMessage',
    defaultMessage: 'Call the owner',
  },
};

Компонент EntryInfo будет выглядеть так:

const EntryInfo = ({ type, password, ownerName, intl }) => {
  let entryInfo;
  if (type === 'common') {
    entryInfo = intl.format(messages.common, { ownerName });
  } else if (type === 'password') {
    entryInfo = intl.format(messages.password, { password });
  } else if (type === 'biometry') {
    entryInfo = intl.format(messages.biometry);
  } else {
    entryInfo = intl.format(messages.defaultMessage);
  }
  return <p>{entryInfo}</p>
};
export default injectIntl(EntryInfo);

Чтобы следовать определенной логике, я просто добавил if-elseif-else, чтобы отобразить соответствующее сообщение с помощью функции intl.format. Это просто, функция intl.format получает соответствующее сообщение, возвращает текст информации о записи и передает его компоненту для рендеринга.

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

Я также передаю объект intl этой новой функции, чтобы вернуть правильную строку.

const getEntryInfo = ({ type, password, ownerName, intl }) => {
  if (type === 'common') {
    return intl.format(messages.common, { ownerName });
  } else if (type === 'password') {
    return intl.format(messages.password, { password });
  } else if (type === 'biometry') {
    return intl.format(messages.biometry);
  } else {
    return intl.format(messages.defaultMessage);
  }
};
const EntryInfo = ({ type, password, ownerName, intl }) => {
  const entryInfo = getEntryInfo({ type, password, ownerName, intl });
  return <p>{entryInfo}</p>
};
export default injectIntl(EntryInfo);

Эта логика больше похожа на случай переключения, когда сравнивается только значение type. Итак, небольшой рефакторинг в getEntryInfo:

const getEntryInfo = ({ type, password, ownerName, intl }) => {
  switch (type) {
    case 'Common':
      return intl.format(messages.common, { ownerName });
    case 'Password':
      return intl.format(messages.password, { password });
    case 'Biometry':
      return intl.format(messages.biometry);
    default:
      return intl.format(messages.defaultMessage);    
  }
};

Тип жестко запрограммирован, поэтому мы также можем реорганизовать эти константы с помощью перечисления:

const ENTRY_INFO_TYPES = Object.freeze({
  COMMON: 'Common',
  PASSWORD: 'Password',
  BIOMETRY: 'Biometry',
});
const getEntryInfo = ({ type, password, ownerName, intl }) => {
  switch (type) {
    case ENTRY_INFO_TYPES.COMMON:
      return intl.format(messages.common, { ownerName });
    case ENTRY_INFO_TYPES.PASSWORD:
      return intl.format(messages.password, { password });
    case ENTRY_INFO_TYPES.BIOMETRY:
      return intl.format(messages.biometry);
    default:
      return intl.format(messages.defaultMessage);    
  }
};

Теперь все готово.

Думая о cohesion, я подумал, что функция getEntryInfo слишком много знает о том, как компонент отображает текстовое сообщение (с помощью intl).

Одна идея - подумать об единственной ответственности каждой функции.

Итак, для функции getEntryInfo мы можем удалить параметр intl как зависимость и построить объект сообщения вместо возврата строки.

const getEntryInfoMessage = ({ type, password, ownerName }) => {
  switch (type) {
    case ENTRY_INFO_TYPES.COMMON:
      return { message: messages.common, values: { ownerName } };
    case ENTRY_INFO_TYPES.PASSWORD:
      return { message: messages.password, values: { password } };
    case ENTRY_INFO_TYPES.BIOMETRY:
      return { message: messages.biometry, values: {} };
    default:
      return { message: messages.defaultMessage, values: {} };
  }
};

И используйте это в компоненте:

const EntryInfo = ({ type, password, ownerName, intl }) => {
  const entryInfoMessage = getEntryInfoMessage({ type, password, ownerName });
  return <p>
    {intl.format(
      entryInfoMessage.message,
      entryInfoMessage.values
    )}
  </p>
}

В качестве рефакторинга компонента мы можем деструктурировать объект сообщения:

const EntryInfo = ({ type, password, ownerName, intl }) => {
  const { message, values } = getEntryInfoMessage({ type, password, ownerName });
  return <p>{intl.format(message, values)}</p>;
}

Он более читабельный и менее подробный.

Для объекта сообщения мы можем создать простую функцию для обработки создания объекта сообщения:

const buildMessageObject = (message, values = {}) => ({
  message,
  values,
});
const getEntryInfoMessage = ({ type, password, ownerName }) => {
  switch (type) {
    case ENTRY_INFO_TYPES.COMMON:
      return buildMessageObject(messages.common, { ownerName });
    case ENTRY_INFO_TYPES.PASSWORD:
      return buildMessageObject(messages.password, { password });
    case ENTRY_INFO_TYPES.BIOMETRY:
      return buildMessageObject(messages.biometry);
    default:
      return buildMessageObject(messages.defaultMessage);
  }
};

Взгляните на аргумент values = {}. Мы добавляем этот пустой объект в качестве значения по умолчанию, чтобы ничего не передавать в случаях biometry и default.

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

Заключительный компонент

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

const messages = defineMessages({
  common: {
    id: 'app.containers.entryInfo.owner',
    defaultMessage: 'Owner {ownerName} will be there',
  },
  password: {
    id: 'app.containers.entryInfo.password',
    defaultMessage: 'Password: {password}',
  },
  biometry: {
    id: 'app.containers.entryInfo.biometry',
    defaultMessage: 'Type: biometry',
  },
  defaultMessage: {
    id: 'app.containers.entryInfo.default',
    defaultMessage: 'Call the owner',
  },
}
const ENTRY_INFO_TYPES = Object.freeze({
  COMMON: 'Common',
  PASSWORD: 'Password',
  BIOMETRY: 'Biometry',
});
const buildMessageObject = (message, values = {}) => ({
  message,
  values,
});
const getEntryInfoMessage = ({ type, password, ownerName }) => {
  switch (type) {
    case ENTRY_INFO_TYPES.COMMON:
      return buildMessageObject(messages.common, { ownerName });
    case ENTRY_INFO_TYPES.PASSWORD:
      return buildMessageObject(messages.password, { password });
    case ENTRY_INFO_TYPES.BIOMETRY:
      return buildMessageObject(messages.biometry);
    default:
      return buildMessageObject(messages.defaultMessage);
  }
};
const EntryInfo = ({ type, password, ownerName, intl }) => {
  const { message, values } = getEntryInfoMessage({ type, password, ownerName });
  return <p>{intl.format(message, values)}</p>;
}
export default injectIntl(EntryInfo);

Ресурсы

Надеюсь, вам понравился этот контент. Поддержите мою работу над Ko-Fi

Мой Twitter и Github. ☺