Главная страница
Навигация по странице:

  • Чтение кода сверху вниз: правило понижения

  • Создание, анализ ирефакторинг


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница8 из 49
    1   ...   4   5   6   7   8   9   10   11   ...   49
    листинг 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

    1   ...   4   5   6   7   8   9   10   11   ...   49


    написать администратору сайта