Книга: Чистый код: создание, анализ и рефакторинг. Библиотека программиста
Назад: 3. Функции
Дальше: 5. Форматирование

4. Комментарии

04_01.tif 

Не комментируйте плохой код — перепишите его.

Брайан У. Керниган и П. Дж. Плауэр

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

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

Грамотное применение комментариев должно компенсировать нашу неудачу в выражении своих мыслей в коде. Обратите внимание на слово «неудачу». Я абсолютно серьезно. Комментарий — всегда признак неудачи. Мы вынуждены использовать комментарии, потому что нам не всегда удается выразить свои мысли без них, однако гордиться здесь нечем.

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

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

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

MockRequest request;

private final String HTTP_DATE_REGEXP =

    "[SMTWF][a-z]{2}\\,\\s[0-9]{2}\\s[JFMASOND][a-z]{2}\\s"+

    "[0-9]{4}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\sGMT";

private Response response;

private FitNesseContext context;

private FileResponder responder;

private Locale saveLocale;

// Пример: "Tue, 02 Apr 2003 22:18:49 GMT"

Другие переменные экземпляра (вероятно, добавленные позднее) вклинились между константой HTTP_DATE_REGEXP и пояснительным комментарием.

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

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

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

Комментарии не компенсируют  плохого кода

Одной из распространенных причин для написания комментариев является низкое качество кода. Вы пишете модуль и видите, что код получился запутанным и беспорядочным. Вы знаете, что разобраться в нем невозможно. Поэтому вы говорите себе: «О, да это стоит прокомментировать!» Нет! Лучше исправьте свой код!

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

Объясните свои намерения в коде

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

// Проверить, положена ли работнику полная премия

if ((employee.flags & HOURLY_FLAG) &&

    (employee.age > 65))

Или с таким:

if (employee.isEligibleForFullBenefits())

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

Хорошие комментарии

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

Юридические комментарии

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

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

// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.

// Публикуется на условиях лицензии GNU General Public License версии 2 и выше.

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

Информативные комментарии

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

// Возвращает тестируемый экземпляр Responder.

protected abstract Responder responderInstance();

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

А вот другой, более уместный пример:

// Поиск по формату: kk:mm:ss EEE, MMM dd, yyyy

Pattern timeMatcher = Pattern.compile(

       "\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

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

Представление намерений

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

public int compareTo(Object o)

{

  if(o instanceof WikiPagePath)

  {

    WikiPagePath p = (WikiPagePath) o;

    String compressedName = StringUtil.join(names, "");

    String compressedArgumentName = StringUtil.join(p.names, "");

    return compressedName.compareTo(compressedArgumentName);

  }

  return 1; // Больше, потому что относится к правильному типу.

}

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

public void testConcurrentAddWidgets() throws Exception {

  WidgetBuilder widgetBuilder =

    new WidgetBuilder(new Class[]{BoldWidget.class});

    String text = "'''bold text'''";

    ParentWidget parent =

      new BoldWidget(new MockWidgetRoot(), "'''bold text'''");

    AtomicBoolean failFlag = new AtomicBoolean();

    failFlag.set(false);

 

    // Мы пытаемся спровоцировать "состояние гонки",

    // создавая большое количество программных потоков.

    for (int i = 0; i < 25000; i++) {

      WidgetBuilderThread widgetBuilderThread =

        new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);

      Thread thread = new Thread(widgetBuilderThread);

      thread.start();

    }

    assertEquals(false, failFlag.get());

}

Прояснение

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

public void testCompareTo() throws Exception

{

  WikiPagePath a = PathParser.parse("PageA");

  WikiPagePath ab = PathParser.parse("PageA.PageB");

  WikiPagePath b = PathParser.parse("PageB");

  WikiPagePath aa = PathParser.parse("PageA.PageA");

  WikiPagePath bb = PathParser.parse("PageB.PageB");

  WikiPagePath ba = PathParser.parse("PageB.PageA");

  assertTrue(a.compareTo(a) == 0);    // a == a

  assertTrue(a.compareTo(b) != 0);    // a != b

  assertTrue(ab.compareTo(ab) == 0);  // ab == ab

  assertTrue(a.compareTo(b) == -1);   // a < b

  assertTrue(aa.compareTo(ab) == -1); // aa < ab

  assertTrue(ba.compareTo(bb) == -1); // ba < bb

  assertTrue(b.compareTo(a) == 1);    // b > a

  assertTrue(ab.compareTo(aa) == 1);  // ab > aa

  assertTrue(bb.compareTo(ba) == 1);  // bb > ba

}

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

