Создаём Самодокументируемый JavaScript Код
Без всякого сомнения, найти в коде комментарии, которые являются устаревшими или не слишком информативными – это не слишком приятное событие.
К тому же, с комментариями есть ещё несколько проблем – вы изменяете какой-то код и забываете удалить или обновить соответствующий комментарий. Такие комментарии не повлияют на работоспособность кода программы, но представьте, что может произойти, например, при отладке приложения. Вы читаете комментарии, в них говорится об одной вещи, в то время, как код выполняет совершенно другую. В конечном итоге, возможно, вы потратите большое количество своего времени, будучи введены в заблуждение подобными несоответствиями.
С другой стороны, писать код совсем без комментариев – тоже вариант так себе. Практически невозможно найти пример программы, где отсутствие комментирование кода привнесло бы положительные изменения.
Тем не менее, существует некоторое количество способов, существенно уменьшающих потребность в комментировании. Мы можем использовать определённые подходы программирования для упрощения и большей ясности нашего кода, о которых поговорим в этой статье.
Это поспособствует более лёгкому пониманию создаваемого нами кода, а также поможет улучшить разработку приложения в целом.
По существу, такой подход к написанию кода называется самодокументированным. Мы разберём, как применить такой подход прямо сейчас в вашем процессе работы, плюс ко всему – рассматриваемые техники применимы не только к JavaScript, но и к другим языкам программирования.
Обзор некоторых подходов самодокументации кода
Большинство разработчиков добавляют комментарии в создаваемый код. В данной статье, мы сосредоточимся только на коде – комментарии важны, без сомнения, но это тема для отдельного разговора.
В общем, мы можем разделить подходы к самодокументации на три больших категории:
- Структурный подход. В этом случае, структура кода и директорий создают вкупе как можно более ясное понимание происходящего в приложении
- Родственные имена. Функции и переменные именуются схожим образом, причём им даются осознанные имена, объясняющие их назначение
- Выбор подходящего синтаксиса. Использование определённых особенностей языка для создания понятного кода. Или наоборот, не использование некоторых синтаксических конструкций во избежание чрезмерного усложнения читаемости кода.
Всё это звучит хорошо, но вот на практике можно столкнуться с тем, что не всегда бывает понятно, какой подход лучше всего применить именно к этому случаю. Поэтому, мы разберём подробно каждый из них на практических примерах.
Структурный подход
Давайте взглянем на примеры использования структурного подхода, суть которого заключается в создании как можно более ясной и последовательной композиции кода и директорий, в которых он помещён.
Перенос кода в функции
Это именно то, что зовётся “выделением функции” – берём написанный нами код и помещаем его в новую функцию, тем самым мы “выделяем” часть кода для функции. К примеру, попробуйте угадать с одного раза, что делает этот код:
var temperature = ( amount * 1.8 ) + 32;
Здесь могли бы помочь комментарии, чтобы прояснить происходящее, но мы можем выделить этот код в функцию, чтобы сделать его самодокументируемым:
var temperature = calculateFahrenheit( amount );
function calculateFahrenheit( tempCelsius )
{
return ( tempCelsius * 1.8 ) + 32;
}
Мы перенесли код для вычисления значения температуры в новую функцию, имя которой является достаточно понятным для того, чтобы прояснить суть происходящих вычислений. К тому же, мы получили полезный метод, который можно использовать заново несколько раз, что позволит избежать повторения кода.
Использование функций вместо условий
Встречающиеся условия с несколькими операндами являются трудными для быстрого понимания, особенно без сопутствующих им комментариев. Мы можем избежать использование таких конструкций таким же способом, как и в случае с выделением кода в функцию.
Например, этот код:
if( ! data.amountMax || ! data.amountMin ) {}
Мы превратим с помощью выделения его в функцию, в такой код:
function isHasAmount( data )
{
return data.amountMax && data.amountMin;
}
if( ! isHasAmount( data ) ) {}
Таким образом, хоть мы и получили увеличение количества кода, но простота восприятия в таком случае стоит того, чтобы смириться с этим недостатком.
Использование переменных вместо условий
Использование переменных подобно выделению кода в функцию, только вместо функции, мы поместим условие в переменную.
Возьмём для примера уже использованное условие if
:
if( ! data.amountMax || ! data.amountMin ) {}
И вместо выделения кода в функцию, мы поместим его в переменную, что даст нам такое же ясное понимание работы условия:
var isHasAmount = data.amountMax && data.amountMin;
if( ! isHasAmount ) {}
В некоторых случаях, например, когда используемая вами логика весьма необычна и применяется в одном определённом месте, такой подход является наиболее оптимальным. Ну а самым распространённым вариантом применения такого подхода будут математические выражения.
Например, такое выражение:
return x * y + ( a / b );
Мы можем заменить на следующий код:
var multiply = x * y,
divide = a / b;
return multiply + divide;
Даже в таком простом математическом выражение можно получить выгоду от использования выделения кода в переменные.
Использование классов и интерфейсов
Создание чёткой структуры класса с корректно названными методами и свойствами является отличным публичным интерфейсом, к которому, после его создания, может обратиться любой разработчик и ему будет понятна работа такого класса с первого взгляда.
Рассмотрим такой пример класса:
class Voldemar
{
setPosition( value )
{
this.position = value;
}
getPosition()
{
return this.position;
}
}
В этом классе может содержаться ещё множество другого кода, но мы рассмотрим пример создания самодокументируемого публичного интерфейса на простом примере.
Итак, при взгляде на этот класс, можно ли сразу же сказать, как его нужно использовать? Конечно, если потратить немного времени, то ответ будет очевиден. Созданные методы в этом классе имеют осознанные имена, по которым можно определить их назначение. Но всё-таки не совсем ясно, как мы должны их использовать. И в таком случае пригодились бы дополнительные комментарии или документация к этому классу, чтобы прояснить ситуацию.
Но что, если мы немного изменим код данного класса:
class Voldemar
{
sit()
{
this.position = 'sit';
}
stand()
{
this.position = 'stand';
}
isSitting()
{
return this.position === 'sit';
}
}
Стало намного понятней, как же использовать этот класс. Обратите внимание на то, что мы всего лишь изменили
публичный интерфейс, а свойство, отвечающее за положение Вольдемара осталось тем же this.position
.
Теперь можно понять работу класса Voldemar
просто с первого взгляда. Таким образом, можно сказать, что даже
несмотря на хорошее именование свойств и методов класса, общая картина его работы может быть недостаточна
понятна. И проделав нехитрые манипуляции с кодом, можно получить огромную выгоду от наглядности представления
внутреннего состояния класса.
Использование группировки кода
Применение данного подхода также можно рассматривать, как один из видов самодокументации.
При определении переменных хорошей практикой будет их размещение непосредственно в том месте, где они используются, а также группировать их вместе, основываясь на здравом смысле.
Подобное размещение переменных будет указателем на взаимоотношения между различными частями кода приложения, и каждый, кто будет рассматривать их в будущем легко определит какую именно часть кода они затрагивают.
Рассмотрим пример кода:
var amount = 1;
yeah();
abc();
nope( amount );
yep( 1234 );
good( amount );
Можно ли без всяких усилий сказать, сколько раз в коде используется переменная amount
? Если сравнить вот с
таким вариантом:
var amount = 1;
nope( amount );
good( amount );
abc();
yeah();
yep( 1234 );
То применение группировки для общих по смыслу использования функций и переменных может значительно улучшить читаемость кода.
Использование чистых функций
Чистые функцию являются намного более лёгкими для понимания их работы, чем те функции, которые изменяют вводные данные.
В двух словах, что такое чистая функция, можно сказать так: если при вызове функции с одинаковыми параметрами она всегда возвращает одинаковый результат работы, то такую функцию можно назвать “чистой”. Это значит, что подобные функции не должны иметь никаких побочных эффектов в виде изменения данных и не должны полагаться на разные состояния данных, таких, как время, свойства объектов и т.д.
Функции такого типа значительно легче воспринимаются из-за того, что все значения, которые влияют на результат работы передаются в такую функцию в явном виде. Вам не нужно будет копаться в коде, чтобы выяснить что за данные и откуда они приходят, или что влияет на результат работы, т.к. всё будет видно с первого взгляда.
Для примера можно рассмотреть стандартные функции JavaScript slice()
и splice()
. Функция
slice()
всегда вернёт
одинаковый результат при одинаковых вводных значениях, поэтому её можно считать чистой. В свою очередь функция
splice()
каждый раз возвращает разные значения, отрезая часть массива, что будет побочным эффектом.
var myArray = [ 1, 2, 3, 4, 5 ];
/*
* Пример чистой функции
*/
myArray.slice( 0, 3 ); // [ 1, 2, 3 ]
myArray.slice( 0, 3 ); // [ 1, 2, 3 ]
myArray.slice( 0, 3 ); // [ 1, 2, 3 ]
/*
* Пример не чистой функции
*/
myArray.splice( 0, 3 ); // [ 1, 2,3 ]
myArray.splice( 0, 3 ); // [ 4, 5 ]
myArray.splice( 0, 3 ); // []
Ещё одна причина, по которой чистые функции хорошо подходят для создания самодокументируемого кода – это то, что вы всегда можете быть уверенным в результате их работы. Функция всегда вернет ожидаемый результат на основе тех параметров, которые вы ей передали и процесс работы чистой функции не будет затронут каким бы то ни было внешним воздействием.
Хорошим примером, когда всё может пойти не так, как вы ожидали, может послужить метод
document.write()
. Иногда,
при определённых обстоятельствах, использование данного метода может привести к тому, что в результате его работы
перед вами окажется просто чистая страница браузера. Именно поэтому, разработчики стараются избегать его
использования без веской на то причины.
Создание структуры директорий и файлов
При именовании директорий или файлов следует всегда придерживаться какого-то определённого вами или командой разработчиков соглашения для конкретного проекта. В случае, если для проекта нет чёткого соглашения именования, тогда будет уместно следовать тем правилам, которые приняты в использованном языке программирования.
Именование
Существует популярная цитата о двух самых сложных вещах в программировании:
Есть только две сложные вещи в программировании: инвалидация кэша и именование чего-либо.
— Phil Karlton
Поэтому, давайте рассмотрим несколько способов того, как мы можем сделать наш код более самодокументируемым, применяя определённые принципы.
Именование функций
Существует несколько простых принципов, благодаря которым, именований функций становится несложным делом:
- Старайтесь избегать неопределённых слов таких, как “handle” или “manage”, например:
handleArray()
,manageSmth()
. Как понять по такому имени, что делают эти функции? - Используйте глаголы действия, например:
openDoor()
,takeBook()
– функции с такими именами явно показывают, что они производят какие-то действия - Укажите возвращаемое значение, к примеру:
getTenNumbers()
,readObject()
. Но не стоит применять данную рекомендацию всегда и для всего – стоит руководствоваться здравым смыслом и подходящим для использования данного принципа случаем - В языках программирования со строгой типизацией, или при использовании TypeScript для JavaScript, следует указывать тип возвращаемого значения для функции
Именование переменных
К именованию переменных можно применить пару хороших правил:
- Указывайте единицы измерения. Если у вас есть числовые значения, то вы можете включить в название
переменной ожидаемую единицу измерения. Например, именование
widthEm
вместоwidth
будет хорошим указанием того значения, которое должно быть присвоено данной переменной - Не используйте сокращения! Такие имена для переменных, как
x
илиy
могут значительно усложнить понимание вашего кода. За исключением простых переменных для счётчиков в циклах, стоит воздержаться от использования коротких имён.
Старайтесь следовать установленному соглашению для именования
Если вы не работаете в команде, где существует принятые соглашения по именованию, то попробуйте установить для себя определённые правила, которые вы будете стараться соблюдать при написании кода. Например, если у вас есть объект определённого типа, назовите его таким же именем:
var list = getList();
Таким образом, не стоит назвать его, например:
var element = getList();
В итоге, если вы будете придерживаться определённых правил везде в коде своего приложения, тогда любому, кому придётся иметь дело с вашим кодом, не составит труда понять и разобраться в нём.
Используйте содержательные описания для ошибок
Можно предположить с большой долей вероятности, что самая любимая разработчиками ошибка – это
undefined
. Здесь
стоит сделать важное замечание, что данная ошибка является одним из
примитивных
типов JavaScript и сама по себе
не несёт какой бы то ни было информативности. Поэтому, не стоит полагаться на успешное решение ошибок в
приложении на основе таких скудных данных, вместо этого, нужно постараться, чтобы в коде вашего приложения было
достаточное количество информативных обработчиков для возникающих ошибок.
Давайте разберём то, что делает сообщение об ошибке полезным и позволяющими оперативно решить проблему:
- Сообщение должно описывать возникшую проблему
- При возможности, сообщение должно включать в себя другие переменные или данные, которые могли бы стать причиной ошибки
- Сообщение об ошибке должно помочь нам решить проблему, поэтому, хорошей практикой будет включить в него ссылку на актуальную документацию к методу
Выбор подходящего синтаксиса
Способ самодокументации кода на основе синтаксиса во многом зависит от выбранного языка программирования. К примеру, такие языки, как Ruby или Perl позволяют использовать множество синтаксических трюков, которыми, впрочем, не стоит слишком злоупотреблять.
Но так как мы рассматриваем в этой статье все примеры на JavaScript, то продолжим использовать этот язык.
Не используйте синтаксические трюки
Отличным способом ввести человека в ступор – это использовать странные синтаксические конструкции, например:
amazingStuff && doSomething();
Данный код можно прекрасно заменить понятным и читаемым вариантом:
if( amazingStuff )
{
doSomething();
}
Синтаксические трюки ещё никому не приносили особой пользы, а вот запутать читающего такой код могут очень легко. Поэтому, всегда стоит предпочитать второй вариант написания кода.
Используйте константы вместо “магических” значений
Если у вас есть какие-то определённые значения, например, цифровые или строковые данные, то хорошей практикой будет поместить их в именованные константы. Даже если всё в коде кажется ясным на данный момент, скорей всего, через пару-тройку месяцев никто не будет иметь понятия, что значит эта цифра или строка. Чтобы избежать этого, используйте константы:
const IS_JON_SNOW_ALIVE = 'YES';
Избегайте булевых флагов
Использование булевых флагов может значительно затруднить восприятие кода. Например:
myVoldemar.setAge( { amount: 22 }, true );
При чтении такого кода, можно ли с первого взгляда сказать, что значит передаваемый параметр true
? Мы не имеем
представления об этом параметре, до тех пор, пока не обратимся к исходному коду метода setAge()
и не потратим
своё время.
Вместо этого, можно добавить дополнительный метод, или переименовать существующий, примерно так:
myVoldemar.mergeAge( { amount: 22 } );
Теперь при чтении такого кода можно сразу сказать, что он делает.
Используйте особенности выбранного языка
Использование определённых особенностей языка программирования может помочь нам более явно выразить наше намерения с помощью кода.
Хорошим примером выражения намерения через использование особенности JavaScript, будет работа с массивом:
var names = [];
for( var i = 0; i < someStuff.length; i++ )
{
names.push( someStuff[ i ].name );
}
Этот код проходит в цикле по массиву с данными и формирует новый массив значений name
. Но чтобы это понять, нам
необходимо заглянуть в тело цикла. И если сравнить этот код с вариантом, где используется специфичный для
JavaScript метод map()
:
var names = someStuff.map( function( stuff )
{
return stuff.name;
} );
То получается, что мы сразу же понимаем результат работы метода map()
. Может быть на таком простом примере не
слишком видна выгода от использования определённого метода, но при наличии сложной логики выгода станет
очевидной. С другим подобными полезными методами JavaScript можно ознакомиться в документации от
MDN.
Ещё одним хорошим примером особенности языка JavaScript будет ключевое слово const
.
Довольно-таки распространённая ситуация, когда вы объявляете переменные, значения которых никогда не будет меняться. Например, при загрузке модулей через CommonJS:
var someModule = require( 'someModule' );
Здесь получается именно та ситуация, когда мы можем более явно выразить своё намерение через указание ключевого слова const
– никогда не изменять
данное значение:
const someModule = require( 'someModule' );
И в качестве дополнительного преимущества мы получим то, что если кто-то случайно изменить эту константу, то мы получим сообщение об ошибке.
Анти-паттерны
Теперь, когда в вашем распоряжении столько подходов к созданию кода, вы можете сделать много классных вещей. Тем не менее, есть некоторые замечания, о которых стоит упомянуть.
Выделение кода в функции ради самого выделения
Некоторые люди являются сторонниками использования микроскопических функций, и если вы стараетесь выделять в функции весь возможный код, то это именно то, что получится. Такое чрезмерное использование выделения кода может пагубно повлиять на читаемость и понимание работы приложения.
Представьте, что вы занимаетесь отладкой какого-то кода и в нём вы видите функцию x()
, которая использует
функцию y()
, которая, в свою очередь вызывает z()
и т.д.
И если такие функции используются только в одном месте, то гораздо более уместным вариантом будет применить подход, основанный на замене выражений переменными, о котором мы уже поговорили.
Без фанатизма
При написании кода не может существовать абсолютно правильных решений, поэтому, если что-то вам кажется не слишком хорошей идеей, то не стоит тратить на это все свои силы. Всегда существует альтернатива.
Заключение…
Создание самодокументируемого кода является отличным вариантом достижения легкости в поддержки и развития вашего приложения. Каждый дополнительный комментарий в коде требует вашего внимания при изменении кода, поэтому, сведение к минимуму таких комментариев может стать отличным вариантом уменьшения временных затрат.
Тем не менее, самодокументрируемый код не может стать полной заменой документации и комментариев. К примеру, код приложения ограничен в выражениях, и чтобы полностью раскрыть его смысл могут пригодиться комментарии. Документирование API также является важной частью для библиотек, т.к. прочитать весь код не представляется возможным, если, конечно, ваша библиотека не имеет маленький размер.
При создании статьи были использованы следующие источники: