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

image

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

Принципы из «Элементов стиля» не случайно живы до сих пор. Всё дело в том, что обычно их применение делает тексты лучше. Обычно автор книги оказывается прав. Отклоняться от них стоит лишь в тех случаях, когда на то есть веская причина — а не из-за прихоти или личных предпочтений.

Многие рекомендации из главы «Основные принципы композиции» применимы к программному коду:

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

Почти то же самое можно сказать и о стиле кода:

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

Функция как единица композиции


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

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

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

В JavaScript можно выделить три типа функций.

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

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

?Одна задача — одна функция


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

Надо сказать, что процедурные функции нарушают и правило «одна функция — одна задача», и правило, касающееся слабо связанных языковых конструкций. Однако, без таких функций не обойтись.

Идеальная функция — это простая, детерминированная, чистая функция, обладающая следующими основными свойствами:

  • Одни и те же входные данные всегда дают один и тот же выход.
  • При её вызове отсутствуют побочные эффекты.

Избыточный код


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

Уильям Странк-младший, «Элементы стиля»

Лаконичный код весьма важен в деле разработки ПО, так как чем больше кода — тем больше мест, где можно допустить ошибку. Меньший объём кода означает меньше мест, где может скрыться ошибка, что ведёт к уменьшению количества ошибок.

Лаконичный код легче читать, так как он имеет более высокий уровень соотношения полезных данных к информационным «помехам». Читателю нужно отсеять меньше синтаксического «шума» для того, чтобы понять смысл программы. Таким образом, меньший объём кода означает меньше синтаксического «шума», и, как результат, более чёткую передачу смысла.

Если выразиться словами из «Элементов стиля», сжатый код — это код энергичный. Вот, например, такая конструкция:

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

Её можно сократить до такой:

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

Тем, кто знаком с минималистичным синтаксисом, характерным для стрелочных функций (они появились в ES6 в 2015-м), легче читать эту запись, а не код из первого примера. Ненужные элементы, такие, как скобки, ключевое слово function, инструкция return, здесь опущены.

В первом варианте много служебных синтаксических конструкций. Это и скобки, и ключевое слово function, и return. Они, для того, кто знаком со стрелочными функциями — не более, чем синтаксический «шум». И, в современном JavaScript, подобные конструкции существуют лишь для того, чтобы код могли читать те, кто пока недостаточно уверенно владеет ES6. Хотя, ES6 стал стандартом языка ещё в 2015-м, так что время узнать его получше уже давно пришло.

Ненужные переменные


Иногда мы даём имя чему-то, для чего оно не очень-то и нужно. Скажем, некоей промежуточной переменной, без которой можно обойтись. Почему это вредно? Проблема тут в том, что человеческий мозг имеет ограниченные ресурсы кратковременной памяти. Встретив в тексте программы переменную, мы вынуждены запоминать её. Если имён много, наша память переполняется, при чтении периодически приходится возвращаться назад…

Именно поэтому опытные разработчики приучают себя к устранению ненужных переменных.

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

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

Избавившись от ненужного, код можно переписать так:

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

Ещё один распространённый подход к сокращению числа переменных заключается в применении композиции функций и так называемой «бесточечной нотации».

Бесточечная нотация — это способ объявления функций без упоминания аргументов, которыми оперируют эти функции. Обычными способами применения такого подхода являются каррирование и композиция функций.

Вот пример каррирования:

const add2 = a => b => a + b;
// теперь мы можем объявить бесточечную версию inc(),
// которая позволяет добавить 1 к любому числу.
const inc = add2(1);
inc(3); // 4

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

Взглянем на пример, в котором используется композиция функций. Композиция функций — это применение функции к результатам, возвращаемым другой функцией. Осознаёте вы это или нет, но вы применяете композицию функций постоянно.

Например, когда пользуетесь цепочками вызовов методов вроде .map() и promise.then(). Если обратиться к наиболее общей форме записи композиции функций, то получится такая конструкция: f(g(x)). В математике это обычно записывают как f ? g, что читается как «применение функции f к результату функции g».

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

