Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
листинг 2 .1 . Переменные с неясным контекстом private void printGuessStatistics(char candidate, int count) { String number; String verb; String pluralModifier; if (count == 0) { number = "no"; verb = "are"; pluralModifier = "s"; } else if (count == 1) { number = "1"; verb = "is"; pluralModifier = ""; продолжение 51 52 Глава 2 . Содержательные имена листинг 2 .1 (продолжение) } else { number = Integer.toString(count); verb = "are"; pluralModifier = "s"; } String guessMessage = String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier ); print(guessMessage); } Функция длинновата, а переменные используются на всем ее протяжении . Чтобы разделить функцию на меньшие смысловые фрагменты, следует создать класс GuessStatisticsMessage и сделать три переменные полями этого класса . Тем самым мы предоставим очевидный контекст для трех переменных — теперь абсолютно очевидно, что эти переменные являются частью GuessStatisticsMessage . Уточне- ние контекста также позволяет заметно улучшить четкость алгоритма за счет его деления на меньшие функции (листинг 2 .2) . листинг 2 .2 . Переменные с контекстом public class GuessStatisticsMessage { private String number; private String verb; private String pluralModifier; public String make(char candidate, int count) { createPluralDependentMessageParts(count); return String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier ); } private void createPluralDependentMessageParts(int count) { if (count == 0) { thereAreNoLetters(); } else if (count == 1) { thereIsOneLetter(); } else { thereAreManyLetters(count); } } private void thereAreManyLetters(int count) { number = Integer.toString(count); verb = "are"; pluralModifier = "s"; } private void thereIsOneLetter() { 52 Несколько слов напоследок 53 number = "1"; verb = "is"; pluralModifier = ""; } private void thereAreNoLetters() { number = "no"; verb = "are"; pluralModifier = "s"; } } не добавляйте избыточный контекст Если вы работаете над вымышленным приложением «Gas Station Deluxe», не стоит снабжать имя каждого класса префиксом GSD . В сущности, вы работаете против собственного инструментария . Введите букву «G», нажмите клавишу за- вершения — и вы получите длинный-предлинный список всех классов в системе . Разумно ли это? IDE пытается помочь вам, так стоит ли ей мешать? Допустим, вы изобрели класс MailingAddress в учетном модуле GSD и присвоили ему имя GSDAccountAddress . Позднее адрес используется в приложении, обеспе- чивающем связь с клиентами . Будете ли вы использовать GSDAccountAddress ? На- сколько подходящим выглядит это имя? Десять из 17 символов либо избыточны, либо не относятся к делу . Короткие имена обычно лучше длинных, если только их смысл понятен читате- лю кода . Не включайте в имя больше контекста, чем необходимо . Имена accountAddress и customerAddress хорошо подходят для экземпляров класса Address , но для классов такой выбор неудачен . Address — вот хорошее имя класса . Если потребуется подчеркнуть различия между MAC-адресами, адресами портов и веб-адресами, я подумаю об использовании имен PostalAddress , MAC и URI . Полу- ченные имена становятся более точными, а это, собственно, и является главной целью всего присваивания имен . несколько слов напоследок Основные трудности с выбором хороших имен обусловлены необходимостью хороших описательных навыков и единого культурного фона . Это вопрос препо- давания, а не вопрос техники, экономики или управления . В результате многие специалисты, работающие в этой области, так и не научились хорошо справляться с этой задачей . Люди также опасаются переименований из страха возражений со стороны других разработчиков . Мы не разделяем эти опасения, а изменение имен (в лучшую сто- рону) вызывает у нас только благодарность . Большей частью мы не запоминаем 53 54 Глава 2 . Содержательные имена имена классов и методов . Современные инструменты берут на себя подобные мелочи, а мы следим за тем, чтобы программный код читался как абзацы и пред- ложения или хотя бы как таблицы и структуры данных (предложение не всегда является лучшим способом отображения данных) . Возможно, своими переиме- нованиями — как и любыми другими усовершенствованиями кода — вы кого-то удивите . Пусть это вас не останавливает . Последуйте этим правилам и посмотрите, не станет ли ваш код более удобочи- таемым . Если вы занимаетесь сопровождением чужого кода, попробуйте решить проблемы средствами рефакторинга . Это даст немедленный результат и продол- жит приносить плоды в долгосрочной перспективе . 54 Функции На заре эпохи программирования системы строились из программ, функций и подпрограмм . До наших дней дожили только функции . Они образуют первый уровень структуризации в любой программе, и их грамотная запись является основной темой этой главы . Рассмотрим код в листинге 3 .1 . В FitNesse 1 трудно найти длинную функцию, но после некоторых поисков мне это все же удалось . Функция не только длинна, 1 Тестовая программа, распространяемая с открытым кодом — www .tnese .org . 3 55 56 Глава 3 . Функции но она содержит повторяющиеся фрагменты кода, множество загадочных строк, а также странные и неочевидные типы данных и функции API . Попробуйте ра- зобраться в ней за три минуты . Посмотрим, что вам удастся понять . листинг 3 .1 . HtmlUtil.java (FitNesse 20070619) public static String testableHtml( PageData pageData, boolean includeSuiteSetup ) throws Exception { WikiPage wikiPage = pageData.getWikiPage(); StringBuffer buffer = new StringBuffer(); if (pageData.hasAttribute("Test")) { if (includeSuiteSetup) { WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_SETUP_NAME, wikiPage ); if (suiteSetup != null) { WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup); String pagePathName = PathParser.render(pagePath); buffer.append("!include -setup .") .append(pagePathName) .append("\n"); } } WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage); if (setup != null) { WikiPagePath setupPath = wikiPage.getPageCrawler().getFullPath(setup); String setupPathName = PathParser.render(setupPath);" buffer.append("!include -setup .") .append(setupPathName) .append("\n"); } } buffer.append(pageData.getContent()); if (pageData.hasAttribute("Test")) { WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage); if (teardown != null) { WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown); String tearDownPathName = PathParser.render(tearDownPath); buffer.append("\n") .append("!include -teardown .") .append(tearDownPathName) .append("\n"); } 56 Функции 57 if (includeSuiteSetup) { WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage ); if (suiteTeardown != null) { WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath (suiteTeardown); String pagePathName = PathParser.render(pagePath); buffer.append("!include -teardown .") .append(pagePathName) .append("\n"); } } } pageData.setContent(buffer.toString()); return pageData.getHtml(); } Удалось ли вам разобраться с функцией за три минуты? Вероятно, нет . В ней происходит слишком много всего, и притом на разных уровнях абстракции . За- гадочные строки и непонятные вызовы функций смешиваются в конструкциях if двойной вложенности, к тому же зависящих от состояния флагов . Но после выделения нескольких методов, переименований и небольшой рес- труктуризации мне удалось представить смысл этой функции в девяти строках листинга 3 .2 . Посмотрим, удастся ли вам разобраться в ней за следующие три минуты . листинг 3 .2 . HtmlUtil.java (переработанная версия) public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite ) throws Exception { boolean isTestPage = pageData.hasAttribute("Test"); if (isTestPage) { WikiPage testPage = pageData.getWikiPage(); StringBuffer newPageContent = new StringBuffer(); includeSetupPages(testPage, newPageContent, isSuite); newPageContent.append(pageData.getContent()); includeTeardownPages(testPage, newPageContent, isSuite); pageData.setContent(newPageContent.toString()); } return pageData.getHtml(); } Если только вы не занимаетесь активным изучением FitNesse, скорее всего, вы не разберетесь во всех подробностях . Но по крайней мере вы поймете, что функция включает в тестовую страницу какие-то начальные и конечные блоки, а потом 57 58 Глава 3 . Функции генерирует код HTML . Если вы знакомы с JUnit 1 , то, скорее всего, поймете, что эта функция является частью тестовой инфраструктуры на базе Web . И конечно, это правильное предположение . Прийти к такому выводу на основании листин- га 3 .2 несложно, но из листинга 3 .1 это, мягко говоря, неочевидно . Что же делает функцию из листинга 3 .2 такой понятной и удобочитаемой? Как заставить функцию передавать намерения разработчика? Какие атрибуты функ- ции помогут случайному читателю составить интуитивное представление о вы- полняемых ей задачах? Компактность! Первое правило: функции должны быть компактными . Второе правило: функции должны быть еще компактнее . Я не могу научно обосновать свое утверждение . Не ждите от меня ссылок на исследования, доказывающие, что очень маленькие функции лучше больших . Я могу всего лишь сказать, что я почти четыре деся- тилетия писал функции всевозможных размеров . Мне доводилось создавать кошмарных монстров в 3000 строк . Я написал бесчисленное множество функций длиной от 100 до 300 строк . И я писал функции от 20 до 30 строк . Мой практи- ческий опыт научил меня (ценой многих проб и ошибок), что функции должны быть очень маленькими . В 80-е годы считалось, что функция должна занимать не более одного экрана . Конечно, тогда экраны VT100 состояли из 24 строк и 80 столбцов, а редакторы использовали 4 строки для административных целей . В наши дни с мелким шрифтом на хорошем большом мониторе можно разместить 150 символов в строке и 100 и более строк на экране . Однако строки не должны состоять из 150 символов, а функции — из 100 строк . Желательно, чтобы длина функции не превышала 20 строк . Насколько короткой может быть функция? В 1999 году я заехал к Кенту Беку в его дом в Орегоне . Мы посидели и позанимались программированием . В один момент он показал мне симпатичную маленькую программу Java/Swing, которую он назвал Sparkle . Программа создавала на экране визуальный эффект, очень по- хожий на эффект волшебной палочки феи-крестной из фильма «Золушка» . При перемещении мыши с курсора рассыпались замечательные блестящие искор- ки, которые осыпались к нижнему краю экрана под воздействием имитируемого гравитационного поля . Когда Кент показал мне код, меня поразило, насколько компактными были все функции . Многие из моих функций в программах Swing растягивались по вертикали чуть ли не на километры . Однако каждая функция в программе Кента занимала всего две, три или четыре строки . Все функции были предельно очевидными . Каждая функция излагала свою историю, и каждая исто- 1 Программа модульного тестирования для Java, распространяемая с открытым кодом — www .junit .org . 58 Правило одной операции 59 рия естественным образом подводила вас к началу следующей истории . Вот какими короткими должны быть функции 1 ! Более того, функции должны быть еще короче, чем в листинге 3 .2! На деле ли- стинг 3 .2 следовало бы сократить до листинга 3 .3 . листинг 3 .3 . HtmlUtil.java (переработанная версия) public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { if (isTestPage(pageData)) includeSetupAndTeardownPages(pageData, isSuite); return pageData.getHtml(); } Блоки и отступы Из сказанного выше следует, что блоки в командах if , else , while и т . д . должны состоять из одной строки, в которой обычно содержится вызов функции . Это не только делает вмещающую функцию более компактной, но и способствует доку- ментированию кода, поскольку вызываемой в блоке функции можно присвоить удобное содержательное имя . Кроме того, функции не должны содержать вложенных структур, так как это приводит к их увеличению . Максимальный уровень отступов в функции не должен превышать одного-двух . Разумеется, это упрощает чтение и понимание функций . Правило одной операции Совершенно очевидно, что функция из листинга 3 .1 выполняет множество операций . Она создает буфе- ры, производит выборку данных, ищет унаследован- ные страницы, строит пути, присоединяет загадочные строки, генерирует код HTML… и это еще не все . С другой стороны, в листинге 3 .3 выполняется всего одна простая операция: включение в тестовую стра- ницу начальных и конечных блоков . Следующий совет существует в той или иной форме не менее 30 лет . 1 Я спросил Кента, не сохранилась ли у него эта программа, но ему не удалось ее найти . Об- шарил все свои старые компьютеры — тоже безуспешно . Остались лишь мои воспомина- ния об этой программе . 59 60 Глава 3 . Функции ФУНКЦИЯ ДОЛЖНА ВЫПОЛНЯТЬ ТОЛЬКО ОДНУ ОПЕРАЦИЮ . ОНА ДОЛЖНА ВЫПОЛНЯТЬ ЕЕ ХОРОШО . И НИЧЕГО ДРУГОГО ОНА ДЕ- ЛАТЬ НЕ ДОЛЖНА . Проблема в том, что иногда бывает трудно определить, что же считать «одной операцией» . В листинге 3 .3 выполняется одна операция? Легко возразить, что в нем выполняются минимум три операции: 1 . Функция проверяет, является ли страница тестовой страницей . 2 . Если является, то в нее включаются начальные и конечные блоки . 3 . Для страницы генерируется код HTML . Так как же? Сколько операций выполняет функция — одну или три? Обратите внимание: три этапа работы функции находятся на одном уровне абстракции под объявленным именем функции . Ее можно было бы описать в виде короткого TO 1 -абзаца: TO RenderPageWithSetupsAndTeardowns, мы проверяем, является ли страница тестовой, и если является — включаем начальные и конечные блоки . В любом случае для страницы генерируется код HTML . Если функция выполняет только те действия, которые находятся на одном уров- не под объявленным именем функции, то эта функция выполняет одну операцию . В конце концов, функции пишутся прежде всего для разложения более крупной концепции (иначе говоря, имени функции) на последовательность действий на следующем уровне абстракции . Вполне очевидно, что листинг 3 .1 содержит множество различных действий на разных уровнях абстракции . Поэтому в нем явно выполняется более одной операции . Даже листинг 3 .2 содержит два уровня абстракции; это доказывает- ся тем, что нам удалось его сократить . С другой стороны, осмысленно сокра- тить листинг 3 .3 очень трудно . Команду if можно вынести в функцию с именем includeSetupsAndTeardownsIfTestPage , но это простая переформулировка кода без изменения уровня абстракции . Итак, чтобы определить, что функция выполняет более одной операции, попро- буйте извлечь из нее другую функцию, которая бы не являлась простой пере- формулировкой реализации [G34] . Секции в функциях Взгляните на листинг 4 .7 на с . 98 . Обратите внимание: функция generatePrimes разделена на секции (объявления, инициализация, отбор) . Это очевидный при- знак того, что функция выполняет более одной операции . Функцию, выполняю- щую только одну операцию, невозможно осмысленно разделить на секции . 1 В языке LOGO ключевое слово TO использовалось так же, как в Ruby и Python использу- ется «def» . Таким образом, каждая функция начиналась со слова «TO» . 60 Один уровень абстракции на функцию 61 Один уровень абстракции на функцию Чтобы убедиться в том, что функция выполняет «только одну операцию», не- обходимо проверить, что все команды функции находятся на одном уровне абстракции . Легко убедиться, что листинг 3 .1 нарушает это правило . Некоторые из его концепций — например, getHtml() — находятся на очень высоком уровне абстракции; другие (скажем, String pagePathName = PathParser.render(pagePath) ) — на среднем уровне . Наконец, третьи — такие, как .append("\n") — относятся к чрезвычайно низкому уровню абстракции . Смешение уровней абстракции внутри функции всегда создает путаницу . Чита- тель не всегда понимает, является ли некоторое выражение важной концепцией или второстепенной подробностью . Что еще хуже, при их смешении функция постепенно начинает обрастать все большим количеством второстепенных под- робностей . Чтение кода сверху вниз: правило понижения Код должен читаться как рассказ — сверху вниз [KP78, p . 37] . За каждой функцией должны следовать функции следующего уровня абстрак- ции . Это позволяет читать код, последовательно спускаясь по уровням аб- стракции в ходе чтения списка функций . Я называю такой подход «правилом понижения» . Сказанное можно сформулировать и иначе: программа должна читаться так, словно она является набором TO -абзацев, каждый из которых описывает текущий уровень абстракции и ссылается на последующие TO -абзацы следующего нижнего уровня . Чтобы включить начальные и конечные блоки, мы сначала включаем началь- ные блоки, затем содержимое тестовой страницы, а затем включаем конечные блоки . • Чтобы включить начальные блоки, мы сначала включаем пакетные началь- ные блоки, если имеем дело с пакетом тестов, а затем включаем обычные начальные блоки . • Чтобы включить пакетные начальные блоки, мы ищем в родительской иерархии страницу SuiteSetUp и добавляем команду include с путем к этой странице . Чтобы найти в родительской иерархии… Опыт показывает, что программистов очень трудно научить следовать этому правилу и писать функции, остающиеся на одном уровне абстракции . Тем не менее освоить этот прием очень важно . Он играет ключевую роль для создания коротких функций, выполняющих только одну операцию . Построение кода по аналогии с набором последовательных TO -абзацев — эффективный метод под- держания единого уровня абстракции . 61 |