Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 10 Общие принципы использования переменных 243 Команды, в которых используются два набора переменных. GetOldData( oldData, &numOldData ); GetNewData( newData, &numNewData ); totalOldData = Sum( oldData, numOldData ); totalNewData = Sum( newData, numNewData ); PrintOldDataSummary( oldData, totalOldData, numOldData ); PrintNewDataSummary( newData, totalNewData, numNewData ); SaveOldDataSummary( totalOldData, numOldData ); SaveNewDataSummary( totalNewData, numNewData ); } Этот небольшой фрагмент заставляет следить сразу за шестью переменными: oldData, newData, numOldData, numNewData, totalOldData и totalNewData. В следу# ющем примере благодаря разделению кода на два логических блока это число снижено до трех: Пример более понятного использования двух наборов переменных (C++) void SummarizeData( ... ) { Команды, в которых используются «старые данные» (oldData). GetOldData( oldData, &numOldData ); totalOldData = Sum( oldData, numOldData ); PrintOldDataSummary( oldData, totalOldData, numOldData ); SaveOldDataSummary( totalOldData, numOldData ); Команды, в которых используются «новые данные» (newData). GetNewData( newData, &numNewData ); totalNewData = Sum( newData, numNewData ); PrintNewDataSummary( newData, totalNewData, numNewData ); SaveNewDataSummary( totalNewData, numNewData ); } Каждый из двух блоков, полученных при разделении кода, короче, чем первона# чальный блок, и содержит меньше переменных. Такой код легче понять, а если вам придется разбить его на отдельные методы, меньшие блоки, содержащие мень# шее число переменных, позволят выполнять эту задачу эффективнее. Разбивайте группы связанных команд на отдельные методы При прочих равных условиях переменная из более короткого метода обычно характеризуется меньшим интервалом между обращениями и меньшим временем жизни, чем переменная из более крупного метода. Разбиение группы связанных команд на отдельные методы позволяет уменьшить область видимости, которую может иметь переменная. Начинайте с самой ограниченной области видимос' ти и расширяйте ее только при необходимости Что# бы минимизировать область видимости переменной, поста# райтесь сделать ее как можно более локальной. Область ви# Перекрестная ссылка О гло- бальных переменных см. раздел 13.3. > > > 244 ЧАСТЬ III Переменные димости гораздо сложнее сжать, чем расширить — иначе говоря, превратить гло# бальную переменную в переменную класса сложнее, чем наоборот. Защищенные данные#члены класса также сложнее превратить в закрытые, чем закрытые в за# щищенные. Так что, если сомневаетесь, выбирайте наименьшую возможную об# ласть видимости переменной: попытайтесь сделать переменную локальной для от# дельного цикла, локальной для конкретного метода, затем — закрытой перемен# ной класса, затем — защищенной, далее попробуйте включить ее в пакет (если ваш язык программирования поддерживает пакеты) и лишь в крайнем случае сделай# те ее глобальной. Комментарии по поводу минимизации области видимости Подход к минимизации области видимости переменных часто зависит от точки зрения на вопросы «удобства» и «интеллектуальной управляемости». Некоторые программисты делают многие переменные глобальными для того, чтобы облег# чить доступ к ним и не беспокоиться о списках параметров и правилах области видимости. В их умах удобство доступа к глобальным переменным перевешивает связанную с этим опасность. Другие предпочитают делать переменные как можно более локальными, потому что локальная область видимости спо# собствует интеллектуальной управляемости. Чем больше информации вы скрыли, тем меньше вам нужно удерживать в уме в каждый конкретный момент времени и тем ниже вероятность того, что вы допустите ошибку, забыв одну из многих деталей, о которых нужно было помнить. Разница между философией «удобства» и философией «интеллектуаль# ной управляемости» сводится к различию между ориентацией на напи# сание программы и ориентацией на ее чтение. Максимизация области видимости может облегчить написание программы, но программу, в которой каж# дый метод может вызвать любую переменную в любой момент времени, сложнее понять, чем код, основанный на грамотно организованных методах. Сделав дан# ные глобальными, вы не сможете ограничиться пониманием работы одного ме# тода: вы должны будете понимать работу всех других методов, которые вместе с ним используют те же глобальные данные. Подобные программы сложно читать, сложно отлаживать и сложно изменять. Так что ограничивайте область видимости каждой перемен# ной минимальным фрагментом кода. Можете ограничить ее одним циклом или одним методом — великолепно! Не по# лучается — ограничьте область видимости методами одно# го класса. Если и это невозможно, создайте методы досту# па, позволяющие использовать переменную совместно с другими классами. «Голые» глобальные данные требуются редко, если вообще та# кое бывает. Перекрестная ссылка Идея ми- нимизации области видимости связана с идеей сокрытия ин- формации [см. подраздел «Скрывайте секреты (к вопро- су о сокрытии информации)» раздела 5.3]. Перекрестная ссылка О методах доступа см. подраздел «Исполь- зование методов доступа вме- сто глобальных данных» разде- ла 13.3. ГЛАВА 10 Общие принципы использования переменных 245 10.5. Персистентность «Персистентность» — это еще одно слово, характеризующее длительность суще# ствования данных. Персистентность принимает несколько форм. Некоторые пе# ременные «живут»: пока выполняется конкретный блок кода или метод: например, это перемен# ные, объявленные внутри цикла for языка C++ или Java; столько, сколько вы им позволяете: в Java переменные, созданные при помо# щи оператора new, «живут» до сборки мусора; в C++ созданные аналогично переменные существуют, пока не будут уничтожены с помощью оператора delete; до завершения программы: этому описанию соответствуют глобальные пере# менные в большинстве языков, а также статические переменные в языках C++ и Java; всегда: такими переменными могут быть значения, которые вы храните в БД между запусками программы, — так, например, в случае интерактивной про# граммы, позволяющей пользователям настраивать цвета экрана, вы могли бы хранить эти цвета в файле, считывая их при каждой загрузке программы. Главная проблема персистентности возникает, когда вы предполагаете, что пере# менная существует дольше, чем есть на самом деле. Переменная похожа на пакет молока в холодильнике. Предполагается, что он может храниться неделю. Иног# да он хранится месяц, и с молоком ничего не происходит, а иногда молоко ски# сает через пять дней. Переменная может быть столь же непредсказуема. Если вы попытаетесь использовать значение переменной после окончания нормальной дли# тельности ее существования, получите ли вы ее прежнее значение? Иногда зна# чение переменной «скисает», и вы получаете ошибку. В других случаях компиля# тор оставляет старое значение неизменным, позволяя вам воображать, что все нормально. Проблем подобного рода можно избежать. Включайте в программу отладочный код или утвержде# ния для проверки важных переменных на предмет до# пустимости их значений. Если значения недопустимы, отображайте предупреждение, рекомендующее присту# пить к поиску кода неверной инициализации. Завершив работу с переменными, присваивайте им «не# допустимые значения». Скажем, после освобождения памяти при помощи опе# ратора delete вы может установить указатель в null. Исходите из того, что данные не являются персистентными. Так, если при воз# врате из метода переменная имеет определенное значение, не предполагайте, что оно будет таким же при следующем вызове метода. Это правило неакту# ально, если вы используете специфические возможности языка, гарантирую# щие неизменность значения переменной, такие как ключевое слово static языков C++ и Java. Выработайте привычку объявлять и инициализировать все данные перед их использованием. Если видите обращение к данным, но не можете найти по# близости команду их инициализации, отнеситесь к этому с подозрением! Перекрестная ссылка Отладоч- ный код легко включать в ме- тоды доступа (см. подраздел «Преимущества методов досту- па» раздела 13.3). 246 ЧАСТЬ III Переменные 10.6. Время связывания Одним из аспектов инициализации, серьезно влияющим на удобство сопровож# дения и изменения программы, является «время связывания» — момент, когда переменная и ее значение связываются вместе (Thimbleby, 1988). Связываются ли они при написании кода? При его компиляции? При загрузке? При выполнении программы? В другое время? Иногда выгоднее использовать как можно более позднее время связывания. В целом чем позже вы выполняете связывание, тем более гибким будет ваш код. В следую# щем примере связывание выполняется максимально рано — при написании кода: Пример связывания во время написания кода (Java) titleBar.color = 0xFF; // 0xFF — шестнадцатеричное значение синего цвета Значение 0xFF связывается с переменной titleBar.color во время написания кода, поскольку 0xFF — литерал, жестко закодированный в программе. Как правило, это неудачное решение, потому что при изменении одного значения 0xFF может утратиться его соответствие литералам 0xFF, используемым в других фрагментах с той же целью. В следующем примере связывание переменной выполняется чуть позднее, во время компиляции кода: Пример связывания во время компиляции (Java) private static final int COLOR_BLUE = 0xFF; private static final int TITLE_BAR_COLOR = COLOR_BLUE; titleBar.color = TITLE_BAR_COLOR; В данном случае TITLE_BAR_COLOR является именованной константой — выраже# нием, вместо которого компилятор подставляет конкретное значение при ком# пиляции. Этот подход почти всегда лучше, чем жесткое кодирование. Он облег# чает чтение кода, потому что имя константы TITLE_BAR_COLOR лучше характери# зует представляемое ей значение, чем 0xFF. Он облегчает изменение цвета заго# ловка окна (title bar), так как изменение константы будет автоматически отраже# но во всех местах, где она используется. При этом он не приводит к снижению быстродействия программы в период выполнения. Вот пример еще более позднего связывания — в период выполнения: Пример связывания в период выполнения (Java) titleBar.color = ReadTitleBarColor(); ReadTitleBarColor() — это метод, который во время выполнения программы чита# ет значение из реестра Microsoft Windows, файла свойств Java или подобного места. Этот код более понятен и гибок, чем код с жестко закодированным значением. Чтобы изменить значение titleBar.color; вам не нужно изменять программу: доста# точно просто изменить содержание файла, из которого метод ReadTitleBarColor() ГЛАВА 10 Общие принципы использования переменных 247 читает значение цвета. Так часто делают при разработке интерактивных прило# жений, предоставляющих возможность настройки их параметров. Кроме того, время связывания может определяться тем, когда вызывается метод ReadTitleBarColor(). Его можно вызывать при загрузке программы, при создании окна или при каждой перерисовке окна: каждый последующий вариант соответ# ствует все более позднему времени связывания. Итак, в нашем примере переменная может связываться со значением следующим образом (в других случаях детали могут быть несколько иными): при написании кода (с использованием магических чисел); при компиляции (с использованием именованной константы); при загрузке программы (путем чтения значения из внешнего источника, та# кого как реестр Windows или файл свойств Java); при создании объекта (например, путем чтения значения при каждом созда# нии окна); по требованию (например, посредством чтения значения при каждой перери# совке окна). В целом чем раньше время связывания, тем ниже гибкость и ниже сложность кода. Что до первых двух вариантов, то именованные константы по многим причинам предпочтительнее магических чисел, и вы можете получить гибкость, обеспечи# ваемую именованными константами, просто используя хорошие методики про# граммирования. При дальнейшем повышении уровня гибкости повышается и слож# ность нужного для его поддержания кода, а вместе с ней и вероятность ошибок. Так как эффективность программирования зависит от минимизации сложности, опытный программист будет обеспечивать такой уровень гибкости, какой нужен для удовлетворения требований, но не более того. 10.7. Связь между типами данных и управляющими структурами Между типами данных и управляющими структурами существуют четко опреде# ленные отношения, впервые описанные британским ученым Майклом Джексоном (Jackson, 1975). В этом разделе мы их вкратце обсудим. Джексон проводит связи между тремя типами данных и соответствующими управ# ляющими структурами. Последовательные данные соответствуют последо' вательности команд Последовательные данные (sequen# tial data) — это набор блоков данных, используемых в оп# ределенном порядке (рис. 10#2). Если у вас есть пять команд подряд, обрабатывающих пять разных значений, они формируют последователь# ность команд. Если бы вам нужно было прочитать из файла последовательные данные (фамилию сотрудника, номер карточки социального обеспечения, адрес, телефон и возраст), вы включили бы в код последовательность команд. Перекрестная ссылка О после- довательном порядке выполне- ния команд см. главу 14. 248 ЧАСТЬ III Переменные Рис. 10'2. Последовательными называются данные, обрабатываемые в определенном порядке Селективные данные соответствуют операторам if и case Вообще селективные данные (selective data) пред# ставляют собой набор, допускающий использование одно# го и только одного элемента данных в каждый конкретный момент времени (рис. 10#3). Соответствующими командами, выполняющими фак# тический выбор данных, являются операторы if%then%else или case. Так, програм# ма расчета зарплаты должна была бы выполнять разные действия в зависимости от того, какой является оплата труда конкретного сотрудника: сдельной или по# временной. Опять#таки шаблоны кода соответствуют шаблонам данных. Рис. 10'3. Селективные данные допускают использование только одного из нескольких элементов Итеративные данные соответствуют циклам Ите# ративные данные (iterative data) представляют собой дан# ные одного типа, повторяющиеся более одного раза (рис. 10#4). Обычно они хранятся как элементы контейнера, записи файла или элементы массива. Скажем, вы могли бы хранить в файле список номеров карточек соци# ального обеспечения, для чтения которого было бы разумно использовать соот# ветствующий цикл. Рис. 10'4. Итеративные данные повторяются Реальные данные могут быть комбинацией последовательных, селективных и итеративных данных. Для описания сложных видов данных подойдет комбина# ция простых. Перекрестная ссылка Об услов- ных операторах см. главу 15. Перекрестная ссылка О циклах см. главу 16. ГЛАВА 10 Общие принципы использования переменных 249 10.8. Единственность цели каждой переменной Есть несколько тонких способов использования переменных более чем с одной целью, однако подобных тонкостей лучше избегать. Используйте каждую переменную только с одной целью Иногда есть соблазн вызвать одну переменную в двух разных местах для решения двух разных задач. Обычно в таких случаях переменной приходится присваивать не# удачное имя, соответствующее одной из ее целей, или использовать для решения обеих задач «временную» переменную (как правило, с бесполезным именем x или temp). Следующий пример иллюстрирует использование временной переменной с двойной целью: Пример использования переменной с двойной целью — плохой подход (C++) // Вычисление корней квадратного уравнения. // Предполагается, что дискриминант (b*b4*a*c) неотрицателен. temp = Sqrt( b*b 4*a*c ); root[O] = ( b + temp ) / ( 2 * a ); root[1] = ( b temp ) / ( 2 * a ); // корни меняются местами temp = root[0]; root[0] = root[1]; root[1] = temp; Вопрос: какие отношения связывают temp в первых строках кода и temp в последних? Ответ: никакие. Из#за использо# вания в обоих случаях одной переменной создается впечат# ление, что две задачи связаны, хотя на самом деле это не так. Создание уникальных переменных для каждой цели делает код понятнее. Вот улучшенный вариант: Пример использования двух переменных для двух целей — хороший подход (C++) // Вычисление корней квадратного уравнения. // Предполагается, что дискриминант (b*b4*a*c) неотрицателен. discriminant = Sqrt( b*b 4*a*c ); root[0] = ( b + discriminant ) / ( 2 * a ); root[1] = ( b discriminant ) / ( 2 * a ); // корни меняются местами oldRoot = root[0]; root[0] = root[1]; root[1] = oldRoot; Перекрестная ссылка Парамет- ры методов также должны иметь только одну цель. О параметрах методов см. раздел 7.5. 250 ЧАСТЬ III Переменные Избегайте переменных, имеющих скрытый смысл Другой способ исполь# зования переменной более чем с одной целью заключается в том, что разные зна# чения переменной имеют разный смысл. Ниже я привел несколько примеров. Значение переменной pageCount представляет число отпечатанных страниц, однако, если оно равно #1, произошла ошибка. Если значение переменной customerId меньше 500 000, оно представ# ляет номер заказчика, больше — вы вычитаете из него 500 000 для опре# деления номера просроченного счета. Положительные значения переменной bytesWritten представляют число байт, записанных в выходной файл, а отрицательные — номер диска, используемо# го для вывода данных. Избегайте подобных переменных, имеющих скрытый смысл. Формально это на# зывается «гибридным сопряжением» (hybrid coupling) (Page#Jones, 1988). Перемен# ная разрывается между двумя задачами, а это означает, что для решения одной из задач ее тип не подходит. В одном из наших примеров переменная pageCount в нормальной ситуации определяет число страниц — это целое число. Однако зна# чение %1 указывает на ошибку — целое число работает по совместительству буле# вой переменной! Даже если применение переменных с двойной целью вам понятно, его не пой# мет кто#то другой. Дополнительная ясность, которой можно достигнуть благода# ря использованию двух переменных для хранения двух видов данных, удивит вас. Никто не упрекнет вас в том, что вы впустую тратите ресурсы компьютера. Убеждайтесь в том, что используются все объявленные пере' менные Использование переменной с множественной целью имеет про# тивоположность: переменную можно не использовать вообще. Кард, Черч и Агрести обнаружили, что наличие неиспользуемых переменных коррелирова# ло с более высоким уровнем ошибок (Card, Church, and Agresti, 1986). Выработай# те привычку проверять, что используются все объявленные переменные. Некото# рые компиляторы и утилиты (такие как lint) предупреждают о наличии неисполь# зуемых переменных. Контрольный список: общие вопросы использования данных Инициализация переменных В каждом ли методе проверяется корректность входных параметров? Переменные объявляются около места их использова- ния в первый раз? Инициализировали ли вы переменные при их объявле- нии, если такое возможно? Если переменные невозможно объявить и инициализи- ровать одновременно, вы инициализировали их около места использования в первый раз? Правильно ли инициализируются счетчики и аккумуляторы? Выполняется ли их повторная инициализация, если она необходима? http://cc2e.com/1092 Перекрестная ссылка Конт- рольный список вопросов о специфических типах данных см. в главе 12, а контрольный список вопросов, касающихся именования переменных, — в главе 11. |