Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
100 Глава 4 . Комментарии листинг 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; 100 Литература 101 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 . 101 Форматирование Мы хотим, чтобы читатель, заглянувший «под капот» программы, был поражен увиденным — нашей аккуратностью, логичностью и вниманием к мелочам . Мы хотим, чтобы на него произвела впечатление стройность кода . Мы хотим, чтобы он уважительно поднял брови при просмотре модулей . Мы хотим, чтобы наша работа выглядела профессионально . Если вместо этого читатель видит беспорядочную массу кода, словно написанного шайкой пьяных матросов, то он заключит, что такое же неуважение к мелочам проникло и во все остальные аспекты проекта . Вы должны позаботиться о том, чтобы ваш код был хорошо отформатирован . Выберите набор простых правил, определяющих формат кода, и последовательно 5 102 Цель форматирования 103 применяйте их в своей работе . Если вы работаете в составе группы, то группа должна выработать согласованный набор правил форматирования, соблюдаемых всеми участниками . Также полезно иметь средства автоматизации, которые при- меняют правила форматирования за вас . цель форматирования Прежде всего я твердо заявляю: форматирование кода важно . Оно слишком важ- но, чтобы не обращать на него внимания, и слишком важно, чтобы относиться к нему с религиозным пылом . Форматирование кода направлено на передачу информации, а передача информации является первоочередной задачей про- фессионального разработчика . Возможно, вы думали, что первоочередная задача профессионального разработ- чика – «сделать так, чтобы программа заработала» . Надеюсь, к этому моменту книга уже заставила вас отказаться от этих представлений . Функциональность, созданная сегодня, вполне может измениться в следующей версии, но удобо- читаемость вашего кода окажет сильное воздействие на все изменения, которые когда-либо будут внесены . Стиль кодирования и удобочитаемость создают пре- цеденты, которые продолжают влиять на сопровождаемость и расширяемость кода уже после того, как исходный код изменился до неузнаваемости . Стиль и дисциплина программирования продолжают жить, даже если ваш код остался в прошлом . Так какие же аспекты форматирования помогают нам лучше пере- дать свои мысли? Вертикальное форматирование Начнем с вертикальных размеров . Насколько большим должен быть исходный файл? В Java размер файла тесно связан с размером класса . Мы поговорим о раз- мерах классов, когда речь пойдет о классах, а пока давайте займемся размером файлов . Насколько большими должны быть исходные файлы Java? Оказывается, суще- ствует широчайший диапазон размеров и весьма заметные различия в стиле . Некоторые из этих различий показаны на рис . 5 .1 . На рисунке изображены семь разных проектов: Junit, FitNesse, TestNG, Time and Money (Tam), JDepend, Ant и Tomcat . Отрезки, проходящие через прямоуголь- ники, показывают минимальную и максимальную длину файла в каждом проекте . Прямоугольник изображает приблизительно одну треть (стандартное откло- нение 1 ) от диапазона длин файлов . Середина прямоугольника соответствует 1 Прямоугольник представляет диапазон «сигма/2» выше и ниже среднего значения . Да, я знаю, что распределение длин файлов не является нормальным, поэтому стандартное отклонение не может считаться математически точным . Но я и не стремлюсь к точности . Я хочу лишь дать представление о происходящем . 103 104 Глава 5 . Форматирование среднему арифметическому . Таким образом, средний размер файла в проекте FitNesse составляет около 65 строк, а около трети файлов имеет размер от 40 до 100+ строк . Наибольший файл FitNesse занимает около 400 строк, а наимень- ший — всего 6 строк . Обратите внимание: на графике используется логарифми- ческая шкала, поэтому незначительные изменения в вертикальной координате подразумевают очень большие изменения в абсолютном размере . Рис . 5 .1 . Распределение длин файлов по логарифмической шкале (высота прямоугольника = сигма) Junit, FitNesse и Time and Money состоят из относительно небольших файлов . Ни один размер файла не превышает 500 строк, а большинство файлов не превышает 200 строк . Напротив, в Tomcat и Ant встречаются файлы из нескольких тысяч строк, а около половины имеет длину более 200 строк . Что это означает для нас? То, что достаточно серьезную систему (объем FitNesse приближается к 50 000 строк) можно построить из файлов, типичная длина ко- торых составляет 200 строк, с верхним пределом в 500 строк . Хотя это не должно считаться раз и навсегда установленным правилом, такие показатели весьма желательны . Маленькие файлы обычно более понятны, чем большие . Газетная метафора Представьте себе хорошо написанную газетную статью . Естественно, статья чи- тается по вертикали . В самом начале обычно располагается заголовок с общей темой статьи; он помогает вам решить, представляет ли статья интерес для вас . В первом абзаце приводится краткое изложение сюжета на уровне общих кон- цепций, без приведения каких-либо подробностей . По мере продвижения к кон- 104 Цель форматирования 105 цу статьи объем детализации непрерывно растет, пока вы не узнаете все даты, имена, цитаты и т . д . Исходный файл должен выглядеть как газетная статья . Имя файла должно быть простым, но содержательным . Одного имени должно быть достаточно для того, чтобы читатель понял, открыл ли он нужный модуль или нет . Начальные блоки исходного файла описывают высокоуровневые концепции и алгоритмы . Степень детализации увеличивается при перемещении к концу файла, а в самом конце собираются все функции и подробности низшего уровня в исходном файле . Газета состоит из множества статей, в большинстве своем очень коротких . Другие статьи чуть длиннее . И лишь немногие статьи занимают всю газетную страницу . Собственно, именно этим газеты так удобны . Если бы они состояли из одной длинной статьи с неупорядоченной подборкой фактов, дат и имен, то мы бы про- сто не смогли их читать . Вертикальное разделение концепций Практически весь код читается слева направо и сверху вниз . Каждая строка представляет выражение или условие, а каждая группа строк представляет за- конченную мысль . Эти мысли следует отделять друг от друга пустыми строками . Для примера возьмем листинг 5 .1 . Объявление пакета, директива(-ы) импорта и все функции разделяются пустыми строками . Это чрезвычайно простое пра- вило оказывает глубокое воздействие на визуальную структуру кода . Каждая пустая строка становится зрительной подсказкой, указывающей на начало новой самостоятельной концепции . В ходе просмотра листинга ваш взгляд привлекает первая строка, следующая за пустой строкой . листинг 5 .1 . BoldWidget.java package fitnesse.wikitext.widgets; import java.util.regex.*; public class BoldWidget extends ParentWidget { public static final String REGEXP = "'''.+?'''"; private static final Pattern pattern = Pattern.compile("'''(.+?)'''", Pattern.MULTILINE + Pattern.DOTALL }; public BoldWidget(ParentWidget parent, String text) throws Exception { super(parent); Matcher match = pattern.matcher(text); match.find(); addChildWidgets(match.group(1)); } продолжение 105 106 Глава 5 . Форматирование листинг 5 .1 (продолжение) public String render() throws Exception { StringBuffer html = new StringBuffer(""); html.append(childHtml()).append(""); return html.toString(); } } Удаление пустых строк, как в листинге 5 .2, имеет весьма тяжелые последствия для удобочитаемости кода . листинг 5 .2 . BoldWidget.java package fitnesse.wikitext.widgets; import java.util.regex.*; public class BoldWidget extends ParentWidget { public static final String REGEXP = "'''.+?'''"; private static final Pattern pattern = Pattern.compile("'''(.+?)'''", Pattern.MULTILINE + Pattern.DOTALL); public BoldWidget(ParentWidget parent, String text) throws Exception { super(parent); Matcher match = pattern.matcher(text); match.find(); addChildWidgets(match.group(1));} public String render() throws Exception { StringBuffer html = new StringBuffer(""); html.append(childHtml()).append(""); return html.toString(); } } Эффект становится еще более заметным, если на секунду отвести глаза от ли- стинга . В первом примере группировка строк сразу бросается в глаза, а второй пример выглядит как сплошная каша, притом что два листинга различаются только вертикальными разделителями . Вертикальное сжатие Если вертикальные пропуски разделяют концепции, то вертикальное сжатие под- черкивает тесные связи . Таким образом, строки кода, между которыми существу- ет тесная связь, должны быть «сжаты» по вертикали . Обратите внимание на то, как бесполезные комментарии в листинге 5 .3 нарушают группировку двух пере- менных экземпляров . листинг 5 .3 public class ReporterConfig { /** * Имя класса слушателя */ 106 Цель форматирования 107 private String m_className; /** * Свойства слушателя */ private List m_properties = new ArrayList (); public void addProperty(Property property) { m_properties.add(property); } Листинг 5 .4 читается гораздо проще . Он нормально воспринимается «с одного взгляда» — по крайней мере, для меня . Я смотрю на него и сразу вижу, что пере- до мной класс с двумя переменными и одним методом; для этого мне не прихо- дится вертеть головой или бегать по строчкам глазами . В предыдущем листинге для достижения того же уровня понимания приходится потрудиться намного больше . листинг 5 .4 public class ReporterConfig { private String m_className; private List m_properties = new ArrayList (); public void addProperty(Property property) { m_properties.add(property); } Вертикальные расстояния Вам когда-нибудь доводилось метаться по классу, прыгая от одной функции к другой, прокручивая исходный файл вверх-вниз, пытаясь разобраться, как функции связаны друг с другом и как они работают, — только для того, чтобы окончательно заблудиться в его запутанных нагромождениях? Когда-нибудь искали определение функции или переменной по цепочкам наследования? Все это крайне неприятно, потому что вы стараетесь понять, как работает система, а вместо этого вам приходится тратить время и интеллектуальные усилия на поиски и запоминание местонахождения отдельных фрагментов . Концепции, тесно связанные друг с другом, должны находиться поблизости друг от друга по вертикали [G10] . Разумеется, это правило не работает для концепций, находящихся в разных файлах . Но тесно связанные концепции и не должны находиться в разных файлах, если только это не объясняется очень вескими до- водами . Кстати, это одна из причин, по которой следует избегать защищенных переменных . Если концепции связаны настолько тесно, что они находятся в одном исходном файле, их вертикальное разделение должно показывать, насколько они важны для понимания друг друга . Не заставляйте читателя прыгать туда-сюда по ис- ходным файлам и классам . 107 108 Глава 5 . Форматирование Объявления переменных. Переменные следует объявлять как можно ближе к месту использования . Так как мы имеем дело с очень короткими функциями, локальные переменные должны перечисляться в начале каждой функции, как в следующем примере из Junit 4 .3 . private static void readPreferences() { InputStream is= null; try { is= new FileInputStream(getPreferencesFile()); setPreferences(new Properties(getPreferences())); getPreferences().load(is); } catch (IOException e) { try { if (is != null) is.close(); } catch (IOException e1) { } } } Управляющие переменные циклов обычно объявляются внутри конструкции цикла, как в следующей симпатичной маленькой функции из того же источника . public int countTestCases() { int count= 0; for (Test each : tests) count += each.countTestCases(); return count; } В отдельных случаях переменная может объявляться в начале блока или непо- средственно перед циклом в длинной функции . Пример такого объявления пред- ставлен в следующем фрагменте очень длинной функции из TestNG . for (XmlTest test : m_suite.getTests()) { TestRunner tr = m_runnerFactory.newTestRunner(this, test); tr.addListener(m_textReporter); m_testRunners.add(tr); invoker = tr.getInvoker(); for (ITestNGMethod m : tr.getBeforeSuiteMethods()) { beforeSuiteMethods.put(m.getMethod(), m); } for (ITestNGMethod m : tr.getAfterSuiteMethods()) { afterSuiteMethods.put(m.getMethod(), m); } } Переменные экземпляров, напротив, должны объявляться в начале класса . Это не увеличивает вертикальное расстояние между переменными, потому что 108 Цель форматирования 109 в хорошо спроектированном классе они используются многими, если не всеми, методами класса . Размещение переменных экземпляров становилось причиной ожесточенных споров . В C++ обычно применялось так называемое правило ножниц, при кото- ром все переменные экземпляров размещаются внизу . С другой стороны, в Java они обычно размещаются в начале класса . Я не вижу причин для использования каких-либо других конвенций . Здесь важно то, что переменные экземпляров должны объявляться в одном хорошо известном месте . Все должны знать, где следует искать объявления . Для примера рассмотрим странный класс TestSuite из JUnit 4 .3 .1 . Я основательно сократил этот класс, чтобы лучше выразить свою мысль . Где-то в середине ли- стинга вдруг обнаруживаются объявления двух переменных экземпляров . Если бы автор сознательно хотел спрятать их, трудно найти более подходящее место . Читатель кода может наткнуться на эти объявления только случайно (как я) . public class TestSuite implements Test { static public Test createTest(Class extends TestCase> theClass, String name) { } public static Constructor extends TestCase> getTestConstructor(Class extends TestCase> theClass) throws NoSuchMethodException { } public static Test warning(final String message) { } private static String exceptionToString(Throwable t) { } private String fName; private Vector public TestSuite() { } public TestSuite(final Class extends TestCase> theClass) { } public TestSuite(Class extends TestCase> theClass, String name) { } } 109 110 Глава 5 . Форматирование Зависимые функции. Если одна функция вызывает другую, то эти функции должны располагаться вблизи друг от друга по вертикали, а вызывающая функ- ция должна находиться над вызываемой (если это возможно) . Тем самым фор- мируется естественная структура программного кода . Если это правило будет последовательно соблюдаться, читатели кода будут уверены в том, что опреде- ления функций следуют неподалеку от их вызовов . Для примера возьмем фраг- мент FitNesse из листинга 5 .5 . Обратите внимание на то, как верхняя функция вызывает нижние, и как они, в свою очередь, вызывают функции более низкого уровня . Такая структура позволяет легко найти вызываемые функции и значи- тельно улучшает удобочитаемость всего модуля . |