Посмотрим, как этот приём позволяет писать более чистый код:

const g = n => n + 1;
const f = n => n * 2;
// С использованием точечной нотации:
const incThenDoublePoints = n => {
  const incremented = g(n);
  return f(incremented);
};
incThenDoublePoints(20); // 42
// compose2 — Принимает две функции и возвращает их композицию
const compose2 = (f, g) => x => f(g(x));
// В бесточечной нотации:
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42

То же самое можно сделать с любой функцией.

Функтором называют объект, реализующий функцию маппинга. Например, в JS это массивы (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(), которая применяет функции справа налево, и pipe(), которая применяет функции слева направо.

Например, в Lodash такие функции называются, соответственно, compose() и flow(). Когда я применяю эту библиотеку, то пользуюсь функцией flow() так:

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

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

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

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

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

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

Действительный залог


Действительный залог обычно означает более ясное и живое выражение мысли, нежели страдательный.

Уильям Странк-младший, «Элементы стиля»

Давайте программным конструкциям настолько ясные и чёткие имена, насколько это возможно:

  • 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)

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


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

  • 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» и «able». Вот примеры:

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

Последовательности слабо связанных языковых конструкций


… череда высказываний скоро становится однообразной и скучной.

Уильям Странк-младший, «Элементы стиля».


Разработчики наполняют функции последовательностями языковых конструкций. Эти конструкции задуманы так, чтобы выполнялись они одна за другой, по сути, являясь примером череды слабо связанных высказываний. Подобный подход, когда в некоем блоке программы собрано слишком много таких вызовов, ведёт к появлению так называемого «спагетти-кода».

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

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

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

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

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

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

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

Разделение операций, кроме того, открывает дорогу к их независимому тестированию. В процессе написания кода я постоянно запускаю модульные тесты для того, чтобы сразу же оценивать влияние на приложение вносимых в него изменений. Однако, если, как в нашем примере, объединить код рендеринга элемента управления с кодом загрузки исходных данных, не получится просто передать какие-нибудь условные данные функции вывода элемента для тестовых целей. Тут придётся тестировать всё — и загрузку, и подготовку, и вывод данных. Это, если проверить надо лишь что-то одно, приведёт к неоправданным затратам времени: данные, например, надо загрузить по сети, обработать, вывести в браузер… Для получения результатов тестирования придётся ждать дольше, чем при проверке отдельного компонента. Разделение функций позволит тестировать их отдельно от других частей приложения.

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

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

Хранение кода и других элементов программ, направленных на решение одной задачи


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

Вот, например, два варианта иерархии файлов для ToDo-приложения. Первый вариант представляет группировку файлов по типу:

.
+-- components
¦   +-- todos
¦   L-- user
+-- reducers
¦   +-- todos
¦   L-- user
L-- tests
    +-- todos
    L-- user

Второй — группировку по логическому принципу:

.
+-- todos
¦   +-- component
¦   +-- reducer
¦   L-- test
L-- user
    +-- component
    +-- reducer
    L-- test

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

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

Использование утвердительной формы для имён переменных и при построении выражений


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

Уильям Странк-младший, «Элементы стиля»

Перейдём сразу к примерам имён переменных:

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

?Условный оператор


Такая конструкция:

if (err) return reject(err);
// делаем что-нибудь...

… лучше такой:

if (!err) {
  // ... делаем что-нибудь
} else {
  return reject(err);
}

?Тернарный оператор


Так:

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

…лучше, чем так:

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

?Об отрицательных высказываниях


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

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

?Аргументы функций, принимающих значения null и undefined


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

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

…лучше, чем:

const createEvent = (
  title = 'Untitled',
  timeStamp = Date.now(),
  description = ''
) => ({ title, description, timeStamp });
// позже...
const birthdayParty = createEvent(
  'Birthday Party',
  undefined, // Этого можно было избежать
  'Best party ever!'  
);

