Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 13 Нестандартные типы данных 327 Распространенные проблемы с глобальными данными Если вы без разбора используете глобальные переменные или считаете невозмож- ность их применения ненужным ограничением, то, вероятно, вы еще не проник- лись значимостью принципов модульности и сокрытия информации. Модульность, сокрытие информации и связанное с ними использование хорошо спроектиро- ванных классов, может, и не панацея, но они помогают сделать большие программы понятнее и легче в сопровождении. Когда вы это поймете, вам захочется писать методы и классы как можно меньше взаимодействующие с глобальными перемен- ными и внешним миром. Можно привести массу проблем, связанных с глобальными данными, но в основ- ном они сводятся к следующим вариантам. Непреднамеренные изменения глобальных данных Вы можете изменить зна- чение глобальной переменной в одном месте и ошибочно думать, что оно оста- лось прежним где-то в другом. Такая проблема известна как «побочный эффект». Например, в этом фрагменте theAnswer является глобальной переменной: Пример побочного эффекта (Visual Basic) theAnswer — глобальная переменная. theAnswer = GetTheAnswer() GetOtherAnswer() изменяет theAnswer. otherAnswer = GetOtherAnswer() Значение averageAnswer неправильно. averageAnswer = (theAnswer + otherAnswer) / 2 Вы предполагаете, что вызов GetOtherAnswer() не изменяет значение theAnswer, потому что иначе среднее значение в третьей строке будет вычислено неверно. На самом деле GetOtherAnswer() все-таки изменяет theAnswer, и в программе воз- никает ошибка. Причудливые и захватывающие проблемы при использовании псевдони- мов для глобальных данных Использование псевдонима означает обращение к переменной по двум и более именам. Это происходит, когда глобальная пере- менная передается в метод, а там используется и в качестве глобальной перемен- ной, и в качестве параметра. Вот пример метода, работающего с глобальной пе- ременной: Пример метода, подверженного проблеме с псевдонимами (Visual Basic) Sub WriteGlobal( ByRef inputVar As Integer ) inputVar = 0 globalVar = inputVar + 5 MsgBox( “Input Variable: “ & Str( inputVar ) ) MsgBox( “Global Variable: “ & Str( globalVar ) ) End Sub > > > 328 ЧАСТЬ III Переменные А вот код вызывающего метода с глобальной переменной в качестве аргумента: Пример вызова метода с аргументом, демонстрирующим проблему псевдонимов (Visual Basic) WriteGlobal( globalVar ) Поскольку inputVar инициализируется 0, и WriteGlobal() добавляет 5 к inputVar, чтобы получить новое значение globalVar, вы ожидаете, что globalVar будет на 5 больше, чем inputVar. Но вот неожиданный результат: Результат проблемы с псевдонимами Input Variable: 5 Global Variable: 5 Хитрость в том, что globalVar и inputVar — на самом деле одна и та же перемен- ная! Поскольку globalVar передается в WriteGlobal() вызывающим методом, к ней обращаются с помощью двух разных имен. Поэтому результат вызовов MsgBox() отличается от ожидаемого: они показывают одну и ту же переменную дважды, хотя и используют два разных имени. Проблемы реентерабельности глобальных данных Сейчас все чаще встречается код, который может выполняться одновременно нескольки- ми потоками. Многопоточное программирование создает вероятность обращения к глобальным данным будут обращаться не только из разных методов, но и из разных экземпляров одной и той же программы. В такой среде вы долж- ны быть уверены, что глобальные данные сохранят свои значения, даже если бу- дет запущено несколько копий программы. Это важная проблема, и вы сможете ее избежать, используя технологии, предложенные ниже. Затруднение повторного использования кода, вызванное глобальными данными Для использования кода из одной программы в другой вам нужно выта- щить его из первой программы и внедрить во вторую. В идеале вы могли бы извлечь отдельный метод или класс, встроить в другую программу и наслаждаться жизнью. Глобальные данные усложняют картину. Если класс, который вы хотите исполь- зовать повторно, читает или записывает глобальные данные, вы не сможете про- сто перенести его в новую программу. Вам придется изменить либо новую про- грамму, либо старый класс, чтобы они стали совместимы. Правильным решением будет модификация старого класса, чтобы он не использовал глобальные данные: сделав это, вы сможете в следующий раз повторно использовать этот класс без дополнительных усилий. Неправильным решением будет модификация новой программы с целью создания таких же глобальных данных, какие требуются ста- рому классу. Это как вирус — глобальные данные не только влияют на исходный код, но и распространяются по новым программам, использующим какие-либо классы из старой. Проблемы с неопределенным порядком инициализации глобальных данных Порядок, в котором данные из разных «единиц трансляции» (файлов) будут ини- циализироваться, в некоторых языках программирования (в частности, C++) не определен. Если при инициализации глобальной переменной из одного файла ГЛАВА 13 Нестандартные типы данных 329 используется глобальная переменная из другого файла, значение второй перемен- ной предсказать сложно, если только вы не предпримете специальные действия для их инициализации в правильном порядке. Эта проблема решается с помощью обходного маневра, описанного в правиле 47 книги Скотта Мейерса «Effective C++» (Meyers, 1998). Но изощренность решения как раз и иллюстрирует ту излишнюю сложность, которую привносят глобальные данные. Нарушение модульности и интеллектуальной управляемости, привноси- мое глобальными данными Сущность создания программ, состоящих из бо- лее чем нескольких сотен строк кода, заключается в управлении сложностью. Един- ственный способ, позволяющий интеллектуально управлять большой программой, — это разбить ее на части так, чтобы в каждый момент времени думать только об одной из них. Модульность — наиболее мощный инструмент для разбиения про- граммы на части. Глобальные данные проделывают дыры в возможности модуляризации. Если вы используете глобальные данные, разве вы можете сосредоточиться только на од- ном методе? Нет. Вам приходится сосредоточиваться на этом методе и на всех других, в которых используются те же глобальные данные. Хотя эти данные и не разрушают модульность программы полностью, они ее ослабляют, и это доста- точная причина, чтобы найти лучшее решение ваших проблем. Причины для использования глобальных данных Ревнители чистоты данных иногда утверждают, что программисты никогда не должны использовать глобальные данные. Но большинство программ работают с «глобальными данными» в широком смысле этого слова. Записи в базе данных являются глобальными, так же как и данные конфигурационных файлов, напри- мер реестра Windows. Именованные константы — это тоже глобальные данные, хотя и не глобальные переменные. При аккуратном применении глобальные переменные могут быть полезны в не- которых ситуациях. Хранение глобальных значений Иногда какие-то данные концептуально от- носятся к целой программе. Это может быть переменная, отражающая состояние программы, скажем, режим командной строки, или интерактивный, или нормаль- ный режим, или режим восстановления после сбоев. Или это может быть инфор- мация, необходимая в течение всей программы, например, таблица с данными, используемая всеми методами программы. Эмуляция именованных констант Хотя C++, Java, Visual Basic и большинство современных языков поддерживают именованные константы, некоторые языки, такие как Python, Perl, Awk и язык сценариев UNIX, до сих пор — нет. Вы мо- жете использовать глобальные переменные как подстановки для именованных кон- стант, если ваш язык их не поддерживает. Так, вы можете заменить константные значения 1 и 0 глобальными переменными TRUE и FALSE, установленными в 1 и 0. Или вы можете заменить число 66, используемое как число строк на странице, переменной LINES_PER_PAGE = 66. Этот подход позволяет упростить дальнейшее изменение кода, кроме того, его легче читать. Такое упорядоченное применение Перекрестная ссылка Об имено- ванных константах см. раздел 12.7 330 ЧАСТЬ III Переменные глобальных данных — отличный пример программирования с использованием языка, а не на языке (см. раздел 34.4). Эмуляция перечислимых типов Вы также можете использовать глобальные переменные для эмуляции перечислимых типов в таких языках, как Python, кото- рые напрямую такие типы не поддерживают. Оптимизация обращений к часто используемым данным Иногда перемен- ная так часто вызывается, что упоминается в списке параметров каждого метода. Вместо того чтобы включать ее в каждый список параметров, вы можете сделать ее глобальной. Однако случаи, когда к переменной обращаются отовсюду, редки. Обычно она используется ограниченным набором методов. Их вы можете объе- динить в класс вместе с данными, с которыми они работают. Позднее мы вернем- ся к этому вопросу. Исключение бродячих данных Иногда вы передаете данные методу или классу только для того, чтобы передать в другой метод или класс. Например, у вас может быть объект-обработчик ошибок, применяемый в каждом методе. Если метод в се- редине цепочки вызовов не использует этот объект, он называется «бродячим» (tramp data). Применение глобальных переменных помогает исключить бродячие данные. Используйте глобальные данные только как последнее средство Прежде чем вы решите использовать глобальные данные, рассмотрите следующие альтернативы. Начните с объявления всех переменных локальными и делайте их глобаль- ными только по необходимости Изначально сделайте все переменные локаль- ными по отношению к конкретным методам. Если выяснится, что они нужны еще где-то, сделайте их сначала закрытыми или защищенными переменными класса, прежде чем вы решите сделать их глобальными. Если в конце концов выяснится, что их придется сделать глобальными, сделайте, но только после того, как в этом убедитесь. Если вы с самого начала объявите переменную глобальной, вы никог- да не сделаете ее локальной, но если она сначала будет локальной, вам, возмож- но, не понадобится делать ее глобальной. Различайте глобальные переменные и переменные-члены класса Некото- рые переменные действительно глобальны в том плане, что к ним обращаются из любого места программы. Другие — на самом деле классовые переменные — интенсивно используются только некоторымо набором методов. Вполне нормально обращаться к классовой переменной из нескольких методов сколь угодно интен- сивно. Если методу вне класса нужно использовать эту переменную, предоставьте ее значение посредством метода доступа. Не обращайтесь к членам класса напря- мую (как если бы эти переменные были глобальными), даже если ваш язык про- граммирования это позволяет. Этот совет равносилен высказыванию «Модуляри- зируйте! Модуляризируйте! Модуляризируйте!». Используйте методы доступа Создание методов доступа — основной под- ход для решения проблем с глобальными данными (см. следующий раздел). ГЛАВА 13 Нестандартные типы данных 331 Используйте методы доступа вместо глобальных данных Все, что вы можете сделать с глобальными данными, вы можете сделать лучше, используя методы доступа. Применение методов доступа — основ- ная технология реализации абстрактных типов данных и достижения со- крытия информации. Даже если вы не хотите создавать полноценный абстракт- ный тип данных, вы все равно можете использовать методы доступа для центра- лизации управления данными и для защиты от изменений. Преимущества методов доступа Использование методов доступа имеет несколько преимуществ. 쐽 Вы получаете централизованный контроль над данными. Если позднее вы об- наружите более подходящую реализацию структуры, вам не придется изменять код везде, где она упоминается. Изменения не всколыхнут всю вашу програм- му — они останутся внутри методов доступа. 쐽 Вы можете быть уверены, что все ссылки на переменную изолированы. Добавляя элемент в стек с помощью таких выражений, как stack.array[ stack.top ] = newElement, вы легко можете забыть проверить переполнение стека и допустить серьезную ошибку. Используя же методы доступа (скажем, PushStack( newElement )), вы можете написать проверку переполнения стека в методе PushStack(). Провер- ка будет выполняться автоматически, и вы сможете про нее забыть. 쐽 Вы автоматически получаете главные преимущества со- крытия информации. Методы доступа представляют со- бой примеры сокрытия информации, даже если вы и не намеревались использовать их в этих целях. Вы можете изменять содержимое метода доступа, не затрагивая остальную часть программы. Эти методы позволяют вам отремонтировать интерьер вашего дома, оставив фасад прежним, так что ваши друзья смогут его узнать. 쐽 Методы доступа легко преобразуются в абстрактные типы данных. Одно из преимуществ этих методов в том, что вы можете создавать более высокий уро- вень абстракции, чем при использовании глобальных данных напрямую. На- пример, вместо кода if lineCount > MAX_LINES вы сможете, используя метод до- ступа, написать if PageFull(). Это небольшое изменение документирует цель проверки if lineCount прямо в коде программы. Оно дает небольшой выигрыш в читабельности, но постоянное внимание к таким деталям и создает разли- чие между красиво написанным ПО и наскоро слепленным кодом. Как использовать методы доступа Здесь представлена краткая версия теории и практики методов доступа: Скройте данные в классе. Объявите их с помощью ключевого слова static или аналогично- го, чтобы гарантировать их существование в единственном экземпляре. Напиши- те методы, позволяющие получать и изменять данные. Потребуйте, чтобы код вне класса использовал эти методы, а не данные напрямую. Например, если у вас есть глобальная статусная переменная g_globalStatus, опи- сывающая общее состояние программы, вы можете создать два метода доступа: Перекрестная ссылка Об изоля- ции данных см. раздел 8.5. Перекрестная ссылка О сокры- тии информации см. подраздел «Скрывайте секреты (к вопро- су о сокрытии информации)» раздела 5.3. 332 ЧАСТЬ III Переменные globalStatus. Get() и globalStatus. Set(), каждый из которых делает то, что сказано в ее названии. Эти методы обращаются к переменной, спрятанной внутри класса, заменяющего g_globalStatus. Остальная часть программы может получать все пре- имущества бывшей глобальной переменной, вызывая globalStatus.Get() и global- Status.Set(). Если язык не поддерживает классы, вы все равно можете создавать методы доступа для манипуляции глобальными данными. Однако вам придется устанавливать ограничения доступа к глобальным данным с помощью стандартов ко- дирования, а не встроенных средств языка. Вот несколько основных принципов применения методов доступа для сокрытия глобальных переменных в языках, не имеющих встроенной поддержки этого. Требуйте, чтобы весь код обращался к данным через методы доступа Хо- рошим соглашением будет начинать имена всех глобальных переменных с пре- фикса g_, и в дальнейшем требовать, чтобы никакой код не обращался к перемен- ным с префиксом g_ напрямую, кроме методов доступа к этим переменным. Весь остальной код работает с этими данными через методы доступа. Не валите все глобальные данные в одну кучу Если вы сложите все глобаль- ные данные в одну большую кучу и напишете для них методы доступа, вы решите проблему глобальных данных, но утратите некоторые преимущества абстрактных типов данных и сокрытия информации. Раз уж вы пишете методы доступа, обду- майте, к каким классам принадлежит каждая глобальная переменная, и затем упа- куйте данные и методы доступа к ним в этот класс. Управляйте доступом к глобальным переменным с помощьюблокировок По аналогии с управлением параллельным доступом к многопользовательской ба- зой данных, блокировка требует, чтобы перед вызовом или обновлением значения глобальной переменной ее помечали для изменений (check out). После использо- вания переменную можно освободить (check in). Если пока она занята (т. е. поме- чена для изменений), другая часть программы попытается к ней обратиться, про- цедура блокировки выводит сообщение об ошибке или генерирует исключение. Такое описание механизма блокировок опускает многие тонкости в написании кода, полностью поддерживающего параллельное выполнение. По этой причине упрощенные схемы блокировок вроде этой наиболее полезны на стадии разработки. Пока схема тщательно не продумана, она ско- рее всего не будет достаточно надежна для работы в про- мышленной версии. При вводе программы в эксплуатацию такой код должен быть заменен на более безопасный и вы- полняющий более элегантные действия, чем вывод сообщений об ошибках. Так, при обнаружении ситуации, когда несколько частей программы пытаются забло- кировать одну и ту же глобальную переменную, он мог бы записать сообщение об ошибке в файл. Перекрестная ссылка Ограниче- ние доступа к глобальным пе- ременным, даже если ваш язык не поддерживает это напрямую, — пример программирования с использованием языка, а не на языке (см. раздел 34.4). Перекрестная ссылка О плани- ровании различий между рабо- чей и промышленной версиями программы см. подраздел «За- планируйте удаление отладоч- ных средств» раздела 8.6, а так- же раздел 8.7. ГЛАВА 13 Нестандартные типы данных 333 Такой способ защиты во время разработки довольно легко реализовать, если вы используете методы доступа. Но это было бы затруднительно сделать, если бы вы обращались к данным напрямую. Встройте уровень абстракции в методы доступа Разрабатывайте методы доступа в области определения задачи, а не на уровне деталей реализации. Этот подход позволяет улучшить читабельность, а также страхует от изменения дета- лей реализации. Сравните пары выражений в табл. 13-1: Табл. 13-1. Обращение к глобальным данным напрямую и с помощью метода доступа Непосредственное использование Обращение к глобальным данным через глобальных данных методы доступа node = node.next account = NextAccount( account ) node = node.next employee = NextEmployee( employee ) node = node.next rateLevel = NextRateLevel( rateLevel ) event = eventQueue[ queueFront ] event = HighestPriorityEvent() event = eventQueue[ queueBack ] event = LowestPriorityEvent() Смысл первых трех примеров в том, что абстрактный метод доступа гораздо ин- формативнее общей структуры. Если вы используете структуры напрямую, вы одновременно делаете слишком многое: во-первых, показываете, что выполняет структура (переход к следующему элементу в связном списке), а во-вторых — что происходит по отношению к сущности, которую она представляет (выбор номе- ра счета, следующего работника или процентной ставки). Это слишком тяжелая ноша для простой операции присваивания в структуре данных. Сокрытие инфор- мации за абстрактными методами доступа позволяет коду самому говорить за себя и заставляет читать программу на уровне области определения задачи, а не на уровне деталей реализации. Выполняйте доступ к данным на одном и том же уровне абстракции Если вы используете метод доступа для выполнения какого-то действия со структурой, все остальные действия должны производиться с помощью таких методов. Если вы считываете данные с помощью метода доступа, то и записывайте их с помощью метода. Если вы вызываете InitStack() для инициализации стека и PushStack() для добавления в него элементов, то вы создали целостное представление данных. Если же вы извлекаете элементы с помощью выражения value = array[ stack.top ], то это представление данных противоречиво. Противоречивость усложняет код для по- нимания. Создайте метод PopStack() и используйте вместо value = array[ stack.top ]. В примерах выражений в табл. 13-1. две операции с очере- дями событий происходят параллельно. Добавление в оче- редь — наиболее сложная из этих двух операций в таблице и потребует нескольких строк кода для поиска места вставки события, сдвига остальных элементов очереди для выделе- ния места новому событию, и установки нового начала или конца очереди. Удаление события из очереди по сложности будет примерно та- ким же. Если во время кодирования сложные операции будут помещены в мето- Перекрестная ссылка Примене- ние методов доступа для оче- реди событий предполагает не- обходимость создания класса (см. главу 6). 334 ЧАСТЬ III Переменные ды, а в остальных будет применяться прямой доступ к данным, это создаст безоб- разное, нераспараллеливаемое использование структуры. Теперь сравните пары выражений в табл. 13-2: Табл. 13-2. Распараллеливаемое и нераспараллеливаемое применение сложных данных Нераспараллеливаемое Распараллеливаемое использование использование сложных данных сложных данных event = EventQueue[ queueFront ] event = HighestPriorityEvent() event = EventQueue[ queueBack ] event = LowestPriorityEvent() AddEvent( event ) AddEvent( event ) eventCount = eventCount - 1 RemoveEvent( event ) Может показаться, что эти принципы стоит применять только в больших програм- мах, однако методы доступа показали себя как эффективный способ решения проблем с глобальными данными. В качестве бонуса они делают код более чита- бельным и добавляют гибкость. Как уменьшить риск использования глобальных данных Как правило, глобальные данные должны быть переменными класса, который не был правильно спроектирован или разработан. В редких случаях данные действи- тельно должны быть глобальными, но доступ к ним может осуществляться посред- ством оболочки методов доступа, что позволит минимизировать потенциальные проблемы. В крохотном числе оставшихся вариантов вам действительно необхо- димы глобальные данные. В этих случаях вы можете рассматривать принципы, перечисленные ниже, как прививки, дающие возможность пить воду в зарубежной поездке: они болезненны, но увеличивают шансы остаться здоровым. Разработайте соглашения по именованию, которые сделают глобальные переменные очевидными Вы мо- жете избежать некоторых ошибок, просто сделав очевидным факт, что вы работаете с глобальными данными. Если вы ис- пользуете глобальные переменные для нескольких целей (например, как переменные и как замену именованных кон- стант), убедитесь, что ваши соглашения по именованию делают различия между этими типами использования. Создайте хорошо аннотированный список всех глобальных переменных Если соглашение по именованию указывает, что данная переменная является гло- бальной, будет полезно показать, что эта переменная делает. Список глобальных переменных — один из наиболее полезных инструментов, который может иметь программист, работающий с вашей программой. Не храните промежуточных результатов в глобальных переменных Если вам нужно вычислить новое значение глобальной переменной, присвойте ей окон- чательный результат в конце вычислений, а не храните в ней результаты проме- жуточных расчетов. Перекрестная ссылка О согла- шениях по именованию глобаль- ных переменных см. подраздел «Идентифицируйте глобальные переменные» раздела 11.4. |