Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 23 Отладка 549 Общий подход к отладке Рассматриваете ли вы отладку как возможность лучше изучить программу, ошибки, качество кода и подход к решению проблем? Избегаете ли вы суеверного подхода к отладке, основанного на методе проб и ошибок? Полагаете ли вы, что ошибки допущены именно вами? Используете ли вы научный метод для стабилизации несистематических ошибок? Используете ли вы научный метод для нахождения дефектов? Используете ли вы несколько методик поиска дефектов? Проверяете ли вы корректность исправлений? Анализируете ли вы предупреждения компилятора? Используете ли вы ин- струменты профилирования выполнения программы, среды тестирования, леса и интерактивные отладчики? Дополнительные ресурсы Ниже я привел список книг, посвященных отладке. Agans, David J. Debugging: The Nine Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems. Amacom, 2003. В этой книге рассматриваются общие принципы отладки, не зави- сящие ни от языка, ни от среды. Myers, Glenford J. The Art of Software Testing. New York, NY: John Wiley & Sons, 1979. Седьмая глава этой классической книги посвящена отладке. Allen, Eric. Bug Patterns In Java. Berkeley, CA: Apress, 2002. В данной книге описывает- ся методика отладки программ Java, концептуально очень похожая на описанную в этой главе. Как и здесь, в ней рассмотрен «Научный метод отладки», проведено различие между отладкой и тестированием и определены частые ошибки. Названия двух следующих книг могут навести на мысль о том, что они адресова- ны только разработчикам программ для Microsoft Windows и .NET, однако это не так: помимо всего прочего, в них вы найдете обсуждение отладки в целом, сове- ты по использованию утверждений, а также описания методик кодирования, по- могающих предотвращать ошибки. Robbins, John. Debugging Applications for Microsoft .NET and Microsoft Windows. Red- mond, WA: Microsoft Press, 2003 1 McKay, Everett N. and Mike Woodring. Debugging Windows Programs: Strategies, Tools, and Techniques for Visual C++ Programmers. Boston, MA: Addison-Wesley, 2000. http://cc2e.com/2375 1 Роббинс Дж. Отладка приложений для Microsoft .NET и Microsoft Windows. — М.: Русская Редак- ция, 2004. — Прим. перев. 550 ЧАСТЬ V Усовершенствование кода Ключевые моменты 쐽 Отладка — это тот этап разработки программы, от которого зависит возмож- ность ее выпуска. Конечно, лучше всего вообще избегать ошибок, используя другие методики, описанные в этой книге. Однако потратить время на улучшение на- выков отладки все же стоит, потому что эффективность отладки, выполняемой лучшими и худшими программистами, различается минимум в 10 раз. 쐽 Систематичный подход к поиску и исправлению ошибок — непременное ус- ловие успешности отладки. Организуйте отладку так, чтобы каждый тест при- ближал вас к цели. Используйте Научный Метод Отладки. 쐽 Прежде чем приступать к исправлению программы, поймите суть проблемы. Случайные предположения о причинах ошибок и случайные исправления толь- ко ухудшат программу. 쐽 Установите в настройках компилятора самый строгий уровень диагностики и устраняйте причины всех ошибок и предупреждений. Как вы исправите неуло- вимые ошибки, если будете игнорировать явные? 쐽 Инструменты отладки значительно облегчают разработку ПО. Найдите их и используйте, но не забывайте, что у вас есть еще и голова. ГЛАВА 23 Отладка 551 Г Л А В А 2 4 Рефакторинг Содержание 쐽 24.1. Виды эволюции ПО 쐽 24.2. Введение в рефакторинг 쐽 24.3. Отдельные виды рефакторинга 쐽 24.4. Безопасный рефакторинг 쐽 24.5. Стратегии рефакторинга Связанные темы 쐽 Советы по устранению дефектов: раздел 23.3 쐽 Подход к оптимизации кода: раздел 25.6 쐽 Проектирование при конструировании: глава 5 쐽 Классы: глава 6 쐽 Высококачественные методы: глава 7 쐽 Совместное конструирование: глава 21 쐽 Тестирование, выполняемое разработчиками: глава 22 쐽 Области вероятных изменений: подраздел «Определите области вероятных изменений» раздела 5.3 Миф: в начале реализации программного проекта проводит- ся методичная выработка требований, и составляется устой- чивый список аспектов ответственности программы. Про- ектирование соответствует требованиям и выполняется со всей тщательностью, благодаря чему кодирование от начала до конца протекает линейно: вы пишете код, тестируете его и оставляете в покое. Согласно этому мифу значительные изменения кода возможны только при сопровождении ПО, т. е. после выпуска первоначальной версии системы. Реальность: во время разработки первоначальной версии системы в код вносятся значительные изменения. Многие из изменений во время ко- дирования не менее масштабны, чем изменения, характерные для стадии сопровождения программы. В зависимости от размера проекта кодирование, от- ладка и блочное тестирование обычно составляют от 30 до 65% общего объема http://cc2e.com/2436 Все удачные программы изме- няются. Фред Брукс (Fred Brooks) 552 ЧАСТЬ V Усовершенствование кода работы над проектом (см. главу 27). Если бы кодирование и блочное тестирова- ние были линейными однократными процессами, они составляли бы не более 20– 30% общего объема работы. Однако даже в хорошо управляемых проектах требо- вания изменяются примерно на 1–4% в месяц (Jones, 2000). Изменения требова- ний неизбежно приводят к изменениям кода, порой весьма существенным. Другая реальность: современные методики разработки предполагают больший масштаб изменений кода во время конструирования. Целью более старых методик (достигалась она или нет — другой вопрос) было предотвращение изменений кода. Современные подходы снижают предсказуемость кодирования. Они в большей степени ориентированы на код, поэтому вы вполне можете ожидать, что на протяжении жизненного цикла проекта код будет изме- няться сильнее, чем когда бы то ни было. 24.1. Виды эволюции ПО Эволюция ПО похожа на биологическую эволюцию тем, что лишь немногие му- тации выгодны. При здоровой эволюции кода его развитие напоминает превра- щение обезьян в неандертальцев, а неандертальцев — в программистов, которые, как известно, являются самыми высокоразвитыми существами на Земле. Однако иногда эволюционные силы проявляют себя иным образом, ввергая программу в спираль деградации. Главное различие между видами эволюции программы в том, повышает- ся или снижается ее качество в результате изменений. Если при исправ- лении ошибок вы опираетесь на суеверия и устраняете лишь симптомы проблем, качество снижается. Если же вы рассматриваете изменения как возмож- ности улучшить первоначальный проект программы, — повышается. При сниже- нии качества программа начинает походить на молчащую канарейку в шахте, о которой я уже говорил. Это знак того, что программа развивается в неверном на- правлении. Характер эволюции программы зависит и от того, когда в нее вносятся измене- ния: во время конструирования или во время сопровождения. Изменения во вре- мя конструирования обычно вносят первоначальные разработчики, которые еще хорошо помнят код программы. Система еще не используется, поэтому програм- мисты испытывают давление только со стороны графика разработки, а не сотен сердитых пользователей, возмущенных неработоспособностью системы. По той же причине изменения во время конструирования можно вносить более свобод- но: система находится в более динамичном состоянии, а следствия ошибок ме- нее серьезны. Из этого следует, что во время разработки ПО эволюционирует не так, как при сопровождении. Философия эволюции ПО Слабость многих подходов к эволюции ПО объясняется тем, что она пускается на самотек. Осознав, что эволюция ПО во время его разработки — неизбежный и важный процесс и спланировав его, вы сможете извлечь из него выгоду. ГЛАВА 24 Рефакторинг 553 Эволюция заключает в себе и опасность, и возможность приближения к совершенству. При необходимости измене- ния кода старайтесь улучшить его, чтобы облегчить внесе- ние изменений в будущем. В процессе написания програм- мы вы всегда узнаете о ней что-то новое. Получив возмож- ность изменения программы, используйте то, что вы узна- ли, для ее улучшения. Пишите первоначальный код и его изменяйте, держа в уме дальнейшие изменения. Главное Правило Эволюции ПО состоит в том, что эволюция должна повышать внутреннее качество программы. О том, как этого добиться, я расскажу в следующих разделах. 24.2. Введение в рефакторинг Важнейшей стратегией достижения цели Главного Правила Эволюции ПО явля- ется рефакторинг, который Мартин Фаулер определяет как «изменение внутрен- ней структуры ПО без изменения его наблюдаемого поведения, призванное об- легчить его понимание и удешевить модификацию» (Fowler, 1999). Слово «рефак- торинг» возникло из слова «факторинг», которое изначально использовал в кон- тексте структурного программирования Ларри Константайн, назвавший так мак- симально возможную декомпозицию программы на составляющие части (Yourdon and Constantine, 1979). Разумные причины выполнения рефакторинга Иногда код деградирует при сопровождении, а иногда он изначально имеет не- высокое качество. В обоих случаях на это — и на необходимость рефакторинга — указывают некоторые предупреждающие знаки, иногда называемые «запахами» (smells) (Fowler, 1999). Они описаны ниже. Код повторяется Повторение кода почти всегда говорит о неполной факто- ризации системы на этапе проектирования. Повторение кода заставляет парал- лельно изменять сразу несколько фрагментов программы и нарушает правило, ко- торое Эндрю Хант и Дэйв Томас назвали «принципом DRY»: Don’t Repeat Yourself (не повторяйтесь) (Hunt and Thomas, 2000). Думаю, лучше всех это правило сфор- мулировал Дэвид Парнас: «Копирование и вставка кода — следствие ошибки про- ектирования» (McConnell, 1998b). Метод слишком велик В объектно-ориентированном программировании ме- тоды, не умещающиеся на экране монитора, требуются редко и обычно свидетель- ствуют о попытке насильно втиснуть ногу структурного программирования в объектно-ориентированный ботинок. Одному из моих клиентов поручили разделить самый объемный метод унаследо- ванной системы, включающий более 12 000 строк. Приложив немалые усилия, он смог уменьшить объем этого метода только примерно до 4000 строк. Одним из способов улучшения системы является повышение ее модульности — увеличение числа хорошо определенных и удачно названных методов, успешно решающих только одну задачу. Если обстоятельства заставляют вас пересмотреть Не бывает кода, настолько гро- моздкого, изощренного или сложного, чтобы его нельзя было ухудшить при сопровождении. Джеральд Вайнберг (Gerald Weinberg) 554 ЧАСТЬ V Усовершенствование кода фрагмент кода, используйте эту возможность для проверки модульности методов, содержащихся в этом фрагменте. Если вам кажется, что после разделения одного метода на несколько код станет яснее, создайте дополнительные методы. Цикл слишком велик или слишком глубоко вложен в другие циклы Подхо- дящим кандидатом на преобразование в метод часто оказывается тело цикла — это помогает лучше факторизовать код и снизить сложность цикла. Класс имеет плохую связность Если класс имеет множество никак не связан- ных аспектов ответственности, разбейте его на несколько классов, так чтобы каж- дый из них получил связный набор аспектов. Интерфейс класса не формирует согласованную абстракцию Даже клас- сы, получившие при рождении связный интерфейс, могут терять первоначальную согласованность. В результате необдуманных изменений, повышающих удобство использования класса за счет целостности его интерфейса, интерфейс иногда становится монстром, не поддающимся сопровождению и не улучшающим интел- лектуальную управляемость программы. Метод принимает слишком много параметров Как правило, хорошо фак- торизованные программы включают много небольших хорошо определенных методов, не нуждающихся в большом числе параметров. Длинный список пара- метров — свидетельство того, что абстракция, формируемая интерфейсом мето- да, неудачна. Отдельные части класса изменяются независимо от других частей Иногда класс имеет две (или более) разных области ответственности. Если это так, вы заметите, что вы изменяете или одну часть класса, или другую, и лишь немно- гие изменения затрагивают обе части класса. Это признак того, что класс следует разделить на несколько классов в соответствии с отдельными областями ответ- ственности. При изменении программы требуется параллельно изменять несколько классов Мне известен один проект, в котором был составлен контрольный список где-то из 15 классов, требующих изменения при добавлении нового вида выход- ных данных. Если вы уже в который раз изменяете один и тот же набор классов, подумайте, можно ли реорганизовать код этих классов так, чтобы изменения за- трагивали только один класс. Опыт говорит мне, что этого идеала достичь нелег- ко, но стремиться к нему нужно. Вам приходится параллельно изменять несколько иерархий наследования Если при создании каждого нового подкласса одного класса вам приходится со- здавать подкласс другого класса, вы имеете дело с особым видом параллельного изменения. Решите эту проблему. Вам приходится параллельно изменять несколько блоков case В самих по себе блоках case ничего плохого, но, если вы параллельно изменяете похожие блоки case в нескольких частях программы, спросите себя, не лучше ли использовать наследование. Родственные элементы данных, используемые вместе, не организованы в классы Если вы неоднократно используете один и тот же набор элементов дан- ных, рассмотрите целесообразность объединения этих данных и выполняемых над ними операций в отдельный класс. ГЛАВА 24 Рефакторинг 555 Метод использует больше элементов другого класса, чем своего собст- венного Это значит, что метод нужно переместить в другой класс и вызывать из старого класса. Элементарный тип данных перегружен Элементарные типы данных могут представлять бесконечное число сущностей реального мира. Если вы собираетесь представить распространенную сущность — скажем, денежную сумму — целочис- ленным или другим элементарным типом данных, подумайте: не создать ли вме- сто этого простой класс Money, чтобы компилятор мог выполнять контроль ти- пов объектов Money, дабы можно было проверять значения, присваиваемые этим объектам, и т. д. Если и Money, и Temperature будут представлены целыми числа- ми, компилятор не сможет предупредить вас об ошибочных операциях вида bank- Balance = recordLowTemperature. Класс имеет слишком ограниченную функциональность Иногда рефакто- ринг приводит к сокращению функциональности класса. Если класс не соответ- ствует своему званию, спросите себя, не удалить ли его вообще, распределив все аспекты его ответственности между другими классами. По цепи методов передаются бродячие данные Данные, передаваемые в ме- тод лишь затем, чтобы он передал их другому методу, называются «бродячими» (tramp data) (Page-Jones, 1988). Это не всегда плохо, но в любом случае спросите себя, согласуется ли передача конкретных данных с абстракцией, формируемой интерфейсом каждого из методов. Если с абстракциями интерфейсов порядок, с передачей данных тоже все в норме. Если нет, найдите способ, позволяющий улуч- шить согласованность интерфейса каждого метода. Объект-посредник ничего не делает Если роль класса сводится к перена- правлению вызовов методов в другие классы, подумайте, не устранить ли его и вызывать другие классы непосредственно. Один класс слишком много знает о другом классе Инкапсуляция (сокрытие информации) — наверное, самый эффективный способ улучшения интеллекту- альной управляемости программ и минимизации волновых эффектов изменений кода. Увидев, что один класс знает о другом больше, чем следует (это относится и к производным классам, знающим слишком много о своих предках), постарай- тесь сделать инкапсуляцию более строгой. Метод имеет неудачное имя Если методу присвоено плохое имя, измените определение метода, исправьте все его вызовы и перекомпилируйте программу. Какой бы трудной эта задача ни была сейчас, потом она станет еще труднее — поэтому, обнаружив проблему, устраните ее как можно быстрее. Данные-члены сделаны открытыми Мне кажется, что предоставление откры- того доступа к данным-членам не бывает разумным решением. Это стирает грань между интерфейсом и реализацией, неизбежно нарушает инкапсуляцию и огра- ничивает гибкость программы. Непременно подумайте над сокрытием открытых данных-членов при помощи методов доступа. Подкласс использует только малую долю методов своих предков Обыч- но такая ситуация возникает, когда подкласс создается потому, что базовый класс по чистой случайности содержит нужные ему методы, а не потому, что подкласс логически является потомком базового класса. Попробуйте достичь лучшей ин- 556 ЧАСТЬ V Усовершенствование кода капсуляции, изменив характер отношений между подклассом и базовым классом с «является» на «содержит»: преобразуйте базовый класс в данные-члены бывше- го подкласса и откройте доступ только к тем методам бывшего подкласса, кото- рые по-настоящему нужны. Сложный код объясняется при помощи комментариев В важности коммен- тариев трудно усомниться, но их не следует использовать для объяснения плохо- го кода. Старая мудрость гласит: «Не документируйте плохой код — перепишите его» (Kernighan and Plauger, 1978). Код содержит глобальные переменные При пересмотре фрагмента, в котором используются глобальные перемен- ные, изучите его получше. Возможно, за время, прошедшее с тех пор, как вы изучали этот фрагмент, вам пришел в го- лову способ устранения глобальных переменных. За это время вы уже забыли кое-какие аспекты кода — сейчас ис- пользование глобальных переменных может показаться вам довольно запутанным, чтобы вы сочли нужным изобретение более ясного подхода. Возможно, сейчас вы лучше представляете, как изолиро- вать глобальные переменные при помощи методов доступа и как пострадает про- грамма, если оставить все как есть. Соберитесь с силами и внесите в код выгод- ные изменения. Написание первоначального кода ушло в достаточно далекое прошлое, чтобы вы могли объективно отнестись к своей работе, но не настолько далекое, чтобы вы не смогли вспомнить все, что нужно для внесения правильных исправлений. Ранние ревизии кода — прекрасная возможность его улучшить. Перед вызовом метода выполняется подготовительный код (после вызова метода выполняется код «уборки») Подобный код должен вызывать у вас по- дозрение: Пример кода подготовки к вызову метода и кода уборки — плохой код (C++) Подготовительный код — дурной знак. WithdrawalTransaction withdrawal; withdrawal.SetCustomerId( customerId ); withdrawal.SetBalance( balance ); withdrawal.SetWithdrawalAmount( withdrawalAmount ); withdrawal.SetWithdrawalDate( withdrawalDate ); ProcessWithdrawal( withdrawal ); Код уборки — еще один дурной знак. customerId = withdrawal.GetCustomerId(); balance = withdrawal.GetBalance(); withdrawalAmount = withdrawal.GetWithdrawalAmount(); withdrawalDate = withdrawal.GetWithdrawalDate(); Похожий признак плохого кода — наличие специального конструктора, прини- мающего подмножество нормальных данных инициализации и нужного для на- писания чего-нибудь вроде: Перекрестная ссылка О глобаль- ных переменных см. раздел 13.3. О различиях между гло- бальными данными и данными класса см. подраздел «Ошибоч- ное представление о данных класса как о глобальных дан- ных» раздела 5.3. > > |