Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
листинг 5 .5 . WikiPageResponder.java public class WikiPageResponder implements SecureResponder { protected WikiPage page; protected PageData pageData; protected String pageTitle; protected Request request; protected PageCrawler crawler; public Response makeResponse(FitNesseContext context, Request request) throws Exception { String pageName = getPageNameOrDefault(request, "FrontPage"); loadPage(pageName, context); if (page == null) return notFoundResponse(context, request); else return makePageResponse(context); } private String getPageNameOrDefault(Request request, String defaultPageName) { String pageName = request.getResource(); if (StringUtil.isBlank(pageName)) pageName = defaultPageName; return pageName; } protected void loadPage(String resource, FitNesseContext context) throws Exception { WikiPagePath path = PathParser.parse(resource); crawler = context.root.getPageCrawler(); crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler()); page = crawler.getPage(context.root, path); if (page != null) pageData = page.getData(); } private Response notFoundResponse(FitNesseContext context, Request request) throws Exception { 110 Цель форматирования 111 return new NotFoundResponder().makeResponse(context, request); } private SimpleResponse makePageResponse(FitNesseContext context) throws Exception { pageTitle = PathParser.render(crawler.getFullPath(page)); String html = makeHtml(context); SimpleResponse response = new SimpleResponse(); response.setMaxAge(0); response.setContent(html); return response; } Заодно этот фрагмент дает хороший пример хранения констант на соответствую- щем уровне [G35] . Константу «FrontPage» можно было бы объявить в функции getPageNameOrDefault , но тогда хорошо известная и ожидаемая константа оказалась бы погребенной в функции неуместно низкого уровня . Лучше переместить эту константу вниз – от того места, где ее следовало бы ввести, к месту ее фактиче- ского использования . Концептуальное родство. Некоторые фрагменты кода требуют, чтобы их раз- местили вблизи от других фрагментов . Такие фрагменты обладают определен- ным концептуальным родством . Чем сильнее родство, тем меньше должно быть вертикальное расстояние между ними . Как мы уже видели, родство может быть основано на прямой зависимости (когда одна функция вы- зывает другую) или на использовании перемен- ных в функциях . Однако существуют и другие разновидности родства . Например, родство воз- никает в том случае, если группа функций вы- полняет аналогичные операции . Возьмем следую- щий фрагмент кода из Junit 4 .3 .1: public class Assert { static public void assertTrue(String message, boolean condition) { if (!condition) fail(message); } static public void assertTrue(boolean condition) { assertTrue(null, condition); } static public void assertFalse(String message, boolean condition) { assertTrue(message, !condition); } 111 112 Глава 5 . Форматирование static public void assertFalse(boolean condition) { assertFalse(null, condition); } Эти функции обладают сильным концептуальным родством, потому что они используют единую схему выбора имен и выполняют разные варианты одной базовой операции . Тот факт, что они вызывают друг друга, вторичен . Даже без него эти функции все равно следовало бы разместить поблизости друг от друга . Вертикальное упорядочение Как правило, взаимозависимые функции должны размещаться в нисходящем порядке . Иначе говоря, вызываемая функция должна располагаться ниже вы- зывающей функции � . Так формируется логичная структура модуля исходного кода – от высокого уровня к более низкому . Как и в газетных статьях, читатель ожидает, что самые важные концепции бу- дут изложены сначала, причем с минимальным количеством второстепенных де талей . Низкоуровневые подробности естественно приводить в последнюю очередь . Это позволяет нам бегло просматривать исходные файлы, извлекая суть из нескольких начальных функций, без погружения в подробности . Ли- стинг 5 .5 имеет именно такую структуру . Возможно, еще лучшие примеры встречаются в листинге 15 .5 на с . 299 и в листинге 3 .7 на с . 75 . Горизонтальное форматирование Насколько широкой должна быть строка? Чтобы ответить на этот вопрос, мы проанализируем ширину строк в типичных программах . Как и в предыдущем случае, будут проанализированы семь разных проектов . На рис . 5 .2 показано рас- пределение длин строк во всех семи проектах . Закономерность впечатляет, осо- бенно около 45 символов . Фактически каждый размер от 20 до 60 соответствует примерно одному проценту от общего количества строк . Целых 40 процентов! Возможно, еще 30 процентов составляют строки с длиной менее 10 символов . Помните, что на графике используется логарифмическая шкала, поэтому разброс в области свыше 80 символов очень важен . Программисты явно предпочитают более короткие строки . Это наводит на мысль, что строки лучше делать по возможности короткими . Установленное Холлеритом старое ограничение в 80 символов выглядит излишне жестким; я ничего не имеют против строк длиной в 100 и даже 120 символов . Но более длинные строки, вероятно, вызваны небрежностью программиста . 112 Горизонтальное форматирование 113 Рис . 5 .2 . Распределение ширины строк в Java Прежде я использовал это правило, чтобы мне не приходилось прокручивать про- граммный код вправо . Но современные мониторы стали настолько широкими, а молодые программисты выбирают настолько мелкие шрифты, что на экране по- мещается до 200 символов . Не делайте этого . Лично я установил себе «верхнюю планку» в 120 символов . Горизонтальное разделение и сжатие Горизонтальные пропуски используются для группировки взаимосвязанных элементов и разделения разнородных элементов . Рассмотрим следующую функцию: private void measureLine(String line) { lineCount++; int lineSize = line.length(); totalChars += lineSize; lineWidthHistogram.addLine(lineSize, lineCount); recordWidestLine(lineSize); } Знаки присваивания окружены пробелами, обеспечивающими их визуальное выделение . Операторы присваивания состоят из двух основных элементов: левой и правой частей . Пробелы наглядно подчеркивают это разделение . С другой стороны, я не стал отделять имена функций от открывающих скобок . Это обусловлено тем, что имя функции тесно связано с ее аргументами . Пробе- лы изолируют их вместо того, чтобы объединять . Я также разделил аргументы 113 114 Глава 5 . Форматирование в скобках пробелами, чтобы выделить запятые и подчеркнуть, что аргументы не зависят друг от друга . Пробелы также применяются для визуального обозначения приоритета опера- торов: public class Quadratic { public static double root1(double a, double b, double c) { double determinant = determinant(a, b, c); return (-b + Math.sqrt(determinant)) / (2*a); } public static double root2(int a, int b, int c) { double determinant = determinant(a, b, c); return (-b - Math.sqrt(determinant)) / (2*a); } private static double determinant(double a, double b, double c) { return b*b - 4*a*c; } } Обратите внимание, как хорошо читаются формулы . Между множителями нет пробелов, потому что они обладают высоким приоритетом . Слагаемые разделя- ются пробелами, так как сложение и вычитание имеют более низкий приоритет . К сожалению, в большинстве программ форматирования кода приоритет опе- раторов не учитывается, и во всех случаях применяются одинаковые пропуски . Нетривиальные изменения расстояний, как в приведенном примере, теряются после переформатирования кода . Горизонтальное выравнивание Когда я был ассемблерным программистом 1 , горизонтальное выравнивание использовалось для визуального выделения некоторых структур . Когда я пере- шел на C, C++, а в конце концов и на Java, я продолжал выравнивать все имена переменных в группах объявлений или все правосторонние значения в группах команд присваивания . Мой код выглядел примерно так: public class FitNesseExpediter implements ResponseSender { private Socket socket; private InputStream input; private OutputStream output; private Request request; private Response response; private FitNesseContext context; protected long requestParsingTimeLimit; private long requestProgress; 1 Кого я пытаюсь обмануть? Я так и остался ассемблерным программистом . Парня можно разлучить с «металлом», но в душе «металл» все равно живет! 114 Горизонтальное форматирование 115 private long requestParsingDeadline; private boolean hasError; public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception { this.context = context; socket = s; input = s.getInputStream(); output = s.getOutputStream(); requestParsingTimeLimit = 10000; } Однако потом я обнаружил, что такое выравнивание не приносит пользы . Оно визуально выделяет совсем не то, что требуется, и отвлекает читателя от моих истинных намерений . Например, в приведенном выше списке объявлений чита- тель просматривает имена переменных, не обращая внимания на их типы . Анало- гичным образом в списке команд присваивания возникает соблазн просмотреть правосторонние значения, не замечая оператора присваивания . Ситуация усу- губляется тем, что средства автоматического форматирования обычно удаляют подобное выравнивание . Поэтому в итоге я отказался от этого стиля форматирования . Сейчас я отдаю предпочтение невыровненным объявлениям и присваиваниям, как в следую- щем фрагменте, потому что они помогают выявить один важный дефект . Если в программе встречаются длинные списки, нуждающиеся в выравнивании, то проблема кроется в длине списка, а не в отсутствии выравнивания . Длина спи- сков объявлений в классе FitNesseExpediter наводит на мысль, что этот класс необходимо разделить . public class FitNesseExpediter implements ResponseSender { private Socket socket; private InputStream input; private OutputStream output; private Request request; private Response response; private FitNesseContext context; protected long requestParsingTimeLimit; private long requestProgress; private long requestParsingDeadline; private boolean hasError; public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception { this.context = context; socket = s; input = s.getInputStream(); output = s.getOutputStream(); requestParsingTimeLimit = 10000; } 115 116 Глава 5 . Форматирование Отступы Исходный файл имеет иерархическую структуру . В нем присутствует ин- формация, относящаяся к файлу в целом; к отдельным классам в файле; к мето- дам внутри классов; к блокам внутри методов и рекурсивно – к блокам внутри блоков . Каждый уровень этой иерархии образует область видимости, в кото- рой могут объявляться имена и в которой интерпретируются исполняемые команды . Чтобы создать наглядное представление этой иерархии, мы снабжаем строки ис- ходного кода отступами, размер которых соответствует их позиции в иерархии . Команды уровня файла (такие, как большинство объявлений классов) отступов не имеют . Методы в классах сдвигаются на один уровень вправо от уровня клас- са . Реализации этих методов сдвигаются на один уровень вправо от объявления класса . Реализации блоков сдвигаются на один уровень вправо от своих внишних блоков и т . д . Программисты широко используют эту схему расстановки отступов в своей ра- боте . Чтобы определить, к какой области видимости принадлежат строки кода, они визуально группируют строки по левому краю . Это позволяет им быстро пропускать области видимости, не относящиеся к текущей ситуации (например, реализации команд if и while ) . У левого края ищутся объявления новых методов, новые переменные и даже новые классы . Без отступов программа становится практически нечитаемой для людей . Следующие программы идентичны с син- таксической и семантической точки зрения: public class FitNesseServer implements SocketServer { private FitNesseContext context; public FitNesseServer(FitNesseContext context) { this.context = context; } public void serve(Socket s) { serve(s, 10000); } public void serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new FitNesseExpediter(s, context); sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); } catch(Exception e) { e.printStackTrace(); } } } ----- public class FitNesseServer implements SocketServer { private FitNesseContext context; public FitNesseServer(FitNesseContext context) { this.context = context; } public void serve(Socket s) { serve(s, 10000); } public void serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new FitNesseExpediter(s, context); sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); 116 Горизонтальное форматирование 117 } catch (Exception e) { e.printStackTrace(); } } } Наше зрение быстро охватывает структуру файла с отступами . Мы почти мгно- венно находим переменные, конструкторы и методы . Всего за несколько секунд можно понять, что класс предоставляет простой интерфейс для работы с сокетом, с поддержкой тайм-аута . С другой стороны, разобраться в версии без отступов без тщательного анализа практически невозможно . Нарушения отступов. Иногда возникает соблазн нарушить правила расста- новки отступов в коротких командах if , коротких циклах while или коротких функциях . Но каждый раз, когда я поддавался этому искушению, я почти всегда возвращался и расставлял отступы, как положено . Таким образом, я стараюсь не сворачивать блоки в одну строку, как в этом фрагменте: public class CommentWidget extends TextWidget { public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?"; public CommentWidget(ParentWidget parent, String text){super(parent, text);} public String render() throws Exception {return ""; } } Вместо этого я предпочитаю развернутые блоки с правильными отступами: public class CommentWidget extends TextWidget { public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?"; public CommentWidget(ParentWidget parent, String text) { super(parent, text); } public String render() throws Exception { return ""; } } Вырожденные области видимости Иногда тело цикла while или команды for не содержит команд, то есть является вырожденным, как в следующем фрагменте . Я не люблю такие структуры и ста- раюсь избегать их . А когда это невозможно, я по крайней мере слежу за тем, чтобы пустое тело имело правильные отступы и было заключено в фигурные скобки . Вы не представляете, как часто меня обманывала точка с запятой, молчаливо прячу- щаяся в конце цикла while в той же строке . Если не сделать эту точку хорошо за- метной, разместив ее в отдельной строке, ее попросту слишком сложно разглядеть: while (dis.read(buf, 0, readBufferSize) != -1) ; 117 118 Глава 5 . Форматирование Правила форматирования в группах У каждого программиста есть свои люби- мые правила форматирования, но если он работает в группе, то должен руководство- ваться групповыми правилами . Группа разработчиков согласует единый стиль форматирования, который в даль- нейшем применяется всеми участниками . Код программного продукта должен быть оформлен в едином стиле . Он не должен выглядеть так, словно был написан несколькими личностями, расходящимися во мнениях по поводу оформления . В начале работы над проектом FitNesse в 2002 году я провел встречу с группой для выработки общего стиля программирования . На это потребовалось около 10 минут . Мы решили, где будем расставлять фигурные скобки, каким будет размер отступов, по какой схеме будут присваиваться имена классов, переменных и мето- дов и т . д . Затем эти правила были закодированы в системе форматирования кода нашей рабочей среды, и в дальнейшем мы неуклонно придерживались их . Это были не те правила, которые предпочитаю лично я; это были правила, выбранные группой . И я, как участник группы, неуклонно соблюдал их при написании кода в проекте FitNesse . Хорошая программная система состоит из набора удобочитаемых документов, оформленных в едином, согласованном стиле . Читатель должен быть уверен в том, что форматные атрибуты, встречающиеся в одном исходном файле, будут иметь точно такой же смысл в других файлах . Ни в коем случае не усложняйте исходный код, допуская его оформление в нескольких разных стилях . Правила форматирования от дядюшки Боба Правила, которые использую лично я, очень просты; они представлены в коде листинга 5 .6 . Перед вами пример того, как сам код становится лучшим докумен- том, описывающим стандарты кодирования . листинг 5 .6 . CodeAnalyzer.java public class CodeAnalyzer implements JavaFileAnalysis { private int lineCount; private int maxLineWidth; private int widestLineNumber; private LineWidthHistogram lineWidthHistogram; private int totalChars; public CodeAnalyzer() { lineWidthHistogram = new LineWidthHistogram(); } 118 Горизонтальное форматирование 119 public static List List findJavaFiles(parentDirectory, files); return files; } private static void findJavaFiles(File parentDirectory, List for (File file : parentDirectory.listFiles()) { if (file.getName().endsWith(".java")) files.add(file); else if (file.isDirectory()) findJavaFiles(file, files); } } public void analyzeFile(File javaFile) throws Exception { BufferedReader br = new BufferedReader(new FileReader(javaFile)); String line; while ((line = br.readLine()) != null) measureLine(line); } private void measureLine(String line) { lineCount++; int lineSize = line.length(); totalChars += lineSize; lineWidthHistogram.addLine(lineSize, lineCount); recordWidestLine(lineSize); } private void recordWidestLine(int lineSize) { if (lineSize > maxLineWidth) { maxLineWidth = lineSize; widestLineNumber = lineCount; } } public int getLineCount() { return lineCount; } public int getMaxLineWidth() { return maxLineWidth; } public int getWidestLineNumber() { return widestLineNumber; } public LineWidthHistogram getLineWidthHistogram() { return lineWidthHistogram; } продолжение 119 |