Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
81 Истину можно найти только в одном месте: в коде . Только код может правдиво сообщить, что он делает . Это единственный источник действительно досто верной информации . Таким образом, хотя комментарии иногда необходимы, мы потра- тим немало усилий для того, чтобы свести их использование к минимуму . Комментарии не компенсируют плохого кода Одной из распространенных причин для написания комментариев является низкое качество кода . Вы пишете модуль и видите, что код получился запутан- ным и беспорядочным . Вы знаете, что разобраться в нем невозможно . Поэтому вы говорите себе: «О, да это стоит прокомментировать!» Нет! Лучше исправьте свой код! Ясный и выразительный код с минимумом комментариев гораздо лучше громозд- кого, сложного кода с большим количеством комментариев . Не тратьте время на написание комментариев, объясняющих созданную вами путаницу, — лучше потратьте его на исправление . Объясните свои намерения в коде И все же в некоторых ситуациях код оказывается не лучшим средством для объяснений . К сожалению, многие программисты воспринимают этот факт иначе: они полагают, что код никогда не является хорошим средством для объ- яснений . А это, разумеется, неправда . С каким бы кодом вы предпочли работать — с таким: // Проверить, положена ли работнику полная премия if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) Или с таким: if (employee.isEligibleForFullBenefits()) Чтобы объяснить большую часть ваших намерений в коде, достаточно нескольких секунд . Нередко задача сводится с созданию функции, которая сообщает то же, что и комментарий, который вы собираетесь написать . хорошие комментарии Впрочем, необходимые и полезные комментарии все же существуют . Мы рас- смотрим несколько примеров, которые, на мой взгляд, стоят затраченных на них битов . И все же следует помнить, что по-настоящему хороший комментарий — тот, без которого вам удастся обойтись . 81 82 Глава 4 . Комментарии юридические комментарии Иногда корпоративные стандарты кодирования заставляют нас вставлять ком- ментарии по юридическим соображениям . Например, заявление об авторских правах — необходимая информация, которая вполне может размещаться в ком- ментарии в начале каждого файла с исходным кодом . Ниже приведен стандартный заголовок комментария, который вставляется в на- чало каждого исходного файла в FitNesse . К счастью, наша IDE автоматически сворачивает этот комментарий, чтобы он не загромождал экран . // Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved. // Публикуется на условиях лицензии GNU General Public License версии 2 и выше. Такие комментарии не должны представлять собой комментарии или юриди- ческие трактаты . Вместо того чтобы перечислять в комментарии все условия, по возможности ограничьтесь ссылкой на стандартную лицензию или другой внешний документ . Информативные комментарии Иногда бывает полезно включить в комментарий пояснение к коду . Возьмем сле- дующий комментарий, объясняющий возвращаемое значение абстрактного ме- тода: // Возвращает тестируемый экземпляр Responder. protected abstract Responder responderInstance(); Такие комментарии бывают полезными, но там, где это возможно, информацию лучше передавать в имени функции . Например, в данном примере вполне можно обойтись и без комментария — достаточно переименовать функцию в responder- BeingTested А вот другой, более уместный пример: // Поиск по формату: kk:mm:ss EEE, MMM dd, yyyy Pattern timeMatcher = Pattern.compile( "\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*"); На этот раз комментарий сообщает, что регулярное выражение предназначено для идентификации времени и даты, отформатированных функцией SimpleDateFormat. format с заданной форматной строкой . И все же код стал бы лучше (и понятнее), если бы мы переместили этот код в специальный класс, преобразующий форматы даты и времени . Тогда комментарий, вероятно, стал бы излишним . Представление намерений Иногда комментарий выходит за рамки полезной информации о реализации и описывает намерения, заложенные в решение . В следующем примере мы видим интересный пример архитектурного решения, документированного в коммента- рии . Автор решил, что при сравнении двух объектов объекты его класса должны находиться в порядке сортировки выше, чем объекты любого другого класса . 82 Хорошие комментарии 83 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"); 83 84 Глава 4 . Комментарии 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 } Конечно, при этом возникает существенный риск, что пояснительный коммен- тарий окажется неверным . Просмотрите код примера и убедитесь, как трудно проверить его правильность . Это объясняет как необходимость пояснений, так и связанный с ними риск . Итак, прежде чем писать такие комментарии, убеди- тесь в том, что лучшего способа не существует, и еще внимательнее следите за их правильностью . Предупреждения о последствиях Иногда бывает полезно предупредить других программистов о нежелательных последствиях от каких-либо действий . На- пример, следующий комментарий объяс- няет, почему конкретный тестовый сцена- рий был отключен: // Не запускайте, если только не располагаете // излишками свободного времени. 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 не является потоково-безопасным, // поэтому экземпляры должны создаваться независимо друг от друга. 84 Хорошие комментарии 85 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())); 85 86 Глава 4 . Комментарии Комментарии 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 , это означает, что файл свойств отсутствует; в этом случае должны загружаться 86 Плохие комментарии 87 все настройки по умолчанию . Но кто загружает эти настройки? Были ли они загружены перед вызовом 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 . Эти комментарии только загромож- дают код и скрывают его смысл . Никакой пользы для документирования от них нет . Что еще хуже, я привел только несколько начальных комментариев — в этом модуле их намного больше . 87 88 Глава 4 . Комментарии листинг 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; 88 Плохие комментарии 89 /** * Родительский контейнер, по отношению к которому * данный контейнер является дочерним. */ 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 станет ис- тинным . После этого бедный программист будет долго отлаживать программу, пытаясь понять, почему его код выполняется так медленно . 89 90 Глава 4 . Комментарии Обязательные комментарии Правила, говорящие, что каждая функция должна иметь комментарий Javadoc или что каждая переменная должна быть помечена комментарием, — обычная глупость . Такие комментарии только загромождают код, распространяют недо- стоверную информацию и вызывают общую путаницу и дезориентацию . Например, требование обязательного комментария Javadoc для каждой функции приводит к появлению монстров вроде листинга 4 .3 . Бессмысленные коммента- рии не приносят никакой пользы . Они только запутывают код, повышая риск обмана и недоразумений . |