Примечание. Этот пост теперь является частью книги Программное обеспечение для написания.

В 1920 году Элементы стиля Уильяма Странка-младшего ». был опубликован, в котором изложены руководящие принципы для стиля английского языка, выдержавшие испытание временем. Вы можете улучшить свой код, применив аналогичные стандарты к своему стилю кода.

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

К исходному коду применимы почти все рекомендации из элементарных принципов композиции:

  • Сделайте абзац единицей композиции: по одному абзацу на каждую тему.
  • Опускайте ненужные слова.
  • Используйте активный голос.
  • Избегайте череды бессвязных предложений.
  • Соедините связанные слова вместе.
  • Оформляйте заявления в позитивной форме.
  • Используйте параллельное построение на параллельных концепциях.

Мы можем применить почти идентичные концепции к стилю кода:

  1. Сделайте функцию единицей композиции. Одно задание на каждую функцию.
  2. Опустите ненужный код.
  3. Используйте активный голос.
  4. Избегайте череды необъяснимых заявлений.
  5. Храните связанный код вместе.
  6. Обращайте внимание на утверждения и выражения в позитивной форме.
  7. Используйте параллельный код для параллельных концепций.

1. Сделайте функцию единицей композиции. Одно задание на каждую функцию.

Суть разработки программного обеспечения - композиция. Мы создаем программное обеспечение, составляя вместе модули, функции и структуры данных.

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

Модули - это просто коллекции одной или нескольких функций или структур данных, а структуры данных - это то, как мы представляем состояние программы, но ничего интересного не происходит, пока мы не применим функцию.

В JavaScript есть три вида функций:

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

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

Одно для каждой функции: если ваша функция предназначена для ввода-вывода, не смешивайте этот ввод-вывод с отображением (вычислением). Если ваша функция предназначена для отображения, не смешивайте ее с вводом-выводом. По определению, процедурные функции нарушают это правило. Процедурные функции также нарушают другой принцип: избегайте череды разрозненных заявлений.

Идеальная функция - это простая, детерминированная, чистая функция:

  • Учитывая один и тот же ввод, всегда возвращать один и тот же вывод
  • Без побочных эффектов

См. Также Что такое чистая функция?

2. Опустите ненужный код.

«Энергичный текст лаконичен. Предложение не должно содержать ненужных слов, абзац - ненужных предложений по той же причине, по которой в чертеже не должно быть ненужных линий, а в машине - ненужных частей. Это требует не того, чтобы писатель делал все предложения короткими или избегал всех деталей и рассматривал темы только в общих чертах, а чтобы каждое слово говорило об этом ». [Излишние слова опущены.]
~ Уильям Странк-младший, "Элементы стиля"

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

Краткий код более разборчив, потому что он имеет более высокое отношение сигнал / шум: читатель должен отсеивать меньше синтаксического шума, чтобы понять смысл. Меньше кода = меньше синтаксического шума = более сильный сигнал для передачи смысла.

Если позаимствовать слово из "Элементов стиля": краткий код более энергичный.

function secret (message) {
  return function () {
    return message;
  }
};

Может быть уменьшено до:

const secret = msg => () => msg;

Это гораздо удобнее для тех, кто знаком с краткими стрелочными функциями (введенными в 2015 году с ES6). Он опускает ненужный синтаксис: фигурные скобки, ключевое слово function и оператор return.

Первый включает ненужный синтаксис. Фигурные скобки, ключевое слово function и оператор return бесполезны для тех, кто знаком с кратким синтаксисом стрелок. Он существует только для того, чтобы сделать код знакомым для тех, кто еще не владеет ES6.

ES6 является языковым стандартом с 2015 года. Пора познакомиться.

Пропустить ненужные переменные

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

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

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

const getFullName = ({firstName, lastName}) => {
  const fullName = firstName + ' ' + lastName;
  return fullName;
};

vs…

const getFullName = ({firstName, lastName}) => (
  firstName + ' ' + lastName
);

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

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

Давайте посмотрим на пример использования карри:

const add2 = a => b => a + b;
// Now we can define a point-free inc()
// that adds 1 to any number.
const inc = add2(1);
inc(3); // 4

Взгляните на определение функции inc(). Обратите внимание, что здесь не используется ключевое слово function или синтаксис =>. Нет места для перечисления параметров, потому что функция не использует список параметров внутри. Вместо этого он возвращает функцию, которая знает, как обращаться с аргументами.

Давайте посмотрим на другой пример с использованием композиции функций. Композиция функций - это процесс применения функции к результату другого приложения функции. Осознаёте вы это или нет, но вы постоянно используете композицию функций. Вы используете его всякий раз, когда объединяете такие методы, как, например, .map() или promise.then(). В самом простом виде это выглядит так: f(g(x)). В алгебре это сочинение обычно пишется f ∘ g (часто произносится как «ф после g» или «f составлено с g»).

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

const g = n => n + 1;
const f = n => n * 2;
// With points:
const incThenDoublePoints = n => {
  const incremented = g(n);
  return f(incremented);
};
incThenDoublePoints(20); // 42
// compose2 - Take two functions and return their composition
const compose2 = (f, g) => x => f(g(x));
// Point-free:
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42