Шаблоны и решение схожих задач


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

Уильям Странк-младший, «Элементы стиля»

При создании приложений программисту часто надо решать очень похожие друг на друга задачи. Кода, который может повторяться, обычно гораздо больше, чем совершенно уникального. Как результат, в ходе работы приходится постоянно делать одно и то же. Хорошо здесь то, что это даёт возможность для обобщения похожего кода и создания абстракций. Для этого достаточно выявить одинаковые части кода, выделить их и использовать везде, где они нужны. А в ходе разработки обращать внимание лишь на уникальные для того или иного фрагмента приложения конструкции. На самом деле, именно этой цели служат различные библиотеки и фреймворки.

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

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

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

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

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

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


Стандарт ES6 был принят в 2015-м, но и сегодня, через два года, многие разработчики избегают новых возможностей. Они стремятся писать код, который, по их мнению, легче читать, лишь потому, что им так привычнее. Среди таких новых возможностей — оператор , стрелочные функции, неявный возврат. Уход от новых технологий ради привычных, но устаревших — большая ошибка. Знакомство с новым происходит через практику, а после того, как возможности ES6 становятся привычными, совершенно очевидным становится их преимущество перед альтернативами из ES5. Сжатый код проще в сравнении с перегруженной синтаксическими конструкциями альтернативой.

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

  • Он менее подвержен ошибкам.
  • Его легче отлаживать.

Если, при таком подходе, подумать об ошибках, то получается следующее:

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

Если учесть вышесказанное, то компактный код обладает ещё и следующими полезными свойствами:

  • Его легче писать.
  • Его легче читать.
  • Его легче поддерживать.

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

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

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

Уважаемые читатели! А какими возможностями ES6 пользуетесь вы?
Поделиться с друзьями
-->

