Статьи

15 способов написать самодокументируемый JavaScript

Эта статья была рецензирована Тимом Севериеном и Марком Брауном . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Разве не забавно найти комментарий в коде, который совершенно неуместен и бесполезен?

Совершить несложную ошибку: вы изменили какой-то код и забыли удалить или обновить комментарий. Плохой комментарий не нарушит ваш код, но представьте, что произойдет при отладке. Вы читаете комментарий. Это говорит об одном, а код – о другом. Вы, вероятно, в конечном итоге потратите время на выяснение этого, а в худшем случае это может даже ввести вас в заблуждение!

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

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

Это не только облегчает понимание нашего кода, но и помогает улучшить общий дизайн программы!

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

Обзор методов

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

Мы можем разделить методы самодокументирования кода на три большие категории:

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

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

структурная

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

Переместить код в функцию

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

Например, попытайтесь угадать, что делает следующая строка:

var width = (value - 0.5) * 16;

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

 var width = emToPixels(value);

function emToPixels(ems) {
    return (ems - 0.5) * 16;
}

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

Заменить условное выражение на функцию

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

 if(!el.offsetWidth || !el.offsetHeight) {
}

Какова цель вышеуказанного условия?

 function isVisible(el) {
    return el.offsetWidth && el.offsetHeight;
}

if(!isVisible(el)) {
}

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

Заменить выражение переменной

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

Давайте снова посмотрим на пример с предложениями if:

 if(!el.offsetWidth || !el.offsetHeight) {
}

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

 var isVisible = el.offsetWidth && el.offsetHeight;
if(!isVisible) {
}

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

Наиболее распространенное использование этого метода – математические выражения:

 return a * b + (c / d);

Мы можем уточнить выше, разделив расчет:

 var multiplier = a * b;
var divisor = c / d;
return multiplier + divisor;

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

Интерфейсы классов и модулей

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

Давайте посмотрим на пример:

 class Box {
    setState(state) {
        this.state = state;
    }

    getState() {
        return this.state;
    }
}

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

Можете ли вы рассказать, как использовать этот класс? Может быть, с небольшим количеством работы, но это не очень очевидно.

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

Что делать, если мы изменили его на что-то вроде этого:

 class Box {
    open() {
        this.state = 'open';
    }

    close() {
        this.state = 'closed';
    }

    isOpen() {
        return this.state === 'open';
    }
}

Намного легче увидеть использование, не так ли? Обратите внимание, что мы изменили только публичный интерфейс; внутреннее представление остается неизменным со свойством this.state

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

Группировка кодов

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

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

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

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

 var foo = 1;

blah()
xyz();

bar(foo);
baz(1337);
quux(foo);

Вы можете сразу увидеть, сколько раз использовался foo Сравните это с этим:

 var foo = 1;
bar(foo);
quux(foo);

blah()
xyz();

baz(1337);

Сгруппированные вместе все виды использования foo

Используйте чистые функции

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

Что такое чистая функция? При вызове функции с одинаковыми параметрами, если она всегда выдает один и тот же вывод, это, скорее всего, так называемая «чистая» функция. Это означает, что функция не должна иметь никаких побочных эффектов или полагаться на состояние – например, время, свойства объекта, Ajax и т. Д.

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

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

Хорошим примером того, где это идет не так, является document.write() Опытные разработчики JS знают, что вы не должны использовать его, но многие начинающие сталкиваются с этим. Иногда это работает хорошо – но иногда, при определенных обстоятельствах, это может стереть всю страницу в чистоте. Разговор о побочном эффекте!

Чтобы лучше понять, что такое чистая функция, см. Статью « Функциональное программирование: чистые функции» .

Структура каталогов и файлов

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

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

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

Именование

Есть популярная цитата о двух сложных вещах в информатике:

В компьютерных науках есть только две сложные вещи: аннулирование кэша и присвоение имен. Фил Карлтон

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

Переименовать функцию

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

  • Избегайте использования расплывчатых слов, таких как «handle» или «manage»: handleLinks()manageObjects() Что делает любой из них?
  • Используйте активные глаголы: cutGrass()sendFile()
  • Укажите возвращаемое значение: getMagicBullet()readFile() Это не то, что вы всегда можете сделать, но полезно, когда это имеет смысл.
  • Языки со строгой типизацией могут использовать сигнатуры типов, чтобы помочь также указать возвращаемые значения.

Переименовать переменную

С переменными вот два хороших эмпирических правила:

  • Укажите единицы измерения: если у вас есть числовые параметры, вы можете включить ожидаемую единицу измерения. Например, widthPxwidth
  • Не используйте ярлыки: ab

Следуйте установленным правилам именования

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

 var element = getElement();

Не внезапно решите назвать это узлом:

 var node = getElement();

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

Используйте значимые ошибки

Не определено не является объектом!

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

Что делает сообщение об ошибке значимым?

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

Синтаксис

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

Давайте посмотрим на некоторые из них, которые происходят с JavaScript.

Не используйте синтаксические трюки

Не используйте странные трюки. Вот хороший способ запутать людей:

 imTricky && doMagic();

Это эквивалентно этому гораздо более вменяемому виду кода:

 if(imTricky) {
    doMagic();
}

Всегда предпочитайте последнюю форму. Синтаксические хитрости никому не помогут.

Используйте именованные константы, избегайте магических значений

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

 const MEANING_OF_LIFE = 42;

(Если вы не используете ES6, вы можете использовать var

Избегайте логических флагов

Булевы флаги могут сделать код сложным для понимания. Учти это:

 myThing.setData({ x: 1 }, true);

В чем смысл true Вы абсолютно не представляете, если только вы не покопаетесь в источнике setData()

Вместо этого вы можете добавить другую функцию или переименовать существующую функцию:

 myThing.mergeData({ x: 1 });

Теперь вы можете сразу сказать, что происходит.

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

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

Хорошим примером этого в JavaScript являются методы итерации массива:

 var ids = [];
for(var i = 0; i < things.length; i++) {
  ids.push(things[i].id);
}

Приведенный выше код собирает список идентификаторов в новый массив. Однако, чтобы узнать это, нам нужно прочитать все тело цикла. Сравните это с использованием map()

 var ids = things.map(function(thing) {
  return thing.id;
});

В этом случае мы сразу знаем, что это создает новый массив чего-то, потому что это цель map() Это может быть полезно, особенно если у вас более сложная логика зацикливания. В MDN есть список других итерационных функций .

Другим примером с JavaScript является ключевое слово const

Часто вы объявляете переменные, значения которых никогда не меняются. Очень распространенный пример – загрузка модулей с помощью CommonJS:

 var async = require('async');

Мы можем сделать намерение никогда не менять это еще более ясным:

 const async = require('async');

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

Анти-паттерны

Имея все эти методы в вашем распоряжении, вы можете сделать много хорошего. Тем не менее, есть некоторые вещи, о которых вы должны быть осторожны …

Извлечение ради коротких функций

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

Например, представьте, что вы отлаживаете некоторый код. Вы смотрите в функции a() Затем вы обнаружите, что он использует b()c() И так далее.

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

Не заставляйте вещи

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

Вывод

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

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