То же самое можно сделать с любым функтором. функтор - это все, что вы можете сопоставить, например, массивы (Array.map()) или обещания (promise.then()). Напишем другую версию compose2, используя цепочку карт для композиции функций:

const compose2 = (f, g) => x => [x].map(g).map(f).pop();
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42

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

Практически каждая библиотека функционального программирования имеет как минимум две версии утилит compose: compose(), которая применяет функции справа налево, и pipe(), которая применяет функции слева направо.

Лодаш называет их compose() и flow(). Когда я использую их из Lodash, я всегда импортирую это так:

import pipe from 'lodash/fp/flow';
pipe(g, f)(20); // 42

Однако это не намного больше кода, и он делает то же самое:

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
pipe(g, f)(20); // 42

Если эта композиция функций звучит для вас чуждо, и вы не знаете, как ее использовать, подумайте вот о чем:

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

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

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

Помнить:

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

3. Используйте активный голос

«Активный голос обычно более прямой и энергичный, чем пассивный». ~ Уильям Странк младший, Элементы стиля

Называйте вещи как можно точнее.

  • myFunction.wasCalled() лучше, чем myFunction.hasBeenCalled()
  • createUser() лучше, чем User.create()
  • notify() лучше, чем Notifier.doNotification()

Назовите предикаты и логические значения, как если бы они задавали вопросы типа "да" или "нет":

  • isActive(user) лучше, чем getActiveStatus(user)
  • isFirstRun = false; лучше, чем firstRun = false;

Назовите функции с помощью глагольных форм:

  • increment() лучше, чем plusOne()
  • unzip() лучше, чем filesFromZip()
  • filter(fn, array) лучше, чем matchingItemsFromArray(fn, array)

Обработчики событий

Обработчики событий и методы жизненного цикла являются исключением из правила глагола, потому что они используются в качестве квалификаторов; вместо того, чтобы указывать, что делать, они выражают когда делать это. Их следует назвать так, чтобы они читали «‹ когда действовать ›, ‹verb›».

  • element.onClick(handleClick) лучше, чем element.click(handleClick)
  • component.onDragStart(handleDragStart) лучше, чем component.startDrag(handleDragStart)

Во второй форме похоже, что мы пытаемся инициировать событие, а не реагировать на него.

Методы жизненного цикла

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

  • componentWillBeUpdated(doSomething)
  • componentWillUpdate(doSomething)
  • beforeUpdate(doSomething)

В первом примере мы используем пассивный голос (будет обновляться, а не обновляться). Это непросто, и оно не более ясное, чем другие альтернативы.

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

Мы можем еще больше упростить. Поскольку это методы, субъект (компонент) является встроенным. Обращение к нему в названии метода излишне. Подумайте, как бы это было прочитано, если бы вы вызывали эти методы напрямую: component.componentWillUpdate(). Это все равно, что сказать: «Джимми Джимми будет стейк на ужин». Вам не нужно дважды слышать имя объекта.

  • component.beforeUpdate(doSomething) лучше, чем component.beforeComponentUpdate(doSomething)

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

Мне нравится называть функциональные миксины прилагательными. Вы можете часто использовать суффиксы «ing» или «способный», чтобы найти полезные прилагательные. Примеры:

  • const duck = composeMixins(flying, quacking);
  • const box = composeMixins(iterable, mappable);

4. Избегайте череды вольных заявлений.

«… Серия скоро становится однообразной и утомительной».
~ Уильям Странк-младший, «Элементы стиля»

Разработчики часто объединяют последовательности событий в процедуру: группу слабо связанных операторов, предназначенных для выполнения один за другим. Избыток процедур - рецепт спагетти-кода.

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

Рассмотрим следующую последовательность:

const drawUserProfile = ({ userId }) => {
  const userData = loadUserData(userId);
  const dataToDisplay = calculateDisplayData(userData);
  renderProfileData(dataToDisplay);
};

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

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

Например, мы могли бы полностью заменить рендерер, и это не повлияло бы на другие части программы, например, множество настраиваемых рендереров React: ReactNative для собственных приложений iOS и Android, AFrame для WebVR, ReactDOM / Server для рендеринга на стороне сервера. , так далее…

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

Разделение проблем также делает их независимыми. Мне нравится проводить модульное тестирование своих приложений и отображать результаты тестирования с каждым изменением, когда я пишу код. Однако, если мы привязываем код рендеринга к коду загрузки данных, я не могу просто передать фальшивые данные в код рендеринга для целей тестирования. Я должен протестировать весь компонент от начала до конца - процесс, который может занять много времени из-за загрузки браузера, асинхронного сетевого ввода-вывода и т. Д.

Я не получу немедленной обратной связи по своим модульным тестам. Разделение функций позволяет тестировать модули изолированно друг от друга.

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

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

5. Храните связанный код вместе.

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

Например, вот две альтернативные файловые иерархии для приложения To Do по типу и функциям:

Сгруппировано по типу:

.
├── components
│   ├── todos
│   └── user
├── reducers
│   ├── todos
│   └── user
└── tests
    ├── todos
    └── user

Сгруппировано по функциям:

.
├── todos
│   ├── component
│   ├── reducer
│   └── test
└── user
    ├── component
    ├── reducer
    └── test

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

Разместите файлы, связанные по функциям.

6. Обращайте внимание на утверждения и выражения в позитивной форме.

«Делайте однозначные утверждения. Избегайте скучных, бесцветных, нерешительных, уклончивых слов. Используйте слово не как средство отрицания или как антитезу, никогда как средство уклонения ».
~ Уильям Странк-младший, The Elements of Style

  • isFlying лучше, чем isNotFlying
  • late лучше, чем notOnTime

Если утверждения

if (err) return reject(err);
// do something...

…Это лучше чем:

if (!err) {
  // ... do something
} else {
  return reject(err);
}

Троичные

{
  [Symbol.iterator]: iterator ? iterator : defaultIterator
}

…Это лучше чем:

{
  [Symbol.iterator]: (!iterator) ? defaultIterator : iterator
}

Предпочитайте сильные отрицательные утверждения

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

  • if (missingValue) лучше, чем if (!hasValue)
  • if (anonymous) лучше, чем if (!user)
  • if (isEmpty(thing)) лучше, чем if (notDefined(thing))

Избегайте пустых и неопределенных аргументов в вызовах функций

Не требуется, чтобы вызывающие функции передавали undefined или null вместо необязательного параметра. Вместо этого отдайте предпочтение именованным объектам параметров:

const createEvent = ({
  title = 'Untitled',
  timeStamp = Date.now(),
  description = ''
}) => ({ title, description, timeStamp });
// later...
const birthdayParty = createEvent({
  title: 'Birthday Party',
  description: 'Best party ever!'
});

…Это лучше чем:

const createEvent = (
  title = 'Untitled',
  timeStamp = Date.now(),
  description = ''
) => ({ title, description, timeStamp });
// later...
const birthdayParty = createEvent(
  'Birthday Party',
  undefined, // This was avoidable
  'Best party ever!'  
);

Используйте параллельный код для параллельных концепций

«… Параллельное построение требует, чтобы выражения схожего содержания и функции были внешне похожими. Сходство формы позволяет читателю легче распознать сходство содержания и функции ».
~ Уильям Странк-младший, The Elements of Style

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

Компоненты пользовательского интерфейса - отличный пример. Менее десяти лет назад было обычным делом смешивать обновления пользовательского интерфейса с помощью jQuery с логикой приложения и сетевым вводом-выводом. Затем люди начали понимать, что мы можем применять MVC к веб-приложениям на стороне клиента, и люди начали отделять модели от логики обновления пользовательского интерфейса.

В конце концов, веб-приложения остановились на подходе на основе компонентной модели, который позволяет нам декларативно моделировать наши компоненты, используя такие вещи, как JSX или HTML-шаблоны.

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

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

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

Вывод: код должен быть простым, а не упрощенным

Энергичное письмо лаконично. Предложение не должно содержать ненужных слов, абзац - ненужных предложений по той же причине, по которой в чертеже не должно быть ненужных линий, а в машине - ненужных частей. Для этого нужно, чтобы автор не делал все предложения короткими или избегал всех деталей и рассматривал темы только в общих чертах, а чтобы каждое слово говорило. [выделение добавлено.]
~ Уильям Странк младший, "Элементы стиля"

ES6 был стандартизирован в 2015 году, но в 2017 году многие разработчики избегают таких функций, как краткие стрелочные функции, неявный возврат, операторы отдыха и распространения и т. Д., Под видом написания кода, который легче читать, потому что он более знакомый. Это большая ошибка. Знакомство приходит с практикой, а со знанием дела краткие функции ES6 явно превосходят альтернативы ES5: краткий код прост по сравнению с альтернативой с тяжелым синтаксисом.

Код должен быть простым, а не упрощенным.

Учитывая этот краткий код:

  • Менее подвержены ошибкам
  • Легче отлаживать

И с учетом этих ошибок:

  • Чрезвычайно дорого ремонтировать
  • Как правило, вызывает больше ошибок
  • Прервите поток нормальной разработки функций

И, учитывая этот краткий код, также:

  • Легче писать
  • Легче читать
  • Легче поддерживать

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

Предположите, что читатель ничего не знает о реализации, но не думайте, что читатель глуп или что читатель не знает языка.

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

Код должен быть простым, а не упрощенным.

Повышайте уровень своих навыков с живым наставничеством 1: 1

DevAnywhere - это самый быстрый способ повысить уровень навыков JavaScript:

  • Живые уроки
  • Гибкий график
  • 1: 1 наставничество
  • Создавайте настоящие производственные приложения

Эрик Эллиотт является автором Программирования приложений JavaScript (O’Reilly) и соучредителем DevAnywhere.io. Он участвовал в разработке программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC и ведущие музыканты, включая Usher, Frank Ocean, Metallica и многие другие.

Он работает где угодно с самой красивой женщиной в мире.