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

  • Разделение команд и запросов

  • Используйте исключения вместо возвращения кодов ошибок

  • Обработка ошибок как одна операция

  • Структурное программирование

  • Как научиться писать такие функции

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


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница10 из 49
    1   ...   6   7   8   9   10   11   12   13   ...   49
    70
    Глава 3 . Функции
    Выходные аргументы
    Аргументы естественным образом интерпретируются как входные данные функ- ции . Каждый, кто занимался программированием более нескольких лет, наверня- ка сталкивался с необходимостью дополнительной проверки аргументов, которые на самом деле оказывались выходными, а не входными . Пример:
    appendFooter(s);
    Присоединяет ли эта функция s
    в качестве завершающего блока к чему-то дру- гому? Или она присоединяет какой-то завершающий блок к s
    ? Является ли s
    входным или выходным аргументом? Конечно, можно посмотреть на сигнатуру функции и получить ответ:
    public void appendFooter(StringBuffer report)
    Вопрос снимается, но только после проверки объявления . Все, что заставляет обращаться к сигнатуре функции, нарушает естественный ритм чтения кода .
    Подобных «повторных заходов» следует избегать .
    До наступления эпохи объектно-ориентированного программирования без выход- ных аргументов иногда действительно не удавалось обойтись . Но в ОО-языках эта проблема в целом исчезла, потому что сама функция может вызываться для выходного аргумента . Иначе говоря, функцию appendFooter лучше вызывать в виде report.appendFooter();
    В общем случае выходных аргументов следует избегать . Если ваша функция должна изменять чье-то состояние, пусть она изменяет состояние своего объекта- владельца .
    Разделение команд и запросов
    Функция должна что-то делать или отвечать на какой-то вопрос, но не одновре- менно . Либо функция изменяет состояние объекта, либо возвращает информацию об этом объекте . Совмещение двух операций часто создает путаницу . Для примера возьмем следующую функцию:
    public boolean set(String attribute, String value);
    Функция присваивает значение атрибуту с указанным именем и возвращает true
    , если присваивание прошло успешно, или false
    , если такой атрибут не существует .
    Это приводит к появлению странных конструкций вида if (set(
    "
    username
    "
    ,
    "
    unclebob
    "
    ))...
    Представьте происходящее с точки зрения читателя кода . Что проверяет это усло- вие? Что атрибут "username"
    содержит ранее присвоенное значение "unclebob"
    ?
    Или что проверяет атрибуту "username"
    успешно присвоено значение "unclebob"
    ?
    Смысл невозможно вывести из самого вызова, потому что мы не знаем, чем в дан- ном случае является слово set
    — глаголом или прилагательным .
    70

    Используйте исключения вместо возвращения кодов ошибок
    71
    Автор предполагал, что set является глаголом, но в контексте команды if это имя скорее воспринимается как прилагательное . Таким образом, команда читается в виде «Если атрибуту username ранее было присвоено значение unclebob
    », а не
    «присвоить атрибуту username значение unclebob
    , и если все прошло успешно, то…» Можно было бы попытаться решить проблему, переименовав функцию set в setAndCheckIfExists
    , но это не особенно улучшает удобочитаемость команды if
    Полноценное решение заключается в отделении команды от запроса, чтобы в принципе исключить любую неоднозначность .
    if (attributeExists(
    "
    username
    "
    )) {
    setAttribute(
    "
    username
    "
    ,
    "
    unclebob
    "
    );
    }
    Используйте исключения вместо
    возвращения кодов ошибок
    Возвращение кодов ошибок функциями-командами является неочевидным нару- шением принципа разделения команд и запросов . Оно поощряет использование команд в предикатных выражениях if
    :
    if (deletePage(page) == E_OK)
    Такие конструкции не страдают от смешения глаголов с прилагательными, но они приводят к созданию структур слишком глубокой вложенности . При возвраще- нии кода ошибки возникает проблема: вызывающая сторона должна немедленно отреагировать на ошибку .
    if (deletePage(page) == E_OK) {
    if (registry.deleteReference(page.name) == E_OK) {
    if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
    logger.log(
    "
    page deleted
    "
    );
    } else {
    logger.log(
    "
    configKey not deleted
    "
    );
    }
    } else
    {
    logger.log(
    "
    deleteReference from registry failed
    "
    );
    }
    } else {
    logger.log(
    "
    delete failed
    "
    );
    return E_ERROR;
    }
    С другой стороны, если вместо возвращения кодов ошибок используются исклю- чения, то код обработки ошибок изолируется от ветви нормального выполнения и упрощается:
    try {
    deletePage(page);
    71

    72
    Глава 3 . Функции registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
    }
    catch (Exception e) {
    logger.log(e.getMessage());
    }
    Изолируйте блоки try/catch
    Блоки try
    /
    catch выглядят весьма уродливо . Они запутывают структуру кода и смешивают обработку ошибок с нормальной обработкой . По этой причине тела блоков try и catch рекомендуется выделять в отдельные функции .
    public void delete(Page page) {
    try {
    deletePageAndAllReferences(page);
    }
    catch (Exception e) {
    logError(e);
    }
    }
    private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
    }
    private void logError(Exception e) {
    logger.log(e.getMessage());
    }
    В этом примере функция delete специализируется на обработке ошибок . В этой функции легко разобраться, а потом забыть о ней . Функция deletePageAndAllRefer- ences специализируется на процессе полного удаления страницы . Читая ее, можно не обращать внимания на обработку ошибок . Таким образом, код нормального выполнения отделяется от кода обработки ошибок, а это упрощает его понимание и модификацию .
    Обработка ошибок как одна операция
    Функции должны выполнять одну операцию . Обработка ошибок — это одна операция . Значит, функция, обрабатывающая ошибки, ничего другого делать не должна . Отсюда следует, что если в функции присутствует ключевое слово try
    , то оно должно быть первым словом в функции, а после блоков catch
    /
    finally ни- чего другого быть не должно (как в предыдущем примере) .
    72

    Не повторяйтесь
    73
    Магнит зависимостей error .java
    Возвращение кода ошибки обычно подразумевает, что в программе имеется не- кий класс или перечисление, в котором определяются все коды ошибок .
    public enum Error {
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES,
    WAITING_FOR_EVENT;
    }
    Подобные классы называются магнитами зависимостей; они должны импорти- роваться и использоваться многими другими классами . При любых изменениях перечисления
    Error все эти классы приходится компилировать и развертывать заново
    1
    . Это обстоятельство создает негативную нагрузку на класс
    Error
    . Про- граммистам не хочется добавлять новые ошибки, чтобы не создавать себе проблем со сборкой и развертыванием . Соответственно, вместо добавления новых кодов ошибок они предпочитают использовать старые .
    Если вместо кодов ошибок использовать исключения, то новые исключения определяются производными от класса исключения . Их включение в программу не требует перекомпиляции или повторного развертывания
    2
    не повторяйтесь
    3
    Внимательно присмотревшись к листингу 3 .1,
    можно заметить, что один из алгоритмов по- вторяется в нем четыре раза: по одному разу для
    SetUp
    ,
    SuiteSetUp
    ,
    TearDown и
    SuiteTearDown
    Обнаружить это дублирование нелегко, по- тому что четыре вхождения алгоритма пере- мешаны с другим кодом, а в дублировании фрагментов имеются некоторые различия . Тем не менее дублирование создает проблемы, по- тому что оно увеличивает объем кода, а при изменении алгоритма вам придется вносить изменения сразу в четырех местах . Также вчетверо возрастает вероят- ность ошибки .
    1
    Люди, считавшие, что они смогут обойтись без перекомпиляции и повторного разверты- вания, были пойманы и сурово наказаны .
    2
    Пример принципа открытости/закрытости (OCP) [PPP02] .
    3
    Принцип DRY [PRAG] .
    73

    74
    Глава 3 . Функции
    В листинге 3 .7 дублирование устраняется при помощи метода include
    . Снова про- читайте код и обратите внимание, насколько проще читается весь модуль после устранения дублирования .
    Дублирование иногда считается корнем всего зла в программировании . Было создано много приемов и методологий, направленных на контроль и устране- ние дублирования . Возьмем хотя бы нормальные формы баз данных Кодда, пред назначенные для устранения дубликатов в данных . Или другой пример: объектно-ориентированные языки помогают сконцентрировать в базовых классах код, который в других обстоятельствах мог бы дублироваться в раз- ных местах . Структурное программирование, аспектно-ориентированное про- граммирование, компонентно-ориентированное программирование — все эти технологии отчасти являются стратегиями борьбы с дублированием . Похоже, с момента изобретения подпрограмм все новшества в разработке программного обеспечения были направлены исключительно на борьбу с дублированием в ис- ходном коде .
    Структурное программирование
    Некоторые программисты следуют правилам структурного программирования, изложенным Эдгаром Дейкстрой [SP72] . Дейкстра считает, что каждая функция и каждый блок внутри функции должны иметь одну точку входа и одну точку выхода . Выполнение этого правила означает, что функция должна содержать только одну команду return
    , в циклах не должны использоваться команды break или continue
    , а команды goto не должны использоваться никогда и ни при каких условиях .
    Хотя мы с симпатией относимся к целям и методам структурного программиро- вания, в очень компактных функциях эти правила не приносят особой пользы .
    Только при увеличении объема функций их соблюдение обеспечивает суще- ственный эффект .
    Итак, если ваши функции остаются очень компактными, редкие вкрапления множественных return
    , команд break и continue не принесут вреда, а иногда даже повышают выразительность по сравнению с классической реализацией с одной точкой входа и одной точкой выхода . С другой стороны, команда goto имеет смысл только в больших функциях, поэтому ее следует избегать .
    Как научиться писать такие функции?
    Написание программ сродни любому другому виду письменной работы . Когда вы пишете статью или доклад, вы сначала излагаете свои мысли, а затем «причесы- ваете» их до тех пор, пока они не будут хорошо читаться . Первый вариант может
    74

    Завершение
    75
    быть неуклюжим и нелогичным; вы переделываете, дополняете и уточняете его, пока он не будет читаться так, как вам хочется .
    Когда я пишу свои функции, они получаются длинными и сложными . В них встречаются многоуровневые отступы и вложенные циклы . Они имеют длинные списки аргументов . Имена выбираются хаотично, а в коде присутствуют дубли- каты . Но у меня также имеется пакет модульных тестов для всех этих неуклюжих строк до последней .
    Итак, я начинаю «причесывать» и уточнять свой код, выделять новые функции, изменять имена и устранять дубликаты . Я сокращаю методы и переупорядочиваю их . Иногда приходится ломать целые классы, но при этом слежу за тем, чтобы все тесты выполнялись успешно .
    В конечном итоге у меня остаются функции, построенные по правилам, изложен- ным в этой главе . Я не записываю их так с самого начала . И вообще не думаю, что кому-нибудь это под силу .
    Завершение
    Каждая система строится в контексте языка, отражающего специфику пред- метной области и разработанного программистами для описания этой системы .
    В этом языке функции играют роль глаголов, а классы — существительных . Не стоит полагать, что мы возвращаемся к кошмарной древней практике, по которой существительные и глаголы в документе с требованиями становились первыми кандидатами для классов и функций системы . Скорее речь идет о гораздо более древней истине . Искусство программирования является (и всегда было) искус- ством языкового проектирования .
    Опытные программисты рассматривают систему как историю, которую они должны рассказать, а не как программу, которую нужно написать . Они исполь- зуют средства выбранного ими языка программирования для конструирования гораздо более богатого и выразительного языка, подходящего для этого пове- ствования . Частью этого предметно-ориентированного языка является иерархия функций, которые описывают все действия, выполняемые в рамках системы .
    В результате искусной рекурсии эти действия формулируются на том самом предметно-ориентированном языке, который они определяют для изложения своей маленькой части истории .
    Эта глава была посвящена механике качественного написания функций . Если вы будете следовать этим правилам, ваши функции будут короткими, удачно названными и хорошо организованными . Но никогда не забывайте, что ваша настоящая цель — «рассказать историю» системы, а написанные вами функции должны четко складываться в понятный и точный язык, который поможет вам в этом .
    75

    76
    Глава 3 . Функции
    листинг 3 .7 . SetupTeardownIncluder.java package fitnesse.html;
    import fitnesse.responders.run.SuiteResponder;
    import fitnesse.wiki.*;
    public class SetupTeardownIncluder {
    private PageData pageData;
    private boolean isSuite;
    private WikiPage testPage;
    private StringBuffer newPageContent;
    private PageCrawler pageCrawler;
    public static String render(PageData pageData) throws Exception {
    return render(pageData, false);
    }
    public static String render(PageData pageData, boolean isSuite)
    throws Exception {
    return new SetupTeardownIncluder(pageData).render(isSuite);
    }
    private SetupTeardownIncluder(PageData pageData) {
    this.pageData = pageData;
    testPage = pageData.getWikiPage();
    pageCrawler = testPage.getPageCrawler();
    newPageContent = new StringBuffer();
    }
    private String render(boolean isSuite) throws Exception {
    this.isSuite = isSuite;
    if (isTestPage())
    includeSetupAndTeardownPages();
    return pageData.getHtml();
    }
    private boolean isTestPage() throws Exception {
    return pageData.hasAttribute("Test");
    }
    private void includeSetupAndTeardownPages() throws Exception {
    includeSetupPages();
    includePageContent();
    includeTeardownPages();
    updatePageContent();
    }
    private void includeSetupPages() throws Exception {
    if (isSuite)
    includeSuiteSetupPage();
    includeSetupPage();
    }
    76

    Завершение
    77
    private void includeSuiteSetupPage() throws Exception {
    include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
    }
    private void includeSetupPage() throws Exception {
    include("SetUp", "-setup");
    }
    private void includePageContent() throws Exception {
    newPageContent.append(pageData.getContent());
    }
    private void includeTeardownPages() throws Exception {
    includeTeardownPage();
    if (isSuite)
    includeSuiteTeardownPage();
    }
    private void includeTeardownPage() throws Exception {
    include("TearDown", "-teardown");
    }
    private void includeSuiteTeardownPage() throws Exception {
    include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
    }
    private void updatePageContent() throws Exception {
    pageData.setContent(newPageContent.toString());
    }
    private void include(String pageName, String arg) throws Exception {
    WikiPage inheritedPage = findInheritedPage(pageName);
    if (inheritedPage != null) {
    String pagePathName = getPathNameForPage(inheritedPage);
    buildIncludeDirective(pagePathName, arg);
    }
    }
    private WikiPage findInheritedPage(String pageName) throws Exception {
    return PageCrawlerImpl.getInheritedPage(pageName, testPage);
    }
    private String getPathNameForPage(WikiPage page) throws Exception {
    WikiPagePath pagePath = pageCrawler.getFullPath(page);
    return PathParser.render(pagePath);
    }
    private void buildIncludeDirective(String pagePathName, String arg) {
    newPageContent
    .append("\n!include ")
    .append(arg)
    .append(" .")
    продолжение
    77

    78
    Глава 3 . Функции
    листинг 3 .7 (продолжение)
    .append(pagePathName)
    .append("\n");
    }
    }
    литература
    [KP78]: Kernighan and Plaugher, The Elements of Programming Style, 2d . ed .,
    McGraw-Hill, 1978 .
    [PPP02]: Robert C . Martin, Agile Software Development: Principles, Patterns, and
    Practices, Prentice Hall, 2002 .
    [GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al ., Addison-Wesley, 1996 .
    [PRAG]: The Pragmatic Programmer, Andrew Hunt, Dave Thomas, Addison-Wesley,
    2000 .
    [SP72]: Structured Programming, O .-J . Dahl, E . W . Dijkstra, C . A . R . Hoare, Aca- demic Press, London, 1972 .
    78

    Комментарии
    Не комментируйте плохой код — перепишите его .
    Брайан У. Керниган и П. Дж. Плауэр
    1
    Ничто не помогает так, как уместный комментарий . Ничто не загромождает модуль так, как бессодержательные и безапелляционные комментарии . Ничто не приносит столько вреда, как старый, утративший актуальность комментарий, распространяющий ложь и дезинформацию .
    Комментарии — не список Шиндлера . Не стоит относиться к ним как к «абсолют- ному добру» . На самом деле комментарии в лучшем случае являются неизбеж- ным злом . Если бы языки программирования были достаточно выразительными или если бы мы умели искусно пользоваться этими языками для выражения
    1
    [KP78], p . 144 .
    4
    79

    80
    Глава 4 . Комментарии своих намерений, то потребность в комментариях резко снизилась бы, а может, и вовсе сошла «на нет» .
    Грамотное применение комментариев должно компенсировать нашу неудачу в выражении своих мыслей в коде . Обратите внимание на слово «неудачу» .
    Я абсолютно серьезно . Комментарий — всегда признак неудачи . Мы вынужде- ны использовать комментарии, потому что нам не всегда удается выразить свои мысли без них, однако гордиться здесь нечем .
    Итак, вы оказались в ситуации, в которой необходимо написать комментарий?
    Хорошенько подумайте, нельзя ли пойти по другому пути и выразить свои на- мерения в коде . Каждый раз, когда вам удается это сделать, — похлопайте себя по плечу . Каждый раз, когда вы пишете комментарий, — поморщитесь и ощутите свою неудачу .
    Почему я так настроен против комментариев? Потому что они лгут . Не всегда и не преднамеренно, но это происходит слишком часто . Чем древнее коммен- тарий, чем дальше он расположен от описываемого им кода, тем больше веро- ятность того, что он просто неверен . Причина проста: программисты не могут нормально сопровождать комментарии .
    Программный код изменяется и эволюционирует . Его фрагменты перемещаются из одного места в другое, раздваиваются, размножаются и сливаются . К сожале- нию, комментарии не всегда сопровождают их — и не всегда могут сопровождать их . Слишком часто комментарии отделяются от описываемого ими кода и пре- вращаются в пометки непонятной принадлежности, с постоянно снижающейся точностью . Посмотрите, что произошло с этим комментарием и той строкой, которую он должен описывать:
    MockRequest request;
    private final String HTTP_DATE_REGEXP =
    "[SMTWF][a-z]{2}\\,\\s[0-9]{2}\\s[JFMASOND][a-z]{2}\\s"+
    "[0-9]{4}\\s[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}\\sGMT";
    private Response response;
    private FitNesseContext context;
    private FileResponder responder;
    private Locale saveLocale;
    // Пример: "Tue, 02 Apr 2003 22:18:49 GMT"
    Другие переменные экземпляра (вероятно, добавленные позднее) вклинились между константой
    HTTP_DATE_REGEXP
    и пояснительным комментарием .
    На это можно возразить, что программисты должны быть достаточно дисципли- нированными, чтобы поддерживать в своем коде актуальные, точные и релевант- ные комментарии . Согласен, должны . Но я бы предпочел, чтобы вместо этого программист постарался сделать свой код настолько четким и выразительным, чтобы комментарии были попросту не нужны .
    Неточные комментарии гораздо вреднее, чем полное отсутствие комментариев .
    Они обманывают и сбивают с толку . Они создают у программиста невыполнимые ожидания . Они устанавливают устаревшие правила, которые не могут (или не должны) соблюдаться в будущем .
    80

    Хорошие комментарии
    1   ...   6   7   8   9   10   11   12   13   ...   49


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