Предупреждения о последствиях

04_02.tif

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

// Не запускайте, если только не располагаете

// излишками свободного времени.

public void _testWithReallyBigFile()

{

  writeLinesToFile(10000000);

  response.setBody(testFile);

  response.readyToSend(this);

  String responseString = output.toString();

  assertSubString("Content-Length: 1000000000", responseString);

  assertTrue(bytesSent > 1000000000);

}

Конечно, в наше время тестовый сценарий следовало бы отключить при помощи атрибута @Ignore с соответствующей пояснительной строкой: @Ignore("Слишком долго выполняется"). Но до появления JUnit 4 запись с начальным символом подчеркивания перед именем метода считалась стандартной. Комментарий, при всей его несерьезности, хорошо доносит свое сообщение до читателя.

А вот другой, более выразительный пример:

public static SimpleDateFormat makeStandardHttpDateFormat()

{

  // Класс SimpleDateFormat не является потоково-безопасным,

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

  SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM  yyyy HH:mm:ss z");

  df.setTimeZone(TimeZone.getTimeZone("GMT"));

  return df;

}

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

Комментарии TODO

Иногда бывает полезно оставить заметки «на будущее» в форме комментариев //TODO. В следующем примере комментарий TODO объясняет, почему функция имеет вырожденную реализацию и что она должна делать в будущем.

// TODO - На данный момент эта функция не используется.

// Ситуация изменится при переходе к отладочной модели.

protected VersionInfo makeVersion() throws Exception

{

  return null;

}

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

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

Усиление

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

String listItemContent = match.group(3).trim();

// Вызов trim() очень важен. Он удаляет начальные пробелы,

// чтобы строка успешно интерпретировалась как список.

new ListItemWidget(this, listItemContent, this.level + 1);

return buildList(text.substring(match.end()));

Комментарии Javadoc в общедоступных API

С хорошо документированным общедоступным API приятно и легко работать. Документация Javadoc для стандартной библиотеки Java убедительно доказывает это утверждение. Без нее писать Java-программы было бы в лучшем случае непросто.

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

Плохие комментарии

Большинство комментариев относится именно к этой категории. Обычно такие комментарии представляют собой «подпорки» для некачественного кода или оправдания сомнительных решений, а их текст напоминает рассуждения вслух самого программиста.

Бормотание

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

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

public void loadProperties()

{

  try

  {

      String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;

      FileInputStream propertiesStream = new FileInputStream(propertiesPath);

      loadedProperties.load(propertiesStream);

  }

  catch(IOException e)

  {

    // Если нет файла свойств, загружаются настройки по умолчанию

  }

}

Что означает комментарий в блоке catch? Очевидно, он что-то означал для автора, но для читателя этот смысл не доходит. Видимо, если мы получаем IOException, это означает, что файл свойств отсутствует; в этом случае должны загружаться все настройки по умолчанию. Но кто загружает эти настройки? Были ли они загружены перед вызовом loadProperties.load? Или вызов loadProperties.load перехватывает исключение, загружает настройки по умолчанию, а затем передает исключение нам, чтобы мы могли его проигнорировать? Или loadProperties.load загружает настройки по умолчанию до того, как вы попытались загрузить файл? Автор пытался успокоить себя относительно того факта, что он оставил блок catch пустым? Или — и это самая пугающая возможность — автор хотел напомнить себе, что позднее нужно вернуться и написать код загрузки настроек по умолчанию?

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

Избыточные комментарии

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

Листинг 4.1. waitForClose

// Вспомогательный метод; возвращает управление, когда значение this.closed истинно.

// Инициирует исключение при достижении тайм-аута.

public synchronized void waitForClose(final long timeoutMillis)

throws Exception

{

  if(!closed)

  {

    wait(timeoutMillis);

    if(!closed)

      throw new Exception("MockResponseSender could not be closed");

  }

}

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

А теперь рассмотрим легион бесполезных, избыточных комментариев Javadoc из листинга 4.2, позаимствованных из Tomcat. Эти комментарии только загромождают код и скрывают его смысл. Никакой пользы для документирования от них нет. Что еще хуже, я привел только несколько начальных комментариев — в этом модуле их намного больше.

Листинг 4.2. ContainerBase.java (Tomcat)

