Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 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. ГЛАВА 13 Нестандартные типы данных 335 Не считайте, что вы не используете глобальные переменные, поместив все данные в чудовищный объект и передавая его всюду Размещение всех возможных данных в одном огромном объекте может формально удовлетворять принципу отказа от глобальных переменных, но это приводит исключительно к накладным расходам и не создает преимуществ реальной инкапсуляции. Если вы используете глобальные данные, делайте это открыто. Не пытайтесь замаскиро# вать это с помощью объектов, страдающих ожирением. Дополнительные ресурсы Далее указаны дополнительные ресурсы, в которых освеща# ются необычные типы данных: Maguire Steve. Writing Solid Code. Redmond, WA: Microsoft Press, 1993. Глава 3 содержит отличное обсуждение опасностей использования указате# лей и множество специальных советов по решению проблем с указателями. Meyers Scott. Effective C++, 2d ed. Reading, MA: Addison#Wesley, 1998; Meyers Scott. More Effective C++. Reading, MA: Addison#Wesley, 1996. Как говорится в названии, эти книги содержат большое количество советов по улучшению программ на C++, включая руководство по безопасному и эффективному использованию указателей. В част# ности, «More Effective C++» содержит отличное обсуждение вопросов управления памятью в языке C++. Контрольный список: применение необычных типов данных Структуры Используете ли вы структуры вместо отдельных переменных для организа- ции и манипуляции группами взаимосвязанных данных? Рассматривали ли вы создание класса как альтернативу использованию структуры? Глобальные данные Действительно ли все переменные объявлены локально или в области види- мости класса, если только они не обязательно должны быть глобальными? Различаются ли в соглашениях по именованию переменных локальные, классовые и глобальные данные? Документированы ли все глобальные переменные? Свободен ли код от псевдоглобальных данных — мамонтообразных объек- тов, содержащих мешанину из данных, передающихся в каждый метод? Используются ли методы доступа вместо глобальных данных? Организованы ли данные и методы доступа к ним в классы? Предоставляют ли методы доступа уровень абстракции, независимый от реализации используемого типа данных? Находятся ли все методы доступа на одном уровне абстракции? Указатели Изолированы ли операции с указателями в методах? Корректны ли обращения к указателям или они могут быть «висячими»? http://cc2e.com/1385 http://cc2e.com/1392 336 ЧАСТЬ III Переменные Проверяет ли код корректность указателей перед их использованием? Проверяется ли корректность переменной, на которую ссылается указатель, перед ее использованием? Присваивается ли указателям пустое значение после их освобождения? Использует ли код все необходимые для читабельности переменные-указа- тели? Освобождаются ли указатели в связных списках в правильном порядке? Выделяет ли программа «резервный парашют» памяти, чтобы иметь воз- можность аккуратно завершить выполнение в случае нехватки памяти? Используются ли указатели только как последнее средство, когда другие методы неприменимы? Ключевые моменты Структуры могут помочь сделать программы менее сложными, упростить их понимание и сопровождение. Принимая решение использовать структуру, подумайте, не будет ли класс под# ходить лучше. Работа с указателями чревата ошибками. Обезопасьте себя, используя методы или классы для доступа к ним и практику защитного программирования. Избегайте глобальных переменных не только потому, что они опасны, но и потому что их можно заменить чем#то лучшим. Если вы не можете отказаться от глобальных переменных, работайте с ними через методы доступа. Эти методы предоставляют все то же и даже больше, что и глобальные переменные. ГЛАВА 13 Нестандартные типы данных 337 Часть IV ОПЕРАТОРЫ Глава 14. Организация последовательного кода Глава 15. Условные операторы Глава 16. Циклы Глава 17. Нестандартные управляющие структуры Глава 18. Табличные методы Глава 19. Общие вопросы управления 338 ЧАСТЬ IV Операторы Г Л А В А 1 4 Организация последовательного кода Содержание 14.1. Операторы, следующие в определенном порядке 14.2. Операторы, следующие в произвольном порядке Связанные темы Общие вопросы управления: глава 19 Код с условными операторами: глава 15 Код с операторами цикла: глава 16 Область видимости переменных и объектов: раздел 10.4 В этой главе мы начнем рассматривать программирование не с точки зрения дан# ных, а с точки зрения выражений. Глава представляет самую простую управляю# щую логику программы: размещение выражений и их блоков в последовательном порядке. Хотя размещение последовательного кода относительно простая задача, некото# рые организационные тонкости влияют на качество, корректность, читабельность и управляемость кода. 14.1. Операторы, следующие в определенном порядке Проше всего организовать такие выражения, для которых важен порядок следо# вания. Вот пример: Пример выражений, для которых важен порядок следования (Java) data = ReadData(); results = CalculateResultsFromData( data ); PrintResults( results ); http://cc2e.com/1465 ГЛАВА 14 Организация последовательного кода 339 Если только в этом фрагменте кода не произойдет нечто непонятное, выражения должны выполняться в указанном порядке. Данные должны быть прочитаны прежде, чем результаты могут быть вычислены, а результаты должны быть вычислены прежде, чем их можно будет напечатать. Основная идея этого примера состоит в зависимостях. Третье выражение зави# сит от второго, второе — от первого. Факт зависимости одного выражения от другого в этом примере понятен из имен методов. А вот здесь зависимости менее очевидны: Пример выражений, для которых порядок следования важен, но не настолько очевиден (Java) revenue.ComputeMonthly(); revenue.ComputeQuarterly(); revenue.ComputeAnnual(); В этом случае квартальный доход вычисляется в предположении, что месячные доходы уже подсчитаны. Знание бухучета, даже в общих чертах, может вам под# сказывать, что квартальные доходы должны вычисляться перед годовыми. Это зависимость, но при простом прочтении кода она не видна. А здесь зависимости не просто не очевидны, но буквально скрыты: Пример выражений, для которых порядковые зависимости скрыты (Visual Basic) ComputeMarketingExpense ComputeSalesExpense ComputeTravelExpense ComputePersonnelExpense DisplayExpenseSummary Допустим, метод ComputeMarketingExpense() инициализирует переменные#члены класса, в которые все остальные методы помещают данные. В этом случае его нужно вызывать перед остальными методами. Как это узнать при прочтении кода? Ис# ходя из того, что вызовы методов не содержат параметров, вы могли бы предпо# ложить, что каждый из этих методов использует данные класса. Но вы не можете знать это наверняка, прочитав этот код. Если зависимости между выражениями требуют размещения их в опре# деленном порядке, требуются дополнительные действия, чтобы сделать зависимости явными. Организуйте код так, чтобы зависимости были очевидными В предыду# щем примере на Visual Basic ComputeMarketingExpense() не должен инициализи# ровать классовые переменные. Имя метода предполагает, что ComputeMarketing% Expense() работает аналогично ComputeSalesExpense(), ComputeTravelExpense() толь# ко с маркетинговыми данными, а не с данными о продажах или другими расхо# дами. То, что ComputeMarketingExpense() инициализирует переменные#члены класса, — случайность, которой следует избегать. Почему инициализация должна выпол# няться в этом методе, а не в двух других? Пока вы не сможете придумать хоро# шую причину для этого, инициализацию классовых переменных следует осуще# 340 ЧАСТЬ IV Операторы ствлять иным методом, например InitializeExpenseData(). Имя метода явно указы# вает на то, что он должен быть вызван перед другими расчетами расходов. Называйте методы так, чтобы зависимости были очевидными В при# мере на Visual Basic метод ComputeMarketingExpense() назван неправильно, посколь# ку он делает больше, чем просто вычисляет расходы на маркетинг: он еще ини# циализирует члены класса. Если вы против создания отдельного метода для ини# циализации данных, дайте по крайней мере методу ComputeMarketingExpense() имя, описывающее все выполняемые им функции. В данном случае ComputeMarke% tingExpenseAndInitializeMemberData() будет более адекватным именем. Вы можете сказать, что это имя ужасно, потому что слишком длинное. Но оно описывает то, что делает метод и вовсе не ужасно. Ужасен сам метод! Используйте параметры методов, чтобы сделать за' висимости очевидными Возвращаясь к примеру на Visual Basic, можно сказать, что, поскольку никакие данные меж# ду методами не передаются, неизвестно, используют ли эти методы одни и те же данные. Переписав код так, чтобы происходила передача дан# ных, вы сообщаете, что порядок выполнения имеет значение. Новый код может выглядеть, например, так: Пример данных, которые позволяют предположить порядковую зависимость (Visual Basic) InitializeExpenseData( expenseData ) ComputeMarketingExpense( expenseData ) ComputeSalesExpense( expenseData ) ComputeTravelExpense( expenseData ) ComputePersonnelExpense( expenseData ) DisplayExpenseSummary( expenseData ) Поскольку все методы используют expenseData, это наводит на мысль, что они могут работать с одними и теми же данными и что порядок выражений может быть важен. В этом примере лучшим подходом может быть преобразование процедур в функ# ции, которые принимают expenseData на входе и возвращают обновленное зна# чение expenseData. Это сделает наличие зависимостей в коде еще более явным. Пример данных и вызовов методов, которые указывают на порядковую зависимость (Visual Basic) expenseData = InitializeExpenseData( expenseData ) expenseData = ComputeMarketingExpense( expenseData ) expenseData = ComputeSalesExpense( expenseData ) expenseData = ComputeTravelExpense( expenseData ) expenseData = ComputePersonnelExpense( expenseData ) DisplayExpenseSummary( expenseData ) Данные могут также указывать, что порядок выполнения не имеет значения, как в этом случае: Перекрестная ссылка Об ис- пользовании методов и их па- раметров см. главу 5. ГЛАВА 14 Организация последовательного кода 341 Пример данных, которые не указывают на порядковую зависимость (Visual Basic) ComputeMarketingExpense( marketingData ) ComputeSalesExpense( salesData ) ComputeTravelExpense( travelData ) ComputePersonnelExpense( personnelData ) DisplayExpenseSummary( marketingData, salesData, travelData, personnelData ) Так как методы в первых четырех строках не имеют общих данных, код подразу# мевает, что порядок их вызова значения не имеет. Поскольку метод в пятой стро# ке использует данные каждого из первых четырех методов, вы можете предполо# жить, что его надо выполнять после всех этих методов. Документируйте неявные зависимости с помощью коммента' риев Попробуйте, во#первых, написать код без порядковых зависимо# стей, во#вторых — написать код, который делает зависимости очевидными. Если вам все еще кажется, что зависимости видны недостаточно ясно, задокумен# тируйте их. Документирование неявных зависимостей — один из аспектов доку# ментирования допущений, сделанных при кодировании, что необходимо для на# писания систем, пригодных для сопровождения и модификации. В примере на Visual Basic будет полезно поместить такие комментарии: Пример выражений, в которых порядковые зависимости скрыты, но разъясняются с помощью комментариев (Visual Basic) ‘ Рассчитываем расходы. В каждом методе используется переменная класса ’ expenseData. Метод DisplayExpenseSummary должен вызываться последним, ’ так как он зависит от данных, вычисленных другими методами. InitializeExpenseData ComputeMarketingExpense ComputeSalesExpense ComputeTravelExpense ComputePersonnelExpense DisplayExpenseSummary В этом коде не используются методики, проясняющие порядковые зависимости. Было бы лучше положиться на такие методики, а не на простые комментарии, но если вы сопровождаете код, находящийся под строгим контролем, или почему# либо не можете его улучшать, используйте документирование для компенсации недостатков кодирования. Проверяйте зависимости с помощью утверждений или кода обработки ошибок Если последовательность кода достаточно критична, вы можете исполь# зовать утверждения или статусные переменные и код обработки ошибок, чтобы за# документировать необходимый порядок. Например, в конструкторе класса вы мо# жете инициализировать член класса isExpenseDataInitialized значением false. Затем в InitializeExpenseData() вы устанавливаете isExpenseDataInitialized в true. Каждая функция, зависящая от инициализации expenseData, может проверить, установле# но ли значение isExpenseDataInitialized в true, прежде чем выполнять операции с expenseData. Если зависимости между методами глубже, вам могут потребоваться такие переменные, как isMarketingExpenseComputed, isSalesExpenseComputed и т. д. 342 ЧАСТЬ IV Операторы Этот способ требует создания новых переменных, нового кода инициализации и нового кода проверки ошибок, что увеличивает возможность добавления ошибок. Преимущества этого подхода должны сравниваться с привнесенной им сложнос# тью и увеличением вероятности появления вторичных ошибок. 14.2. Операторы, следующие в произвольном порядке Вам могут встречаться ситуации, когда кажется, что порядок выполнения несколь# ких выражений или нескольких блоков кода не имеет значения. Одно выражение не зависит от другого и логически из него не следует. Но поскольку упорядочен# ность влияет на читабельность, производительность и качество сопровождения, вы можете использовать второстепенные критерии для определения порядка сле# дования выражений или блоков кода. Главный принцип — это Принцип Схожес# ти: Располагайте взаимосвязанные действия вместе. Размещение кода для чтения сверху вниз Основная идея в том, что необходимо позволить читать программу сверху вниз, а не перескакивая с места на место. Эксперты согласны, что порядок просмотра кода сверху вниз способствует улучшению читабельности. Однако простого раз# мещения последовательности команд сверху вниз недостаточно. Если тому, кто читает ваш код, приходится просматривать всю программу в поиске необходи# мой информации, то такой код нужно реорганизовать. Рассмотрим пример: Пример плохого кода, в котором приходится перескакивать с места на место (C++) MarketingData marketingData; SalesData salesData; TravelData travelData; travelData.ComputeQuarterly(); salesData.ComputeQuarterly(); marketingData.ComputeQuarterly(); salesData.ComputeAnnual(); marketingData.ComputeAnnual(); travelData.ComputeAnnual(); salesData.Print(); travelData.Print(); marketingData.Print(); Допустим, вы хотите выяснить, как рассчитывается marketingData. Вам придется начать с последней строки и проследить все упоминания marketingData вплоть до первой строки. marketingData встречается только в нескольких других местах, но вы должны помнить, как marketingData используется в каждом случае между |