Комментарии (32)


  1. chuikoffru
    22.05.2017 13:45

    Спасибо. Из статьи для себя подчеркнул три важных вещи:
    1) группировать по функционалу
    2) условия if(err) reject
    3) примеры со стрелочными функциями и ненужными переменными.
    ES5 тяжело давался всегда, поэтому ES6 освоил быстро и с большим удовольствием!


    1. Lain_13
      23.05.2017 12:19

      На счёт arrow функций:

      1. Стоит помнить, что если собираетесь поддерживать IE 11 или Safari до 10й ветки, то использовать их не получится. Ещё некоторые браузеры под Android могут подсунуть свинью, включая довольно популярный UC Browser. Opera Mini тоже всё ещё используют (~3%!) и там их тоже нет. Если тебе не интересны 10~15% владельцев мобильных телефонов, то можешь использовать arrow-функции. Иначе стоит подождать, пока их поддержку сделают как минимум в UC Browser.

      2. Они не инициализируют this до момента вызова. Это можно применить для создания функций, обращающихся к this в теле конструктора класса, созданного на основе другого класса, и до вызова super в конструкторе наследуемого класса (в наследуемом классе нельзя вызывать this до вызова super — кидает ошибку). Хотя зачем так вообще может понадобиться сделать для меня загадка и, наверное, лучше так не делать.

      3. Данный тип функций невозможно использовать в качестве конструктора. У них даже prototype нет, чтоб это было возможно.

      4. В теле этих функций нельзя использовать arguments, new.target и super. Все они наследуются из контекста вызова функции. Последние два в них вообще бессмысленно вызывать в виду пункта 3. Если умудритесь сделать все функции arrow-функциями, то попытка обратиться к arguments и вовсе приведёт к ошибке так-как
      эта переменная не будет создана ни в одном контексте. В любом случае лучше не используйте arguments внутри arrow-функций если только не хотите умышленно запутать свой код.


      1. Lain_13
        23.05.2017 12:35

        И да, arrow-функции фактически наследуют this из родительской функции:

        (function(){(function(){console.log(this)})();(()=>console.log(this))();}.bind({}))()
        

        Выдаст в консоли:
        Window {...}
        Object {}


  1. tehSLy
    22.05.2017 15:19
    +3

    ИМО:
    Насчет стиля кода по ES6, тут как по мне, нужно в меру. Мало того, что некотый «сахар» дороже в вызове, дак еще и уменьшает читаемость в некоторых случаях, особенно, когда код превращается в микс из букв и специальных символов.
    Не всегда выпиливание «лишних» переменных способствует восприятию. Когда видишь только агрегирование данных, без промежуточных переменных, куда сложнее держать в голове всю цепочку действий, тогда как с промежуточными переменными легче понимать что творится в коде. Сразу же, сюда — о пункте про if(!value) — чтобы оперировать с отдельной инвертированной переменной, ее нужно объявить, что противоречит пункту о ненужных переменных. Как то автор(не перевода) не смог себе не противоречить.
    В остальном конечно все здраво, хотя есть парочка примеров, не сильно вписывающихся.


    1. Quilin
      22.05.2017 15:46

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

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


      1. Eldhenn
        22.05.2017 16:24
        +3

        То есть переменные бывают ненужными, а функции нет? Превратить десяток «длинных» функций в сотню однострочных — нормально?


        1. Quilin
          22.05.2017 17:18
          +1

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

          Смотрите, допустим у нас есть некоторых размеров функция, в которой присутствует несколько локальных переменных. Насколько я понял, автор оригинальной статьи называет «лишними» те переменные, от объявления которых можно избавиться, никоим образом не повлияв на рантайм приложения.

          Например:

          var buzz = function () {
            var data = getSomeData();
            var result = doSomething(data);
            return result;
          }
          


          Некоторые линтеры сами предложат заменить это на
          var buzz = function () {
            return doSomething(getSomeData());
          }
          


          А в каком-нибудь абстрактном функциональном языке это будет выглядеть и вовсе так:
          buzz = getSomeData * doSomething
          


          Но за этими getSomeData и doSomething вовсе не обязана стоять одна строчка кода (в таком случае ее и так можно заинлайнить, наверное).

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


          1. Eldhenn
            22.05.2017 19:16
            +2

            Почему я полюбил «лишние» переменные.
            Потому что сегодня result сразу возвращается как есть. А завтра он будет обработан и заброшен в структуру, которая и будет возвращаться. Поэтому для себя я принял правило — если в каком-то результате есть смысл, его лучше сохранить. Компилятор сам потом всё оптимизирует, а вот код рефакторить будет проще.


            1. TheShock
              22.05.2017 20:38

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


              1. Eldhenn
                22.05.2017 20:58

                Да, и отладка, разумеется.


                1. Quilin
                  23.05.2017 11:42

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


      1. andreysmind
        23.05.2017 10:25

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


  1. mmxdesign
    22.05.2017 21:31

    одна функция — одна задача


  1. vdonich
    22.05.2017 21:54

    У меня только один вопрос, а причем тут JS?
    Неужели эти общие принципы нельзя применить к другим языкам?
    Мне почему-то кажется (т.е. лень проверять) что на том же Питоне все примеры можно практически один к одному переписать.
    (а так — солидарен, хотя имхо и вполне очевидно во многом)


  1. Holix
    22.05.2017 23:04
    +4

    ES5 имеет более очевидный синтаксис. Стрелки ес6 мой глаз видит как выражения, а не как функции. А присваивания в аргументах стрелок, да еще и без return вообще выглядит как бардак. Единственная полезность стрелок — сохранение this. IMHO.


    1. Eldhenn
      23.05.2017 10:28
      +3

      > Единственная полезность стрелок — сохранение this

      Не, ну и компактный синтаксис для простых коллбеков — которые сами по длине сравнимы с «function(){}».


    1. knotri
      23.05.2017 17:55

      Юзаю и бед не знаю.
      Забыл уже когда писал не стрелочную функцию


  1. hoack
    23.05.2017 00:01
    +1

    Согласен практически со всем за исключением «избыточного кода».

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

    Например, приводимый в статье пример с каррированием. Приведенный в статье код, на мой взгляд, плохо читается. Двойная стрелка

    const add2 = a => b => a + b
    
    потребует некоторого времени на осмысление, а вот следующая строчка
    const inc = add2(1) 
    
    и совсем нехороша: через пару месяцев, взглянув на нее, сможете ли вы с ходу сказать, что inc — функция?


    1. TheShock
      23.05.2017 00:22
      +1

      и совсем нехороша: через пару месяцев, взглянув на нее, сможете ли вы с ходу сказать, что inc — функция?

      А вы правда думаете, что люди, которые пишут такой код — поддерживают его целых два месяца?


      1. andreysmind
        23.05.2017 10:15
        +2

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


      1. tehSLy
        23.05.2017 10:50
        +2

        Кто пишут — может и нет, а вот те, кому он достанется после них — запросто.


      1. justboris
        23.05.2017 11:09

        А чего здесь такого? Вот пример с Github: библиотека recompose, там большая часть кода основана на каррировании.


        Если это нормально оформить в документации, то проблем не возникает.


        1. sumanai
          23.05.2017 15:59
          +1

          Если это нормально оформить в документации

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


          1. justboris
            23.05.2017 16:07

            В том конкретном примере, что я привел, достаточно один раз понять, почему в проекте пишется a => b => {} вместо (a, b) => {} и после этого код читается вполне неплохо.


            1. TheShock
              23.05.2017 17:08

              Недостаточно, потому что встречая в коде func(a)(b) ты не будешь знать — это уже итоговое значение, или всё ещё функция, которая ждет недостающий аргумент


              1. justboris
                23.05.2017 17:35

                Typescript вам в помощь! С ним такие вещи узнаются легко.


                Если же типов нет, то поможет здравая логика, например соглашение, что все функции из модуля Х возвращают еще одну функцию, например add(1)(2). Если же arity у каждой функции разное, то работать с этим неудобно, с этим соглашусь


                1. sumanai
                  23.05.2017 17:55

                  например соглашение, что все функции из модуля Х возвращают еще одну функцию

                  Не поможет, опять придётся читать документацию вместе с кодом.


                  1. justboris
                    23.05.2017 17:58

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


                    1. TheShock
                      23.05.2017 18:18

                      И часто вы видите типизированный код в мире JS? Я как раз TS очень даже пользуюсь. Да вот только хипстеры, которые пишут add(1)(2) обычно считают типизацию порождением сатаны.


                      1. justboris
                        23.05.2017 18:58

                        Бывает, вижу код на TS, бывает и с каррированием, в основном с библиотеках из компонентах для React. И тайпинги для подобных штук, имеются.


                        Не стоит обобщать ваш личный опыт на состояние сообщества в целом.


                        1. TheShock
                          23.05.2017 19:21

                          Вы думаете я не знаю про DefinitelyTyped? Вот только оно, обычно поддерживается сообществом, не всегда есть, те, что есть не обязательно полные, а те, что полные — не обязательно на последнюю версию. Я уже молчу про всякие беты.

                          И, обычно, на такое получаешь бредовый ответ из разряда: «ну так помоги сообществу, напиши сам». Напишу, конечно, куда я денусь, но это рак JS, что авторы библиотек не спешат их типизировать и почему это меня не должно возмущать?

                          Я как раз разрабатываю на React и TS, так что прекрасно знаю «состояние сообщества».


  1. 3al
    23.05.2017 13:20

    Бесточечные функции в меру — это замечательно, но для js, насколько мне известно, нет утилит для перевода функций в бесточечную форму (вроде pointfree для хаскеля) и, что важнее, нет утилит для обратного перевода (вроде pointful) на случай если кто-то из команды слишком увлёкся комбинированием point-free функций.