Руководство по именованию переменных

Польза от именования

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

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

Почему имена переменных?

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

Пример автоматически генерированного кода:

int f1(int a1, Collection<Integer> a2)
{
  int a5 = 0;
  for (int a3 = 0; a3 < a2.size() && a3 < a1; a3++) {
    int a6 = a2.get(a3);
    if (a6 >= 0) {
      System.out.println(a6 + " ");
    } else {
      a5++;
    }
  }
  System.out.println("\n");
  return a5;
}

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

  • Не сокращай;
  • Давай осмысленные имена.

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

int processElements(int numResults, Collection<Integer> collection)
{
  int result = 0;
  for (int count = 0; count < collection.size() && count < numResults; count++) {
    int num = collection.get(count);
    if (num >= 0) {
      System.out.println(num + " ");
    } else {
      result++;
    }
  }
  System.out.println("\n");
  return result;
}

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

  • processElements - большая часть кода "обрабатывает" сущности (в конце концов, код исполняет "процессор"), потому process это семь бесполезных символов которые значат не больше нежели "вычисления". elements не намного лучше. Учитывая то, что функция что то делает с коллекцией, это уже очевидно. Здесь даже ошибка в коде, в том что имя не помогает читателю понять зачем функция нужна.
  • numResults - большая часть кода производит результат (в итоге), поэтому как и в случае с process, Results - семь бесполезных символов. Полное имя переменной numResults подсказывает что предназначена для ограничения количества вывода, но это достаточно расплывчато и требует дополнительных усилий в читателя.

  • collection - зря тратит место, это очевидно что коллекция так как предыдущая конструкция указывает что это коллекция Collection<Integer>.
  • num - Просто повторяет тип объекта(int).
  • result, count - клише программирования; как и в случае с numResults они бесполезны так как слишком обобщенны и не помогают читателю понять код.

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

  • Какова была цель программиста?
  • Что на самом деле делает код?

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

int doSomethingWithCollectionElements(int numberOfResults, 
                                      Collection<Integer> integerCollection)
{
  int resultToReturn = 0;
  for (int variableThatCountsUp = 0; 
       variableThatCountsUp < integerCollection.size() 
         && variableThatCountsUp < numberOfResults; 
       variableThatCountsUp++) {
    int integerFromCollection = integerCollection.get(count);
    if (integerFromCollection >= 0) {
      System.out.println(integerFromCollection + " ");
    } else {
      resultToReturn++;
    }
  }
  System.out.println("\n");
  return resultToReturn;
}

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

На рецензии (код ревю)

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

Другой вид нагрузки - шаблонность. Часто код, делает, что нибудь сложное, он написан кем то другим, рецензент часто переключается на свой код, он просматривает очень много разного кода, каждый день, и мог делать это на протяжении многих лет. Зная все это, рецензенты изо всех сил стараться сохранить фокус во время рецензирования. Таким образом каждая бесполезная буква улетучивает эффективность рецензирования. Нет проблемы разобрать маленький пример непонятного кода. Рецензенты могут разобраться практически в любом коде, если есть достаточно времени и сил (вероятно с вопросами к программисту). Но они не могут себе этого позволить делать каждый раз снова и снова, год за годом. Это сметь через 1000 порезов.

Хороший пример

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

int printFirstNPositive(int n, Collection<Integer> c)
{
  int skipped = 0;
  for (int i = 0; i < c.size() && i < n; i++) {
    int maybePositive = c.get(i);
    if (maybePositive >= 0) {
      System.out.println(maybePositive + " ");
    } else {
      skipped++;
    }
  }
  System.out.println("\n");
  return skipped;
}

Давайте рассмотрим изменение имени каждой переменной для того чтоб увидеть как они помогают делать код легче и понятнее:

  • printFirstNPositive - в отличии от processElements, сейчас стало ясно что программист хотел чтоб эта функция сделала (и появляется шанс заметить ошибку)
  • n - очевидно, учитывая название функции, нет необходимости для более сложного названия
  • c - коллекция не добавляет умственной нагрузки, и к тому же короче на 9 букв для уменьшения нагрузки на читателя. Так как функция короткая и задействована только одна коллекция, то и легко запомнить что c это коллекция целых чисел.

  • skipped - в отличии от results, переменная теперь самодокументированная (без дополнительных комментариев) и понятней что функция возвращает. Так как функция короткая и декларация переменной легко увидеть, называть ее numSkipped будет только тратой трех букв.

  • i - проход по коллекциях используя i уже устоявшаяся идиом, что каждый понимает. Именовать ее как count бесполезно.
  • num - значить практически то же что и int, в то же время maybePositive сложно неправильно понять а так же может уменьшить вероятность ошибки.

Так же сейчас стало возможно увидеть две ошибки в коде. В первой версии кода было не ясно что программист хочет отобразить только положительные числа. Читатель теперь может заметить что здесь ошибка, потому что нуль не положительное число (потому что n должно быть больше нуля, а не больше-равно). (Так же здесь должны быть модульные тесты). И даже больше, так как первый аргумент теперь называется maxToPrint (в отличии от, скажем, maxToConsider), теперь видно что функция не всегда напечатает достаточно элементов в случае, если в коллекции есть отрицательные числа. Написание правильной функции остается в качестве упражнения читателю.

Принципы именования (если конечно же нет лучших)

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

Советы

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

Не добавляйте в название информацию о типе

(прим. пер. имеет смысл только в типизованных языках)

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

Set<Host> hostList = hostSvc.getHosts(zone);