public abstract class ContainerBase

  implements Container, Lifecycle, Pipeline,

  MBeanRegistration, Serializable {

 

  /**

   * Задержка процессора для этого компонента.

   */

  protected int backgroundProcessorDelay = -1;

 

  /**

   * Поддержка событий жизненного цикла для этого компонента.

   */

  protected LifecycleSupport lifecycle =

    new LifecycleSupport(this);

 

  /**

   * Слушатели контейнерных событий для этого контейнера.

   */

  protected ArrayList listeners = new ArrayList();

 

  /**

   * Реализация загрузчика, связанная с контейнером.

   */

  protected Loader loader = null;

 

  /**

   * Реализация журнального компонента, связанная с контейнером.

   */

  protected Log logger = null;

 

  /**

   * Имя журнального компонента.

   */

  protected String logName = null;

 

  /**

   * Реализация менеджера, связанная с контейнером.

   */

  protected Manager manager = null;

 

  /**

   * Кластер, связанный с контейнером.

   */

  protected Cluster cluster = null;

 

  /**

   * Удобочитаемое имя контейнера.

   */

  protected String name = null;

  /**

   * Родительский контейнер, по отношению к которому

   * данный контейнер является дочерним.

   */

  protected Container parent = null;

 

  /**

   * Загрузчик родительского класса, задаваемый при назначении загрузчика.

   */

  protected ClassLoader parentClassLoader = null;

 

  /**

   * Объект Pipeline, связанный с данным контейнером.

   */

  protected Pipeline pipeline = new StandardPipeline(this);

 

  /**

   * Объект Realm, связанный с контейнером.

   */

  protected Realm realm = null;

 

  /**

   * Объект ресурсов DirContext, связанный с контейнером

   */

  protected DirContext resources = null;

Недостоверные комментарии

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

А вы нашли, в чем этот комментарий обманывает читателя? Метод не возвращает управление, когда значение this.closed становится истинным. Он возвращает управление, если значение this.closed истинно; в противном случае метод ожидает истечения тайм-аута, а затем инициирует исключение, если значение this.closed так и не стало истинным.

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

Обязательные комментарии

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

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

Листинг 4.3.

  /**

   *

   * @param title  Название диска

   * @param author Автор диска

   * @param tracks Количество дорожек на диске

   * @param durationInMinutes Продолжительность воспроизведения в минутах

   */

  public void addCD(String title, String author,

                     int tracks, int durationInMinutes) {

    CD cd = new CD();

    cd.title = title;

    cd.author = author;

    cd.tracks = tracks;

    cd.duration = duration;

    cdList.add(cd);

  }

Журнальные комментарии

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

* Изменения (начиная с 11 октября 2001)

* --------------------------

* 11.10.2001  : Реорганизация класса и его перемещение в новый пакет

*               com.jrefinery.date (DG);

* 05.11.2001  : Добавление метода getDescription(), устранение класса

*               NotableDate (DG);

* 12.11.2001  : С устранением класса NotableDate IBD требует включения

*               метода setDescription() (DG); исправление ошибок

*               в методах getPreviousDayOfWeek(), getFollowingDayOfWeek()

*               и getNearestDayOfWeek() (DG);

* 05.12.2001  : Исправление ошибки в классе SpreadsheetDate (DG);

* 29.05.2002  : Перемещение констант месяцев в отдельный интерфейс

*               (MonthConstants) (DG);

* 27.08.2002  : Исправление ошибки в методе addMonths() с подачи N???levka Petr (DG);

* 03.10.2002  : Исправление ошибок по сообщениям Checkstyle (DG);

* 13.03.2003  : Реализация Serializable (DG);

* 29.05.2003  : Исправление ошибки в методе addMonths (DG);

* 04.09.2003  : Реализация Comparable.  Обновление isInRange javadocs (DG);

* 05.01.2005  : Исправление ошибки в методе addYears() (1096282) (DG);

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

Шум

Также в программах нередко встречаются комментарии, не содержащие ничего, кроме «шума». Они лишь утверждают очевидное, не предоставляя никакой новой информации.

/**

* Конструктор по умолчанию.

*/

protected AnnualDateRule() {

}

Да неужели? А как насчет этого:

/** День месяца. */

    private int dayOfMonth;

И наконец, апофеоз избыточности:

/**

* Возвращает день месяца.

*

* @return день месяца.

*/

public int getDayOfMonth() {

  return dayOfMonth;

}

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

Первый комментарий в листинге 4.4 кажется уместным. Он объясняет, почему блок catch игнорируется. Но второй комментарий не несет полезной информации. Видимо, программист настолько вышел из себя при написании этих блоков try/catch в этой функции, что ему понадобилось «выпустить пар».

Листинг 4.4. startSending

private void startSending()

{

  try

  {

    doSending();

  }

  catch(SocketException e)

  {

    // Нормально. Кто-то прервал запрос.

  }

  catch(Exception e)

  {

    try

    {

      response.add(ErrorResponder.makeExceptionString(e));

      response.closeAll();

    }

    catch(Exception e1)

    {

      // Ну хватит уже!

    }

  }

}

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

Листинг 4.5. startSending (переработанная версия)

private void startSending()

{

  try

  {

    doSending();

  }

  catch(SocketException e)

  {

    // Нормально. Кто-то прервал запрос.

  }

  catch(Exception e)

  {

    addExceptionAndCloseResponse(e);

  }

}

 

private void addExceptionAndCloseResponse(Exception e)

{

  try

  {

    response.add(ErrorResponder.makeExceptionString(e));

    response.closeAll();

  }

  catch(Exception e1)

  {

  }

}

Искушение создать очередной «шумовой комментарий» следует заменить решимостью очистить код. Вы сами увидите, что это сделает вашу работу более приятной и эффективной.

Опасный шум

Комментарии Javadoc тоже бывают «шумовыми». Какую пользу приносят следующие комментарии (из хорошо известной библиотеки, распространяемой с открытым кодом)? Ответ: никакой. Это избыточные шумовые комментарии, вызванные неуместным желанием как-то документировать свои действия.

/** Имя. */

private String name;

/** Версия. */

private String version;

/** Название лицензии. */

private String licenceName;

/** Версия. */

private String info;

Прочитайте эти комментарии повнимательнее. Заметили ошибку копирования/вставки? Если авторы не следят за ними в момент написания (или вставки), то как можно ожидать, что эти комментарии принесут пользу читателю?

Не используйте комментарии там, где можно использовать функцию или переменную

Возьмем следующий фрагмент кода:

// Зависит ли модуль из глобального списка <mod> от подсистемы,

// частью которой является наш код?

if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))

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

ArrayList moduleDependees = smodule.getDependSubsystems();

String ourSubSystem = subSysMod.getSubSystem();

if (moduleDependees.contains(ourSubSystem))

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

Позиционные маркеры

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

// Действия //////////////////////////////////

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

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

Комментарии за закрывающей фигурной скобкой

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

Листинг 4.6. wc.java

public class wc {

  public static void main(String[] args) {

    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    String line;

    int lineCount = 0;

    int charCount = 0;

    int wordCount = 0;

    try {

      while ((line = in.readLine()) != null) {

        lineCount++;

        charCount += line.length();

        String words[] = line.split("\\W");

        wordCount += words.length;

      } //while

      System.out.println("wordCount = " + wordCount);

      System.out.println("lineCount = " + lineCount);

      System.out.println("charCount = " + charCount);

    } // try

    catch (IOException e) {

      System.err.println("Error:" + e.getMessage());

    } //catch

  } //main

}

Ссылки на авторов

/* Добавлено Риком */

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

И снова лучшим источником подобной информации является система контроля исходного кода.

Закомментированный код

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

InputStreamResponse response = new InputStreamResponse();

response.setBody(formatter.getResultStream(), formatter.getByteCount());

// InputStream resultsStream = formatter.getResultStream();

// StreamReader reader = new StreamReader(resultsStream);

// response.setContent(reader.read(formatter.getByteCount()));

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

Следующий код взят из общих модулей Apache:

this.bytePos = writeBytes(pngIdBytes, 0);

//hdrPos = bytePos;

writeHeader();

writeResolution();

//dataPos = bytePos;

if (writeImageData()) {

  writeEnd();

  this.pngBytes = resizeByteArray(this.pngBytes, this.maxPos);

}

else {

    this.pngBytes = null;

}

return this.pngBytes;

Почему эти две строки кода закомментированы? Они важны? Их оставили как напоминание о будущих изменениях? Или это «хлам», который кто-то закомментировал сто лет назад и не удосужился убрать из программы?

В 60-е годы закомментированный код мог быть действительно полезен. Но с тех пор у нас давно появились хорошие системы контроля исходного кода. Эти системы запоминают изменения в коде за нас. Нам уже не нужно закрывать их комментариями. Просто удалите ненужный код. Он никуда не исчезнет. Честное слово.

Комментарии HTML

Как видно из следующего фрагмента, HTML в комментариях к исходному коду выглядит отвратительно. Он затрудняет чтение комментариев именно там, где они должны легко читаться — в редакторе/IDE. Если комментарии должны извлекаться внешним инструментом (например, Javadoc) для отображения в веб-странице, то за украшение комментариев соответствующим кодом HTML должен отвечать этот инструмент, а не программист.

/**

* Задача для запуска тестов.

* Задача запускает тесты fitnesse и публикует результаты.

* <p/>

* <pre>

* Usage:

* &lt;taskdef name=&quot;execute-fitnesse-tests&quot;

*     classname=&quot;fitnesse.ant.ExecuteFitnesseTestsTask&quot;

*     classpathref=&quot;classpath&quot; /&gt;

* OR

* &lt;taskdef classpathref=&quot;classpath&quot;

*             resource=&quot;tasks.properties&quot; /&gt;

* <p/>

* &lt;execute-fitnesse-tests

*     suitepage=&quot;FitNesse.SuiteAcceptanceTests&quot;

*     fitnesseport=&quot;8082&quot;

*     resultsdir=&quot;${results.dir}&quot;

*     resultshtmlpage=&quot;fit-results.html&quot;

*     classpathref=&quot;classpath&quot; /&gt;

* </pre>

*/

Нелокальная информация

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

/**

* Порт, на котором будет работать fitnesse. По умолчанию <b>8082</b>.

*

* @param fitnessePort

*/

public void setFitnessePort(int fitnessePort)

{

  this.fitnessePort = fitnessePort;

}

Слишком много информации

Не включайте в комментарии интересные исторические дискуссии или опи­сания подробностей, не относящиеся к делу. Следующий комментарий был извлечен из модуля, который должен был проверять, что функция кодирует и декодирует данные в формате base64. Читателю кода совершенно не нужна заумная информация, содержащаяся в этом комментарии, — вполне достаточно номера RFC.

/*

RFC 2045 - Multipurpose Internet Mail Extensions (MIME)

Часть 1: Формат тел сообщений

раздел 6.8.  Кодирование данных Base64

В процессе кодирования 24-разрядные группы входных битов представляются

в виде выходных строк из 4 закодированных символов. Слева направо 24-разрядная

входная группа образуется посредством конкатенации 38-разрядных входных групп.

Далее эти 24 бита интерпретируются как 4 конкатенированных 6-разрядных группы,

каждая из которых преобразуется в одну цифру алфавита base64. При кодировании

потока битов в кодировке base64 предполагается, что битовый поток упорядочивается

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

первого 8-битового байта, а восьмым - младший бит первого 8-битого байта и т.д.

*/

Неочевидные комментарии

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

         /*

          * Начать с массива, размер которого достаточен для хранения

          * всех пикселов (плюс байты фильтра), плюс еще 200 байт

          * для данных заголовка

          */

         this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

Что такое «байты фильтра»? Они как-то связаны с +1? Или с *3? И с тем и с другим? Один пиксел соответствует одному байту? И почему 200? Цель комментария — объяснить код, который не объясняет сам себя. Плохо, когда сам комментарий нуждается в объяснениях.

Заголовки функций

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

Заголовки Javadoc во внутреннем коде

При всей полезности комментариев Javadoc для API общего пользования не ­применяйте их в коде, не предназначенном для общего потребления. Генерирование страниц Javadoc для внутренних классов и функций системы обычно не приносит реальной пользы, а формализм комментариев Javadoc только отвлекает читателя.

Пример

Модуль в листинге 4.7 был написан для первого учебного курса «XP Immersion». Предполагалось, что он является примером плохого кодирования и стиля комментирования. Кент Бек переработал этот код в куда более приятную форму перед несколькими десятками увлеченных слушателей. Позднее я приспособил этот пример для своей книги «Agile Software Development, Principles, Patterns, and Practices» и статьи в журнале «Software Development». Любопытно, что в то время многие из нас считали этот модуль «хорошо документированным». Теперь мы видим, что он представляет собой ералаш. Посмотрим, сколько разных ошибок комментирования вам удастся найти.

Листинг 4.7. GeneratePrimes.java

/**

* Класс генерирует простые числа в диапазоне до максимального значения,

*  заданного пользователем, по алгоритму "Решета Эратосфена".

* <p>

* Эратосфен Киренский, 276 год до н.э., Ливия --  * 194 год до н.э., Александрия.

* Первый ученый, вычисливший длину земного меридиана. Известен своими работами

* о календарях с високосным годом, заведовал Александрийской библиотекой.

* <p>

* Алгоритм весьма прост. Берем массив целых чисел, начиная с 2, и вычеркиваем

* из него все числа, кратные 2. Находим следующее невычеркнутое число

* и вычеркиваем все его кратные. Повторяем до тех пор, пока не дойдем

* до квадратного корня верхней границы диапазона.

*

* @author Альфонс

* @version 13 февраля 2002 u

*/

