Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 31 Форматирование и стиль 739 totalBill = totalBill + customerPurchases[ customerID ] + SalesTax( customerPurchases[ customerID ] ); Хотя такой стиль не приведет к появлению синтаксической ошибки при повиса# нии операторов && или +, он сильно упрощает поиск операторов по левой гра# нице столбца — там, где текст выровнен, а не по правой — где край текста неров# ный. Его дополнительное преимущество в том, что он позволяет пояснить струк# туру операций, как показано в листинге 31#39. Листинг 31-39. Пример стиля, поясняющего смысл сложной операции (Java) totalBill = totalBill + customerPurchases[ customerID ] + CitySalesTax( customerPurchases[ customerID ] ) + StateSalesTax( customerPurchases[ customerID ] ) + FootballStadiumTax() SalesTaxExemption( customerPurchases[ customerID ] ); Располагайте сильно связанные элементы вместе Разбивая строку на части, оставляйте рядом взаимосвязанные элементы: обращения к массиву, аргументы ме# тода и т. д. Пример, приведенный в листинге 31#40, демонстрирует плохой вариант: Листинг 31-40. Пример неправильного разбиения строки (Java) customerBill = PreviousBalance( paymentHistory[ customerID ] ) + LateCharge( paymentHistory[ customerID ] ); Надо признать, что такой разрыв строки соответствует принципу подчеркивания очевидности незавершенных выражений, но это сделано так, что надобности зат# рудняет чтение выражения. Можно придумать ситуации, когда такое разбиение будет оправданным, но это не тот случай. Лучше оставить все обращения к мас# сиву на одной строке. Листинг 31#41 показывает лучшее форматирование: Листинг 31-41. Пример правильного разбиения строки (Java) customerBill = PreviousBalance( paymentHistory[ customerID ] ) + LateCharge( paymentHistory[ customerID ] ); При переносе строк в вызове метода используйте отступ стандартно' го размера Если в циклах и условных выражениях вы обычно делаете отступ в три пробела, то при переносе строки в вызове метода тоже сделайте отступ в три пробела. Листинг 31#42 содержит несколько примеров: Листинг 31-42. Примеры переноса строк в вызовах методов с применением стандартного размера отступов (Java) DrawLine( window.north, window.south, window.east, window.west, currentWidth, currentAttribute ); SetFontAttributes( faceName[ fontId ], size[ fontId ], bold[ fontId ], italic[ fontId ], syntheticAttribute[ fontId ].underline, syntheticAttribute[ fontId ].strikeout ); 740 ЧАСТЬ VII Мастерство программирования Одной из альтернатив такому подходу служит выравнивание перенесенных строк под первым аргументом, как показано в листинге 31#43: Листинг 31-43. Пример отступов при переносе строк в вызове методов, позволяющих выделить имена методов (Java) DrawLine( window.north, window.south, window.east, window.west, currentWidth, currentAttribute ); SetFontAttributes( faceName[ fontId ], size[ fontId ], bold[ fontId ], italic[ fontId ], syntheticAttribute[ fontId ].underline, syntheticAttribute[ fontId ].strikeout ); С эстетической точки зрения, в сравнении с первым вариантом этот код выгля# дит несколько неровно. Кроме того, этот подход тяжело сопровождать, так как имена методов и аргументов могут изменяться. Большинство программистов со временем склоняются к использованию первого стиля. Упростите поиск конца строки с продолжением Одна из проблем пока# занного выше подхода — сложность поиска конца каждой строки. Альтернативой может служить размещение каждого элемента на отдельной строке и выделение окончания всей группы с помощью закрывающей скобки (листинг 31#44). Листинг 31-44. Пример форматирования переноса строк в вызовах методов, в котором каждый аргумент размещается на отдельной строке (Java) DrawLine( window.north, window.south, window.east, window.west, currentWidth, currentAttribute ); SetFontAttributes( faceName[ fontId ], size[ fontId ], bold[ fontId ], italic[ fontId ], syntheticAttribute[ fontId ].underline, syntheticAttribute[ fontId ].strikeout ); Ясно, что такой подход требует много места. Однако если аргументами функции являются длинные названия полей объектов или имена указателей, например, такие, как два последних в приведенном примере, размещение одного аргумента в строке существенно улучшает читаемость. Знаки ); в конце блока делают заметным его окончание. Вам также не придется переформатировать код при добавлении па# раметра — вы просто добавите новую строку. На практике только небольшая часть методов нуждается в разбиении на строки. Остальные можно располагать на одной строке. Любой из трех вариантов фор# ГЛАВА 31 Форматирование и стиль 741 матирования многострочных вызовов методов работает хорошо при последова# тельном применении. При переносе строк в управляющем выражении делайте отступ стандар' тного размера При нехватке места для цикла for, цикла while или оператора if, перенося строку, сделайте такой же отступ, как и в выражениях внутри цикла или после условия if. В листинге 31#45 приведены два примера: Листинг 31-45. Примеры отступов при переносе строк в управляющих выражениях (Java) while ( ( pathName[ startPath + position ] != ‘;’ ) && В продолжении строки отступ содержит стандартное количество пробелов... ( ( startPath + position ) <= pathName.length() ) ) { } for ( int employeeNum = employee.first + employee.offset; ...так же, как и в этом случае. employeeNum < employee.first + employee.offset + employee.total; employeeNum++ ) { } Приведенный код соответствует критериям, установленным ранее в этой главе. Перенос строки выполняется логично — в нем всегда есть отступ относительно выражения, кото# рое он продолжает. Дальнейшее выравнивание может вы# полняться как обычно — новая строка занимает всего на несколько пробелов больше, чем исходный вариант. Он так же читаем, как и любой другой код, и его так же просто сопровождать. В некоторых случаях вы могли бы повысить удобочитаемость, на# строив отступы или пробелы, но, рассматривая вопросы тонкой настройки, не забывайте о компромиссе с точки зрения удобства сопровождения. Не выравнивайте правые части выражений присваивания В первом из# дании этой книги я рекомендовал выравнивать правые части выражений, содер# жащие присваивания (листинг 31#46): Листинг 31-46. Примеры форматирования в конце строки, используемого при выравнивании выражений присваивания, — плохая практика (Java) customerPurchases = customerPurchases + CustomerSales( CustomerID ); customerBill = customerBill + customerPurchases; totalCustomerBill = customerBill + PreviousBalance( customerID ) + LateCharge( customerID ); customerRating = Rating( customerID, totalCustomerBill ); Перекрестная ссылка Иногда для сложных условий лучше всего поместить их в логичес- кие функции (см. подраздел «Упрощение сложных выраже- ний» раздела 19.1). > > 742 ЧАСТЬ VII Мастерство программирования С высоты 10#летнего опыта могу сказать, что, хотя этот стиль может выглядеть привлекательно, он превращается в настоящую головную боль, когда приходится поддерживать выравнивание знаков равенства при изменении имен переменных и прогоне кода через утилиты, заменяющие пробелы знаками табуляции, а знаки табуляции — пробелами. Кроме того, его тяжело сопровождать при перемещении строк между частями программы, в которых применяются разные размеры отступов. Для обеспечения единообразия с другими принципами отступов, а также учиты# вая вопросы удобства сопровождения, обращайтесь с группами выражений, со# держащими операции присваивания, так же, как вы бы обращались с любыми другими выражениями (листинг 31#47): Листинг 31-47. Пример использования стандартных отступов для выравнивания выражений присваивания — хорошая практика (Java) customerPurchases = customerPurchases + CustomerSales( CustomerID ); customerBill = customerBill + customerPurchases; totalCustomerBill = customerBill + PreviousBalance( customerID ) + LateCharge( customerID ); customerRating = Rating( customerID, totalCustomerBill ); При переносе строк в выражениях присваивания применяйте отступы стандартного размера В листинге 31#47 при переносе строки в третьем при# сваивании используется стандартный размер отступа. Это сделано с той же це# лью, что и отказ от специального форматирования выражений присваивания: из общих соображений читаемости и удобства сопровождения. Размещение одного оператора на строке Современные языки, такие как C++ и Java, позволяют располагать несколько опе# раторов на одной строке. Однако когда дело касается этого вопроса, мощь сво# бодного форматирования оборачивается палкой о двух концах. Следующая стро# ка содержит несколько выражений, которые, с точки зрения логики, вполне мо# гут располагаться в отдельных строках: i = 0; j = 0; k = 0; DestroyBadLoopNames( i, j, k ); Аргументом в защиту размещения нескольких выражений на одной строке может служить факт, что в этом случае требуется меньшее число строк экранного про# странства или бумаги для распечатки, что позволяет одновременно видеть боль# ший объем кода. Это также позволяет сгруппировать взаимосвязанные выраже# ния, а некоторые программисты даже полагают, что так они подсказывают ком# пилятору, как можно оптимизировать код. Все так, но основания для самоограничения, требующие оставлять не более од# ного оператора в строке, гораздо серьезней. Размещение каждого оператора на отдельной строке дает точное представле# ние о сложности программы. При этом не скрывается сложность из#за того, что сложные операторы выглядят тривиальными. Сложные операторы и вы# глядят сложными, простые — простыми. ГЛАВА 31 Форматирование и стиль 743 Размещение нескольких операторов на одной строке не помогает современным компиляторам в оптимизации. Сегодняшние оптимизирующие компиляторы не нужда# ются в подсказках, сделанных с помощью форматирова# ния (см. ниже). Если операторы расположены на отдельных строках, чтение кода происходит сверху вниз, а не сверху вниз и слева направо. При поиске определенной строки у взгляда должна быть возможность придерживаться левого края кода. Он не должен просматривать каждую строку целиком только потому, что в одной строке может быть два оператора. При размещении операторов на отдельных строках легко найти синтаксичес# кие ошибки, если компилятор сообщает только номера строк, где они произош# ли. При расположении нескольких операторов на одной строке ее номер ни# чего не скажет о том, какой оператор содержит ошибку. При размещении операторов на отдельных строках легко выполнять пошаго# вую отладку кода, используя построчные отладчики. Если строка содержит несколько операторов, отладчик выполнит их все одновременно, и вам при# дется переключиться на ассемблерный листинг для выполнения пошаговой отладки отдельных выражений. Когда строка содержит только один оператор, его легко редактировать — можно удалить или временно закомментировать всю строку. Если же на одной стро# ке вы разместили несколько операторов, вам придется выполнять редактиро# вание между остальными операторами. В C++ избегайте выполнения нескольких операций в одной строке (побоч' ные эффекты) Побочные эффекты — это последствия выполнения некоторо# го выражения, проявляющиеся в дополнение к основным результатам выполне# ния этого выражения. Так, в C++ оператор ++, расположенный на одной строке с другими операторами, приводит к проявлению побочного эффекта. Присваива# ние значения переменной и применение левой части этого присваивания в ус# ловном операторе также является примером побочного эффекта. Побочные эффекты снижают читаемость кода. Например, если n равно 4, что напечатает выражение, приведенное в листинге 31#48? Листинг 31-48. Пример непредсказуемого побочного эффекта (C++) PrintMessage( ++n, n + 2 ); 4 и 6? Или 5 и 7? А может, 5 и 6? Правильный ответ: «Ни то, ни другое и не тре# тье». Первый аргумент — ++n — равен 5. Но язык C++ не определяет порядок вы# числения условий выражения или аргументов функции. Поэтому компилятор может вычислить второй аргумент, n + 2, либо до, либо после первого аргумента, и ре# зультат может быть равен 6 или 7 в зависимости от компилятора. В листинге 31# 49 показано, как переписать это выражение, чтобы прояснить свои намерения: Перекрестная ссылка Об опти- мизации производительности на уровне кода см. главы 25 и 26. 744 ЧАСТЬ VII Мастерство программирования Листинг 31-49. Пример избавления от непредсказуемого побочного эффекта (C++) ++n; PrintMessage( n, n + 2 ); Если вы все еще не совсем уверены в том, что побочные эффекты надо выносить в отдельные строки, попробуйте понять, что делает функция, приводимая в лис# тинге 31#50: Листинг 31-50. Пример слишком большого количества операции в строке (C) strcpy( char * t, char * s ) { while ( *++t = *++s ) ; } Некоторые опытные программисты не видят сложности в этом примере, потому что эта функция им знакома. Они смотрят на нее и говорят: «Это функция strcpy()». Однако в нашем случае это не совсем strcpy(). Она содержит ошибку. Если вы ска# зали «Это strcpy()», увидев данный код, вы узнали код, а не прочитали его. В такую же ситуацию вы попадаете при отладке программы: код, на который вы не обрати# ли внимания, потому что «узнали», а не прочли его, может содержать ошибку, по# иск которой займет гораздо больше времени, чем она этого заслуживает. Фрагмент, показанный в листинге 31#51, функционально идентичен первому ва# рианту и гораздо удобней для чтения: Листинг 31-51. Пример читаемого количества операций в каждой строке (C) strcpy( char * t, char * s ) { do { ++t; ++s; *t = *s; } while ( *t != ‘\0’ ); } В этом переформатированном коде ошибка очевидна. Конечно, t и s инкремен# тируются до того, как *s будет скопирована в *t. Первый символ пропускается. Второй пример выглядит продуманней первого, хотя операции, выполняемые во втором примере, идентичны первому. Причина такого впечатления в том, что во втором варианте не скрывается сложность выполняемых действий. Рост производительности также не оправдывает размеще# ния нескольких операций на одной строке. Поскольку обе функции strcpy() логически эквивалентны, можно ожидать, что компилятор сгенерирует для них идентичный код. Од# нако при профилировании обеих функций выяснилось, что для копирования 5 000 000 строк первой функции понадобилось 4,81 секунды, а второй —4,35. В нашем случае «умная» версия показала снижение скорости на 11%, что делает ее гораздо менее умной. Результаты могут изменяться от компилятора к компиля# Перекрестная ссылка О на- стройке кода см. главы 25 и 26. ГЛАВА 31 Форматирование и стиль 745 тору, но в целом они свидетельствуют о том, что пока вы не измерили прирост производительности, следует сначала стремиться к ясности и корректности, а уж затем — к производительности. Даже если вы легко читаете выражения с побочными эффектами, пожалейте тех, кому придется разбираться с вашим кодом. Большинству программистов нужно дважды подумать, чтобы понять выражения с побочными эффектами. Позвольте им использовать мозговые клетки для осмысления более общих вопросов рабо# ты вашего кода, а не синтаксических деталей конкретного выражения. Размещение объявлений данных Располагайте каждое объявление данных в отдельной строке Как показали предыдущие примеры, каждому объявлению данных надо выделять отдельную строку. Тог# да проще дописывать комментарии к каждому объявлению — ведь каждое расположено в отдельной строке. Проще ис# правлять объявления, поскольку все они изолированы друг от друга. Проще находить конкретные переменные, так как можно просканировать одну колонку, а не читать каждую строчку. Проще искать и исправлять синтаксические ошибки — строка, указанная компилятором, содер# жит только одно объявление. А ну#ка, скажите быстренько: какой тип имеет переменная currentBottom в объяв# лении данных, приведенном в листинге 31#52? Листинг 31-52. Пример скопления нескольких объявлений переменных в одной строке (C++) int rowIndex, columnIdx; Color previousColor, currentColor, nextColor; Point previousTop, previousBottom, currentTop, currentBottom, nextTop, nextBottom; Font previousTypeface, currentTypeface, nextTypeface; Color choices[ NUM_COLORS ]; Это, конечно, крайний случай, однако он не так уж далек от гораздо более рас# пространенного стиля, показанного в листинге 31#53: Листинг 31-53. Пример скопления нескольких объявлений переменных в одной строке (C++) int rowIndex, columnIdx; Color previousColor, currentColor, nextColor; Point previousTop, previousBottom, currentTop, currentBottom, nextTop, nextBottom; Font previousTypeface, currentTypeface, nextTypeface; Color choices[ NUM_COLORS ]; Это не такой уж и редкий стиль объявления переменных, а конкретную перемен# ную все так же тяжело найти, поскольку все объявления свалены в кучу. Тип пере# менной тоже тяжело выяснить. А теперь скажите, какой тип имеет nextColor в листинге 31#54? Перекрестная ссылка О доку- ментировании объявлений дан- ных см. подраздел «Комменти- рование объявлений данных» раздела 32.5. Об использовании данных см. в главах 10-13. 746 ЧАСТЬ VII Мастерство программирования Листинг 31-54. Пример читаемого кода, достигнутого благодаря размещению только одной переменной в каждой строке (C++) int rowIndex; int columnIdx; Color previousColor; Color currentColor; Color nextColor; Point previousTop; Point previousBottom; Point currentTop; Point currentBottom; Point nextTop; Point nextBottom; Font previousTypeface; Font currentTypeface; Font nextTypeface; Color choices[ NUM_COLORS ]; Вероятно, переменную nextColor было проще найти, чем nextTypeface в листинге 31#53. Такой стиль характеризуется наличием в каждой строке одного полного объявления, включающего тип переменной. Надо признать, что такой стиль съедает больше экранного пространства — 20 строк, а не 3, как в первом примере, хотя те три строки и выглядели довольно безобраз# но. Я не могу процитировать ни одного исследования, показывающего, что этот стиль приводит к меньшему количеству ошибок или к лучшему пониманию про# граммы. Однако если Салли попросит меня посмотреть ее код, а объявления дан# ных будут выглядеть, как в первом примере, я отвечу: «Ни за что — это читать невозможно». Если они будут выглядеть, как во втором примере, я скажу: «Гм… Может, попозже». А если они будут выглядеть, как в третьем примере, я скажу: «Конечно, с удовольствием!» Объявляйте переменные рядом с местом их первого использования Еще более предпочтительный вариант, чем объявление всех переменных в одном боль# шом блоке, — это стиль, при котором переменная объявляется рядом с местом ее первого использования. Это уменьшает срок службы и время жизни переменной и позволяет проще выполнить рефакторинг кода на меньшие методы (см. подраздел «Делайте время жизни переменных как можно короче» раздела 10.4). Разумно упорядочивайте объявления В листинге 31#54 объявления сгруппи# рованы по типам. Такая группировка обычно имеет смысл, поскольку перемен# ные одинаковых типов часто используются в аналогичных операциях. В других случаях можно упорядочивать их по алфавиту в соответствии с именами перемен# ных. Хотя у алфавитной сортировки много сторонников, мне кажется, затрачи# ваемых не нее усилий она не стоит. Если ваш список переменных настолько дли# нен, что ему помогает алфавитное упорядочиение, ваш метод, вероятно, слишком велик. Разбейте его на части, чтобы создать меньшие по размеру методы с неболь# шим количеством переменных. В C++ при объявлении указателей располагайте звездочку рядом с именем переменной или объявляйте типы'указатели Очень часто приходится ви# |