Следующей распространённой ошибкой является добавление Str или String к имени, или включать тип коллекции в имя. Вот несколько рекомендаций:

Плохие имена Хорошие имена
hostList, hostSet hosts, validHosts
hostInputStream rawHostData
hostStr, hostString hostText, hostJson, hostKey
valueString firstName, lowercasedSKU
intPort portNumber

Суммируя:

  • Используйте имена в множественной форме для переменных которые представляют коллекцию.
  • Если вы намереваетесь добавить тип переменной (int, String, Char), вы должны либо:
    • объяснить что собой представляет переменная
    • объяснить какую трансформацию вы сделали чтоб получить новое значение (перевод в нижний регистр?)

Используйте тевтонские имена

В основном имена должны быть в Тевтонском стиле, которые следуют духу таким языкам как Норвежский, нежели эллиптическую расплывчатость романских языков, таких как Английский. Норвежский имеет больше слов как "tannlege" (буквально "зубной врач") и "sykehus" (буквально "больной дом"), и меньше слов таких как дантист и госпиталь (которые не распадаются на другие английские слова, и, таким образом, сбивает с толку, если вы еще не знаете их значение). Вы должны приложить усилия чтоб ваши переменные были названы в тевтонском духе: понятных с минимальными базовыми знаниями.

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

// Хорошо
Set<Host> overloadedCPUs = hostStatsTable.search(overCPUUtilizationThreshold);
// Плохо
Set<Host> matches = hostStatsTable.search(criteria);

// Хорошо
List<String> lowercasedNames = people.apply(Transformers.toLowerCase());
// Плохо
List<String> newstrs = people.apply(Transformers.toLowerCase());

// Хорошо
Set<Host> findUnhealthyHosts(Set<Host> droplets) {  }
// Плохо
Set<Host> processHosts(Set<Host> hosts) {  }

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

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

class StringTransformer {
  String transform(String input, TransformerChain additionalTransformers);
}

Переместите простые комментарии в имя переменной

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

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

Например:

// Плохо
String name; // First and last name
// Хорошо
String fullName;

// Плохо
int port; // TCP port number
// Хорошо
int tcpPort;

// Плохо
// This is derived from the JSON header
String authCode;
// Хорошо
String jsonHeaderAuthCode;

Избегайте злоупотреблением клише

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

  • val, value
  • items, item
  • result, res, retval
  • tmp, temp
  • count
  • str

Мораторий на эти имена распространяется на их вариации с добавление в имя тип (что в любом случае плохая идея), например: tempString или intStr итп.

Используйте идиомы где именование элементарное

В отличии от клише, есть несколько идиом которые широко распространённые, их можно безопасно использовать, даже если следуя букве закона они слишком загадочны. Некоторые примеры (они специфичны для Java/C, но подход применим и к другим языкам):

  • используйте i, j и k для работы с циклами
  • используйте n для лимитов/количества когда это очевидно
  • используйте e для исключений в операторе catch
// OK
for (int i = 0; i < hosts.size(); i++) { }

// OK
String repeat(String s, int n);

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

Можно использовать короткие имена на коротких расстояниях когда они очевидны

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

  • расстояние между определением и использованием не большое (скажем 5 рядков, определение все время в поле зрения рецензента)
  • нет лучшего имени переменной нежели ее тип
  • рецензент не должен помнить множество вещей в той точке в коде (помните, люди могут помнить около 7 вещей)

Вот пример:

void updateJVMs(HostService s, Filter obsoleteVersions)
{
  // 'hs' is only used once, and is "obviously" a HostService
  List<Host> hs = s.fetchHostsWithPackage(obsoleteVersions);
  // 'hs' is used only within field of vision of reader
  Workflow w = startUpdateWorkflow(hs);
  try {
    w.wait();
  } finally {
    w.close();
  }
}

Вы бы могли написать вот так:

void updateJVMs(HostSevice hostService, Filter obsoleteVersions)
{
  List<Host> hosts = hostService.fetchHostsWithPackage(obsoleteVersions);
  Workflow updateWorkflow = startUpdateWorkflow(hosts);
  try {
    updateWorkflow.wait();
  } finally {
    updateWorkflow.close();
  }
}

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

Удалите необдуманные одноразовые переменные

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

List<Host> results = hostService.fetchMatchingHosts(hostFilter);
return dropletFinder(results);

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

return dropletFinder(hostService.fetchMatchingHosts(hostFilter));

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

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

List<Host> hs = hostService.fetchMatchingHosts(hostFilter);
return DropletFinderFactoryFactory().getFactory().getFinder(region).dropletFinder(hs);

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

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

Может быть сложно прочитать вот такой код:

return hostUpdater.updateJVM(hostService.fetchMatchingHosts(hostFilter),
                             JVMServiceFactory.factory(region).getApprovedVersions(),
                             RegionFactory.factory(region).getZones());

Но вот так выглядит намного лучше:

List<Host> hs = hostService.fetchMatchingHosts(hostFilter);
Set<JVMVersions> js = JVMServiceFactory.factory(region).getApprovedVersions(),
List<AvailabilityZone> zs = RegionFactory(region).getZones();
return hostUpdater.updateJVM(hs, js, zs);

И снова, так как все расстояния короткие и именование простые, то допускаются короткие имена.

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

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

List<Column> pivotedColumns = olapCube.transformAlpha();
updateDisplay(pivotedColumns);

Обратите внимание, в этому случае лучше не использовать короткие имена:

List<Column> ps = olapCube.transformAlpha();
updateDisplay(ps);

Так как это только добавить визуального хаосу без пояснения намерений кода.

Оригинал

Изображение