import java.util.*;

 

public class GeneratePrimes

{

  /**

   * @param maxValue - верхняя граница диапазона.

   */

  public static int[] generatePrimes(int maxValue)

  {

    if (maxValue >= 2) // Единственно допустимый случай

    {

      // Объявления

      int s = maxValue + 1; // Размер массива

      boolean[] f = new boolean[s];

      int i;

 

      // Инициализировать массив значениями true.

      for (i = 0; i < s; i++)

        f[i] = true;

      // Удалить числа, заведомо не являющиеся простыми.

      f[0] = f[1] = false;

 

      // Отсев

      int j;

      for (i = 2; i < Math.sqrt(s) + 1; i++)

      {

        if (f[i]) // Если элемент i не вычеркнут, вычеркнуть кратные ему.

        {

          for (j = 2 * i; j < s; j += i)

            f[j] = false; // Кратные числа не являются простыми.

        }

      }

 

      // Сколько простых чисел осталось?

      int count = 0;

      for (i = 0; i < s; i++)

      {

        if (f[i])

          count++; // Приращение счетчика

      }

 

      int[] primes = new int[count];

 

      // Переместить простые числа в результат

      for (i = 0, j = 0; i < s; i++)

      {

        if (f[i])             // Если простое

          primes[j++] = i;

      }

 

      return primes;  // Вернуть простые числа

    }

    else // maxValue < 2

      return new int[0]; // Вернуть пустой массив при недопустимых входных данных.

  }

}

В листинге 4.8 приведена переработанная версия того же модуля. Обратите внимание: применение комментариев стало намного более ограниченным. Во всем модуле осталось всего два комментария пояснительного характера.

Листинг 4.8. PrimeGenerator.java (переработанная версия)

/**

* Класс генерирует простые числа до максимального значения, заданного

* пользователем, по алгоритму "Решета Эратосфена".

* Берем массив целых чисел, начиная с 2, и вычеркиваем

* из него все числа, кратные 2. Находим следующее невычеркнутое число

* и вычеркиваем все числа, кратные ему. Повторяем до тех пор, пока из массива

* не будут вычеркнуты все кратные.

*/

 

public class PrimeGenerator

{

  private static boolean[] crossedOut;

  private static int[] result;

 

  public static int[] generatePrimes(int maxValue)

  {

    if (maxValue < 2)

      return new int[0];

    else

    {

      uncrossIntegersUpTo(maxValue);

      crossOutMultiples();

      putUncrossedIntegersIntoResult();

      return result;

    }

  }

 

  private static void uncrossIntegersUpTo(int maxValue)

  {

    crossedOut = new boolean[maxValue + 1];

    for (int i = 2; i < crossedOut.length; i++)

      crossedOut[i] = false;

  }

 

  private static void crossOutMultiples()

  {

    int limit = determineIterationLimit();

    for (int i = 2; i <= limit; i++)

      if (notCrossed(i))

        crossOutMultiplesOf(i);

  }

 

  private static int determineIterationLimit()

  {

    // Каждое кратное в массиве имеет простой множитель, больший либо равный

    // квадратному корню из размера массива. Следовательно, вычеркивать элементы,

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

    double iterationLimit = Math.sqrt(crossedOut.length);

    return (int) iterationLimit;

  }

  private static void crossOutMultiplesOf(int i)

  {

    for (int multiple = 2*i;

         multiple < crossedOut.length;

         multiple += i)

      crossedOut[multiple] = true;

  }

 

  private static boolean notCrossed(int i)

  {

    return crossedOut[i] == false;

  }

 

  private static void putUncrossedIntegersIntoResult()

  {

    result = new int[numberOfUncrossedIntegers()];

    for (int j = 0, i = 2; i < crossedOut.length; i++)

      if (notCrossed(i))

        result[j++] = i;

  }

 

  private static int numberOfUncrossedIntegers()

  {

    int count = 0;

    for (int i = 2; i < crossedOut.length; i++)

      if (notCrossed(i))

        count++;

 

    return count;

  }

}

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

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

Литература

[KP78]: Kernighan and Plaugher, The Elements of Programming Style, 2d. ed., McGraw-Hill, 1978.

1 [KP78], p. 144.

Назад: 3. Функции
Дальше: 5. Форматирование