Разработка

Совершенный код: злые однострочники

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

// Реальный код из открытого проекта Хекслета
const programImports = source.program.body
  .filter(item => item.type === ‘ImportDeclaration’)
  .filter(item => item.source.value.startsWith(‘hexlet’));
// Весь код тут https://github.com/Hexlet/hexlet-exercise-kit/blob/master/import-documentation/src/index.js

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

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

Проекты Хекслета – неиссякаемый источник примеров кода для данной статьи. Я выбрал несколько наиболее интересных и ниже проведу их разбор.

// Многие программисты искренне считают, что такой код хорош,
// потому что здесь нет переменных. Но это не так.
compareFiles(JSON.parse(fs.readFileSync(fullPathToFile1)), JSON.parse(fs.readFileSync(fullPathToFile2)))

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

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

const config1 = JSON.parse(fs.readFileSync(fullPathToFile1));
const config2 = JSON.parse(fs.readFileSync(fullPathToFile2));

compareFiles(config1, config2);

Теперь о коде рассуждать проще, потому что большая задача превратилась в несколько маленьких. Стало понятнее, с чем мы имеем дело (это конфиги!). Как бонус, упрощается отладка, так как можно посмотреть, что лежит в промежуточных переменных. Можно ли его улучшить дальше? Да, вложенные вызовы всегда должны вызывать желание избавиться от них. Особенно если вложенные вызовы производят побочные эффекты.

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

f(f2(f3()))); // нужно разбивать
f(f2()); // вероятно не нужно (а может и нужно)

Хозяйке на заметку. Лучший способ избавиться от вложенных вызовов – воспользоваться механизмом pipeline. Благодаря babel он есть и в js.

Другой пример:

expect(genDiff(first, second, format)).toBe(fs.readFileSync(`${__dirname}${result}`, 'UTF-8').trimRight());

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

Отрефакторим этот код:

const fixturePath = path.join(__dirname, result);
const expectedValue = fs.readFileSync(fixturePath, 'UTF-8').trimRight();
const actualValue = genDiff(first, second, format);
expect(actualValue).toBe(expectedValue);

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

Следующий пример:

state.formStatus = ((validator.isURL(value) && (state.rssFlows.some(rssFlow => rssFlow.url === value) === false)) ? 'valid' : 'invalid');

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

Ну и последний пример:

const urls = _.flatMap(['a', 'link', 'script', 'img'], item => $(item).map((i, el) => $(el).attr(attributes[item])).get().filter(el => el[0] === '/' && el.length > 1)

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

Kirill Mokevnin 06 июня 2019
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →