Как важно писать код, который могут читать люди

в 7:13, , рубрики: javascript, Блог компании NIX Solutions, Программирование, читабельность, метки:

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

Яркие примеры нечитаемого кода встречаются на соревнованиях JS1k, цель которых заключается в написании лучших JS-приложений, состоящих из 1024 символов или того меньше. То же самое можно сказать и про JSF*ck, крайне своеобразный стиль программирования, использующий только шесть разных символов для написания JS-кода. Глядя на выложенный на этих сайтах код, вы будете ломать голову, пытаясь понять, что здесь происходит. А представьте, каково это: написать подобный код и спустя месяц пытаться исправить баг.

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

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

Оригинал статьи: https://www.sitepoint.com/importance-of-code-that-humans-can-read/

Разделение кода

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

Оптимизация файлов

Мы годами занимаемся оптимизацией для веба, и JS-файлы не исключение. Выполняя минификацию в ожидании HTTP/2, мы экономили запросы, объединяя несколько скриптов в один. Сегодня можно работать как угодно, взвалив задачу обработки файлов на инструменты вроде Gulp или Grunt. То есть можно программировать так, как хочется, а оптимизацией (например, выполнением конкатенации) пусть занимаются специально обученные приложения.

// Загрузка пользовательских данных из API
var getUsersRequest = new XMLHttpRequest();
getUsersRequest.open('GET', '/api/users', true);
getUsersRequest.addEventListener('load', function() {
    // Делаем что-нибудь с пользователями
});
 
getUsersRequest.send();
 
//---------------------------------------------------
// Здесь начинается другая функциональность.
// Возможно, это шанс разделить код на разные файлы.
//---------------------------------------------------
 
// Загрузка данных о постах из API
var getPostsRequest = new XMLHttpRequest();
getPostsRequest.open('GET', '/api/posts', true);
getPostsRequest.addEventListener('load', function() {
    // Делаем что-нибудь с постами
});
 
getPostsRequest.send();

Функции

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

// Загрузка пользовательских данных из API
function getUsers(callback) {
    var getUsersRequest = new XMLHttpRequest();
    getUsersRequest.open('GET', '/api/users', true);
    getUsersRequest.addEventListener('load', function() {
        callback(JSON.parse(getUsersRequest.responseText));
    });
 
    getUsersRequest.send();
}
 
// Загрузка данных о постах из API
function getPosts(callback) {
    var getPostsRequest = new XMLHttpRequest();
    getPostsRequest.open('GET', '/api/posts', true);
    getPostsRequest.addEventListener('load', function() {
        callback(JSON.parse(getPostsRequest.responseText));
    });
 
    getPostsRequest.send();
}
 
// Благодаря правильному именованию этот код легко понять
// без чтения самих функций
getUsers(function(users) {
    // Делаем что-нибудь с пользователями
});
getPosts(function(posts) {
    // Делаем что-нибудь с постами
});

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

function fetchJson(url, callback) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.addEventListener('load', function() {
        callback(JSON.parse(request.responseText));
    });
 
    request.send();
}
 
// Следующий код всё ещё легко понять
// без чтения предыдущей функции
fetchJson('/api/users', function(users) {
    // Делаем что-нибудь с пользователями
});
fetchJson('/api/posts', function(posts) {
    // Делаем что-нибудь с постами
});

А что если мы захотим создать нового пользователя с помощью POST-запроса? Есть два варианта:

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

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

Объектно-ориентированное программирование

Рассмотрим объекты, часто называемые классами, представляющие собой контекстно-зависимые кластеры функций. Такой объект прекрасно помещается в отдельный файл. В нашем случае можно создать базовую обёртку для XMLHttpRequest. Обратите внимание, что вплоть до 2015 года JavaScript был прототипно-ориентированным языком и не имел классов. Этот факт вместе с прототипным наследованием диктуют определённые правила форматирования кода.

HttpRequest.js

function HttpRequest(url) {
    this.request = new XMLHttpRequest();
 
    this.body = undefined;
    this.method = HttpRequest.METHOD_GET;
    this.url = url;
 
    this.responseParser = undefined;
}
 
HttpRequest.METHOD_GET = 'GET';
HttpRequest.METHOD_POST = 'POST';
 
HttpRequest.prototype.setMethod = function(method) {
    this.method = method;
    return this;
};
 
HttpRequest.prototype.setBody = function(body) {
    if (typeof body === 'object') {
        body = JSON.stringify(body);
    }
 
    this.body = body;
    return this;
};
 
HttpRequest.prototype.setResponseParser = function(responseParser) {
    if (typeof responseParser !== 'function') return;
 
    this.responseParser = responseParser;
    return this;
};
 
HttpRequest.prototype.send = function(callback) {
    this.request.addEventListener('load', function() {
        if (this.responseParser) {
            callback(this.responseParser(this.request.responseText));
        } else {
            callback(this.request.responseText);
        }
    }, false);
 
    this.request.open(this.method, this.url, true);
    this.request.send(this.body);
    return this;
};

app.js

new HttpRequest('/users')
    .setResponseParser(JSON.parse)
    .send(function(users) {
        // Делаем что-нибудь с пользователями
    });
 
new HttpRequest('/posts')
    .setResponseParser(JSON.parse)
    .send(function(posts) {
        // Делаем что-нибудь с постами
    });
 
// Создаём нового пользователя
new HttpRequest('/user')
    .setMethod(HttpRequest.METHOD_POST)
    .setBody({
        name: 'Tim',
        email: 'info@example.com'
    })
    .setResponseParser(JSON.parse)
    .send(function(user) {
        // Делаем что-нибудь с новым пользователем
    });

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

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

Понятный человеку синтаксис

Файлы, функции, объекты — это лишь грубые ориентиры. Они облегчают сканирование нашего кода. А вот сделать код лёгким для чтения — гораздо более тонкое искусство. Мельчайшие детали могут полностью изменить картину. Например, часто в редакторах с помощью вертикальной линии длина строк ограничивается 80 символами. Но есть и много других нюансов!

Именование

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

Функции обычно именуют в СтилеВерблюда: сначала идёт глагол, а затем субъект.

function getApiUrl() { /* ... */ }
function setRequestMethod() { /* ... */ }
function findItemsById(n) { /* ... */ }
function hideSearchForm() { /* ... */ }

Для наименований переменных попробуйте использовать методику перевёрнутой пирамиды: сначала идёт субъект, а затем свойства.

var element = document.getElementById('body'),
    elementChildren = element.children,
    elementChildrenCount = elementChildren.length;
 
// Задавая набор цветов, в наименованиях переменных добавлен префикс “color”
var colorBackground = 0xFAFAFA,
    colorPrimary = 0x663399;
 
// Задавая свойства фона, в качестве основы используется “background”
var backgroundColor = 0xFAFAFA,
    backgroundImages = ['foo.png', 'bar.png'];
 
// Всё зависит от контекста
var headerBackgroundColor = 0xFAFAFA,
    headerTextColor = 0x663399;

В подобных случаях лучше использовать ассоциативные массивы (объекты в JS), тогда можно избавиться от приставки color, и читаемость улучшается.

var colors = {
'backgroundRed' : red,
'primary': greeen
};

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

var URI_ROOT = window.location.href;
Классы обычно именуются в СтилеВерблюда, первая буква прописная.
function FooObject {
    // ...
}

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

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

Компактность и оптимизация

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

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

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

// Ура, кому-то удалось сделать этот однострочник!
var state = isHidden ? 'hidden' : isAnimating ? 'animating' : '';
 
// Ура, кто-то сделал его читабельным!
var state = '';
if (isAnimating) state = 'animating';
if (isHidden) state = 'hidden';

Микрооптимизации, призванные улучшить производительность, часто малоэффективны. И чаще всего они менее читабельны, чем не столь производительные аналоги.

// Это может работать быстро
$el[0].checked;
 
// Это тоже достаточно быстро, но читается легче
// Источник: http://jsperf.com/prop-vs-ischecked/5
$el.prop('checked');
$el.is(':checked');
$el.attr('checked');

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

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

Не-код

Считайте это иронией, но лучший способ сделать код читабельным — добавить неисполняемый синтаксис. Назовём его не-код. Но не забывайте о том, что в JS есть общепринятые стандарты форматирования кода, которыми не стоит пренебрегать. Они будут рассмотрены ниже.

Отступы

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

  • Использование одного выражения на строку.
  • Отступ для выделения блока.
  • Для разделения кода можно использовать дополнительные разрывы.

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

function sendPostRequest(url, data, cb) {
    // Несколько присваиваний сгруппированы и аккуратно выделены отступами
    var requestMethod = 'POST',
        requestHeaders = {
            'Content-Type': 'text/plain'
        };
 
    // Инициализация, конфигурирование и отправка XMLHttpRequest
    var request = new XMLHttpRequest();
    request.addEventListener('load', cb, false);
    request.open(requestMethod, url, false);
    request.send(data);
}

Комментарии

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

  • Объяснения и аргументирования неочевидного кода.
  • Какой баг чинится с помощью этого фикса, с указанием источника.

// Суммирование значений диапазона графика
var sum = values.reduce(function(previousValue, currentValue) {
    return previousValue + currentValue;
});

Не все фиксы очевидны. Внесение дополнительной информации многое проясняет:

if ('addEventListener' in element) {
    element.addEventListener('click', myFunc);
}
// IE8 и ниже не поддерживают .addEventListener,
// поэтому вместо него нужно использовать .attachEvent
// http://caniuse.com/#search=addEventListener
// https://msdn.microsoft.com/en-us/library/ms536343%28VS.85%29.aspx
else {
    element.attachEvent('click', myFunc);
}

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

Встроенная документация

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

/**
* Создание HTTP-запроса
* @constructor
* @param {string} url
*/
function HttpRequest(url) {
    // ...
}
 
/**
* Настройка объекта заголовка
* @param {Object} headers
* @return {HttpRequest}
*/
HttpRequest.prototype.setHeaders = function(headers) {
    for (var header in headers) {
        this.headers[header] = headers[header];
    }
 
    // Возврат в цепочку
    return this;
};

Хотя здесь в комментарии указано param {Object} headers, лучше всё же указывать описание того, что хранится и передаётся в этом параметре. Эту информацию рекомендуется указывать сразу после самого параметра. Допустим, это Holds success request headers( 200 ). Тогда комментарий будет выглядеть так:

@param {Object} headers Holds success request headers( 200 )

Существуют разные форматы документации к функциям, очень важно, чтобы вся команда использовала один и тот же формат. А если разработчик один, то нужно использовать один и тот же формат в разных местах. В описании формальных параметров обязательно указывать ТИ формального параметра. Некоторые IDE налету подсказывают, если передается переменная не того типа. JS — язык с динамической типизацией, и не факт, что IDE выдаст ошибку, но при парсинге таких документаций можно будет избежать большого количества ошибок.

Загадки callback'ов

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

function doRequest(url, success, error) { /* ... */ }
 
doRequest('https://example.com/api/users', function(users) {
    doRequest('https://example.com/api/posts', function(posts) {
        // Делаем что-нибудь с пользователями и постами
    }, function(error) {
        // ошибка с /api/posts
    });
}, function(error) {
    // ошибка с /api/users
});

Для решения обеих задач, в ES2015 (также известном как ES6) был реализован объект Promise.

function doRequest(url) {
    return new Promise(function(resolve, reject) {
        // Инициализируем запрос
        // В случае успеха вызываем resolve(response)
        // В случае ошибки вызываем reject(error)
    });
}
 
// Сначала запрашиваем пользователей
doRequest('https://example.com/api/users')
// Когда все они успешно выполнены, запускаем .then()
.then(function(users) { /* ... */ })
// Когда любой из Promise активизировал функцию reject(), вызываем .catch()
.catch(function(error) { /* ... */ });
 
// Параллельный запуск нескольких Promise
Promise.all([
    doRequest('https://example.com/api/users'),
    doRequest('https://example.com/api/posts')
])
.then(function(responses) { /* ... */ })
.catch(function(error) { /* ... */ });

Хотя пришлось добавить часть кода, в целом его стало легче правильно интерпретировать. Если вы хотите больше узнать о Promise, то можете прочитать JavaScript Goes Asynchronous (and It’s Awesome).

ES6/ES2015

Если вы читали спецификацию ES2015, то могли заметить, что все примеры кода в этой статье относятся к старым версиям (за исключением объекта Promise). Несмотря на то, что ES6 даёт нам очень широкие возможности, к нему есть ряд претензий относительно ужасной читабельности.

Синтаксис с использованием толстой стрелки (fat arrow syntax) определяет функцию, наследующую значение this от её родительской области видимости. Как минимум, для этого она была создана. Также её заманчиво использовать для определения обычных функций.

var add = (a, b) => a + b;
console.log(add(1, 2)); // 3

Другой пример — синтаксис “rest and spread”.

/**
* Суммируем список чисел
* @param {Array} numbers
* @return {Number}
*/
function add(...numbers) {
    return n.reduce(function(previousValue, currentValue) {
        return previousValue + currentValue;
    }, 0);
}
 
add(...[1, 2, 3]);
 
/**
* Суммируем a, b и c
* @param {Number} a
* @param {Number} b
* @param {Number} c
* @return {Number}
*/
function add(a, b, c) {
    return a + b + c;
}
 
add(1, 2, 3);

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

Заключение

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

  • JSHint — линтер JavaScript, позволяющий избегать ошибок в коде.
  • Idiomatic — популярный стандарт стилистического оформления кода, но вовсе не обязательно следовать ему дословно.
  • EditorConfig — определение кросс-редакторных стилей оформления кода.

Очень важно анализировать сложность кода, и существуют разные подходы к анализу. Подробнее об этом можно почитать тут: http://jscomplexity.org/complexity

Помимо инструментов для выдерживания качества и стиля, есть ряд инструментов для улучшения читабельности. Попробуйте разные схемы подсветки синтаксиса, или воспользуйтесь миникартой для иерархической оценки своего скрипта (Atom, Brackets). Например, https://github.com/airbnb/javascript — довольно популярный стандарт форматирования кода в JS. Нужно отметить, что форматирование кода и стандарт зависят от используемых инструментов. Например, в случае с React и JSX будут другие стилистические требования.

Автор: NIX Solutions

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js