Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
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 |