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

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

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ. При выборе стиля при написании кода приходится идти на компромиссы. Удобочитаемость имеет значение, но это зависит от ваших предпочтений и вариантов использования. Используйте свое усмотрение и решите, подходит ли этот стиль для вас и вашей команды.

Что здесь означает "читать как по-английски"?

Что ж, давайте посмотрим на состав некоторых разных утверждений, имеющих одинаковое содержание и значение.

"Запишите этот файл на S3".
Здесь у нас есть глагол write, подлежащее this file и дополнение S3. Если мы напишем это как код, он будет читаться как write(thisFile).toS3(). Никакой шаткости, никакого странного порядка; это читается как английский!

"Запишите этот файл на S3".
Похоже на Йоду, да. Не общий английский, делает это. В коде это выглядит как writeToS3(thisFile). Некоторые естественные языки составляют операторы таким образом, но это не подходит для правильного английского. Однако этот шаблон чрезвычайно распространен в коде.

Почему так напряженно?

Вы, вероятно, уже знаете, что такое время, но если вы этого не знаете, Merriam Webster определяет «время» как: форма глагола, используемая для обозначения прошедшего, настоящего или будущего времени действие или состояние, которое оно обозначает.

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

Они уничтожили документы.
Они уничтожают документы.
Они уничтожат документы.

Есть также разница между утверждениями, командами и вопросами. Вот несколько примеров в будущем времени.

Вы должны защищать секреты.
Защищать секреты.
Вы будете защищать секреты?

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

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

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

import * as fs from 'fs';

const PersistenceUtils = {
  outputJson: (obj) => {
    const json = JSON.stringify(obj, null, 2);
    return {
      toFile: (filename) => {
        fs.writeFileSync(`output/${filename}.json`, json, 'utf8'); 
      },
    }
  },
  outputJsonToFile: (obj, filename) => {
    const json = JSON.stringify(obj, null, 2);
    fs.writeFileSync(`output/${filename}.json`, json, 'utf8'); 
  },
  jsonToFile: (obj) => {
    const json = JSON.stringify(obj, null, 2);
    return (filename) => {
      fs.writeFileSync(`output/${filename}.json`, json, 'utf8'); 
    }
  }
}

export default PersistenceUtils

В этом файле/модуле я экспортирую один объект с именем PersistenceUtils, который я могу импортировать в другие места. Он содержит несколько служебных функций для вывода json в файл. Это произошло из-за намерения уменьшить небольшое количество дублирования.

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

Давайте рассмотрим, как выглядит вызов этих утилит.

PersistenceUtils.outputJson(myObj).toFile('myFile')
PersistenceUtils.outputJsonToFile(myObj, 'myFile')
PersistenceUtils.jsonToFile(myObj)('myFile')

Первый вариант использует некоторую специальную структуру, чтобы сделать возможным такой конкретный API. outputJson возвращает объект, к которому мы можем добавить свойства. В данном случае он содержит одно свойство с именем toFile. Здесь значение этого свойства является функцией, которая позволяет нам вызывать его. Это то, что позволяет нам получить ощущение естественного языка «читается как английский»! 😎

Сравнивая это с двумя оставшимися примерами, он требует своего рода смешанного подхода, который охватывает два других стиля: многопараметрический и каррирование соответственно.

Давайте обдумаем некоторые из уникальных компромиссов, которые это дает нам…

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

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

Отделив что от как, мы повысили гибкость за счет расширяемости. Представьте, что мы хотим отправить вывод json в файл в корзине Simple Storage Service (S3) в Amazon Web Services (AWS), а не в локальный файл. Все, что нам нужно сделать, это определить другое свойство внутри возвращаемого объекта, возможно, как toS3. Не нужно писать новую функцию, дублирующую привязку входного объекта myObj; мы можем сосредоточиться на том, что мы добавляем. Кроме того, мы не меняем существующую функциональность, поэтому не нарушаем никаких тестов и не увеличиваем вероятность ошибок.

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

Реализация любого из этих функциональных свойств не обязательно должна быть конкретной. По всей вероятности, мы хотим, чтобы точные детали для «записи в файл» или «записи в S3» находились где-то еще, возможно, в других служебных функциях, классах или объектах и ​​т. д. Мы можем просто apply их вот внутри этих свойств. Другими словами, эти свойства в основном полезны тем, что помогают создать ориентированный на удобочитаемость шаблон, который облегчает понимание кода, а не потому, что они могут инкапсулировать поведение.

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

const writeToFile = (something) => { /* some S3 stuff */ }
const writeToS3 = (something) => { /* some file stuff */ }

const write = (something) => ({
  toS3: concreteFnWritingToS3,
  toFile: concreteFnWritingToFile,
})

const myObj = {
  1: 2,
  a: 'b',
  foo: 'bar',
}

// same thing, different style
writeToFile(myObj)
writeToS3(myObj)

write(myObj).toS3()
write(myObj).toFile()

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

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

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

const sortWithoutSideEffects = (arr) => [...arr].sort()
const deDuplicate = (arr) => [...new Set(arr)]
const correctFixableIssues = (arr) => { /* use your imagination 🌈 */}
const pruneUnfixableProblems = (arr) => { /* you cant win them all! */ }

const hopesAndDreams = [] // <-- insert actual things here

const dontGiveUpOnYour = (arr) => {
  const sorted = sortWithoutSideEffects(hopesAndDreams)
  const deDuplicated = deDuplicate(sorted)
  const corrected = correctFixableIssues(deDuplicated)

  return pruneUnfixableProblems(corrected)
}

const achieveYour = (arr) => {
  return [hopesAndDreams]
    .map(sortWithoutSideEffects)
    .map(deDuplicate)
    .map(correctFixableIssues)
    .map(pruneUnfixableProblems)
    .flatMap(e => e)
}

dontGiveUpOnYour(hopesAndDreams)
achieveYour(hopesAndDreams)



// alternatively...
const nailedIt = [
  sortWithoutSideEffects,
  deDuplicate,
  correctFixableIssues,
  pruneUnfixableProblems,
].reduce((acc, curr) => curr(acc), hopesAndDreams)

Сравните несколько операторов с одной функциональной цепочкой.

В первом есть много дублирования! Имена переменных также технически неверны, поскольку они не учитывают предыдущие действия, предпринятые над коллекцией. Например. «исправлено», хотя на самом деле его можно было «отсортироватьDeDuplicatedCorrected» 🤪

Что вы предпочитаете?