Совершенный код. Мастер-класс [Стив Макконнелл] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

«Великолепное руководство по стилю программирования и конструированию ПО».
Мартин Фаулер, автор книги «Refactoring»
«Книга Стива Макконнелла… это быстрый путь к мудрому программированию… Его книги
увлекательны, и вы никогда не забудете то, что он рассказывает, опираясь на свой с тру#
дом полученный опыт».
Джон Бентли, автор книги «Programming Pearls, 2d ed»
«Это просто самая лучшая книга по конструированию ПО из всех, что когда#либо попада#
лись мне в руки. Каждый разработчик должен иметь ее и перечитывать от корки до корки
каждый год. Я ежегодно перечитываю ее на протяжении вот уже девяти лет и все еще уз#
наю много нового!»
Джон Роббинс, автор книги «Debugging Applications
for Microsoft .NET and Microsoft Windows»
«Современное ПО должно быть надежным и гибким, а создание защищенного кода начи#
нается с дисциплинированного конструирования программы. За десять лет так и не по#
явилось лучшего руководства по этой теме, чем эта книга.
Майкл Ховард, специалист по защите ПО, корпорация Microsoft;
один из авторов книги «Writing Secure Code»
«Это исчерпывающее исследование тактических аспектов создания хорошо спроектиро#
ванных программ. Книга Макконнелла охватывает такие разные темы, как архитектура,
стандарты кодирования, тестирование, интеграция и суть разработки ПО».
Гради Буч, автор книги «Object Solutions»
«Авторитетная энциклопедия для разработчиков ПО — вот что такое „Совершенный код“.
Подзаголовок „Практическое руководство по конструированию ПО“ характеризует эту 850#
страничную книгу абсолютно точно. Как утверждает автор, она призвана сократить раз#
рыв между знаниями „гуру и лучших специалистов отрасли“ (например, Йордона и Прес#
смана) и общепринятыми методиками разработки коммерческого ПО, а также „помочь
создавать более качественные программы за меньшее время с меньшей головной болью“…
Эту книгу следует иметь каждому разработчику. Ее стиль и содержание в высшей степени
практичны».
Крис Лузли, автор книги «High%Performance Client/Server»
«Полная плодотворных идей книга Макконнелла „Совершенный код“ — это одна из са#
мых понятных работ, посвященных подробному обсуждению методик разработки ПО…»
Эрик Бетке, автор книги «Game Development and Production»
«Кладезь полезной информации и рекомендаций по общим вопросам проектирования и
разработки хорошего ПО».
Джон Демпстер, автор книги «The Laboratory Computer:
A Practical Guide for Physiologists and Neuroscientists»
«Если вы действительно хотите улучшить навыки программирования, обязательно прочтите
книгу „Совершенный код“ Стива Макконнелла».
Джин Дж. Лаброссе, автор книги «Embedded Systems Building Blocks:
Complete and Ready%To%Use Modules in C»
«Стив Макконнелл написал одну из лучших книг по разработке ПО, не привязанных к
конкретной среде…»
Кеннет Розен, один из авторов книги «Unix: The Complete Reference»

«Пару раз в поколение или около того появляются книги, обобщающие накопленный опыт
и избавляющие вас от многих лет мучений… Не могу найти слов, чтобы адекватно опи#
сать все великолепие этой книги. „Совершенный код“ — довольно жалкое название для
такой превосходной работы».
Джефф Дантеманн, журнал «PC Techniques»
«Издательство Microsoft Press опубликовало то, что я считаю самой лучшей книгой по конст#
руированию ПО. Эта книга должна занять место на книжной полке каждого программиста».
Уоррен Кеуффель, журнал «Software Development»
«Эту выдающуюся книгу следует прочесть каждому программисту».
Т. Л. (Фрэнк) Паппас, журнал «Computer»
«Если вы собираетесь стать профессиональным программистом, покупка этой книги, по#
жалуй, станет самым мудрым вложением средств. Можете не читать этот обзор дальше —
просто идите в магазин и купите ее. Как пишет сам Макконнелл, его целью было сокра#
щение разрыва между знаниями гуру и общепринятыми методиками разработки коммер#
ческого ПО… Удивительно, но ему это удалось».
Ричард Матеосян, журнал «IEEE Micro»
«„Совершенный код“ — обязательное чтение для всех… кто имеет отношение к разработ#
ке ПО».
Томми Ашер, журнал «C Users Journal»
«Я вынужден сделать чуть более категоричное заявление, чем обычно, и рекомендовать
книгу Стива Макконнелла „Совершенный код“ всем разработчикам без всяких оговорок…
Если раньше во время работы я держал ближе всего к клавиатуре руководства по API, то
теперь их место заняла книга Макконнелла».
Джим Кайл, журнал «Windows Tech Journal»
«Это лучшая книга по разработке ПО из всех, что я читал».
Эдвард Кенворт, журнал «.EXE»
«Эта книга заслуживает статуса классической, и ее в обязательном порядке должны про#
честь все разработчики и те, кто ими управляет».
Питер Райт, «Program Now»

Посвящаю эту книгу своей жене Эшли, которая не имеет особого отношения
к программированию, но настолько обогащает всю мою остальную жизнь,
что я не могу выразить это в словах.

Steve McConnell

CODE

COMPLETE

Second Edition

Стив Макконнелл

Совершенный

КОД

МАСТЕР-КЛАСС

2010

УДК 004.45
ББК 32.973.26–018.2
М15
Макконнелл С.
М15

Совершенный код. Мастер#класс / Пер. с англ. — М. : Издательство «Русская
редакция», 2010. — 896 стр. : ил.
ISBN 978-5750200641
Более 10 лет первое издание этой книги считалось одним из лучших практических
руководств по программированию. Сейчас эта книга полностью обновлена с учетом
современных тенденций и технологий и дополнена сотнями новых примеров, иллюстрирующих искусство и науку программирования. Опираясь на академические
исследования, с одной стороны, и практический опыт коммерческих разработок ПО —
с другой, автор синтезировал из самых эффективных методик и наиболее эффективных принципов ясное прагматичное руководство. Каков бы ни был ваш профессиональный уровень, с какими бы средствами разработками вы ни работали, какова бы
ни была сложность вашего проекта, в этой книге вы найдете нужную информацию,
она заставит вас размышлять и поможет создать совершенный код.
Книга состоит из 35 глав, предметного указателя и библиографии.

УДК 004.45
ББК 32.973.26–018.2
© 2005-2012, Translation Russian Edition Publishers.
Authorized Russian translation of the English edition of Code Complete, Second Edition, ISBN 9780735619678
© Steven C. McConnell.
This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish
and sell the same.
© 2005-2012, перевод ООО «Издательство «Русская редакция».
Авторизованный перевод с английского на русский язык произведения Code Complete, Second Edition,
ISBN 9780735619678 © Steven C. McConnell.
Этот перевод оригинального издания публикуется и продается с разрешения O’Reilly Media, Inc., которая владеет
или распоряжается всеми правами на его публикацию и продажу.
© 2005-2012, оформление и подготовка к изданию, ООО «Издательство «Русская редакция».
Microsoft, а также товарные знаки, перечисленные в списке, расположенном по адресу:
http://www.microsoft.com/about/legal/en/us/IntellectualProperty/Trademarks/EN-US.aspx
являются товарными знаками или охраняемыми товарными знаками корпорации Microsoft в США и/или других
странах. Все другие товарные знаки являются собственностью соответствующих фирм.
Все названия компаний, организаций и продуктов, а также имена лиц, используемые в примерах, вымышлены
и не имеют никакого отношения к реальным компаниям, организациям, продуктам и лицам.

Содержание

VII

Содержание
Предисловие .................................................................................................................... XIII
Благодарности ................................................................................................................ XIX
Контрольные списки ...................................................................................................... XXI

Часть I

Основы разработки ПО

1 Добро пожаловать в мир конструирования ПО! .................................................... 2
1.1. Что такое конструирование ПО? ................................................................................................. 2
1.2. Почему конструирование ПО так важно? ........................................................................... 5
1.3. Как читать эту книгу ................................................................................................................................ 6
2 Метафоры, позволяющие лучше понять разработку ПО .................................... 8
2.1. Важность метафор ...................................................................................................................................... 8
2.2. Как использовать метафоры? ...................................................................................................... 10
2.3. Популярные метафоры, характеризующие разработку ПО ........................ 12
3 Семь раз отмерь, один раз отрежь: предварительные условия ...................... 21
3.1. Важность выполнения предварительных условий ............................................... 22
3.2. Определите тип ПО, над которым вы работаете ..................................................... 28
3.3. Предварительные условия, связанные
с определением проблемы ...................................................................................................................... 34
3.4. Предварительные условия, связанные с выработкой требований ....... 36
3.5. Предварительные условия, связанные
с разработкой архитектуры .................................................................................................................... 41
3.6. Сколько времени следует посвятить выполнению
предварительных условий? ..................................................................................................................... 52
4 Основные решения, которые приходится принимать
при конструировании ................................................................................................ 58
4.1. Выбор языка программирования ........................................................................................... 59
4.2. Конвенции программирования ............................................................................................... 63
4.3. Волны развития технологий ........................................................................................................ 64
4.4. Выбор основных методик конструирования ............................................................. 66

Часть II

Высококачественный код

5 Проектирование при конструировании ................................................................. 70
5.1. Проблемы, связанные с проектированием ПО ......................................................... 71
5.2. Основные концепции проектирования ........................................................................... 74
5.3. Компоненты проектирования: эвристические принципы ........................... 84
5.4. Методики проектирования ........................................................................................................ 107
5.5. Комментарии по поводу популярных методологий ........................................ 115
6 Классы ........................................................................................................................ 121
6.1. Основы классов: абстрактные типы данных ............................................................ 122
6.2. Качественные интерфейсы классов .................................................................................. 129
6.3. Вопросы проектирования и реализации ..................................................................... 139

VIII

Содержание

6.4. Разумные причины создания классов ............................................................................. 148
6.5. Аспекты, специфические для языков ................................................................................ 152
6.6. Следующий уровень: пакеты классов ............................................................................... 153
7 Высококачественные методы ............................................................................... 157
7.1. Разумные причины создания методов ............................................................................ 160
7.2. Проектирование на уровне методов ................................................................................ 163
7.3. Удачные имена методов ................................................................................................................. 167
7.4. Насколько объемным может быть метод? ................................................................... 169
7.5. Советы по использованию параметров методов ................................................. 170
7.6. Отдельные соображения по использованию функций ................................. 177
7.7. Методы#макросы и встраиваемые методы ................................................................. 178
8 Защитное программирование ................................................................................ 182
8.1. Защита программы от неправильных входных данных .............................. 183
8.2. Утверждения .............................................................................................................................................. 184
8.3. Способы обработки ошибок .................................................................................................... 189
8.4. Исключения ............................................................................................................................................... 193
8.5. Изоляция повреждений, вызванных ошибками ................................................... 198
8.6. Отладочные средства ....................................................................................................................... 200
8.7. Доля защитного программирования в промышленной версии ........... 204
8.8. Защита от защитного программирования ................................................................. 206
9 Процесс программирования с псевдокодом ...................................................... 209
9.1. Этапы создания классов и методов .................................................................................... 210
9.2. Псевдокод для профи ....................................................................................................................... 211
9.3. Конструирование методов с использованием ППП ......................................... 214
9.4. Альтернативы ППП ............................................................................................................................. 225

Часть III

Переменные

10 Общие принципы использования переменных ................................................ 230
10.1. Что вы знаете о данных? ............................................................................................................. 231
10.2. Грамотное объявление переменных ............................................................................. 232
10.3. Принципы инициализации переменных ................................................................ 233
10.4. Область видимости ......................................................................................................................... 238
10.5. Персистентность ............................................................................................................................... 245
10.6. Время связывания ............................................................................................................................. 246
10.7. Связь между типами данных и управляющими структурами ............... 247
10.8. Единственность цели каждой переменной ............................................................ 249
11 Сила имен переменных ......................................................................................... 252
11.1. Общие принципы выбора имен переменных ..................................................... 253
11.2. Именование конкретных типов данных ................................................................... 257
11.3. Сила конвенций именования ............................................................................................... 263
11.4. Неформальные конвенции именования ................................................................... 264
11.5. Стандартизованные префиксы ........................................................................................... 272
11.6. Грамотное сокращение имен переменных ............................................................ 274
11.7. Имена, которых следует избегать ..................................................................................... 277

Содержание

IX

12 Основные типы данных ......................................................................................... 282
12.1. Числа в общем ...................................................................................................................................... 283
12.2. Целые числа ............................................................................................................................................ 284
12.3. Числа с плавающей запятой ................................................................................................... 286
12.4. Символы и строки ............................................................................................................................ 289
12.5. Логические переменные ............................................................................................................ 292
12.6. Перечислимые типы ...................................................................................................................... 294
12.7. Именованные константы .......................................................................................................... 299
12.8. Массивы ...................................................................................................................................................... 301
12.9. Создание собственных типов данных (псевдонимы) ................................. 303
13 Нестандартные типы данных ............................................................................... 310
13.1. Структуры ................................................................................................................................................. 310
13.2. Указатели ................................................................................................................................................... 314
13.3. Глобальные данные ......................................................................................................................... 326

Часть IV

Операторы

14 Организация последовательного кода .............................................................. 338
14.1. Операторы, следующие в определенном порядке ........................................... 338
14.2. Операторы, следующие в произвольном порядке ........................................... 342
15 Условные операторы ............................................................................................. 346
15.1. Операторы if ........................................................................................................................................... 346
15.2. Операторы case ................................................................................................................................... 353
16

Циклы ........................................................................................................................ 359
16.1. Выбор типа цикла ............................................................................................................................. 359
16.2. Управление циклом ........................................................................................................................ 365
16.3. Простое создание цикла — изнутри наружу ......................................................... 378
16.4. Соответствие между циклами и массивами ........................................................... 379

17 Нестандартные управляющие структуры ......................................................... 382
17.1. Множественные возвраты из метода ............................................................................ 382
17.2. Рекурсия ...................................................................................................................................................... 385
17.3. Оператор goto ....................................................................................................................................... 389
17.4. Перспективы нестандартных управляющих структур ................................ 401
18 Табличные методы ................................................................................................. 404
18.1. Основные вопросы применения табличных методов ................................ 405
18.2. Таблицы с прямым доступом ................................................................................................. 406
18.3. Таблицы с индексированным доступом .................................................................... 418
18.4. Таблицы со ступенчатым доступом ................................................................................ 419
18.5. Другие примеры табличного поиска ............................................................................ 422
19 Общие вопросы управления ................................................................................ 424
19.1. Логические выражения ............................................................................................................... 424
19.2. Составные операторы (блоки) ............................................................................................ 436
19.3. Пустые выражения ........................................................................................................................... 437
19.4. Укрощение опасно глубокой вложенности ........................................................... 438

Содержание

X

19.5. Основа программирования: структурное программирование .......... 448
19.6. Управляющие структуры и сложность ........................................................................ 450

Часть V

Усовершенствование кода

20 Качество ПО ............................................................................................................. 456
20.1. Характеристики качества ПО ............................................................................................... 456
20.2. Методики повышения качества ПО ................................................................................ 459
20.3. Относительная эффективность
методик контроля качества ПО ....................................................................................................... 462
20.4. Когда выполнять контроль качества ПО? ................................................................. 466
20.5. Главный Закон Контроля Качества ПО ....................................................................... 467
21 Совместное конструирование ............................................................................. 471
21.1. Обзор методик совместной разработки ПО ......................................................... 472
21.2. Парное программирование .................................................................................................... 475
21.3. Формальные инспекции ............................................................................................................ 477
21.4. Другие методики совместной разработки ПО .................................................... 484
21.5. Сравнение методик совместного конструирования ..................................... 487
22 Тестирование, выполняемое разработчиками ................................................. 490
22.1. Тестирование, выполняемое разработчиками, и качество ПО ........... 492
22.2. Рекомендуемый подход к тестированию, выполняемому
разработчиками ............................................................................................................................................... 494
22.3. Приемы тестирования ................................................................................................................. 496
22.4. Типичные ошибки ............................................................................................................................ 507
22.5. Инструменты тестирования ................................................................................................... 513
22.6. Оптимизация процесса тестирования ........................................................................ 518
22.7. Протоколы тестирования ......................................................................................................... 520
23 Отладка ..................................................................................................................... 524
23.1. Общие вопросы отладки ............................................................................................................ 524
23.2. Поиск дефекта ...................................................................................................................................... 529
23.3. Устранение дефекта ........................................................................................................................ 539
23.4. Психологические аспекты отладки ................................................................................ 543
23.5. Инструменты отладки — очевидные и не очень ............................................... 545
24 Рефакторинг ............................................................................................................ 551
24.1. Виды эволюции ПО ......................................................................................................................... 552
24.2. Введение в рефакторинг ............................................................................................................ 553
24.3. Отдельные виды рефакторинга .......................................................................................... 559
24.4. Безопасный рефакторинг ........................................................................................................ 566
24.5. Стратегии рефакторинга .......................................................................................................... 568
25 Стратегии оптимизации кода ............................................................................... 572
25.1. Общее обсуждение производительности ПО ...................................................... 573
25.2. Введение в оптимизацию кода ............................................................................................ 576
25.3. Где искать жир и патоку? ............................................................................................................ 583
25.4. Оценка производительности ................................................................................................ 588

Содержание

XI

25.5. Итерация .................................................................................................................................................... 590
25.6. Подход к оптимизации кода: резюме ........................................................................... 591
26 Методики оптимизации кода ................................................................................ 595
26.1. Логика ........................................................................................................................................................... 596
26.2. Циклы ............................................................................................................................................................ 602
26.3. Изменения типов данных ......................................................................................................... 611
26.4. Выражения ............................................................................................................................................... 616
26.5. Методы ......................................................................................................................................................... 625
26.6. Переписывание кода на низкоуровневом языке ............................................... 626
26.7. Если что#то одно изменяется, что#то
другое всегда остается постоянным ............................................................................................ 629

Системные вопросы

Часть VI

27 Как размер программы влияет на конструирование ...................................... 634
27.1. Взаимодействие и размер ......................................................................................................... 635
27.2. Диапазон размеров проектов ................................................................................................ 636
27.3. Влияние размера проекта на возникновение ошибок ................................ 636
27.4. Влияние размера проекта на производительность ........................................ 638
27.5. Влияние размера проекта на процесс разработки ......................................... 639
28 Управление конструированием ........................................................................... 645
28.1. Поощрение хорошего кодирования ............................................................................. 646
28.2. Управление конфигурацией .................................................................................................. 649
28.3. Оценка графика конструирования ................................................................................. 655
28.4. Измерения ................................................................................................................................................ 661
28.5. Гуманное отношение к программистам ..................................................................... 664
28.6. Управление менеджером ........................................................................................................... 670
29

Интеграция ............................................................................................................... 673
29.1. Важность выбора подхода к интеграции .................................................................. 673
29.2. Частота интеграции — поэтапная или инкрементная? .............................. 675
29.3. Стратегии инкрементной интеграции ....................................................................... 678
29.4. Ежедневная сборка и дымовые тесты ........................................................................... 686

30 Инструменты программирования ....................................................................... 694
30.1. Инструменты для проектирования ................................................................................. 695
30.2. Инструменты для работы с исходным кодом ....................................................... 695
30.3. Инструменты для работы с исполняемым кодом ............................................. 700
30.4. Инструменты и среды ................................................................................................................... 704
30.5. Создание собственного программного инструментария ....................... 705
30.6. Волшебная страна инструментальных средств .................................................. 707

Часть VII

Мастерство программирования

31 Форматирование и стиль ...................................................................................... 712
31.1. Основные принципы форматирования .................................................................... 713
31.2. Способы форматирования ...................................................................................................... 720
31.3. Стили форматирования ............................................................................................................. 721

XII

Содержание

31.4. Форматирование управляющих структур ............................................................... 728
31.5. Форматирование отдельных операторов ................................................................ 736
31.6. Размещение комментариев ..................................................................................................... 747
31.7. Размещение методов ...................................................................................................................... 750
31.8. Форматирование классов ......................................................................................................... 752
32 Самодокументирующийся код ............................................................................ 760
32.1. Внешняя документация ............................................................................................................... 760
32.2. Стиль программирования как вид документации ........................................... 761
32.3. Комментировать или не комментировать? ............................................................. 764
32.4. Советы по эффективному комментированию .................................................... 768
32.5. Методики комментирования ................................................................................................ 774
32.6. Стандарты IEEE .................................................................................................................................... 795
33 Личность ................................................................................................................... 800
33.1. Причем тут характер? ................................................................................................................... 801
33.2. Интеллект и скромность ............................................................................................................ 802
33.3. Любопытство ......................................................................................................................................... 803
33.4. Профессиональная честность ............................................................................................. 806
33.5. Общение и сотрудничество ................................................................................................... 809
33.6. Творчество и дисциплина ........................................................................................................ 809
33.7. Лень ................................................................................................................................................................. 810
33.8. Свойства, которые менее важны, чем кажется ..................................................... 811
33.9. Привычки .................................................................................................................................................. 813
34 Основы мастерства ................................................................................................ 817
34.1. Боритесь со сложностью ........................................................................................................... 817
34.2. Анализируйте процесс разработки ................................................................................ 819
34.3. Пишите программы в первую очередь для людей и лишь во вторую —
для компьютеров ............................................................................................................................................. 821
34.4. Программируйте с использованием языка, а не на языке ....................... 823
34.5. Концентрируйте внимание с помощью соглашений .................................. 824
34.6. Программируйте в терминах проблемной области ..................................... 825
34.7. Опасайтесь падающих камней ............................................................................................ 827
34.8. Итерируйте, итерируйте и итерируйте ...................................................................... 830
34.9. И да отделена будет религия от разработки ПО ................................................ 831
35 Где искать дополнительную информацию ....................................................... 834
35.1. Информация о конструировании ПО ......................................................................... 835
35.2. Не связанные с конструированием темы ................................................................. 836
35.3. Периодические издания ............................................................................................................ 838
35.4. Список литературы для разработчика ПО .............................................................. 839
35.5. Профессиональные ассоциации ...................................................................................... 841
Библиография ................................................................................................................. 842
Предметный указатель ................................................................................................. 863
Об авторе .......................................................................................................................... 868

Предисловие

Разрыв между самыми лучшими и средними методиками разработки ПО очень широк
— вероятно, шире, чем в любой другой инженерной дисциплине. Средство распро%
странения информации о хороших методиках сыграло бы весьма важную роль.
Фред Брукс (Fred Brooks)
Моей главной целью при написании этой книги было сокращение разрыва между знани#
ями гуру и лучших специалистов отрасли, с одной стороны, и общепринятыми методика#
ми разработки коммерческого ПО — с другой. Многие эффективные методики програм#
мирования годами скрываются в журналах и научных работах, прежде чем становятся
доступными программистской общественности.
Хотя передовые методики разработки ПО в последние годы быстро развивались, общепри#
нятые практически стояли на месте. Многие программы все еще полны ошибок, поставля#
ются с опозданием и не укладываются в бюджет, а многие не отвечают требованиям пользо#
вателей. Ученые обнаружили эффективные методики, устраняющие большинство проблем,
которые отравляют нашу жизнь с 1970#х годов. Однако из#за того, что эти методики редко
покидают страницы узкоспециализированных технических изданий, в большинстве ком#
паний по разработке ПО они еще не используются. Установлено, что для широкого распро#
странения исследовательских разработок обычно требуется от 5 до 15 и более лет (Raghavan
and Chand, 1989; Rogers, 1995; Parnas, 1999). Данная книга призвана ускорить этот процесс
и сделать важные открытия доступными средним программистам.

Кому следует прочитать эту книгу?
Исследования и опыт программирования, отраженные в этой книге, помогут вам созда#
вать высококачественное ПО и выполнять свою работу быстрее и эффективнее. Прочи#
тав ее, вы поймете, почему вы сталкивались с проблемами в прошлом, и узнаете, как избе#
гать их в будущем. Описанные мной методики программирования помогут вам сохранять
контроль над крупными проектами, а также успешно сопровождать и изменять ПО при
изменении требований.

Опытные программисты
Эта книга окажется полезной опытным программистам, желающим получить всесторон#
нее и удобное руководство по разработке ПО. Так как эта книга фокусируется на констру#
ировании — самой известной части жизненного цикла ПО, — описанные в ней методики
будут понятны и программистам, имеющим соответствующее образование, и программи#
стам#самоучкам.

Технические лидеры
Многие технические лидеры используют первое издание этой книги для обучения менее
опытных членов своих групп. Вы также можете использовать эту книгу для восполнения
пробелов в своих знаниях. Если вы — опытный программист, то, наверное, согласитесь
не со всеми моими выводами (обратное было бы странным), но, если вы прочитаете весь
материал и обдумаете каждый поднятый вопрос, едва ли какая#то возникшая проблема
конструирования окажется для вас новой.

XIV

Предисловие

Программисты-самоучки
Если вы не имеете специального образования, вы не одиноки. Ежегодно программистами
становятся около 50 000 человек (BLS, 2004, Hecker 2004), однако число дипломов, вруча#
емых ежегодно в нашей отрасли, составляет лишь около 35 000 (NCES, 2002). Легко прий#
ти к выводу, что многие программисты изучают разработку ПО самостоятельно. Програм#
мисты#самоучки встречаются среди инженеров, бухгалтеров, ученых, преподавателей, вла#
дельцев малого бизнеса и представителей других профессий, которые занимаются про#
граммированием в рамках своей работы, но не всегда считают себя программистами. Каким
бы ни было ваше программистское образование, в этом руководстве вы найдете инфор#
мацию об эффективных методиках программирования.

Студенты
В отличие от программистов, которые обладают опытом, но не могут похвастаться специ#
альным обучением, недавние выпускники вузов часто имеют обширные теоретические знания,
но плохо владеют практическими ноу#хау, связанными с созданием реальных программ.
Передача практических навыков хорошего кодирования зачастую идет медленно, в риту#
альных танцах архитекторов ПО, лидеров проектов, аналитиков и более опытных програм#
мистов. Еще чаще эти навыки приобретаются программистами в результате собственных
проб и ошибок. Эта книга — альтернатива традиционным неспешным интеллектуальным
ритуалам. В ней собраны полезные советы и эффективные стратегии разработки, которые
ранее можно было узнать главным образом только непосредственно у других людей. Это
трамплин для студентов, переходящих из академической среды в профессиональную.

Где еще можно найти эту информацию?
В этой книге собраны методики конструирования из самых разнообразных источников.
Многие знания о конструировании не только разрозненны, но и годами не попадают в
печатные издания (Hildebrand, 1989; McConnell, 1997a). В эффективных, мощных методи#
ках программирования, используемых лучшими программистами, нет ничего мистиче#
ского, однако в повседневной череде неотложных задач очень немногие эксперты выкра#
ивают время на то, чтобы поделиться своим опытом. Таким образом, программистам трудно
найти хороший источник информации о программировании.
Методики, описанные в этой книге, заполняют пустоту, остающуюся в знаниях програм#
мистов после прочтения вводных и более серьезных учебников по программированию.
Что читать человеку, изучившему книги типа «Introduction to Java», «Advanced Java» и
«Advanced Advanced Java» и желающему узнать о программировании больше? Вы можете
читать книги о процессорах Intel или Motorola, функциях ОС Microsoft Windows или Linux
или о другом языке программирования — невозможно эффективно программировать, не
имея хорошего представления о таких деталях. Но эта книга относится к числу тех не#
многих, в которых обсуждается программирование как таковое. Наибольшую пользу при#
носят те методики, которые можно использовать независимо от среды или языка. В дру#
гих источниках такие методики обычно игнорируются, и именно поэтому я сосредото#
чился на них.
Как показано ниже, информация, представленная в этой книге, выжата из многих источ#
ников. Единственным другим способом получения этой информации является изучение
горы книг и нескольких сотен технических журналов, дополненное значительным реаль#
ным опытом. Если вы уже проделали все это, данная книга все равно окажется вам полез#
ной как удобный справочник.

Предисловие

XV

Главные достоинства этой книги
Какой бы ни была ваша ситуация, эта книга поможет вам создавать более качественные
программы за меньшее время с меньшей головной болью.
Полное руководство по конструированию ПО В этой книге обсуждаются такие об#
щие аспекты конструирования, как качество ПО и подходы к размышлению о програм#
мировании. В то же время мы погрузимся в такие детали конструирования, как этапы со#
здания классов, использование данных и управляющих структур, отладка, рефакторинг и
методики и стратегии оптимизации кода. Чтобы изучить эти вопросы, вам не нужно чи#
тать книгу от корки до корки. Материал организован так, чтобы вы могли легко найти кон#
кретную интересующую вас информацию.
Готовые к использованию контрольные списки Эта книга включает десятки конт#
рольных списков, позволяющих оценить архитектуру программы, подход к проектирова#
нию, качество классов и методов, имена переменных, управляющие структуры, формати#
рование, тесты и многое другое.
Самая актуальная информация В этом руководстве вы найдете описания ряда самых
современных методик, многие из которых еще не стали общепринятыми. Так как эта книга
основана и на практике, и на исследованиях, рассмотренные в ней методики будут полез#
ны еще многие годы.
Более общий взгляд на разработку ПО Эта книга даст вам шанс подняться над суе#
той повседневной борьбы с проблемами и узнать, что работает, а что нет. Мало кто из прак#
тикующих программистов обладает временем, необходимым для прочтения сотен книг и
журнальных статей, обобщенных в этом руководстве. Исследования и реальный опыт, на
которых основана данная книга, помогут вам проанализировать ваши проекты и позво#
лят принимать стратегические решения, чтобы не приходилось бороться с теми же вра#
гами снова и снова.
Объективность Некоторые книги по программированию содержат 1 грамм информа#
ции на 10 граммов рекламы. Здесь вы найдете сбалансированные обсуждения достоинств
и недостатков каждой методики. Вы знаете свой конкретный проект лучше всех, и эта книга
предоставит вам объективную информацию, нужную для принятия грамотных решений
в ваших обстоятельствах.
Независимость от языка Описанные мной методики позволяют выжать максимум по#
чти из любого языка, будь то C++, C#, Java, Microsoft Visual Basic или другой похожий язык.
Многочисленные примеры кода Эта книга содержит почти 500 примеров хорошего
и плохого кода. Их так много потому, что лично я лучше всего учусь на примерах. Думаю,
это относится и к другим программистам.

XVI

Предисловие

Примеры написаны на нескольких языках, потому что освоение более одного языка час#
то является поворотным пунктом в карьере профессионального программиста. Как толь#
ко программист понимает, что принципы программирования не зависят от синтаксиса
конкретного языка, он начинает приобретать знания, позволяющие достичь новых высот
качества и производительности труда.
Чтобы как можно более облегчить бремя применения нескольких языков, я избегал ред#
ких возможностей языков, кроме тех фрагментов, в которых именно они и обсуждаются.
Вам не нужно понимать каждый нюанс фрагментов кода, чтобы понять их суть. Если вы
сосредоточитесь на обсуждаемых моментах, вы сможете читать код на любом языке. Что#
бы сделать вашу задачу еще легче, я пояснил важные части примеров.
Доступ к другим источникам информации В данном руководстве приводятся под#
робные сведения о конструировании ПО, но едва ли это последнее слово. В разделах «До#
полнительные ресурсы» я указал другие книги и статьи, которые вы можете прочитать, если
заинтересуетесь той или иной темой.
Web'сайт книги Обновленные контрольные списки, списки
книг и журнальных статей, Web#ссылки и другую информацию
можно найти на Web#сайте cc2e.com. Для получения информации,
связанной с «Code Complete, 2d ed.», введите в браузере cc2e.com/
и четырехзначное число, пример которого показан слева. Читая книгу, вы много раз на#
толкнетесь на такие ссылки.
http://cc2e.com/1234

Что побудило меня написать эту книгу?
Необходимость руководств, отражающих знания об эффективных методиках разработки
ПО, ясна всем членам сообщества разработчиков. Согласно отчету совета Computer Science
and Technology Board максимальное повышение качества и продуктивности разработки
ПО будет достигнуто благодаря систематизации, унификации и распространению суще#
ствующих знаний об эффективных методиках разработки (CSTB, 1990; McConnell, 1997a).
Совет пришел к выводу, что стратегия распространения этих знаний должна быть осно#
вана на концепции руководств по разработке ПО.

Тема конструирования игнорировалась
Одно время разработка ПО и кодирование рассматривались как одно и то же. Однако по
мере идентификации разных процессов цикла разработки ПО лучшие умы отрасли стали
посвящать время анализу и обсуждению методик управления проектами, выработки тре#
бований, проектирования и тестирования. Из#за пристального внимания к этим новым
областям конструирование кода превратилось в бедного родственника разработки ПО.
Кроме того, обсуждению конструирования препятствовало предположение, согласно ко#
торому подход к конструированию как к отдельному процессу разработки ПО подразуме#
вает, что конструирование нужно рассматривать при этом как отдельный этап. На самом
деле процессы и этапы разработки не обязаны быть связаны какими#то отношениями, и
обсуждение процесса конструирования полезно независимо от того, выполняются ли другие
процессы разработки ПО как этапы, итерации или как#то иначе.

Конструирование важно
Другая причина того, что конструирование игнорируется учеными и авторами, заключа#
ется в ошибочной идее, что в сравнении с другими процессами разработки ПО констру#
ирование является относительно механическим процессом, допускающим мало возмож#
ностей улучшения. Ничто не может быть дальше от истины.
На конструирование кода обычно приходятся около 65% работы в небольших и 50% в
средних проектах. Во время конструирования допускаются около 75% ошибок в неболь#

Предисловие

XVII

ших проектах и от 50 до 75% в средних и крупных. Очевидно, что любой процесс, связан#
ный с такой долей ошибок, можно значительно улучшить (подробнее эти статистические
данные рассматриваются в главе 27).
Некоторые авторы указывают, что, хотя ошибки конструирования и составляют высокий
процент от общего числа ошибок, их обычно дешевле исправлять, чем ошибки в требо#
ваниях или архитектуре, поэтому они менее важны. Утверждение, что ошибки конструи#
рования дешевле исправлять, верно, но вводит в заблуждение, потому что стоимость не#
исправленной ошибки конструирования может быть крайней высокой. Ученые обнару#
жили, что одними из самых дорогих ошибок в истории, приведшими к убыткам в сотни
миллионов долларов, были мелкие ошибки кодирования (Weinberg, 1983; SEN, 1990). Не#
высокая стоимость исправления ошибок не подразумевает, что их исправление можно
считать низкоприоритетной задачей.
Ирония ослабления внимания к конструированию состоит в том, что конструирование
— единственный процесс, который выполняется всегда. Требования можно предположить,
а не разработать, архитектуру можно обрисовать в самых общих чертах, а тестирование
можно сократить или вообще опустить. Но если вы собираетесь написать программу, из#
бежать конструирования не удастся, и это делает конструирование на редкость плодотвор#
ной областью улучшения методик разработки.

Отсутствие похожих книг
Когда я начал подумывать об этой книге, я был уверен, что кто#то другой уже написал об
эффективных методиках конструирования. Необходимость такой книги казалась очевид#
ной. Но я обнаружил лишь несколько книг о конструировании, описывающих лишь неко#
торые его аспекты. Одни были написаны 15 или более лет назад и были основаны на от#
носительно редких языках, таких как ALGOL, PL/I, Ratfor и Smalltalk. Другие были написа#
ны профессорами, не работавшими над реальным кодом. Профессора писали о методи#
ках, работающих в студенческих проектах, но часто не имели представления о том, как
эти методики проявят себя вполномасштабных средах разработки. В третьих книгах ав#
торы рекламировали новейшие методологии, игнорируя многие зрелые методики, эффек#
тивность которых прошла проверку временем.
Короче говоря, я не смог найти ни одной книги, автор которой
Когда вместе собираются крихотя бы попытался отразить в ней практические приемы програм#
тики, они говорят о Теме, Коммирования, возникшие благодаря накоплению профессионального
позиции и Идее. Когда вместе
опыта, отраслевым исследованиям и академическим изысканиям.
собираются художники, они гоОбсуждение конструирования нужно было привести в соответ#
ворят о том, где купить дешествие современным языкам программирования, объектно#ориен#
вый скипидар.
тированному программированию и ведущим методикам разработ#
ки. Ясно, что книгу о программировании должен был написать
Пабло Пикассо
человек, знакомый с последними достижениями в области теории
и в то же время создавший достаточно реального кода, чтобы хорошо представлять со#
стояние практической сферы. Я писал эту книгу как всестороннее обсуждение конструи#
рования кода, имеющее целью передачу знаний от одного программиста другому.

К читателям
Я буду рад получить от вас вопросы по темам, обсуждаемым в этой книге, сообщения об
обнаруженных ошибках, комментарии и предложения. Для связи со мной используйте адрес
stevemcc@construx.com или мой Web#сайт www.stevemcconnell.com.
Беллвью, штат Вашингтон
30 мая 2004 года

XVIII

Предисловие

Служба поддержки Microsoft Learning Technical Support
Мы приложили все усилия, чтобы обеспечить точность сведений, изложенных в этой книге.
Поправки к книгам издательства Microsoft Press публикуются в Интернете по адресу:
http://www.microsoft.com/learning/support/
Чтобы подключиться к базе знаний Microsoft и задать вопрос или запросить ту или иную
информацию, откройте страницу:
http://www.microsoft.com/learning/support/search.asp
Если у вас есть замечания, вопросы или предложения по поводу этой книги, присылайте
их в Microsoft Press по обычной почте:
Microsoft Press
Attn: Code Complete 2E Editor
One Microsoft Way
Redmond, WA 98052%6399
или по электронной почте:
mspinput@microsoft.com

Примечание издателя перевода
В книге приняты следующие условные графические обозначения:

Ключевой момент

Достоверные данные

Ужасный код

ГЛАВА 1 Добро пожаловатьв мир конструирования ПО!

XIX

Благодарности

Книги никогда не создаются в одиночку (по крайней мере это относится ко всем
моим книгам), а работа над вторым изданием — еще более коллективное пред#
приятие.
Мне хотелось бы поблагодарить всех, кто принял участие в обзоре данной книги:
это Хакон Агустссон (Hбkon Бgъstsson), Скотт Эмблер (Scott Ambler), Уилл Барнс
(Will Barns), Уильям Д. Бартоломью (William D. Bartholomew), Ларс Бергстром (Lars
Bergstrom), Ян Брокбанк (Ian Brockbank), Брюс Батлер (Bruce Butler), Джей Цин#
котта (Jay Cincotta), Алан Купер (Alan Cooper), Боб Коррик (Bob Corrick), Эл Кор#
вин (Al Corwin), Джерри Девилль (Jerry Deville), Джон Ивз (Jon Eaves), Эдвард Эс#
трада (Edward Estrada), Стив Гоулдстоун (Steve Gouldstone), Оуэйн Гриффитс (Owain
Griffiths), Мэтью Харрис (Matthew Harris), Майкл Ховард (Michael Howard), Энди
Хант (Andy Hunt), Кевин Хатчисон (Kevin Hutchison), Роб Джаспер (Rob Jasper),
Стивен Дженкинс (Stephen Jenkins), Ральф Джонсон (Ralph Johnson) и его группа
разработки архитектуры ПО из Иллинойского университета, Марек Конопка (Marek
Konopka), Джефф Лэнгр (Jeff Langr), Энди Лестер (Andy Lester), Митика Ману (Mitica
Manu), Стив Маттингли (Steve Mattingly), Гарет Маккоан (Gareth McCaughan), Ро#
берт Макговерн (Robert McGovern), Скотт Мейерс (Scott Meyers), Гарет Морган
(Gareth Morgan), Мэтт Пелокин (Matt Peloquin), Брайан Пфладж (Bryan Pflug),
Джеффри Рихтер (Jeffrey Richter), Стив Ринн (Steve Rinn), Даг Розенберг (Doug
Rosenberg), Брайан Сен#Пьер (Brian St. Pierre), Диомидис Спиннелис (Diomidis
Spinellis), Мэтт Стивенс (Matt Stephens), Дэйв Томас (Dave Thomas), Энди Томас#
Крамер (Andy Thomas#Cramer), Джон Влиссидес (John Vlissides), Павел Возенилек
(Pavel Vozenilek), Денни Уиллифорд (Denny Williford), Джек Вули (Jack Woolley) и
Ди Зомбор (Dee Zsombor).
Сотни читателей прислали комментарии к первому изданию этой книги, и еще
больше — ко второму. Спасибо всем, кто потратил время, чтобы поделиться в той
или иной форме своим мнением.
Хочу особо поблагодарить рецензентов из Construx Software, которые провели фор#
мальную инспекцию всей рукописи: это Джейсон Хиллз (Jason Hills), Брейди Хон#
сингер (Bradey Honsinger), Абдул Низар (Abdul Nizar), Том Рид (Tom Reed) и Па#
мела Перро (Pamela Perrott). Я был поистине удивлен тщательностью их обзора,
особенно если учесть, сколько глаз изучило эту книгу до того, как они начали
работать с ней. Спасибо также Брейди, Джейсону и Памеле за помощь в создании
Web#сайта cc2e.com.
Мне было очень приятно работать с Девон Масгрейв (Devon Musgrave) — редак#
тором этой книги. Я работал со многими прекрасными редакторами в других
проектах, но даже на их фоне Девон выделяется добросовестностью и легким

XX

Благодарности

характером. Спасибо, Девон! Благодарю Линду Энглман (Linda Engleman), кото#
рая поддержала идею второго издания — без нее эта книга не появилась бы. Бла#
годарю также других сотрудников издательства Microsoft Press, в их число входят
Робин ван Стинбург (Robin Van Steenburgh), Элден Нельсон (Elden Nelson), Карл
Дилтц (Carl Diltz), Джоэл Панчо (Joel Panchot), Патрисия Массерман (Patricia
Masserman), Билл Майерс (Bill Myers), Сэнди Резник (Sandi Resnick), Барбара Нор#
флит (Barbara Norfleet), Джеймс Крамер (James Kramer) и Прескотт Классен (Prescott
Klassen).
Я хочу еще раз сказать спасибо сотрудникам Microsoft Press, участвовавшим в
подготовке первого издания книги: это Элис Смит (Alice Smith), Арлен Майерс
(Arlene Myers), Барбара Раньян (Barbara Runyan), Кэрол Люк (Carol Luke), Конни
Литтл (Connie Little), Дин Холмс (Dean Holmes), Эрик Стру (Eric Stroo), Эрин О’Кон#
нор (Erin O’Connor), Джинни Макгиверн (Jeannie McGivern), Джефф Кэри (Jeff Carey),
Дженнифер Харрис (Jennifer Harris), Дженнифер Вик (Jennifer Vick), Джудит Блох
(Judith Bloch), Кэтрин Эриксон (Katherine Erickson), Ким Эгглстон (Kim Eggleston),
Лиза Сэндбург (Lisa Sandburg), Лиза Теобальд (Lisa Theobald), Маргарет Харгрейв
(Margarite Hargrave), Майк Халворсон (Mike Halvorson), Пэт Фоджетт (Pat Forgette),
Пегги Герман (Peggy Herman), Рут Петтис (Ruth Pettis), Салли Брунсмен (Sally
Brunsman), Шон Пек (Shawn Peck), Стив Мюррей (Steve Murray), Уоллис Болц (Wallis
Bolz) и Заафар Хаснаин (Zaafar Hasnain).
Наконец, я хотел бы выразить благодарность рецензентам, внесшим такой боль#
шой вклад в первое издание книги: это Эл Корвин (Al Corwin), Билл Кистлер (Bill
Kiestler), Брайан Догерти (Brian Daugherty), Дэйв Мур (Dave Moore), Грег Хичкок
(Greg Hitchcock), Хэнк Меуре (Hank Meuret), Джек Вули (Jack Woolley), Джой Уай#
рик (Joey Wyrick), Марго Пейдж (Margot Page), Майк Клейн (Mike Klein), Майк
Зевенберген (Mike Zevenbergen), Пэт Форман (Pat Forman), Питер Пэт (Peter Pathe),
Роберт Л. Гласс (Robert L. Glass), Тэмми Форман (Tammy Forman), Тони Пискулли
(Tony Pisculli) и Уэйн Бердсли (Wayne Beardsley). Особо благодарю Тони Гарланда
(Tony Garland) за его обстоятельный обзор: за 12 лет я еще лучше понял, как вы#
играла эта книга от тысяч комментариев Тони.

Библиография

XXI

Контрольные списки
Требования ................................................................................................................................................................................................................. 42
Архитектура ............................................................................................................................................................................................................ 54
Предварительные условия ....................................................................................................................................................................... 59
Основные методики конструирования ................................................................................................................................... 69
Проектирование при конструировании .............................................................................................................................. 122
Качество классов ............................................................................................................................................................................................ 157
Высококачественные методы ........................................................................................................................................................... 185
Защитное программирование ......................................................................................................................................................... 211
Процесс программирования с псевдокодом .................................................................................................................. 233
Общие вопросы использования данных ............................................................................................................................. 257
Именование переменных ..................................................................................................................................................................... 288
Основные данные .......................................................................................................................................................................................... 316
Применение необычных типов данных .............................................................................................................................. 343
Организация последовательного кода .................................................................................................................................. 353
Использование условных операторов ................................................................................................................................... 365
Циклы .......................................................................................................................................................................................................................... 388
Нестандартные управляющие структуры ........................................................................................................................... 410
Табличные методы ........................................................................................................................................................................................ 429
Вопросы по управляющим структурам ................................................................................................................................. 459
План контроля качества ......................................................................................................................................................................... 476
Эффективное парное программирование ........................................................................................................................ 484
Эффективные инспекции ..................................................................................................................................................................... 491
Тесты ............................................................................................................................................................................................................................. 532
Отладка ...................................................................................................................................................................................................................... 559
Разумные причины выполнения рефакторинга ......................................................................................................... 570
Виды рефакторинга ..................................................................................................................................................................................... 577
Безопасный рефакторинг ..................................................................................................................................................................... 584
Стратегии оптимизации кода .......................................................................................................................................................... 607
Методики оптимизации кода ........................................................................................................................................................... 642
Управление конфигурацией .............................................................................................................................................................. 669
Интеграция ............................................................................................................................................................................................................ 707
Инструменты программирования ............................................................................................................................................... 724
Форматирование ............................................................................................................................................................................................. 773
Самодокументирующийся код ........................................................................................................................................................ 780
Хорошие методики комментирования .................................................................................................................................. 816

Часть I

ОСНОВЫ РАЗРАБОТКИ ПО



Глава 1. Добро пожаловать в мир конструирования ПО!



Глава 2. Метафоры, позволяющие лучше понять
разработку ПО



Глава 3. Семь раз отмерь, один раз отрежь:
предварительные условия



Глава 4. Основные решения, которые приходится
принимать при конструировании

ЧАСТЬ I

2

Г Л А В А

Основы разработки ПО

1

Добро пожаловать в мир
конструирования ПО!

http://cc2e.com/0178

Содержание
 1.1. Что такое конструирование ПО?
 1.2. Почему конструирование ПО так важно?
 1.3. Как читать эту книгу

Связанные темы
 Кому следует прочитать эту книгу? (см. предисловие)
 Какую выгоду можно извлечь, прочитав эту книгу? (см. предисловие)
 Что побудило меня написать эту книгу? (см. предисловие)

Значение слова «конструирование» вне контекста разработки ПО известно всем: это
то, что делают строители при сооружении жилого дома, школы или небоскреба.
В детстве вы наверняка собирали разные предметы из «конструктора». Вообще под
«конструированием» понимают процесс создания какого#нибудь объекта. Этот про#
цесс может включать некоторые аспекты планирования, проектирования и тес#
тирования, но чаще всего «конструированием» называют практическую часть
создания чего#либо.

1.1.

Что такое конструирование ПО?

Разработка ПО — непростой процесс, который может включать множество ком#
понентов. Вот какие составляющие разработки ПО определили ученые за после#
дние 25 лет:
 определение проблемы;
 выработка требований;
 создание плана конструирования;
 разработка архитектуры ПО, или высокоуровневое проектирование;
 детальное проектирование;
 кодирование и отладка;

ГЛАВА 1 Добро пожаловать в мир конструирования ПО!

3

 блочное тестирование;
 интеграционное тестирование;
 интеграция;
 тестирование системы;
 корректирующее сопровождение.

Если вы работали над неформальными проектами, то можете подумать, что этот
список весьма бюрократичен. Если вы работали над слишком формальными про#
ектами, вы это знаете! Достичь баланса между слишком слабым и слишком силь#
ным формализмом нелегко — об этом мы еще поговорим.
Если вы учились программировать самостоятельно или работали преимущественно
над неформальными проектами, вы, возможно, многие действия по созданию
продукта объединили в одну категорию «программирование». Если вы работаете
над неформальными проектами, то скорее всего, думая о создании ПО, вы пред#
ставляете себе тот процесс, который ученые называют «конструированием».
Такое интуитивное представление о «конструировании» довольно верно, однако
оно страдает от недостатка перспективы. Поэтому конструирование целесообразно
рассматривать в контексте других процессов: это помогает сосредоточиться на
задачах конструирования и уделять адекватное внимание другим важным действи#
ям, к нему не относящимся. На рис. 1#1 показано место конструирования среди
других процессов разработки ПО.

Рис. 1'1. Процессы конструирования изображены внутри серого эллипса.
Главными компонентами конструирования являются кодирование и отладка,
однако оно включает и детальное проектирование, блочное тестирование,
интеграционное тестирование и другие процессы

Как видите, конструирование состоит преимущественно из кодирования
и отладки, однако включает и детальное проектирование, создание пла#
на конструирования, блочное тестирование, интеграцию, интеграцион#

4

ЧАСТЬ I

Основы разработки ПО

ное тестирование и другие процессы. Если бы эта книга была посвящена всем ас#
пектам разработки ПО, в ней было бы приведено сбалансированное обсуждение
всех процессов. Однако это руководство по методам конструирования, так что
остальных тем я почти не касаюсь. Если бы эта книга была собакой, она тщатель#
но принюхивалась бы к конструированию, виляла хвостом перед проектирова#
нием и тестированием и лаяла на прочие процессы.
Иногда конструирование называют «кодированием» или «программированием».
«Кодирование» кажется мне в данном случае не лучшим термином, так как он
подразумевает механическую трансляцию разработанного плана в команды язы#
ка программирования, тогда как конструирование вовсе не механический про#
цесс и часто связано с творчеством и анализом. Смысл слов «программирование»
и «конструирование» кажется мне похожим, и я буду использовать их как равно#
правные.
На рис. 1#1 разработка ПО была изображена в «плоском» виде; более точным от#
ражением содержания этой книги является рис. 1#2.

Рис. 1'2. Кодирование и отладка, детальное проектирование, создание плана
конструирования, блочное тестирование, интеграция, интеграционное тестирова%
ние и другие процессы обсуждаются в данной книге примерно в такой пропорции

На рис. 1#1 и 1#2 процессы конструирования представлены с общей точки зре#
ния. Но что можно сказать об их деталях? Вот некоторые конкретные задачи, свя#
занные с конструированием:
 проверка выполнения условий, необходимых для успешного конструирования;
 определение способов последующего тестирования кода;
 проектирование и написание классов и методов;
 создание и присвоение имен переменным и именованным константам;
 выбор управляющих структур и организация блоков команд;

ГЛАВА 1 Добро пожаловать в мир конструирования ПО!

5

 блочное тестирование, интеграционное тестирование и отладка собственно#

го кода;
 взаимный обзор кода и низкоуровневых программных структур членами

группы;
 «шлифовка» кода путем его тщательного форматирования и комментирования;
 интеграция программных компонентов, созданных по отдельности;
 оптимизация кода, направленная на повышение его быстродействия, и снижение

степени использования ресурсов.
Еще более полное представление о процессах и задачах конструирования вы
получите, просмотрев содержание книги.
Конструирование включает так много задач, что вы можете спросить: «Ладно, а
что не является частью конструирования?» Хороший вопрос. В конструирование
не входят такие важные процессы, как управление, выработка требований, разра#
ботка архитектуры приложения, проектирование пользовательского интерфейса,
тестирование системы и ее сопровождение. Все они не меньше, чем конструиро#
вание, влияют на конечный успех проекта — по крайней мере любого проекта,
который требует усилий более одного#двух человек и длится больше нескольких
недель. Все эти процессы стали предметом хороших книг, многие из которых я
указал в разделах «Дополнительные ресурсы» и в главе 35.

1.2.

Почему конструирование ПО так важно?

Раз уж вы читаете эту книгу, вы наверняка понимаете важность улучшения каче#
ства ПО и повышения производительности труда разработчиков. Многие из са#
мых удивительных современных проектов основаны на применении ПО: Интер#
нет и спецэффекты в кинематографе, медицинские системы жизнеобеспечения
и космические программы, высокопроизводительный анализ финансовых данных
и научные исследования. Эти, а также более традиционные проекты имеют мно#
го общего, поэтому применение улучшенных методов программирования окупится
во всех случаях.
Признавая важность улучшения разработки ПО в целом, вы можете спросить:
«Почему именно конструированию в этой книге уделяется такое внимание?»
Ответы на этот вопрос приведены ниже.
Конструирование — крупная часть процесса разра'
ботки ПО В зависимости от размера проекта на конст#
руирование обычно уходит 30–80 % общего времени работы.
Все, что занимает так много времени работы над проектом,
неизбежно влияет на его успешность.

Перекрестная ссылка О связи
между размером проекта и долей времени, уходящего на конструирование, см. подраздел
«Соотношение между выполняемыми операциями и размер»
раздела 27.5.

Конструирование занимает центральное место в про'
цессе разработки ПО Требования к приложению и его
архитектура разрабатываются до этапа конструирования, чтобы гарантировать его
эффективность. Тестирование системы (в строгом смысле независимого тестиро#
вания) выполняется после конструирования и служит для проверки его правиль#
ности. Конструирование — центр процесса разработки ПО.

ЧАСТЬ I

6

Основы разработки ПО

Повышенное внимание к конструированию может
намного повысить производительность труда от'
дельных программистов В своем классическом иссле#
довании Сэкман, Эриксон и Грант показали, что произво#
дительность труда отдельных программистов во время кон#
струирования изменяется в 10–20 раз (Sackman, Erikson, and Grant, 1968). С тех
пор эти данные были подтверждены другими исследованиями (Curtis, 1981; Mills,
1983; Curtis et al., 1986; Card, 1987; Valett and McGarry, 1989; DeMarco and Lister,
1999№; Boehm et al., 2000). Эта книга поможет всем программистам изучить ме#
тоды, которые уже используются лучшими разработчиками.

Перекрестная ссылка О производительности труда программистов см. подраздел «Индивидуальные различия» раздела 28.5.

Результат конструирования — исходный код — часто является единствен'
ным верным описанием программы Зачастую единственным видом доступ#
ной программистам документации является сам исходный код. Спецификации тре#
бований и проектная документация могут устареть, но исходный код актуален
всегда, поэтому он должен быть максимально качественным. Последовательное при#
менение методов улучшения исходного кода — вот что отличает детальные, кор#
ректные и поэтому информативные программы от устройств Руба Голдберга1 . Эф#
фективнее всего применять эти методы на этапе конструирования.
Конструирование — единственный процесс, который выполняется
во всех случаях Идеальный программный проект до начала конструи#
рования проходит стадии тщательной выработки требований и проекти#
рования архитектуры. После конструирования в идеале должно быть выполнено ис#
черпывающее, статистически контролируемое тестирование системы. Однако в ре#
альных проектах нашего несовершенного мира разработчики часто пропускают
этапы выработки требований и проектирования, начиная прямо с конструирова#
ния программы. Тестирование также часто выпадает из расписания из#за огромно#
го числа ошибок и недостатка времени. Но каким бы срочным или плохо сплани#
рованным ни был проект, куда без конструирования деться? Так что повышение эф#
фективности конструирования ПО позволяет оптимизировать любой проект, ка#
ким бы несовершенным он ни был.

1.3.

Как читать эту книгу

Вы можете читать эту книгу от корки до корки или по отдельным темам. Если вы
предпочитаете первый вариант, переходите к главе 2. Если второй — можете на#
чать с главы 6 и переходить по перекрестным ссылкам к другим темам, которые
вас заинтересуют. Если вы не уверены, какой из этих вариантов вам подходит,
начните с раздела 3.2.

1

Голдберг, Рубен Лушес («Руб») [Goldberg, «Rube» (Reuben Lucius)] (1883–1970) — карика#
турист, скульптор. Известен своими карикатурами, в которых выдуманное им сложное обору#
дование («inventions») выполняет примитивные и никому не нужные операции. Лауреат Пулит#
церовской премии 1948 г. за политические карикатуры. — Прим. перев.

ГЛАВА 1 Добро пожаловать в мир конструирования ПО!

7

Ключевые моменты
 Конструирование — главный этап разработки ПО, без которого не обходится

ни один проект.
 Основные этапы конструирования: детальное проектирование, кодирование,

отладка, интеграция и тестирование приложения разработчиками (блочное
тестирование и интеграционное тестирование).
 Конструирование часто называют «кодированием» и «программированием».
 От качества конструирования во многом зависит качество ПО.
 В конечном счете ваша компетентность в конструировании ПО определяет то,

насколько хороший вы программист. Совершенствованию ваших навыков и
посвящена оставшаяся часть этой книги.

ЧАСТЬ I

8

Г Л А В А

Основы разработки ПО

2

Метафоры, позволяющие
лучше понять разработку ПО

http://cc2e.com/0278

Содержание
 2.1. Важность метафор
 2.2. Как использовать метафоры?
 2.3. Популярные метафоры, характеризующие разработку ПО

Связанная тема
 Эвристика при проектировании: подраздел «Проектирование — эвристический

процесс» в разделе 5.1
Терминология компьютерных наук — одна из самых красочных. Действительно,
в какой еще области существуют стерильные комнаты с тщательно контролируе#
мой температурой, заполненные вирусами, троянскими конями, червями, жучка#
ми и прочей живностью и нечистью?
Все эти яркие метафоры описывают специфические аспекты мира программиро#
вания. Более общие явления характеризуются столь же красочными метафорами,
позволяющих лучше понять процесс разработки ПО.
Остальная часть книги не зависит от обсуждения метафор в этой главе. Можете
пропустить ее, если хотите быстрее добраться до практических советов. Если хотите
яснее представлять разработку ПО, читайте дальше.

2.1.

Важность метафор

Проведение аналогий часто приводит к важным открытиям. Сравнив не совсем
понятное явление с чем#то похожим, но более понятным, вы можете догадаться,
как справиться с проблемой. Такое использование метафор называется «модели#
рованием».
История науки полна открытий, сделанных благодаря метафорам. Так, химик Ке#
куле однажды во сне увидел змею, схватившую себя за хвост. Проснувшись, он
понял, что свойства бензола объяснила бы молекулярная структура, имеющая
похожую кольцевую форму. Дальнейшие эксперименты подтвердили его гипоте#
зу (Barbour, 1966).

ГЛАВА 2 Метафоры, позволяющие лучше понять разработку ПО

9

Кинетическая теория газов была создана на основе модели «бильярдных шаров»,
согласно которой молекулы газа, подобно бильярдным шарам, имеют массу и
совершают упругие соударения.
Волновая теория света была разработана преимущественно путем исследования
сходств между светом и звуком. И свет, и звук имеют амплитуду (яркость — гром#
кость), частоту (цвет — высота) и другие общие свойства. Сравнение волновых
теорий звука и света оказалось столь продуктивным, что ученые потратили мно#
го сил, пытаясь обнаружить среду, которая распространяла бы свет, как воздух
распространяет звук. Они даже дали этой среде название — «эфир», но так и не
смогли ее обнаружить. В данном случае аналогия была такой убедительной, что
ввела ученых в заблуждение.
В целом эффективность моделей объясняется их яркостью и концептуальной
целостностью. Модели подсказывают ученым свойства, отношения и перспективные
области исследований. Иногда модели вводят в заблуждение; как правило, к это#
му приводит чрезмерное обобщение метафоры. Поиск эфира — наглядный при#
мер чрезмерного обобщения модели.
Разумеется, некоторые метафоры лучше других. Хорошими метафорами можно
считать те, что отличаются простотой, согласуются с другими релевантными мета#
форами и объясняют многие экспериментальные данные и наблюдаемые явления.
Рассмотрим, к примеру, колебания камня, подвешенного на веревке. До Галилея
сторонники Аристотеля считали, что тяжелый объект перемещается из верхней
точки в нижнюю, переходя в состояние покоя. В данном случае они подумали бы,
что камень падает, но с осложнениями. Когда Галилей смотрел на раскачивающийся
камень, он видел маятник, поскольку камень снова и снова повторял одно и то
же движение.
Указанные модели фокусируют внимание на совершенно разных факторах. Пос#
ледователи Аристотеля, рассматривавшие раскачивающийся камень как падающий
объект, принимали в расчет вес камня, высоту, на которую он был поднят, и время,
проходящее до достижения камнем состояния покоя. В модели Галилея важными
были другие факторы. Он обращал внимание на вес камня, угловое смещение, ра#
диус и период колебаний маятника. Благодаря этому Галилей открыл законы, кото#
рые последователи Аристотеля открыть не смогли, так как их модель заставила их
наблюдать за другими явлениями и задавать другие вопросы.
Аналогичным образом метафоры способствуют и лучшему пониманию вопросов
разработки ПО. Во время лекции по случаю получения премии Тьюринга в 1973 г.,
Чарльз Бахман (Charles Bachman) упомянул переход от доминировавшего геоцен#
трического представления о Вселенной к гелиоцентрическому. Геоцентрическая
модель Птолемея не вызывала почти никаких сомнений целых 1400 лет. Затем, в
1543 г., Коперник выдвинул гелиоцентрическую теорию, предположив, что цент#
ром Вселенной на самом деле является Солнце, а не Земля. В конечном итоге та#
кое изменение умозрительных моделей привело к открытию новых планет, ис#
ключению Луны из категории планет и переосмыслению места человечества во
Вселенной.

10

ЧАСТЬ I

Основы разработки ПО

Не стоит недооценивать важность метафор. Метафоры имеют одно неоспоримое достоинство: описываемое ими поведение предсказуемо и понятно
всем людям. Это сокращает
объем ненужной коммуникации,
способствует достижению взаимопонимания и ускоряет обучение. По сути метафора — это
способ осмысления и абстрагирования концепций, позволяющий думать в более высокой
плоскости и избегать низкоуровневых ошибок.

Переход от геоцентрического представления к гелиоцент#
рическому в астрономии Бахман сравнил с изменением,
происходившим в программировании в начале 1970#х. В это
время центральное место в моделях обработки данных стали
отводить не компьютерам, а базам данных. Бахман указал,
что создатели ранних моделей стремились рассматривать
все данные как последовательный поток карт, «протекаю#
щий» через компьютер (компьютеро#ориентированный
подход). Суть изменения заключалась в отведении централь#
ного места пулу данных, над которыми компьютер выпол#
няет некоторые действия (подход, ориентированный на БД).

Сегодня почти невозможно найти человека, считающего, что
Солнце вращается вокруг Земли. Столь же трудно предста#
Фернандо Дж. Корбати
вить программиста, который бы думал, что все данные мож#
(Fernando J. Corbatу)
но рассматривать как последовательный поток карт. После
опровержения старых теорий трудно понять, как кто#то
когда#то мог в них верить. Еще удивительнее то, что приверженцам этих старых
теорий новые теории казались такими же нелепыми, как нам старые.
Геоцентрическое представление о Вселенной мешало астрономам, которые сохра#
нили верность этой теории после появления лучшей. Аналогично компьютеро#
ориентированное представление о компьютерной вселенной тянуло назад ком#
пьютерных ученых, которые продолжили придерживаться его после появления
теории, ориентированной на БД.
Иногда люди упрощают суть метафор. На каждый из описанных примеров так и
тянет ответить: «Разумеется, правильная метафора более полезна. Другая метафора
была неверной!» Так#то оно так, но это слишком упрощенное представление. Ис#
тория науки — не серия переходов от «неверных» метафор к «верным». Это серия
переходов от «менее хороших» метафор к «лучшим», от менее полных теорий к более
полным, от адекватного описания одной области к адекватному описанию другой.
В действительности многие модели, на смену которым приходят лучшие модели,
сохраняют свою полезность. Так, инженеры до сих пор решают большинство своих
задач, опираясь на ньютонову динамику, хотя в теоретической физике ее вытес#
нила теория Эйнштейна.
Разработка ПО — относительно молодая область науки. Она еще недостаточно
зрелая, чтобы иметь набор стандартных метафор. Поэтому она включает массу
второстепенных и противоречивых метафор. Одни из них лучше другие — хуже.
Оттого, насколько хорошо вы понимаете метафоры, зависит и ваше понимание
разработки ПО.

2.2.

Как использовать метафоры?

Метафора, характеризующая разработку ПО, больше похожа на про#
жектор, чем на дорожную карту. Она не говорит, где найти ответ —
она говорит, как его искать. Метафора — это скорее эвристический
подход, а не алгоритм.

ГЛАВА 2 Метафоры, позволяющие лучше понять разработку ПО

11

Алгоритмом называют последовательность четко определенных команд, которые
необходимо выполнить для решения конкретной задачи. Алгоритм предсказуем,
детерминирован и не допускает случайностей. Алгоритм говорит, как пройти из
точки А в точку Б не дав крюку, без посещения точек В, Г и Д и без остановок на
чашечку кофе.
Эвристика — это метод, помогающий искать ответ. Результаты его применения
могут быть в некоторой степени случайными, потому что эвристика указывает
только способ поиска, но не говорит, что искать. Она не говорит, как дойти пря#
мо из точки А в точку Б; даже положение этих точек может быть неизвестно. Эв#
ристика — это алгоритм в шутовском наряде. Она менее предсказуема, более за#
бавна и поставляется без 30#дневной гарантии с возможностью возврата денег.
Вот алгоритм, позволяющий добраться до чьего#то дома: поезжайте по шоссе 167
на юг до городка Пюиолап. Сверните на аллею Сауз#Хилл, а дальше 4,5 мили вверх
по холму. Поверните у продуктового магазина направо, а на следующем перекре#
стке — налево. Доехав до дома 714, расположенного на левой стороне улицы,
остановитесь и выходите из автомобиля.
А эвристическое правило может быть таким: найдите наше
последнее письмо. Езжайте в город, указанный на конвер#
те. Оказавшись в этом городе, спросите кого#нибудь, где
находится наш дом. Все нас знают — кто#нибудь с радос#
тью вам поможет. Если никого не встретите, позвоните нам
из телефона#автомата, и мы за вами приедем.

Перекрестная ссылка Об использовании эвристики при
проектировании ПО см. подраздел «Проектирование — эвристический процесс» раздела 5.1.

Различия между алгоритмом и эвристикой тонки, и в чем#то эти два понятия пе#
рекрываются. В этой книге основным различием между ними я буду считать сте#
пень косвенности решения. Алгоритм предоставляет вам сами команды. Эвристика
сообщает вам, как обнаружить команды самостоятельно или по крайней мере где
их искать.
Наличие директив, точно определяющих способ решения проблем программи#
рования, непременно сделало бы программирование более легким, а результаты
более предсказуемыми. Но наука программирования еще не продвинулась так
далеко, а может, этого никогда и не случится. Самая сложная часть программиро#
вания — концептуализация проблемы, и многие ошибки программирования яв#
ляются концептуальными. С концептуальной точки зрения каждая программа
уникальна, поэтому трудно или даже невозможно разработать общий набор ди#
ректив, приводящих к решению во всех случаях. Так что знание общего подхода
к проблемам не менее, а то и более ценно, чем знание точных решений конкрет#
ных проблем.
Как использовать метафоры, характеризующие разработку ПО? Используйте так,
чтобы лучше разобраться в проблемах и процессах программирования, чтобы они
помогали вам размышлять о выполняемых действиях и придумывать лучшие ре#
шения задач. Конечно, вы не сможете взглянуть на строку кода и сказать, что она
противоречит одной из метафор, описываемых в этой главе. Но со временем тот,
кто использует метафоры, лучше поймет программирование и будет быстрее со#
здавать более эффективный код, чем тот, кто их не использует.

12

ЧАСТЬ I

2.3.

Популярные метафоры,
характеризующие разработку ПО

Основы разработки ПО

Множество метафор, описывающих разработку ПО, смутит кого угодно. Дэвид Грайс
утверждает, что написание ПО — это наука (Gries, 1981). Дональд Кнут считает это
искусством (Knuth, 1998). Уоттс Хамфри говорит, что это процесс (Humphrey, 1989).
Ф. Дж. Плоджер и Кент Бек утверждают, что разработка ПО похожа на управле#
ние автомобилем, однако приходят к почти противоположным выводам (Plauger,
1993, Beck, 2000). Алистер Кокберн сравнивает разработку ПО с игрой (Cockburn,
2002), Эрик Реймонд — с базаром (Raymond, 2000), Энди Хант (Andy Hunt) и Дэйв
Томас (Dave Thomas) — с работой садовника, Пол Хекель — со съемкой фильма
«Белоснежка и семь гномов» (Heckel, 1994). Фред Брукс упоминает фермерство,
охоту на оборотней и динозавров, завязших в смоляной яме (Brooks, 1995). Ка#
кие метафоры самые лучшие?

Литературная метафора: написание кода
Самая примитивная метафора, описывающая разработку ПО, берет начало в вы#
ражении «написание кода». Согласно литературной метафоре разработка програм#
мы похожа на написание письма: вы садитесь за стол, берете бумагу, перо и пи#
шете письмо с начала до конца. Это не требует никакого формального планиро#
вания, а мысли, выражаемые в письме, формулируются автором по ходу дела.
На этой метафоре основаны и многие другие идеи. Джон Бентли (Jon Bentley)
говорит, что программист должен быть способен сесть у камина со стаканом брен#
ди, хорошей сигарой, охотничьей собакой у ног и «сформулировать программу»
подобно тому, как писатели создают романы. Брайан Керниган и Ф. Дж. Плоджер
назвали свою книгу о стиле программирования «The Elements of Programming Style»
(Kernighan and Plauger, 1978), обыгрывая название книги о литературном стиле
«The Elements of Style» (Strunk and White, 2000). Программисты часто говорят об
«удобочитаемости программы».
Индивидуальную работу над небольшими проектами метафора написа#
ния письма характеризует довольно точно, но в целом она описывает
разработку ПО неполно и неадекватно. Письма и романы обычно принад#
лежат перу одного человека, тогда как над программами обычно работают группы
людей с разными сферами ответственности. Закончив писать письмо, вы запечаты#
ваете его в конверт и отправляете. С этого момента изменить вы его не можете, и
письмо во всех отношениях является завершенным. Изменить ПО не так уж трудно,
и вряд ли работу над ним можно когда#нибудь признать законченной. Из общего
объема работы над типичной программной системой две трети обычно выполня#
ются после выпуска первой версии программы, а иногда эта цифра достигает целых
90 % (Pigoski, 1997). В литературе поощряется оригинальность. При конструирова#
нии ПО оригинальный подход часто оказывается менее эффективным, чем повтор#
ное использование идей, кода и тестов из предыдущих проектов. Словом, процесс
разработки ПО, соответствующий литературной метафоре, является слишком про#
стым и жестким, чтобы быть полезным.

ГЛАВА 2 Метафоры, позволяющие лучше понять разработку ПО

13

К сожалению, литературная метафора была увековечена в одной из самых попу#
лярных книг по разработке ПО — книге Фреда Брукса «The Mythical Man#Month»
(«Мифический человеко#месяц») (Brooks, 1995). Брукс пишет: «Планируйте вы#
бросить первый экземпляр программы: вам в любом случае придется это сделать».
Перед глазами невольно возникает образ мусорного ведра, полного черновиков
(рис. 2#1).

Рис. 2'1. Литературная метафора наводит на мысль, что процесс
разработки ПО основан на дорогостоящем методе проб и ошибок,
а не на тщательном планировании и проектировании

Подобный подход может быть практичным, если вы пише#
Планируйте выбросить первый
те банальное письмо своей тетушке. Однако расширение
экземпляр программы: вам в люметафоры «написания» ПО вплоть до выбрасывания первого
бом случае придется это сделать.
экземпляра программы — не лучший совет в мире разработ#
Фред Брукc
ки ПО, где крупная система по стоимости уже сравнялась с
10#этажным офисным зданием или океанским лайнером.
Если вы планируете выбросить
Конечно, это не имело бы значения, если б вы имели бес#
первый экземпляр программы,
вы выбросите и второй.
конечные запасы времени и средств. Однако реальные ус#
Крейг Зеруни (Craig Zerouni)
ловия таковы, что разработчики должны создавать програм#
мы с первого раза или хотя бы минимизировать объем до#
полнительных расходов в случае неудач. Другие метафоры лучше иллюстрируют
достижение таких целей.

Сельскохозяйственная метафора: выращивание системы
Некоторые разработчики заявляют, что создание ПО следует рассматривать по
аналогии с выращиванием сельскохозяйственных культур. Вы проектируете от#
дельный блок, кодируете его, тестируете и добавляете в систему, чуть расширяя с
каждым разом ее функциональность. Такое разбиение задачи на множество не#
больших действий позволяет минимизировать проблемы, с которыми можно стол#
кнуться на каждом этапе.
Иногда хороший метод описывается плохой метафорой. В таких случа#
ях попытайтесь сохранить метод и обнаружить лучшую метафору. В дан#
ном случае инкрементный подход полезен, но сельскохозяйственная
метафора просто ужасна.

14

ЧАСТЬ I

Основы разработки ПО

Возможно, идея выполнения небольшого объема работы
зараз и напоминает рост растений, но сельскохозяйствен#
ная аналогия неубедительна и неинформативна, к тому же
ее легко заменить лучшими метафорами, которые описаны
ниже. Сельскохозяйственную метафору трудно расширить
за пределы идеи выполнения чего#либо небольшими час#
тями. Ее приверженцы (рис. 2#2) рискуют в итоге заговорить
об удобрении плана системы, прореживании детального
проекта, повышении урожайности кода путем оптимизации землеустройства и
уборке урожая самого кода. Вы начнете говорить о чередовании посевов C++ и о
том, что было бы неплохо оставить систему под паром для повышения концент#
рации азота на жестком диске.

Дополнительные сведения О другой сельскохозяйственной метафоре, употребляемой в контексте сопровождения ПО, см. главу «On the Origins of Designer
Intuition» книги «Rethinking Systems Analysis and Design» (Weinberg, 1988).

Слабость данной метафоры заключается в предположении, что у вас нет прямого
контроля над развитием ПО. Вы просто сеете семена кода весной и, если на то
будет воля Великой Тыквы, осенью получите невиданный урожай кода.

Рис. 2'2. Нелегко адекватно расширить сельскохозяйственную метафору
на область разработки ПО

Метафора жемчужины: медленное приращение системы
Иногда, говоря о выращивании ПО, на самом деле имеют в виду приращение, или
аккрецию (accretion). Две этих метафоры тесно связаны, но вторая более убеди#
тельна. Приращение характеризует процесс формирования жемчужины за счет
отложения небольших объемов карбоната кальция. В геологии и юриспруденции
под аккрецией понимают увеличение территории суши посредством отложения
содержащихся в воде пород.
Это не значит, что вы должны освоить создание кода из оса#
дочных пород; это означает, что вы должны научиться добав#
лять в программные системы по небольшому фрагменту за
раз. Другими словами, которые в связи с этим приходят на
ум, являются термины «инкрементный», «итеративный», «адап#
тивный» и «эволюционный». Инкрементное проектирование, конструирование и
тестирование — одни из самых эффективных концепций разработки ПО.

Перекрестная ссылка О применении инкрементных стратегий
при интеграции системы см.
раздел 29.2.

При инкрементной разработке вы сначала создаете самую простую версию систе#
мы, которую можно было бы запустить. Она может не принимать реальных дан#
ных, может не выполнять над ними реальных действий, может не генерировать
реальные результаты — она должна быть просто скелетом, достаточно крепким,

ГЛАВА 2 Метафоры, позволяющие лучше понять разработку ПО

15

чтобы поддерживать реальную систему по мере ее разработки. Она может вызы#
вать поддельные классы для каждой из определенных вами основных функций. Такая
система похожа на песчинку, с которой начинается образование жемчужины.
Создав скелет, вы начинаете понемногу наращивать плоть. Каждый из фиктивных
классов вызаменяете реальным. Вместо того чтобы имитировать ввод данных, вы
пишете код, на самом деле принимающий реальные данные. А вместо имитации
вывода данных — код, на самом деле выводящий данные. Вы продолжаете добав#
лять нужные фрагменты, пока не получаете полностью рабочую систему.
Эффективность такого подхода можно подтвердить двумя впечатляющими при#
мерами. Фред Брукс, который в 1975 г. предлагал выбрасывать первый экземпляр
программы, заявил, что за десять лет, прошедших с момента написания им зна#
менитой книги «Мифический человеко#месяц», ничто не изменяло его работу и
ее эффективность так радикально, как инкрементная разработка (Brooks, 1995).
Аналогичное заявление было сделано Томом Гилбом в революционной книге
«Principles of Software Engineering Management» (Gilb, 1988), в которой он пред#
ставил метод эволюционной поставки программы (evolutionary delivery) и разрабо#
тал многие основы современного гибкого программирования (agile programming).
Многие другие современные методологии также основаны на идее инкрементной
разработки (Beck, 2000; Cockburn, 2002; Highsmith, 2002; Reifer, 2002; Martin, 2003;
Larman, 2004).
Достоинство инкрементной метафоры в том, что она не дает чрезмерных обеща#
ний. Кроме того, она не так легко поддается неуместному расширению, как сель#
скохозяйственная метафора. Раковина, формирующая жемчужину, — хороший
вариант визуализации инкрементной разработки, или аккреции.

Строительная метафора: построение ПО
Метафора «построения» ПО полезнее, чем метафоры «написания» или «вы#
ращивания» ПО, так как согласуется с идеей аккреции ПО и предостав#
ляет более детальное руководство. Построение ПО подразумевает нали#
чие стадий планирования, подготовки и выполнения, тип и степень выраженно#
сти которых зависят от конкретного проекта. При изучении этой метафоры вы
найдете и другие параллели.
Для построения метровой башни требуется твердая рука, ровная поверхность и
10 пивных банок, для башни же в 100 раз более высокой недостаточно иметь в
100 раз больше пивных банок. Такой проект требует совершенно иного плани#
рования и конструирования.
Если вы строите простой объект, скажем, собачью конуру, вы можете пойти в
хозяйственный магазин, купить доски, гвозди, и к вечеру у Фидо будет новый дом.
Если вы забудете про лаз или допустите какую#нибудь другую ошибку, ничего
страшного: вы можете ее исправить или даже начать все сначала (рис. 2#3). Все,
что вы при этом потеряете, — время. Такой свободный подход уместен и в неболь#
ших программных проектах. Если вы плохо спроектируете 1000 строк кода, то
сможете выполнить рефакторинг или даже начать проект заново, и это не приве#
дет к крупным потерям.

16

ЧАСТЬ I

Основы разработки ПО

Рис. 2'3. За ошибку, допущенную при создании простого объекта,
приходится расплачиваться лишь потраченным временем и, возможно,
некоторым разочарованием

Построить дом сложнее, и плохое проектирование при этом приводит к куда более
серьезным последствиям. Сначала вы должны решить, какой тип здания вы хоти#
те построить, что аналогично определению проблемы при разработке ПО. Затем
вы с архитектором должны разработать и утвердить общий план, что похоже на
разработку архитектуры. Далее вы чертите подробные чертежи и нанимаете бри#
гаду строителей — это аналогично детальному проектированию ПО. Вы готовите
стройплощадку, закладываете фундамент, создаете каркас дома, обшиваете его,
кроете крышу и проводите в дом все коммуникации — это похоже на конструи#
рование ПО. Когда строительство почти завершено, в дело вступают ландшафт#
ные дизайнеры, маляры и декораторы, делающие дом максимально удобным и
привлекательным. Это напоминает оптимизацию ПО. Наконец, на протяжении
всего строительства вас посещают инспекторы, проверяющие стройплощадку, фун#
дамент, электропроводку и все, что можно проверить. При разработке ПО этому
соответствуют обзоры и инспекция проекта.
И в строительстве, и в программировании увеличение сложности и масштаба
проекта сопровождается ростом цены ошибок. Конечно, для создания дома нуж#
ны довольно дорогие материалы, однако главной статьей расходов является
оплата труда рабочих. Перемещение стены на 15 см обойдется дорого не потому,
что при этом будет потрачено много гвоздей, а потому, что вам придется опла#
тить дополнительное время работы строителей. Чтобы не тратить время на ис#
правление ошибок, которых можно избежать, вы должны как можно лучше вы#
полнить проектирование (рис. 2#4). Материалы, необходимые для создания про#
граммного продукта, стоят дешевле, чем стройматериалы, однако затраты на ра#
бочую силу в обоих случаях примерно одинаковы. Изменение формата отчета
обходится ничуть не дешевле, чем перемещение стены дома, потому что главным
компонентом затрат в обоих случаях является время людей.

ГЛАВА 2 Метафоры, позволяющие лучше понять разработку ПО

Рис. 2'4.

17

Более сложные объекты требуют более тщательного планирования

Какие еще параллели можно провести между сооружением дома и разработкой
ПО? При возведении дома никто не пытается конструировать вещи, которые можно
купить. Здравомыслящему человеку и в голову не придет самостоятельно разра#
батывать и создавать стиральную машину, холодильник, шкафы, окна и двери, если
все это можно приобрести. Создавая программную систему, вы поступите так же.
Вы будете в полной мере использовать возможности высокоуровневого языка
вместо того, чтобы писать собственный код на уровне ОС. Возможно, вы исполь#
зуете также встроенные библиотеки классов#контейнеров, научные функции, клас#
сы пользовательского интерфейса и классы для работы с БД. Обычно невыгодно
писать компоненты, которые можно купить готовыми.
Однако если вы хотите построить нестандартный дом с первоклассной меблиров#
кой, мебель, возможно, придется заказать. Вы можете заказать встроенные посу#
домоечную машину и холодильник, чтобы они выглядели как часть обстановки.
Вы можете заказать окна необычных форм и размеров. Такое изготовление пред#
метов на заказ имеет параллели и в мире разработки ПО. При работе над прило#
жением высшего класса для достижения более высокой скорости и точности рас#
четов или реализации необычного интерфейса иногда приходится создавать соб#
ственные научные функции, собственные классы#контейнеры и другие компоненты.
И конструирование дома, и конструирование ПО можно оптимизировать, выполнив
адекватное планирование. Если создавать ПО в неверном порядке, его будет трудно
писать, тестировать и отлаживать. Сроки затянутся, да и весь проект может завер#
шиться неудачей из#за чрезмерной сложности отдельных компонентов, не позво#
ляющей разработчикам понять работу всей системы.
Тщательное планирование не значит исчерпывающее или чрезмерное. Вы може#
те спланировать основные структурные компоненты и позднее решать, чем по#
крыть пол, в какой цвет окрасить стены, какой использовать кровельный матери#
ал и т. д. Хорошо спланированный проект открывает больше возможностей для
изменения решения на более поздних этапах работы. Чем лучше вам известен тип
создаваемого ПО, тем больше деталей вы можете принимать как данное. Вы про#

18

ЧАСТЬ I

Основы разработки ПО

сто должны убедиться в проведении достаточного планирования, чтобы его не#
достаток не привел позднее к серьезным проблемам.
Аналогия конструирования также помогает понять, почему разные программные
проекты призывают к разным подходам разработки. Склад и медицинский центр
или ядерный реактор также требуют разных степеней планирования, проектиро#
вания и контроля качества, и никто не стал бы одинаково подходить к строитель#
ству школы, небоскреба и жилого дома с тремя спальнями. Работая над ПО, вы обыч#
но можете использовать гибкие упрощенные подходы, но иногда для обеспече#
ния безопасности и других целей необходимы и жесткие, тщательно продуман#
ные подходы.
Проблема изменения ПО приводит нас к еще одной параллели. Перемещение не#
сущей стены на 15 см обходится гораздо дороже, чем перемещение перегородки
между комнатами. Аналогично внесение структурных изменений в программу тре#
бует больших затрат, чем добавление или удаление второстепенных возможностей.
Наконец, проведение аналогии с домостроительством позволяет лучше понять ра#
боту над очень крупными программными проектами. При создании очень крупно#
го объекта цена неудачи слишком высока, поэтому объект надо спроектировать
тщательнейшим образом. Строительные организации скрупулезно разрабатывают
и инспектируют свои планы. Все крупные здания создаются с большим запасом
прочности; лучше заплатить на 10 % больше за более прочный материал, чем рис#
ковать крушением небоскреба. Кроме того, большое внимание уделяется времени.
При возведении Эмпайр Стейт Билдинг время прибытия каждого грузовика, постав#
лявшего материалы, задавалось с точностью до 15 минут. Если грузовик не прибы#
вал в нужное время, задерживалась работа над всем проектом.
Аналогично, очень крупные программные проекты требуют планирования более
высокого порядка, чем просто крупные проекты. Кейперс Джонс сообщает, что
программная система из одного миллиона строк кода требует в среднем 69 видов
документации (Jones, 1998). Спецификация требований к такой системе обычно
занимает 4000–5000 страниц, а проектная документация вполне может быть еще
в 2 или 3 раза более объемной. Маловероятно, чтобы один человек мог понять
весь проект такого масштаба или даже прочитать всю документацию, поэтому
подобные проекты требуют более тщательной подготовки.
По экономическому масштабу некоторые программные проекты сравнимы с воз#
ведением «Эмпайр Стейт Билдинг», и контролироваться они должны соответству#
ющим образом.
Метафора построения#конструирования может быть расши#
рена во многих других направлениях, именно поэтому она
столь эффективна. Благодаря этой метафоре отрасль раз#
работки ПО обогатилась многими популярными термина#
ми, такими как архитектура ПО, леса (scaffolding), констру#
ирование и фундаментальные классы. Наверное, вы сможете
назвать и другие примеры.

Дополнительные сведения Грамотные комментарии по поводу расширения метафоры конструирования см. в статье «What
Supports the Roof?» (Starr 2003).

ГЛАВА 2 Метафоры, позволяющие лучше понять разработку ПО

19

Применение методов разработки ПО:
интеллектуальный инструментарий
Люди, эффективно разрабатывающие высококачественное ПО, многие
годы посвятили сбору методов, хитростей и магических заклинаний. Эти
методы — не правила, а аналитические инструменты. Хороший рабочий
знает предназначение каждого инструмента и умеет эффективно его использовать.
Программисты не исключение. Чем больше вы узнаете о программировании, тем
больше аналитических инструментов и знаний об их своевременном и правиль#
ном использовании накапливается в вашем интеллектуальном инструментарии.
Консультанты по вопросам разработки ПО иногда совету#
Перекрестная ссылка О выбоют программистам придерживаться одних методов разра#
ре методов проектирования и их
ботки ПО в ущерб другим. Это печально, потому что, если
комбинировании см. раздел 5.3.
вы станете использовать только одну методологию, вы уви#
дите весь мир в терминах этой методологии. В некоторых случаях это сделает
недоступными для вас другие методы, лучше подходящие для решения текущей
проблемы. Метафора инструментария поможет вам держать все методы, способы
и хитрости в пределах досягаемости и применять их в уместных обстоятельствах.

Комбинирование метафор
Метафоры имеют эвристическую, а не алгоритмическую природу, поэтому
они не исключают друг друга. Вы можете использовать и метафору ак#
креции, и метафору конструирования. Если хотите, можете представлять
разработку ПО как написание письма, комбинируя эту метафору с вождением
автомобиля, охотой на оборотней или образом динозавра, увязшего в смоляной
луже. Используйте любые метафоры или их комбинации, которые стимулируют
ваше мышление или помогают общаться с другими членами группы.
Использование метафор — дело тонкое. Чтобы метафора привела вас к ценным
эвристическим догадкам, вы должны ее расширить. Но если ее расширить черес#
чур или в неверном направлении, она может ввести в заблуждение. Как и любой
мощный инструмент, метафоры можно использовать неверным образом, однако
благодаря своей мощи они могут стать ценным компонентом вашего интеллек#
туального инструментария.

Дополнительные ресурсы
Среди книг общего плана, посвященных метафорам, моде#
http://cc2e.com/0285
лям и парадигмам, главное место занимает «The Structure of
Scientific Revolutions» (3d ed. Chicago, IL: The University of
Chicago Press, 1996) Томаса Куна (Thomas S. Kuhn). В своей книге, увидевшей свет
в 1962 г., Кун рассказывает о возникновении, развитии и смене теорий. Этот труд,
вызвавший множество споров по вопросам философии науки, отличается яснос#
тью, лаконичностью и включает массу интересных примеров взлетов и падений
научных метафор, моделей и парадигм.

20

ЧАСТЬ I

Основы разработки ПО

Статья «The Paradigms of Programming». 1978 Turing Award Lecture («Communications
of the ACM», August 1979, pp. 455–60) Роберта У. Флойда (Robert W. Floyd) пред#
ставляет собой увлекательное обсуждение использования моделей при разработ#
ке ПО; некоторые аспекты рассматриваются в ней в контексте идей Томаса Куна.

Ключевые моменты
 Метафоры являются по природе эвристическими, а не алгоритмическими,

поэтому зачастую они немного небрежны.
 Метафоры помогают понять процесс разработки ПО, сопоставляя его с дру#

гими, более знакомыми процессами.
 Некоторые метафоры лучше, чем другие.
 Сравнение конструирования ПО с возведением здания указывает на необхо#

димость тщательной подготовки к проекту и проясняет различие между круп#
ными и небольшими проектами.
 Аналогия между методами разработки ПО и инструментами в интеллектуаль#

ном инструментарии программиста наводит на мысль, что в распоряжении
программистов имеется множество разных инструментов и что ни один ин#
струмент не является универсальным. Выбор правильного инструмента — одно
из условий эффективного программирования.
 Метафоры не исключают друг друга. Используйте комбинацию метафор, наи#

более эффективную в вашем случае.

ГЛАВА 2 Метафоры, позволяющие лучше понять разработку ПО

Г Л А В А

21

3

Семь раз отмерь,
один раз отрежь:
предварительные условия

Содержание
 3.1. Важность выполнения предварительных условий

http://cc2e.com/0309

 3.2. Определите тип ПО, над которым работаете
 3.3. Предварительные условия, связанные с определением проблемы
 3.4. Предварительные условия, связанные с выработкой требований
 3.5. Предварительные условия, связанные с разработкой архитектуры
 3.6. Сколько времени посвятить выполнению предварительных условий?

Связанные темы
 Основные решения, которые приходится принимать при конструировании:

глава 4
 Влияние размера проекта на предварительные условия и процесс конструи#

рования ПО: глава 27
 Связь между качеством ПО и аспектами его конструирования: глава 20
 Управление конструированием ПО: глава 28
 Проектирование ПО: глава 5

Перед началом конструирования дома строители просматривают чертежи, про#
веряют, все ли разрешения получены, и исследуют фундамент. К сооружению
небоскреба, жилого дома и собачьей конуры строители готовились бы по#разно#
му, но, каким бы ни был проект, перед началом конструирования они провели бы
добросовестную подготовку с учетом всех особенностей проекта.
В этой главе мы рассмотрим компоненты подготовки к конструированию ПО. Как
и в строительстве, конечный успех программного проекта во многом определя#
ется до начала конструирования. Если фундамент ненадежен или планирование
выполнено небрежно, на этапе конструирования вы в лучшем случае сможете
только свести вред к минимуму.

22

ЧАСТЬ I

Основы разработки ПО

Популярная у плотников поговорка «семь раз отмерь, один раз отрежь» очень
актуальна на этапе конструирования ПО, затраты на который иногда составляют
аж 65% от общего бюджета проекта. В неудачных программных проектах конст#
руирование иногда приходится выполнять дважды, трижды и даже больше. Как и
в любой другой отрасли, повторение самой дорогостоящей части программного
проекта ни к чему хорошему привести не может.
Хотя чтение этой главы является залогом успешного конструирования ПО, само
конструирование в ней не обсуждается. Если вы уже хорошо разбираетесь в цик#
ле разработки ПО или вам не терпится добраться до обсуждения конструирова#
ния, можете перейти к главе 5. Если вам не нравится идея выполнения предвари#
тельных условий конструирования, просмотрите раздел 3.2, чтобы узнать, какую
роль они играют в вашем случае, а затем вернитесь к разделу 3.1, в котором опи#
сываются расходы, связанные с их невыполнением.

3.1.

Важность выполнения предварительных
условий

Перекрестная ссылка Повышение внимания к качеству ПО —
самый эффективный способ
повышения производительности труда программистов. (см.
раздел 20.5).

Общей чертой всех программистов, создающих высокока#
чественное ПО, является использование высококачествен#
ных методов, ставящих ударение на качестве ПО в самом
начале, середине и конце проекта.

Если вы подчеркиваете качество в конце проекта, это при#
ходится на этап тестирования системы. Именно о тестиро#
вании думают многие люди, представляя процесс гарантии качества ПО, но тести#
рование — только один из компонентов стратегии гарантии качества, и не самый
важный. Тестирование не позволяет обнаружить такие ошибки, как создание не того
приложения или создание нужного приложения не тем образом; эти ошибки дол#
жны быть определены и устранены раньше — до начала конструирования.
Если вы уделяете повышенное внимание качеству в середине работы над
проектом, вы подчеркиваете методы конструирования, которым и посвя#
щена большая часть этой книги.
Если вы подчеркиваете качество в начале проекта, вы качественно выполняете пла#
нирование, определение требований и проектирование. Если вы спроектирова#
ли автомобиль «Понтиак Ацтек», то сколько бы вы его ни тестировали, он никог#
да не превратится в «Роллс#Ройс». Вы можете создать самый лучший «Ацтек», но
если вам нужен «Роллс#Ройс», это нужно планировать с самого начала. При раз#
работке ПО такому планированию соответствуют определение проблемы и
определение и проектирование решения.
Конструирование — средний этап работы, поэтому ко времени начала конструи#
рования успех проекта уже частично предопределен. И все же во время констру#
ирования вы хотя бы должны быть в состоянии определить, насколько благопо#
лучна ваша ситуация, и вернуться назад, если на горизонте показались черные тучи
неудачи. В оставшейся части этой главы я подробно расскажу, почему адекватная
подготовка к конструированию так важна и как определить, действительно ли вы
готовы перейти к нему.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

23

Актуальны ли предварительные условия
для современных программных проектов?
Порой говорят, что предварительные действия, такие как
разработка архитектуры, проектирование и планирование Выбор методологии не должен
быть невежественным. Она долпроекта, в современных условиях бесполезны. Такие заяв# жна быть основана на самом ноления не подтверждаются ни прошлыми, ни современны# вом и эффективном и дополнеми исследованиями (подробности см. ниже). Оппоненты на старым и заслуживающим
предварительных условий обычно приводят примеры не# доверия.
Харлан Миллз
удачного выполнения предварительных условий и делают
(Harlan Mills)
вывод, что такая работа неэффективна. Тем не менее под#
готовку к конструированию можно выполнить успешно, и
данные, накопленные с 1970#х, свидетельствуют о том, что в таких случаях работа
над проектом оказывается эффективнее.
Общая цель подготовки — снижение риска: адекватное планирование
позволяет исключить главные аспекты риска на самых ранних стадиях
работы, чтобы основную часть проекта можно было выполнить макси#
мально эффективно. Безусловно, главные факторы риска в создании ПО — неудач#
ная выработка требований и плохое планирование проекта, поэтому подготовка
направлена в первую очередь на оптимизацию этих этапов.
Так как подготовка к конструированию не является точной наукой, специфиче#
ский подход к снижению риска будет в значительной степени определяться осо#
бенностями проекта (см. раздел 3.2).

Причины неполной подготовки
Возможно, вам кажется, что все профессионалы знают о важности подготовки и
всегда до начала конструирования проверяют выполнение предварительных ус#
ловий. Увы, это не так.
Зачастую причина неполной подготовки к конструированию
Дополнительные сведения О проПО объясняется тем, что отвечающие за нее разработчики фессиональной программе разране имеют нужного опыта. Для планирования проекта, созда# ботки ПО, поощряющей примения адекватной бизнес#модели, разработки полных и точ# нение этих навыков, см. главу 16
ных требований и высококачественной архитектуры нуж# книги «Professional Software Development» (McConnell, 2004).
но обладать далеко не тривиальными навыками, однако
большинство разработчиков этому не обучены. Если разра#
ботчики не знают, как выполнять предварительную работу,
http://cc2e.com/0316
рекомендация «выполнять больше такой работы» не имеет
смысла: если работа изначально выполняется некачествен#
но, ее выполнение в больших объемах не принесет никакой пользы! Объяснение
выполнения этих действий не является предметом данной книги, однако в разде#
ле «Дополнительные ресурсы» в конце главы я привел массу источников, позво#
ляющих получить такой опыт.
Некоторые программисты умеют готовиться к конструированию, но пренебрега#
ют подготовкой, потому что не могут устоять перед искушением пораньше при#
ступить к кодированию. Если вы принадлежите к их числу, могу дать два совета.
Первый: прочитайте следующий раздел. Возможно, у вас откроются глаза на не#

24

ЧАСТЬ I

Основы разработки ПО

которые вещи. Второй: уделяйте внимание проблемам, с которыми сталкиваетесь.
Поработав над несколькими крупными программами, вы прекрасно поймете пользу
заблаговременного планирования. Положитесь на свой опыт.
Наконец, еще одна причина пренебрежения подготовкой к конструированию со#
стоит в том, что менеджеры прохладно относятся к программистам, которые тра#
тят на это время. Это довольно странно: такие люди, как Барри Бом (Barry Boehm),
Гради Буч (Grady Booch) и Карл Вигерс (Karl Wiegers), отстаивают важность выра#
ботки требований и проектирования уже 25 лет, и менеджеры, казалось бы, уже
должны понимать, что разработка ПО не ограничивается кодированием, но...
Несколько лет назад я работал над проектом Минобороны,
и как#то на этапе выработки требований нас посетил кура#
тор проекта — генерал. Мы сказали ему, что работаем над
требованиями: большей частью общаемся с клиентами,
определяем их потребности и разрабатываем проект при#
ложения. Он, однако, настаивал на том, чтобы увидеть код.
Мы сказали, что у нас нет кода, и тогда он отправился в ра#
бочий отдел, намереваясь хоть кого#нибудь из 100 человек поймать за програм#
мированием. Огорченный тем, что почти все из них находились не за своими ком#
пьютерами, этот крупный человек наконец указал на инженера рядом со мной и
проревел: «А он что делает? Он ведь пишет код!» Вообще#то этот инженер рабо#
тал над утилитой форматирования документов, но генерал хотел увидеть код, нашел
что#то похожее на него и хотел, чтобы хоть кто#то писал код, так что мы сказали
ему, что он прав: это код.

Дополнительные сведения Ряд
интересных вариаций на эту
тему см. в классическом труде
Джеральда Вайнберга «The Psychology of Computer Programming» (Weinberg, 1998).

Этот феномен известен как синдром WISCA или WIMP: «Why Isn’t Sam Coding
Anything? (Почему Сэм не пишет код?)» или «Why Isn’t Mary Programming (Почему
Мэри не программирует)?»
Если менеджер проекта претендует на роль бригадного генерала и приказывает
вам немедленно начать программировать, вы можете с легкостью ответить: «Есть,
сэр!» (И впрямь, какое вам дело? Умудренные опытом ветераны должны отвечать
за свои слова.) Это плохой ответ, и у вас есть несколько лучших вариантов. Во#
первых, вы можете решительно отвергнуть неэффективную методику работы. Если
у вас нормальные отношения с начальником и все в порядке с банковским сче#
том, это может сработать.
Во#вторых, вы можете притвориться, что работаете над кодом. Разложите на сто#
ле листинги старой программы и продолжайте работать над требованиями и ар#
хитектурой как ни в чем не бывало. Так вы выполните проект быстрее и качествен#
нее. Порой этот подход находят неэтичным, но начальник#то останется доволен!
В#третьих, вы можете посвятить руководителя в нюансы технических проектов.
Это хороший подход, потому что он увеличивает число грамотных руководите#
лей в мире. В следующем подразделе приведено подробное обоснование важно#
сти выполнения предварительных условий до начала конструирования.
Наконец, вы можете найти другую работу. Независимо от экономических подъе#
мов и спадов хороших программистов всегда не хватает (BLS, 2002), а жизнь слиш#
ком коротка, чтобы тратить ее на работу в отсталом учреждении при наличии
множества лучших вариантов.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

25

Самый веский аргумент в пользу выполнения
предварительных условий перед началом конструирования
Допустим, вы уже забрались на гору определения проблемы, прошли милю по пути
выработки требований, сбросили грязную одежду у фонтана архитектуры и ис#
купались в чистых водах подготовленности. Следовательно, вы знаете, что перед
реализацией системы нужно понимать, что и как она будет делать.
Один из аспектов профессии разработчика — посвящение профанов в осо#
бенности процесса разработки ПО. Этот раздел поможет вам в общении
с менеджерами и руководителями, еще блуждающих во тьме. В нем под#
робно описан веский аргумент в пользу адекватного определения требований и
проектирования архитектуры до начала кодирования, тестирования и отладки. Изу#
чите его, сядьте перед начальником и поговорите о процессе программирования
по душам.

Обращение к логике
Подготовка к проекту — одно из главных условий эффективного программирова#
ния, и это логично. Объем планирования зависит от масштаба проекта. С управ#
ленческой точки зрения, планирование подразумевает определение сроков, числа
людей и компьютеров, необходимых для выполнения работ. С технической — пла#
нирование подразумевает получение представления о создаваемой системе, позво#
ляющего не истратить деньги на создание неверной системы. Иногда пользовате#
ли не четко знают, что желают получить, и для определения их требований может
понадобиться больше усилий, чем хотелось бы. Как бы то ни было, это дешевле, чем
создать не то, что нужно, похерить результат и начать все заново.
До начала создания системы не менее важно подумать и о том, как вы собирае#
тесь ее создавать. Никому не хочется тратить время и деньги на бесплодные блуж#
дания по лабиринту.

Обращение к аналогии
Создание программной системы похоже на любой другой проект, требующий
людских и финансовых ресурсов. Возведение дома начинается не с забивания
гвоздей, а с создания, анализа и утверждения чертежей. При разработке ПО на#
личие технического плана означает не меньше.
Никто не наряжает новогоднюю елку, не установив ее. Никто не разводит огонь,
не открыв дымоход. Никто не отправляется в долгий путь с пустым бензобаком.
Никто не принимает душ в одежде и не надевает носки после обуви. И т. д., и т. п.
Программисты — последнее звено пищевой цепи разработки ПО. Архитекторы
поглощают требования, проектировщики потребляют архитектуру, а программи#
сты — проект приложения.
Сравните пищевую цепь разработки ПО с реальной пищевой цепью. В экологи#
чески чистой среде водные жучки служат пищей рыбам, которыми в свою оче#
редь питаются чайки. Это здоровая пищевая цепь. Если на каждом этапе разра#
ботки ПО у вас будет здоровая пища, результатом станет здоровый код, написан#
ный довольными программистами.

26

ЧАСТЬ I

Основы разработки ПО

Если среда загрязнена, жучки плавают в ядерных отходах, а рыба плещется в неф#
тяных пятнах. Чайкам не повезло больше всего: находясь в конце пищевой цепи,
они травятся и нефтью, и ядерными отходами. Если ваши требования неудачны,
они отравляют архитектуру, которая в свою очередь травит процесс конструиро#
вания. Результат? Раздражительные программисты и полное изъянов ПО.
При планировании в высокой степени итеративного проекта вы до начала кон#
струирования должны определить важнейшие требования и архитектурные эле#
менты, влияющие на каждый конструируемый фрагмент программы. Строителям,
собирающимся строить поселок, не нужна полная информация о каждом доме до
начала возведения первого дома, однако они должны исследовать место, соста#
вить план канализации и электрических линий и т. д. Если строители плохо под#
готовятся, канализационные трубы, возможно, придется проводить в уже постро#
енный дом.

Обращение к данным
Исследования последних 25 лет убедительно доказали выгоду правильного выпол#
нения проектов с первого раза и дороговизну внесения изменений, которых можно
было избежать.
Ученые из компаний Hewlett#Packard, IBM, Hughes Aircraft, TRW и других
организаций обнаружили, что исправление ошибки к началу конструи#
рования обходится в 10–100 раз дешевле, чем ее устранение в конце ра#
боты над проектом, во время тестирования приложения или после его выпуска
(Fagan, 1976; Humphrey, Snyder, and Willis, 1991; Leffingwell 1997; Willis et al., 1998;
Grady, 1999; Shull et al., 2002; Boehm and Turner, 2004).
Общий принцип прост: исправлять ошибки нужно как можно раньше. Чем доль#
ше дефект сохраняется в пищевой цепи разработки ПО, тем больше вреда он
приносит на следующих этапах. Так как раньше всего вырабатываются требова#
ния, ошибки, допущенные на этом этапе, присутствуют в системе дольше и обхо#
дятся дороже. Кроме того, дефекты, внесенные в систему раньше, оказывают бо#
лее широкое влияние, чем дефекты, внесенные позднее. Это также повышает цену
более ранних дефектов.
Вот данные об относительной дороговизне исправления дефектов в за#
висимости от этапов их внесения и обнаружения (табл. 3#1):

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

27

Табл. 3-1. Средняя стоимость исправления дефектов в зависимости
от времени их внесения и обнаружения
Время обнаружения дефекта
Время
внесения
дефекта

Выработка Проектирование
требований архитектуры

Тестирование После
Конструирование системы
выпуска ПО

Выработка
требований

1

3

5–10

10

10–100

Проектиро#
вание архи#
тектуры



1

10

15

25–100

Конструи#
рование





1

10

10–25

Источник: адаптировано из работ «Design and Code Inspections to Reduce Errors in
Program Development» (Fagan 1976), «Software Defect Removal» (Dunn, 1984), «Software
Process Improvement at Hughes Aircraft» (Humphrey, Snyder, and Willis, 1991), «Calculating
the Return on Investment from More Effective Requirements Management» (Leffingwell,
1997), «Hughes Aircraft’s Widespread Deployment of a Continuously Improving Software
Process» (Willis et al., 1998), «An Economic Release Decision Model: Insights into Software
Project Management» (Grady, 1999), «What We Have Learned About Fighting Defects» (Shull
et al., 2002) и «Balancing Agility and Discipline: A Guide for the Perplexed» (Boehm and
Turner, 2004).

Эти данные говорят, например, о том, что дефект архитектуры, исправление ко#
торого при проектировании архитектуры обходится в $1000, может во время те#
стировании системы вылиться в $15 000 (рис. 3#1).

Этап внесения
дефекта

Затраты

Выработка требований
Проектирование архитектуры
Конструирование
Выработка требований
После выпуска ПО
Конструирование
Проектирование архитектуры Тестирование системы

Этап обнаружения дефекта

Рис. 3'1. С увеличением интервала между моментами внесения и обнаружения
дефекта стоимость его исправления сильно возрастает. Это верно и для очень
последовательных проектов (выработка требований и проектирование на 100%
выполняются заблаговременно), и для очень итеративных (аналогичный показатель
равен 5%)

ЧАСТЬ I

28

Основы разработки ПО

В большинстве проектов основная часть усилий по исправлению дефек#
тов все еще приходится на правую часть рис. 3#1, а значит, на отладку и
переделывание работы уходит около 50% времени типичного цикла раз#
работки ПО (Mills, 1983; Boehm, 1987; Cooper and Mullen, 1993; Fishman, 1996; Haley,
1996; Wheeler, Brykczynski, and Meeson, 1996; Jones, 1998; Shull et al., 2002; Wiegers,
2002). В десятках компаний было обнаружено, что политика раннего исправле#
ния дефектов может в два и более раз снизить финансовые и временные затраты
на разработку ПО (McConnell, 2004). Это очень веский довод в пользу как можно
более раннего нахождения и решения проблем.

Тест готовности руководителя
Если вам кажется, что ваш руководитель понимает важность выполнения предва#
рительных условий до начала конструирования, попросите его пройти следую#
щий тест, чтобы убедиться в этом.
Какие из этих утверждений являются самоисполняющимися пророчествами?
 Предлагаю приступить к кодированию прямо сейчас, потому что нам предстоит

потратить много времени на отладку.
 Мы не выделили много времени на тестирование, поскольку не ожидаем об#

наружить много дефектов.
 Мы изучили требования и проект приложения так хорошо, что я не представ#

ляю, какие крупные проблемы могут у нас возникнуть во время кодирования
или отладки.
Все эти высказывания — самоисполняющиеся пророчества. Стремитесь к тому,
чтобы исполнилось последнее.
Если вы еще не уверены, что предварительные условия актуальны для вашего
проекта, следующий раздел поможет вам принять окончательное решение.

3.2.

Определите тип ПО, над которым
вы работаете

Обобщая 20 лет исследований разработки ПО, Кейперс Джонс, руководитель ис#
следовательских работ в компании Software Productivity Research, заявил, что он
и его коллеги сталкивались с 40 разными методами сбора требований, 50 вари#
антами проектирования ПО и 30 видами тестирования, применявшимися в про#
ектах, реализуемых более чем на 700 языках программирования (Jones, 2003).
Разные типы проектов призывают к разным сочетаниям подготовки и конструи#
рования. Каждый проект уникален, однако обычно проекты подпадают под общие
стили разработки. Вот три самых популярных типа проектов, а также оптималь#
ные в большинстве случаев методы работы над ними (табл. 3#2):

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

29

Табл. 3-2. Оптимальные методы работы над программными проектами
трех популярных типов
Тип ПО
Системы целевого
назначения

Встроенные системы,
от которых зависит
жизнь людей

Встроенное ПО.
Игры.
Интернет#сайты.
Пакетное ПО.
Программные
инструменты.
Web#сервисы.

Авиационное ПО.
Встроенное ПО.
ПО для медицинских
устройств.
Операционные
системы.
Пакетное ПО.

Модели жизнен Гибкая разработка
ного цикла
(экстремальное
программирование,
методология Scrum,
разработка на осно#
ве временных окон
и т. д.).
Эволюционное.
прототипирование.

Поэтапная поставка.
Эволюционная
поставка.
Спиральная
разработка.

Поэтапная поставка.
Спиральная
разработка.
Эволюционная
поставка.

Планирование
и управление

Инкрементное пла#
нирование проекта.
Планирование тести#
рования и гарантии
качества по мере
надобности.
Неформальный
контроль над
изменениями.

Базовое заблаговре#
менное планирование.
Базовое планирова#
ние тестирования.
Планирование
гарантии качества
по мере надобности.
Формальный контроль
над изменениями.

Исчерпывающее
заблаговременное
планирование.
Исчерпывающее
планирование тести#
рования.
Исчерпывающее пла#
нирование гарантии
качества.
Тщательный контроль
над изменениями.

Выработка
требований

Неформальная
спецификация
требований.

Полуформальная спе#
цификация требований.
Обзоры требований
по мере надобности.

Формальная специ#
фикация требований.
Формальные инспек#
ции требований.

Бизнес-системы

Типичные
приложения

Интернет#сайты.
Сайты в интрасетях.
Системы управле#
ния материально#
техническим
снабжением.
Игры.
Системы управле#
ния информацией.
Системы выплаты
заработной платы.

(см. след. стр.)

30

ЧАСТЬ I

Основы разработки ПО

Табл. 3-2. (окончание)
Тип ПО

от которых зависит
Бизнес-системы

Встроенные системы,
Системы целевого
назначения

жизнь людей

Проектирова
ние

Комбинация
проектирования
и кодирования.

Проектирование
архитектуры.
Неформальное деталь#
ное проектирование.
Обзоры проекта
по мере надобности.

Проектирование
архитектуры.
Формальные инспек#
ции архитектуры.
Формальное деталь#
ное проектирование.
Формальные инспек#
ции детального
проекта.

Конструирова
ние

Парное или индиви#
дуальное программи#
рование.
Неформальная про#
цедура регистрации
кода или ее отсутст#
вие.

Парное или индиви#
дуальное программи#
рование.
Неформальная проце#
дура регистрации кода.
Обзоры кода по мере
надобности.

Парное или индиви#
дуальное программи#
рование.
Формальная процеду#
ра регистрации кода.
Формальные инспек#
ции кода.

Тестирование
и гарантия
качества

Разработчики тести#
руют собственный код.
Предварительная
разработка тестов.
Тестирование отдель#
ной группой прово#
дится в малом объеме
или не проводится
вообще.

Разработчики тестируют
собственный код.
Предварительная разра#
ботка тестов.
Отдельная группа
тестирования.

Разработчики тести#
руют собственный
код.
Предварительная
разработка тестов.
Отдельная группа
тестирования.
Отдельная группа
гарантии качества.

Внедрение
приложения

Неформальная проце# Формальная процедура Формальная процеду#
дура внедрения.
внедрения.
ра внедрения.

Работая над реальными проектами, вы столкнетесь с бесчисленными вариация#
ми на три темы, указанные в таблице, однако можно сделать и некоторые общие
выводы. При разработке бизнес#систем предпочтительно использовать высоко#
итеративные подходы, при которых планирование, выработка требований и про#
ектирование архитектуры перемежаются с конструированием, тестированием си#
стемы и гарантией качества. Системы, от которых зависит жизнь людей, требуют
более последовательных подходов, поскольку стабильность требований — одно
из условий высочайшей надежности системы.

Влияние итеративных подходов
на предварительные условия
Кое#кто утверждает, что при использовании итеративных методов не нужно осо#
бо возиться с предварительными условиями, но эта точка зрения неверна. Итера#

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

31

тивные подходы ослабляют следствия неадекватной подготовки, но не устраня#
ют их. Давайте изучим табл. 3#3, в которой приведены данные о проектах, в нача#
ле которых не были выполнены предварительные условия. Первый проект выпол#
няется последовательно, при этом дефекты обнаруживаются только на этапе тес#
тирования; второй выполняется итеративно, и разработчики находят дефекты по
мере работы. В первом случае основной объем работы по исправлению дефектов
откладывается на конец проекта, что приводит к росту расходов (табл. 3#1). При
итеративном подходе дефекты исправляются по мере развития проекта, что по#
зволяет снизить общие расходы. Табл. 3#3 и 3#4 приведены исключительно в ил#
люстративных целях, однако соотношение затрат при этих двух общих подходах
хорошо подтверждается исследованием, описанным выше.

Табл. 3-3. Влияние невыполнения предварительных условий
на последовательный и итеративный проекты
Подход 1: последовательный
подход без выполнения
предварительных условий

Подход 2: итеративный подход
без выполнения
предварительных условий

Степень
завершенности
проекта

Затраты
на работу

Затраты
на исправление
дефектов

Затраты
на работу

Затраты
на исправление
дефектов

20%

$100 000

$0

$100 000

$75 000

40%

$100 000

$0

$100 000

$75 000

60%

$100 000

$0

$100 000

$75 000

80%

$100 000

$0

$100 000

$75 000

100%

$100 000

$0

$100 000

$75 000

Затраты на исправ# $0
ление дефектов
в конце проекта

$500 000

$0

$0

СУММА

$500 000

$500 000

$500 000

$375 000

ОБЩАЯ СУММА

$1 000 000

$875 000

Итеративный проект с сокращенной программой выполнения предварительных
условий или без нее отличается от аналогичного последовательного проекта двумя
аспектами. Во#первых, при итеративном подходе затраты на исправление дефек#
тов обычно ниже, потому что дефекты выявляются раньше. И все же это проис#
ходит в конце каждой итерации, и исправление дефектов требует повторного
проектирования, кодирования и тестирования фрагментов ПО, что делает затра#
ты более крупными, чем они могли бы быть.
Во#вторых, при итеративных подходах затраты распределяются по всему проекту,
а не группируются в его конце. В конце концов и при итеративных, и при после#
довательных подходах общая сумма затрат окажется похожей, но в первом случае
она не будет казаться столь крупной, потому что будет уплачена по частям.

32

ЧАСТЬ I

Основы разработки ПО

Адекватное внимание к выполнению предварительных условий позволяет снизить
затраты независимо от типа используемого подхода (табл. 3#4). Итеративные
подходы обычно по многим параметрам лучше последовательных, однако итера#
тивный подход, при котором пренебрегают предварительными условиями, может
в итоге оказаться гораздо дороже, чем должным образом подготовленный после#
довательный проект.

Табл. 3-4. Влияние выполнения предварительных условий
напоследовательный и итеративный проекты
Подход 3: последовательный
подход с выполнением
предварительных условий

Подход 4: итеративный подход
с выполнением
предварительных условий

Степень
завершенности
проекта

Затраты
на работу

Затраты
на исправление
дефектов

Затраты
на работу

Затраты
на исправление
дефектов

20%

$100 000

$20 000

$100 000

$10 000

40%

$100 000

$20 000

$100 000

$10 000

60%

$100 000

$20 000

$100 000

$10 000

80%

$100 000

$20 000

$100 000

$10 000

100%

$100 000

$20 000

$100 000

$10 000

Затраты на исправ# $0
ление дефектов
в конце проекта

$0

$0

$0

СУММА

$500 000

$100 000

$500 000

$50 000

ОБЩАЯ СУММА

$600 000

$550 000

Эти данные наводят на мысль, что большинство проектов не являются
ни полностью последовательными, ни полностью итеративными. Опре#
делять требования или выполнять проектирование на 100% наперед не#
практично, однако обычно определение самых важных требований и архитектур#
ных элементов на раннем этапе оказывается выгодным.
Одно популярное практическое правило состоит в том, что#
бы заблаговременно определить около 80% требований,
предусмотреть время для более позднего определения до#
полнительных требований и выполнять по мере работы си#
стематичный контроль изменений, принимая только самые
важные требования. Возможен и другой вариант: вы можете определить заранее
20% только самых важных требований и разрабатывать оставшуюся часть ПО
небольшими фрагментами, определяя дополнительные требования и дорабаты#
вая проект приложения по мере прогресса (рис. 3#2 и 3#3).

Перекрестная ссылка Об адаптации подхода разработки к программам разных размеров см.
главу 27.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

33

Выработка требований

Проектирование архитектуры

Детальное проектирование

Конструирование

Гарантия качества/Тестирование системы
Время

Рис. 3'2. Этапы работы над проектами — даже самыми последовательными —
обычно несколько перекрываются
Выработка требований

Проектирование архитектуры

Детальное проектирование

Конструирование

Гарантия качества/Тестирование системы
Время

Рис. 3'3. В других случаях этапы перекрываются на всем протяжении проекта.
Понимание степени выполнения предварительных условий и соответствующая
адаптация проекта — одно из условий успешного конструирования

Выбор между итеративным и последовательным подходом
Степень, в которой предварительные условия должны быть выполнены наперед,
зависит от типа проекта (табл. 3#2), формальности проекта, технической среды,
возможностей сотрудников и бизнес#модели проекта. Вы можете выбрать более
последовательный подход (при котором вопросы решаются заблаговременно), если:
 требования довольно стабильны;
 проект приложения прост и относительно понятен;
 группа разработчиков знакома с прикладной областью;
 проект не связан с особым риском;

ЧАСТЬ I

34

Основы разработки ПО

 важна долговременная предсказуемость проекта;
 затраты на изменение требований, проекта приложения и кода скорее всего

окажутся высокими.
Более итеративный подход (при котором вопросы решаются по мере работы)
можно предпочесть, если:
 требования относительно непонятны или вам кажется, что они могут оказать#

ся нестабильными по другим причинам;
 проект приложения сложен, не совсем ясен или и то и другое;
 группа разработчиков незнакома с прикладной областью;
 проект сопряжен с высоким риском;
 долговременная предсказуемость проекта не играет особой роли;
 затраты на изменение требований, проекта приложения и кода скорее всего

будут низкими.
Как бы то ни было, итеративные подходы эффективны гораздо чаще, чем после#
довательные. Вы можете адаптировать предварительные условия к своему кон#
кретному проекту, как считаете нужным, сделав их более или менее формальны#
ми или полными. Мы подробнее обсудим разные подходы к крупным и неболь#
шим (или формальным и неформальным) проектам в главе 27.
Суть предварительных условий конструирования в том, что вам следует сначала
определить, какие из них уместны для вашего проекта. В некоторых проектах
предварительным условиям уделяется слишком мало времени, что приводит к
дестабилизации на этапе конструирования и препятствует планомерному разви#
тию проекта, хотя этого можно было избежать. В других проектах слишком мно#
го работы выполняется наперед; программисты, работающие над такими проек#
тами, слепо следуют требованиям и планам, которые впоследствии оказываются
неудачными, и это также может снижать эффективность конструирования.
Итак, изучив табл. 3#2, вы определили, какие предварительные условия уместны
для вашего проекта, поэтому в оставшейся части главы я расскажу, как определить,
были ли выполнены отдельные предварительные условия конструирования.

3.3.

Предварительные условия, связанные
с определением проблемы

Первое предварительное условие, которое нужно выполнить
перед конструированием, — ясное формулирование пробле#
мы, которую система должна решать. Это еще иногда назы#
вают «видением продукции», «формулированием точки зре#
ния», «формулированием задачи» или «определением про#
дукции». Я буду называть это условие «определением про#
Энди Хант и Дэйв Томас (Andy
блемы». Так как книга посвящена конструированию про#
Hunt and Dave Thomas)
грамм, прочитав этот раздел, вы не научитесь разрабатывать
определение проблемы, но узнаете, как определить, есть ли
оно вообще и станет ли оно надежной основой конструирования.

Если ограничения и условия
описываются как «коробка», то
хитрость в том, чтобы найти
именно коробку… Не думайте о
чем-то глобальном — найдите
коробку.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

35

Определение проблемы — это просто формулировка сути проблемы без каких#
либо намеков на ее возможные решения. Оно может занимать пару страниц, но
обязательно должно звучать как проблема. Фраза «наша система Gigatron не справ#
ляется с обработкой заказов» звучит как проблема и является хорошим ее опре#
делением. Однако фраза «мы должны оптимизировать модуль автоматизирован#
ного ввода данных, чтобы система Gigatron справлялась с обработкой заказов» —
плохое определение проблемы. Оно похоже не на проблему, а на решение.
Определение проблемы предшествует выработке детальных требований, которая
является более глубоким исследованием проблемы (рис. 3#4).

Будущие
улучшения

Тестирование системы
Конструирование
Проектирование архитектуры
Выработка требований
Определение проблемы

Рис. 3'4.

Определение проблемы — фундамент всего процесса программирования

Проблему следует формулировать на языке, понятном пользователю, а сама про#
блема должна быть описана с пользовательской точки зрения. Обычно проблему
не следует формулировать в компьютерных терминах, потому что оптимальным
ее решением может оказаться не компьютерная программа. Допустим, вы нужда#
етесь в вычислении годовой прибыли. Вычисление квартальной прибыли вы уже
компьютеризировали. Если вы застрянете на программировании, то решите, что
в имеющееся приложение нужно просто добавить функцию вычисления годовой
прибыли со всеми вытекающими отсюда последствиями: затратами на оплату труда
разработчиков и т. п. Если же вы малость подумаете, то просто чуть поднимете
зарплату секретарю, который будет один раз в год суммировать четыре числа на
калькуляторе.
Исключения из этого правила допустимы, если проблема связана с компьютера#
ми: программы компилируются слишком медленно, или инструменты програм#
мирования полны ошибок. В подобных случаях проблему можно сформулировать
в компьютерных, привычных для программистов терминах.
Не имея хорошего определения проблемы, можно потратить усилия на решение
не той проблемы (рис. 3#5).

36

Рис. 3'5.

ЧАСТЬ I

Основы разработки ПО

Прежде чем стрелять, убедитесь в том, что знаете, куда целитесь

Неудачное определение проблемы грозит пустой тратой времени на ре#
шение не той проблемы. Разумеется, нужную проблему вы при этом тоже
не решите.

3.4.

Предварительные условия, связанные
с выработкой требований

Требования подробно описывают, что должна делать программная система, а их
выработка — первый шаг к решению проблемы. Выработка требований также
известна как «разработка требований», «анализ требований», «анализ», «определение
требований», «спецификация», «функциональная спецификация».

Зачем нужны официальные требования?
Важность явного набора требований объясняется несколькими причинами.
Явные требования помогают гарантировать, что функциональность системы опре#
деляется пользователем, а не программистом. Если требования сформулированы
явно, пользователь может проанализировать и утвердить их. Если явных требова#
ний нет, программисту обычно самому приходится вырабатывать их во время про#
граммирования. Явные требования позволяют не гадать, чего хочет пользователь.
Кроме того, наличие явных требований помогает избегать споров. Требования
позволяют определить функциональность системы до начала программирования.
Если вы не согласны с другим программистом по поводу каких#то аспектов про#
граммы, вы можете разрешить споры, взглянув на написанные требования.
Внимание к требованиям помогает свести к минимуму изменения сис#
темы после начала разработки. Обнаружив при кодировании ошибку в
коде, вы измените несколько строк, и работа продолжится. Если же во
время кодирования вы найдете ошибку в требованиях, придется изменить про#
ект программы, чтобы он соответствовал измененным требованиям. Возможно,
при этом придется отказаться от части старого проекта, а поскольку в соответ#
ствии с ней уже написан некоторый код, на реализацию нового проекта уйдет
больше времени, чем могло бы. Вы также должны будете отказаться от кода и те#
стов, на которые повлияло изменение требований, и написать их заново. Даже код,
оставшийся нетронутым, нужно будет заново протестировать для гарантии того,
что изменение не привело к появлению новых ошибок.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

37

Как вы помните, исследования, проведенные во многих организациях, сви#
детельствуют о том, что при работе над крупными проектами исправле#
ние ошибки в требованиях, обнаруженной на этапе проектирования ар#
хитектуры, обычно обходится втрое дороже, чем исправление аналогичной ошибки,
найденной на этапе выработки требований (табл. 3#1). Такая же ошибка, обнару#
женная при кодировании, обходится дороже уже в 5–10, при тестировании сис#
темы — в 10, а после выпуска программы — в 10–100 раз. В менее крупных про#
ектах с меньшими административными расходами последний показатель ближе
к 5–10, чем к 100 (Boehm and Turner, 2004). Как бы то ни было, думаю, что допол#
нительные расходы нужны вам меньше всего.
Адекватное определение требований — одно из важнейших условий успеха про#
екта, возможно, даже более важное, чем использование эффективных методов
конструирования (рис. 3#6). Определению требований посвящены многие хоро#
шие книги, поэтому в нескольких следующих разделах я не буду рассказывать, как
правильно определять требования, — вместо этого я расскажу, как узнать, хоро#
шо ли они определены, и как выжать максимум из имеющихся требований.

Рис. 3'6. Не имея грамотно определенных требований, вы можете правильно
представлять общую проблему, но упустить из виду ее специфические аспекты

Миф о стабильных требованиях
Стабильные требования — Святой Грааль разработки ПО.
Требования подобны воде. ОпиПри стабильных требованиях смена этапов разработки ар# раться на них легче, если они
хитектуры, проектирования, кодирования и тестирования заморожены.
приложения происходит упорядоченно, предсказуемо и
Аноним
спокойно. Это просто рай для разработчиков! Вы можете
точно планировать расходы и совсем не волнуетесь о том, что реализация какой#
то функции обойдется в 100 раз дороже из#за того, что клиенту она придет в го#
лову только после отладки.
Всем нам хотелось бы надеяться, что, как только клиент утвердил требования,
никаких изменений не произойдет. Однако чаще всего клиент не может точно
сказать, что ему нужно, пока не будет написан некоторый код. Проблема не в том,
что клиенты — более низкая форма жизни. Подумайте: чем больше вы работаете
над проектом, тем лучше вы его понимаете; то же относится и к клиентам. Про#

38

ЧАСТЬ I

Основы разработки ПО

цесс разработки помогает им лучше понять собственные потребности, что часто
приводит к изменению требований (Curtis, Krasner and Iscoe, 1988; Jones 1998;
Wiegers, 2003). Если вы планируете жестко следовать требованиям, на самом деле
вы собираетесь не реагировать на потребности клиента.
Какой объем изменений типичен? Исследования, проведенные в IBM и
других компаниях, показали, что при реализации среднего проекта тре#
бования во время разработки изменяются примерно на 25% (Boehm, 1981;
Jones, 1994; Jones, 2000), на что приходится 70–85% объема повторной работы над
типичным проектом (Leffingwell, 1997; Wiegers, 2003).
Возможно, вы считаете, что «Понтиак Ацтек» — самый великолепный автомобиль
из когда#либо созданных, являетесь членом Общества Верящих в Плоскую Землю
и каждые четыре года совершаете паломничество в Розуэлл, штат Нью#Мексико, на
место приземления инопланетян. Если это так, можете и дальше верить в то, что
требования в ваших проектах меняться не будут. Если же вы уже перестали верить
в Санта#Клауса или хотя бы прекратили признаваться в этом, вы можете кое#что
предпринять, чтобы свести зависимость от изменений требований к минимуму.

Что делать при изменении требований во время
конструирования программы?
Следующие действия позволяют максимально легко перенести измене#
ния требований во время конструирования.
Оцените качество требований при помощи контрольного списка,
приведенного в конце раздела Если требования недостаточно хороши, прекра#
тите работу, вернитесь назад и исправьте их. Конечно, вам может показаться, что
прекращение кодирования на этом этапе приведет к отставанию от графика. Но
если вы едете из Чикаго в Лос#Анджелес и видите знаки, указывающие путь в Нью#
Йорк, разве можно считать изучение карты пустой тратой времени? Нет. Если вы
не уверены в правильности выбранного направления, остановитесь и проверьте его.
Убедитесь, что всем известна цена изменения требований Думая о новой
функции, клиенты приходят в возбуждение. Кровь у них разжижается, перепол#
няет продолговатый мозг, и они впадают в эйфорию, забывая обо всех собрани#
ях, посвященных обсуждению требований, о церемонии подписания и всех до#
кументах. Угомонить таких одурманенных новыми функциями людей проще всего,
заявив: «Ого, это действительно прекрасная идея! Но ее нет в документе требова#
ний, поэтому я должен пересмотреть график работы и смету, чтобы вы могли
решить, хотите ли вы реализовать это прямо сейчас или позднее». Слова «график»
и «смета» отрезвляют куда лучше, чем кофе и холодный душ, и многие требова#
ния быстро превращаются в пожелания.
Если руководители вашей организации не понимают важность предварительной
выработки требований, укажите им, что изменения во время выработки требова#
ний обходятся гораздо дешевле, чем на более поздних этапах. Используйте для
их убеждения раздел «Самый веский аргумент в пользу выполнения предваритель#
ных условий перед началом конструирования».

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

39

Задайте процедуру контроля изменений Если клиент Перекрестная ссылка О том, что
никак не может успокоиться, подумайте об установлении делать с изменениями проекта
стенда контроля изменений для рассмотрения вносимых приложения и самого кода, см.
предложений. В том, что клиенты изменяют точку зрения и раздел 28.2.
понимают, что им нужны дополнительные возможности, нет
ничего аномального. Проблема в том, что они вносят предложения так часто, что
вы не поспеваете за ними. Наличие процедуры контроля изменений осчастливит
всех: вы будете знать, что вам придется работать с изменениями только в опреде#
ленные периоды времени, а клиенты увидят, что вам небезразличны их пожелания.
Используйте те подходы к разработке, которые адап'
Перекрестная ссылка Об итератируются к изменениям Некоторые подходы к разра# тивных подходах к разработке см.
ботке ПО позволяют особенно легко реагировать на изме# подраздел «Используйте итеранения требований. Подход эволюционного прототипиро# цию» раздела 5.4 и раздел 29.3.
вания (evolutionary prototyping) помогает исследовать тре#
бования к системе до начала ее создания. Эволюционная поставка ПО подразу#
мевает предоставление системы пользователям по частям. Вы можете создать фраг#
мент системы, получить от пользователей отзывы, немного подкорректировать
проект, внести несколько изменений и приступить к созданию другого фрагмен#
та. Суть этого подхода — короткие циклы разработки, позволяющие быстро реа#
гировать на пожелания пользователей.
Оставьте проект Если требования особенно неудачны
или изменчивы и никакой из предыдущих советов не рабо#
тает, завершите проект. Даже если вы не можете на самом
деле завершить его, подумайте об этом. Подумайте о том,
насколько хуже он должен стать, чтобы вы от него отказа#
лись. Если такая ситуация возможна, спросите себя, чем она
отличается от текущей ситуации.
Помните о бизнес'модели проекта Многие проблемы
с требованиями исчезают при воспоминании о коммерче#
ских предпосылках проекта. Требования, которые сначала
казались прекрасными идеями, могут оказаться ужасными,
когда вы оцените затраты. Программисты, которые прини#
мают во внимание коммерческие следствия своих решений,
ценятся на вес золота, и я был бы рад получить свою комис#
сию за этот совет.

Дополнительные сведения О подходах к разработке, поддерживающих гибкие требования, см.
книгу «Rapid Development»
(McConnell, 1996).

Перекрестная ссылка О различиях между формальными и неформальными проектами (которые часто объясняются различиями размеров проектов) см.
главу 27.

Контрольный список: требования
Следующий контрольный список содержит вопросы, позвоhttp://cc2e.com/0323
ляющие определить качество требований. Ни книга, ни этот
список не научат вас грамотно вырабатывать требования.
Используйте его во время конструирования для определения того, насколько
прочна земля, на которой вы стоите.
Не все вопросы будут актуальны для вашего проекта. Если вы работаете над
неформальным проектом, над некоторыми вопросами даже не нужно задумываться. Другие вопросы важны, но не требуют формальных ответов. Однако если

40

ЧАСТЬ I

Основы разработки ПО

вы работаете над крупным формальным проектом, вам, наверное, следует ответить на каждый вопрос.
Специфические функциональные требования
 Определены ли все способы ввода данных в систему с указанием источника, точности, диапазона значений и частоты ввода?
 Определены ли все способы вывода данных системой с указанием назначения, точности, диапазона значений, частоты и формата?
 Определены ли все форматы вывода для Web-страниц, отчетов и т. д.?
 Определены ли все внешние аппаратные и программные интерфейсы?
 Определены ли все внешние коммуникационные интерфейсы с указанием
протоколов установления соединения, проверки ошибок и коммуникации?
 Определены ли все задачи, в выполнении которых нуждается пользователь?
 Определены ли данные, используемые в каждой задаче, и данные, являющиеся результатом выполнения каждой задачи?
Специфические нефункциональные требования (требования к качеству)
 Определено ли ожидаемое пользователем время реакции для всех необходимых операций?
 Определены ли другие временные параметры, такие как время обработки
данных, скорость их передачи и пропускная способность системы?
 Определен ли уровень защищенности системы?
 Определена ли надежность системы, в том числе такие аспекты, как следствия сбоев в ее работе, информация, которая должна быть защищена от
сбоев, и стратегия обнаружения и исправления ошибок?
 Определены ли минимальные требования программы к объему памяти и
свободного дискового пространства?
 Определены ли аспекты удобства сопровождения системы, в том числе
способность системы адаптироваться к изменениям специфических функций, ОС и интерфейсов с другими приложениями?
 Включено ли в требования определение успеха? Или неудачи?
Качество требований
 Написаны ли требования на языке, понятном пользователям? Согласны ли
с этим пользователи?
 Нет ли конфликтов между требованиями?
 Определено ли приемлемое равновесие между параметрами-антагонистами, такими как устойчивость к нарушению исходных предпосылок и корректность?
 Не присутствуют ли в требованиях элементы проектирования?
 Согласован ли уровень детальности требований? Следует ли какое-нибудь
требование определить подробнее? Менее подробно?
 Достаточно ли ясны и понятны требования, чтобы их можно было передать
независимой группе конструирования? Согласны ли с этим разработчики?
 Каждое ли требование релевантно для проблемы и ее решения? Можно ли
проследить каждое требование до его источника в проблемной среде?

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

41

 Можно ли протестировать каждое требование? Можно ли будет провести

независимое тестирование, которое позволит сказать, выполнены ли все
требования?
 Определены ли все возможные изменения требований и вероятность каждого изменения?
Полнота требований
 Указаны ли недостающие требования, которые невозможно определить до

начала разработки?
 Полны ли требования в том смысле, что если приложение будет удовлетво-

рять всем требованиям, то оно будет приемлемо?
 Не вызывают ли какие-нибудь требования у вас дискомфорта? Исключили

ли вы требования, которые не поддаются реализации и были включены лишь
для успокоения клиента или начальника?

3.5.

Предварительные условия, связанные
с разработкой архитектуры

Архитектура — это высокоуровневая часть проекта прило#
Перекрестная ссылка О проекжения, каркас, состоящий из деталей проекта (Buschman et
тировании на всех уровнях см.
al., 1996; Fowler, 2002; Bass Clements, Kazman 2003; Clements
главы 5–9.
et al., 2003). Архитектуру также называют «архитектурой си#
стемы», «высокоуровневым проектом» и «проектом высокого уровня». Как прави#
ло, архитектуру описывают в единственном документе, называемом «специфика#
цией архитектуры» или «высокоуровневым проектом». Некоторые разработчики
проводят различие между архитектурой и высокоуровневым проектом: архитек#
турой называют характеристики всей системы, тогда как высокоуровневым про#
ектом — характеристики, описывающие подсистемы или наборы классов, но не
обязательно в масштабе всей системы.
Так как эта книга посвящена конструированию, прочитав этот раздел, вы не уз#
наете, как разрабатывать архитектуру ПО, — вы научитесь определять качество
имеющейся архитектуры. Однако разработка архитектуры на один шаг ближе к
конструированию, чем выработка требований, поэтому архитектуру мы рассмот#
рим подробнее, чем требования.
Почему разработку архитектуры следует рассматривать как предваритель#
ное условие конструирования? Потому что качество архитектуры опре#
деляет концептуальную целостность системы, которая в свою очередь
определяет итоговое качество системы. Продуманная архитектура предоставляет
структуру, нужную для поддержания концептуальной целостности в масштабе си#
стемы. Она предоставляет программистам руководство, уровень детальности ко#
торого соответствует их навыкам и выполняемой работе. Она позволяет разделить
работу на части, над которыми отдельные разработчики и группы могут трудить#
ся независимо.
Хорошая архитектура облегчает конструирование. Плохая архитектура делает его
почти невозможным. Другую проблему, связанную с плохой архитектурой, иллю#
стрирует рис. 3#7.

42

ЧАСТЬ I

Основы разработки ПО

Рис. 3'7. Не имея хорошей архитектуры, вы можете решать правильную проблему,
но прийти к неправильному решению. Успешное конструирование может оказаться
невозможным

Внесение изменений в архитектуру при конструировании и на последу#
ющих этапах обходится недешево. Время, необходимое для исправления
ошибки в архитектуре ПО, сопоставимо со временем, нужным для исправ#
ления ошибки в требованиях, т. е. превышает временные затраты на исправление
ошибки в коде (Basili and Perricone, 1984; Willis, 1998). Изменения архитектуры
похожи на изменения требований еще и тем, что кажущиеся небольшими изме#
нения могут иметь далеко идущие последствия. Чем бы ни были обусловлены
изменения архитектуры — исправлением ошибок или внесением улучшений, —
чем раньше вы осознаете их необходимость, тем лучше.

Типичные компоненты архитектуры
Удачные архитектуры имеют много общего. Если всю систе#
му вы создаете самостоятельно, работа над архитектурой
будет перекрываться с более детальным проектированием.
В этом случае вам следует по крайней мере обдумать каждый
компонент архитектуры. Если вы работаете над системой, архитектура которой была
разработана кем#то другим, вы должны уметь определять ее важные компоненты
без охотничьей собаки и увеличительного стекла. Как бы то ни было, далее приве#
ден список компонентов архитектуры, на которые следует обратить внимание.

Перекрестная ссылка О низкоуровневом проектировании программы см. главы 5–9.

Организация программы
В первую очередь архитектура должна включать общее опи#
сание системы. Без такого описания вам будет трудно со#
ставить согласованную картину из тысячи деталей или хотя
бы десятка отдельных классов. Если бы система была моза#
Альберт Эйнштейн
икой из 12 фрагментов, ее мог бы с легкостью собрать и го#
довалый ребенок. Головоломку из 12 подсистем собрать труд#
нее, но, если вы не сможете сделать этого, вы не поймете, какой вклад вносит в
систему разрабатываемый вами класс.

Если вы не можете объяснить
что-то шестилетнему ребенку,
значит, вы сами этого не понимаете.

Архитектура должна включать подтверждения того, что при ее разработке были
рассмотрены альтернативные варианты, и обосновывать выбор окончательной
организации системы. Никому не хочется разрабатывать класс, если его роль в
системе не кажется хорошо обдуманной. Описывая альтернативные варианты,

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

43

архитектура обосновывает организацию системы и показывает, что роль каждо#
го класса была тщательно рассмотрена. В одном обзоре методик проектирования
было обнаружено, что обоснование проекта программы не менее важно для ее
сопровождения, чем сам проект (Rombach, 1990).
Архитектура должна определять основные компоненты
Перекрестная ссылка Об испрограммы. В зависимости от размера программы ее ком#
пользуемых при проектировапонентами могут быть отдельные классы или подсистемы,
нии компонентах разных уровсостоящие из нескольких классов. Каждый компонент яв#
ней см. подраздел «Уровни проектирования» раздела 5.2.
ляется классом или набором классов/методов, которые в
совокупности реализуют высокоуровневые функции про#
граммы, такие как взаимодействие с пользователем, отображение Web#страниц,
интерпретация команд, инкапсуляция бизнес#правил или доступ к данным. За
каждую функцию приложения, указанную в требованиях, должен отвечать хотя
бы один компонент. Если функцию реализуют несколько компонентов, они дол#
жны сотрудничать, а не конфликтовать.
Архитектура должна четко определять ответственность каж#
дого компонента. Компонент должен иметь одну область
ответственности и как можно меньше знать об областях
ответственности других компонентов. Сведя к минимуму
объем сведений, известных компонентам о других компо#
нентах, вы сможете локализовать информацию о проекте
приложения в отдельных компонентах.

Перекрестная ссылка Минимизация объема сведений, известных компонентам друг о друге, — главный аспект сокрытия
информации. Подробности см.
в подразделе «Скрывайте секреты (к вопросу о сокрытии
информации)» раздела 5.3.

Архитектура должна ясно определять правила коммуника#
ции для каждого компонента. Она должна описывать, какие другие компоненты
данный компонент может использовать непосредственно, какие косвенно, а ка#
кие вообще не должен использовать.

Основные классы
Архитектура должна определять основные классы приложе#
Перекрестная ссылка О проекния, их области ответственности и механизмы взаимодей#
тировании классов см. главу 6.
ствия с другими классами. Она должна описывать иерархии
классов, а также изменения состояний и время существова#
ния объектов. Если система достаточно велика, архитектура должна описывать орга#
низацию классов в подсистемы.
Архитектура должна описывать другие рассматривавшиеся варианты организации
классов и обосновывать итоговый вариант. Не все классы системы нужно описы#
вать в спецификации архитектуры. Ориентируйтесь на правило 80/20: описывайте
20% классов, которыми на 80% определяется поведение системы (Jacobsen, Booch,
and Rumbaugh, 1999; Kruchten, 2000).

Организация данных
Архитектура должна описывать основные виды формата
Перекрестная ссылка Об исфайлов и таблиц. Она должна описывать рассмотренные аль#
пользовании переменных см.
тернативы и обосновывать итоговые варианты. Если при#
главы 10–13.
ложение использует список идентификаторов клиентов и
разработчики архитектуры решили реализовать его при помощи списка с после#

44

ЧАСТЬ I

Основы разработки ПО

довательным доступом, в документации должно быть сказано, почему этот вид
списка лучше, чем список с произвольным доступом, стек или хэш#таблица. Эта
информация окажет вам неоценимую помощь во время конструирования и со#
провождения программы, подсказав, чем руководствовались разработчики архи#
тектуры. Без нее вы будете чувствовать себя зрителем, который смотрит иност#
ранный фильм без субтитров.
Прямой доступ к данным обычно следует предоставлять только одной подсисте#
ме или классу; исключения возможны при использовании классов или методов
доступа, обеспечивающих доступ к данным, контролируемым абстрактным обра#
зом. Подробнее об этом см. подраздел «Скрывайте секреты (к вопросу о сокры#
тии информации)» раздела 5.3.
Архитектура должна определять высокоуровневую организацию и содержание всех
используемых БД. Архитектура должна объяснять, почему одна БД предпочтитель#
нее, чем несколько (или наоборот), почему БД предпочтительнее, чем однород#
ные файлы, определять возможные типы взаимодействия приложения с другими
программами, использующими те же данные, объяснять, как будут отображаться
данные, и т. д.

Бизнес-правила
Архитектура, зависимая от специфических бизнес#правил, должна определять их
и описывать их влияние на проект системы. Возьмем для примера бизнес#прави#
ло, согласно которому информация о клиентах должна устаревать не более чем
на 30 секунд. В данном случае в спецификации архитектуры должно быть указа#
но, как это правило повлияло на выбор метода обеспечения актуальности данных
и их синхронизации.

Пользовательский интерфейс
Пользовательский интерфейс (GUI) часто проектируется на этапе выработки тре#
бований. Если это не так, его следует определить на этапе разработки архитекту#
ры. Архитектура должна описывать главные элементы формата Web#страниц, GUI,
интерфейс командной строки и т. д. Удобство GUI может в итоге определить по#
пулярность или провал программы.
Архитектура должна быть модульной, чтобы GUI можно было изменить, не зат#
ронув бизнес#правил и модулей программы, отвечающих за вывод данных. Напри#
мер, архитектура должна обеспечивать возможность сравнительно легкой заме#
ны группы классов интерактивного интерфейса на группу классов интерфейса
командной строки. Такая возможность весьма полезна; во многом это объясняет#
ся тем, что интерфейс командной строки удобен для тестирования ПО на уровне
блоков или подсистем.
http://cc2e.com/0393

Проектирование GUI заслуживает отдельной книги, и мы его
рассматривать не будем.

Управление ресурсами
Архитектура должна включать план управления ограниченными ресурсами, такими
как соединения с БД, потоки и дескрипторы. При разработке драйверов, встро#
енных систем и других приложений, которые будут работать в условиях ограни#

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

45

ченной памяти, архитектура должна также определять способ управления памя#
тью. Архитектура должна включать оценку ресурсов, используемых в номиналь#
ном режиме и при экстремальной нагрузке. В простейшем случае эти оценки
должны подтвердить, что предполагаемая среда использования приложения бу#
дет располагать нужными ресурсами. В более сложной ситуации в приложении,
возможно, придется реализовать более активное управление выделенными ему
ресурсами. Если это так, архитектуру менеджера ресурсов нужно спроектировать
не менее тщательно, чем любой другой компонент системы.

Безопасность
Архитектура должна определять подход к безопасности на
уровне проекта приложения и на уровне кода. Если модель
угроз до сих пор не разработана, это следует сделать при
проектировании архитектуры. О безопасности нужно по#
мнить и при разработке принципов кодирования, в том
числе методик обработки буферов и ненадежных данных
(данных, вводимых пользователями, файлов «cookie», кон#
фигурационных данных и данных других внешних интер#
фейсов), подходов к шифрованию, уровню подробности
сообщений об ошибках, защите секретных данных, нахо#
дящихся в памяти, и другим вопросам.

http://cc2e.com/0330

Дополнительные сведения Прекрасное обсуждение защиты ПО
см. в книге «Writing Secure Code,
2d Ed.» (Howard and LeBlanc 2003)
и в январском номере журнала
«IEEE Software» за 2002 год.

Производительность
В требованиях следует определить показатели производи#
тельности. Если они связаны с использованием ресурсов,
надо определить приоритеты для разных ресурсов, в том
числе соотношение быстродействия, использования памя#
ти и затрат.

Дополнительные сведения О проектировании высокопроизводительных систем см. книгу Конни
Смит «Performance Engineering of
Software Systems» (Smith, 1990).

Архитектура должна включать оценки производительности
и объяснять, почему разработчики архитектуры считают эти показатели дости#
жимыми. Если они могут быть не достигнуты, это тоже должно быть отражено в
архитектуре. Если для достижения некоторых показателей требуются специфи#
ческие алгоритмы или типы данных, также укажите это в спецификации архитек#
туры. Кроме того, в архитектуре можно указать объем пространства и время, вы#
деляемые каждому классу или объекту.

Масштабируемость
Масштабируемостью называют возможность системы адаптироваться к росту тре#
бований. Архитектура должна описывать, как система будет реагировать на рост
числа пользователей, серверов, сетевых узлов, записей в БД, транзакций и т. д. Если
развитие системы не предполагается и ее масштабируемость не играет роли, это
должно быть явно указано в архитектуре.

Взаимодействие с другими системами
Если некоторые данные или ресурсы будут общими для разрабатываемой систе#
мы и других программ или устройств, в архитектуре нужно указать, как это будет
реализовано.

46

ЧАСТЬ I

Основы разработки ПО

Интернационализация/локализация
«Интернационализацией» называют реализацию в программе поддержки региональ#
ных стандартов. Вместо слова «internationalization» часто используется аббревиату#
ра «I18n», составленная из первой и последней букв слова и числа букв между ними.
«Локализацией» (известной как «L10n» по той же причине) называют перевод интер#
фейса программы и реализацию в ней поддержки конкретного языка.
Вопросы интернационализации заслуживают особого внимания при разработке
архитектуры интерактивной системы. Большинство интерактивных систем вклю#
чает десятки или сотни подсказок, индикаторов состояния, вспомогательных со#
общений, сообщений об ошибках и т. д., поэтому нужно оценить объем ресурсов,
используемых строками. Если разрабатывается коммерческая программа, архитек#
тура должна показывать, что при ее создании были рассмотрены типичные вопро#
сы, связанные со строками и наборами символов, такие как выбор набора симво#
лов (ASCII, DBCS, EBCDIC, MBCS, Unicode, ISO 8859 и т. д.) и типа строк (строки C,
строки Visual Basic и т. д.), а также способа изменения строк, который не требовал
бы изменения кода, и метода перевода строк на иностранные языки, оказывающе#
го минимальное влияние на код и GUI. Строки можно встроить в код, инкапсули#
ровать в класс и использовать посредством интерфейса или сохранить в файле
ресурсов. Архитектура должна объяснять, какой вариант выбран и почему.

Ввод-вывод
Ввод#вывод — еще одна область, на которую стоит обратить внимание при про#
ектировании архитектуры. Архитектура должна определять схему чтения данных:
упреждающее чтение, чтение с задержкой или по требованию. Кроме того, она
должна описывать уровень, на котором будут определяться ошибки ввода#выво#
да: на уровне полей, записей, потоков данных или файлов.

Обработка ошибок
Обработка ошибок — одна из самых сложных проблем современной ин#
форматики, и к ней нельзя относиться с пренебрежением. По оценкам
некоторых ученых код на целых 90% состоит из блоков обработки ис#
ключительных ситуаций, ошибок и т. п., из чего следует, что только 10% кода от#
вечают за номинальный режим работы программы (Shaw in Bentley, 1982). Раз уж
на обработку ошибок приходится такая большая часть кода, стратегия их согла#
сованной обработки должна быть выражена в архитектуре.
Обработку ошибок часто рассматривают на уровне конвенции кодирования, если
вообще рассматривают. Однако она оказывает влияние на всю систему, поэтому
лучше всего рассматривать ее на уровне архитектуры. Вот некоторые вопросы, на
которые нужно обратить внимание.
 Является ли обработка ошибок корректирующей или ориентированной на их

простое обнаружение? В первом случае программа может попытаться восста#
новиться от последствий ошибки. Во втором — может продолжить работу как
ни в чем не бывало или завершиться. Как бы то ни было, она должна извес#
тить пользователя об ошибке.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

47

 Является ли обнаружение ошибок активным или пассивным? Система может

активно предвосхищать ошибки (например, проверяя корректность данных,
введенных пользователем) или пассивно реагировать на них только в том случае,
если избежать их не удалось (например, когда введенные пользователем дан#
ные привели к численному переполнению). В обоих случаях сделанный вы#
бор повлияет на дизайн GUI.
 Как программа поступает при обнаружении ошибки? Обнаружив ошибку, про#

грамма может отбросить данные, вызвавшие ошибку, может отнестись к ошибке
как положено и перейти в состояние обработки ошибки или может выполнить
оставшиеся действия и уведомить пользователя о том, что (где#то) были обна#
ружены ошибки.
 Каковы соглашения обработки сообщений об ошибках? Если в спецификации

архитектуры не определена единственная согласованная стратегия, GUI пока#
жется непонятной комбинацией разных интерфейсов, относящихся к разным
частям программы. Чтобы избежать этого, при проектировании архитектуры
нужно определить соглашения вывода сообщений об ошибках.
 Как обрабатываются исключения? Архитектура должна определять, когда код

может генерировать исключения, где они будут перехватываться, как регист#
рироваться в журнале, документироваться и т. д.
 На каком уровне программы обрабатываются ошибки?

Вы можете обрабатывать их в точке обнаружения, пере#
давать классу обработки ошибок или возвращать по цепи
вызовов.
 Какова ответственность каждого класса за проверку по#

Перекрестная ссылка Еще один
аспект стратегии обработки
ошибок, который следует рассмотреть на архитектурном
уровне, — согласованный метод
обработки недопустимых параметров. Примеры см. в главе 8.

лучаемых данных? Каждый класс отвечает за проверку
собственных данных или есть группа классов, проверя#
ющих данные для всей системы? Могут ли классы кон#
кретного уровня полагать, что полученные ими данные корректны?

 Хотите ли вы использовать механизм обработки ошибок, встроенный в среду

программирования, или создать собственный? Если в среде реализован кон#
кретный подход к обработке ошибок, это не значит, что он лучше всего соот#
ветствует вашим требованиям.

Отказоустойчивость
При разработке архитектуры системы следует указать ожидаемый уровень ее отка#
зоустойчивости. Отказоустойчивостью называют совокупность свойств системы, по#
вышающих ее надежность путем обнаружения ошибок, восстановления, если это
возможно, и изоляции их плохих последствий, если восстановление невозможно.
Например, вычисление системой квадратного корня можно сделать отказоустой#
чивым несколькими способами.
 В случае неудачи система может вернуться в предыдущее состояние и попро#

бовать вычислить корень еще раз. Если первый ответ неверен, она может вер#
нуться в состояние, при котором все наверняка было правильно, и продолжить
работу с этого момента.

48

ЧАСТЬ I

Основы разработки ПО

Дополнительные сведения Хорошее введение в вопросы отказоустойчивости см. в июльском номере журнала «IEEE Software» за 2001 год. Кроме того,
в статьях этого номера есть
ссылки на многие отличные
книги и статьи, посвященные
данной теме.

 Система может включать вспомогательный код, выпол#
няемый приобнаружении ошибки в основном коде. Так, если
первый ответ кажется ошибочным, система может переклю#
читься на альтернативный метод вычисления квадратного
корня.

 Система может применять алгоритм голосования. Она
может включать три класса, вычисляющих квадратный ко#
рень разными способами. Каждый класс вычисляет квадрат#
ный корень, после чего система сравнивает полученные
результаты. В зависимости от реализованного типа отказоустойчивости сис#
тема может использовать среднее, срединное или наиболее вероятное значе#
ние из трех.

 Система может заменять ошибочное значение поддельным значением, кото#

рое положительно скажется на работе оставшейся части системы.
Другими подходами к отказоустойчивости являются перевод системы при обна#
ружении ошибки в состояние частичной работоспособности или ограниченной
фунциональности. Система может отключиться или автоматически перезапустить
себя. Конечно, эти примеры упрощены. Отказоустойчивость — захватывающая и
сложная область, но не она является темой этой книги.

Возможность реализации архитектуры
Разработчики могут сомневаться в том, способна ли система достигнуть заданных
показателей производительности, работать при ограниченности ресурсов и бу#
дет ли она адекватно поддержана средами реализации. Архитектура должна под#
тверждать, что система технически осуществима. Если невозможность реализации
какого#то компонента может сделать проект неработоспособным, в архитектуре
должно быть отражено, как изучались эти вопросы: при помощи прототипов,
исследований или иначе. Эти аспекты риска следует устранить до начала полно#
масштабного конструирования.

Избыточная функциональность
Надежностью называют способность системы продолжать работу после обнару#
жения ошибки. Частенько в спецификации архитектуры разработчики определя#
ют более надежную систему, чем указано в требованиях. Одна из причин этого в
том, что система, состоящая из многих частей, удовлетворяющих минимальным
требованиям к надежности, в целом может оказаться менее надежной, чем нуж#
но. В мире ПО цепь не так крепка, как слабейшее звено; она так слаба, как все слабые
звенья, вместе взятые. В спецификации архитектуры должно быть явно указано,
могут ли программисты реализовать в своих блоках программы избыточную фун#
кциональность или они должны создать простейшую работоспособную систему.
Определить отношение к реализации избыточной функциональности особенно
важно потому, что многие программисты делают это автоматически, из чувства
профессиональной гордости. Явно выразив ожидания в архитектуре, вы сможете
избежать феномена, при котором некоторые классы исключительно надежны, а
другие лишь отвечают требованиям.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

49

Купить или создавать самим?
Самый радикальный подход к созданию ПО — не создавать
Перекрестная ссылка Список
его вообще, а купить или загрузить из Интернета бесплат#
типов коммерческих программное ПО с открытым исходным кодом. Вы можете приобре#
ных компонентов см. в подразделе «Библиотеки кода» раздести элементы управления GUI, менеджеры БД, процессоры
ла 30.3.
изображений, компоненты для работы с графикой и диа#
граммами, компоненты для коммуникации по Интернету,
компоненты обеспечения безопасности и шифрования, обработки электронных
таблиц и текста — список почти бесконечен. Одним из главных достоинств про#
граммирования с использованием современных GUI#сред является объем функ#
циональности, который вы получаете автоматически: классы для работы с графи#
кой, менеджеры диалоговых окон, обработчики событий клавиатуры и мыши, код,
поддерживающий любые принтеры и мониторы и т. д.
Если архитектура не подразумевает применение готовых компонентов, она дол#
жна объяснять, в каких аспектах компоненты, которые будут разработаны, ока#
жутся лучше готовых библиотек и компонентов.

Повторное использование
Если план предусматривает применение существующего кода, тестов, форматов
данных и т. д., архитектура должна объяснять, как повторно использованные ре#
сурсы будут адаптированы к другим архитектурным особенностям, если это бу#
дет сделано.

Стратегия изменений
Так как при создании продукта и программисты, и пользо#
Перекрестная ссылка О систеватели обучаются, приложение скорее всего в период разра#
матичной обработке изменений
ботки будет изменяться. Причинами этого могут быть изме#
см. раздел 28.2.
нения типов данных, форматов файлов, функциональности,
реализация новых функций и т. д. Изменения могут быть новыми возможностями,
которые были запланированы заранее или не были реализованы в первой версии
системы. Поэтому разработчику архитектуры ПО следует сделать ее достаточно
гибкой, чтобы в систему можно было легко внести вероятные изменения.
Архитектура должна четко описывать стратегию изменений.
Ошибки проектирования часто
Архитектура должна показывать, что возможные улучшения
являются довольно тонкими и
рассматривались и что реализация наиболее вероятных
объясняются эволюцией, при
которой по мере реализации
улучшений окажется наиболее простой. Если вероятны из#
новых функций и возможностей
менения форматов ввода или вывода данных, стиля взаимо#
разработчики забывают о сдедействия с пользователями или требований к обработке,
ланных ранее предположениях.
архитектура должна показывать, что все эти изменения были
Фернандо Дж. Корбати
предвосхищены и каждое из них будет ограничено неболь#
(Ferrnando J. Corbatу)
шим числом классов. Архитектурный план внесения изме#
нений может быть совсем простым: включить в файлы данных номера версий, за#
резервировать поля на будущее, спроектировать файлы так, чтобы в них можно
было добавить новые таблицы и т. д. Если применяется генератор кода, архитек#
тура должна показывать, что он поддерживает возможность внесения предпола#
гаемых изменений.

50

ЧАСТЬ I

Основы разработки ПО

В архитектуре должны быть отражены стратегии, которые
позволяют программистам не ограничивать имеющийся у
них выбор раньше времени. Так, архитектура может опре#
делять, что вместо жестко закодированных тестов if будет
применяться метод, основанный на проверке таблиц. Дан#
ные таблиц можно хранить во внешнем файле, а не вклю#
чать в программу, что позволит вносить в нее изменения без перекомпиляции.

Перекрестная ссылка О мерах,
позволяющих не ограничивать
возможность выбора, см. подраздел «Тщательно выбирайте
время связывания» раздела 5.3.

Общее качество архитектуры
Хорошая спецификация архитектуры должна описывать классы системы, инфор#
мацию, скрываемую каждым классом, и обосновывать принятые и отвергнутые
варианты проекта системы.
Архитектура должна быть продуманным концептуальным
целым, включающим несколько специфических дополнений.
Главный тезис самой популярной книги по разработке ПО
«Мифический человеко#месяц» гласит, что основной пробле#
мой, характерной для крупных систем, является поддержание их концептуальной
целостности (Brooks, 1995). Хорошая архитектура должна соответствовать про#
блеме. Изучая архитектуру, вы должны испытывать удовольствие от того, насколько
естественным и простым кажется решение. Вам должно казаться, что проблема и
архитектура неразрывно связаны.

Перекрестная ссылка О соотношении атрибутов качества см.
раздел 20.1.

Вам следует знать, как архитектура изменялась во время ее проектирования. Каж#
дое изменение должно быть четко согласовано с общей концепцией. Архитекту#
ра не должна быть похожа на проект бюджета Конгресса США, предусматриваю#
щий расходы на мероприятия, повышающие популярность чиновников.
Цели архитектуры должны быть четко сформулированы. Проект системы, глав#
ным требованием к которой является модифицируемость, будет отличаться от
проекта системы, которая должна показывать высочайшую производительность,
даже если по функциональности обе системы будут одинаковы.
В архитектуре должны быть обоснованы важнейшие принятые решения. С подо#
зрением относитесь к обоснованиям из разряда «мы всегда так делали». Здесь
уместно вспомнить одну поучительную историю. Бет хотела приготовить туше#
ное мясо по прославленному рецепту, передававшемуся из поколения в поколе#
ние в семье ее мужа Абдула. Абдул сказал, что его мать солила кусок мяса, перчи#
ла, обрезала его края, укладывала в горшок, закрывала и ставила в духовку. На вопрос
Бет «Зачем обрезать оба края?» Абдул ответил: «Не знаю, я всегда так делал. Спро#
шу у мамы». Он позвонил ей и услышал: «Не знаю, просто я так всегда делала. Спрошу
у твоей бабушки». А бабушка заявила: «Понятия не имею, почему вы так делаете.
Я делала так потому, что мой горшок был маловат».
Хорошая архитектура ПО не зависит ни от платформы, ни от языка. Пожалуй, вы
не сможете проигнорировать среду конструирования, однако максимальная не#
зависимость от среды позволит вам устоять перед соблазном создать слишком
подробную архитектуру и избежать работы, которую лучше выполнять во время
конструирования. Если программа ориентирована на конкретную платформу или
должна быть разработана на конкретном языке, это правило неактуально.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

51

При разработке архитектуры следует соблюдать баланс между недостаточным и
чрезмерным определением системы. Ни на какую часть архитектуры не следует
обращать больше внимания, чем она заслуживает; не следует разрабатывать одну
часть в ущерб другой. Архитектура должна отражать все требования, не включая
ненужных элементов.
В архитектуре должны быть явно определены области риска, указаны его причи#
ны и описаны действия по сведению риска к минимуму.
Архитектура должна включать описания системы с разных точек зрения. Планы
дома включают поэтажный план, план перекрытий, электрические схемы и т. д.
Качество архитектуры ПО также повысится, если включить в нее описания раз#
ных взглядов на систему, которые позволят найти ошибки и помогут программи#
стам полностью понять проект системы (Kruchten, 1995).
Наконец, элементы архитектуры не должны вызывать у вас чувство неловкости.
В архитектуру не следует включать что#то только для того, чтобы угодить началь#
нику. Архитектура не должна включать ничего, что было бы трудно понять. Именно
вы будете претворять ее в жизнь — как же вы ее реализуете, если не будете в ней
разбираться?

Контрольный список: архитектура
Следующий список вопросов позволяет сделать вывод о
http://cc2e.com/0337
качестве архитектуры. Этот список не является исчерпывающим руководством по проектированию архитектуры —
это прагматичный способ оценки того, что вы получаете на программистском
конце пищевой цепи разработки ПО. Используйте его как основу для создания
собственного контрольного списка. Как и в случае аналогичного списка вопросов о требованиях, при работе над неформальным проектом некоторые вопросы будут неактуальны, однако при работе над более крупным проектом большинство из них пригодится.

Специфические аспекты архитектуры
 Ясно ли описана общая организация программы? Включает ли спецификация грамотный обзор архитектуры и ее обоснование?
 Адекватно ли определены основные компоненты программы, их области
ответственности и взаимодействие с другими компонентами?
 Все ли функции, указанные в спецификации требований, реализуются разумным, не слишком большим и не слишком малым, числом компонентов?
 Приведено ли описание самых важных классов и их обоснование?
 Приведено ли описание организации данных и ее обоснование?
 Приведено ли описание организации и содержания БД?
 Определены ли все важные бизнес-правила? Описано ли их влияние на
систему?
 Описана ли стратегия проектирования GUI?
 Сделан ли GUI модульным, чтобы его изменения не влияли на оставшуюся
часть программы?
 Приведено ли описание стратегии ввода-вывода данных и ее обоснование?

ЧАСТЬ I

52

Основы разработки ПО

 Указаны ли оценки степени использования ограниченных ресурсов, таких














как потоки, соединения с БД, дескрипторы, пропускная способность сети?
Приведено ли описание стратегии управления такими ресурсами и ее обоснование?
Описаны ли требования к защищенности архитектуры?
Определяет ли архитектура требования к объему и быстродействию всех
классов, подсистем и функциональных областей?
Описывает ли архитектура способ достижения масштабируемости системы?
Рассмотрены ли вопросы взаимодействия системы с другими системами?
Описана ли стратегия интернационализации/локализации?
Определена ли согласованная стратегия обработки ошибок?
Определен ли подход к отказоустойчивости системы (если это требуется)?
Подтверждена ли возможность технической реализации всех частей системы?
Определен ли подход к реализации избыточной функциональности?
Приняты ли необходимые решения относительно «покупки или создания»
компонентов системы?
Описано ли в спецификации, как повторно используемый код будет адаптирован к другим аспектам архитектуры?
Сможет ли архитектура адаптироваться к вероятным изменениям?

Общее качество архитектуры
 Все ли требования отражены в архитектуре?
 Является ли какая-нибудь часть системы чрезмерно или недостаточно проработанной? Заданы ли явные ожидания по этому поводу?
 Является ли вся архитектура концептуально целостной?
 Независим ли высокоуровневый проект системы от платформы и языка,
который будет использован для его реализации?
 Указаны ли мотивы принятия всех основных решений?
 Удовлетворяет ли вас — программиста, который будет реализовывать систему, — разработанная архитектура?

3.6.

Сколько времени следует посвятить
выполнению предварительных условий?

Перекрестная ссылка Объем
времени, необходимый для работы над предварительными
условиями, зависит от типа
проекта. Об адаптации предварительных условий к специфическому проекту см. раздел 3.2.

Время, уходящее на определение проблемы, выработку тре#
бований и проектирование архитектуры ПО, зависит от
особенностей проекта. Как правило, если проект развива#
ется без проблем, работа над требованиями, архитектурой
и предварительным планированием поглощает 10–20% уси#
лий и 20–30% времени (McConnell, 1998; Kruchten, 2000). Эти
показатели не включают затраты на детальное проектиро#
вание — оно является частью конструирования.

Если требования нестабильны и вы работаете над крупным формальным проектом,
то для решения проблем с требованиями, которые будут обнаружены на ранних
этапах конструирования, вам, вероятно, понадобятся услуги специалиста по ана#
лизу требований. Предусмотрите консультации с ним и выделите ему время на ре#
визию требований — и пригодная для работы версия требований в ваших руках.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

53

Если требования нестабильны и вы работаете над небольшим неформальным
проектом, вам, наверное, придется решать проблемы с требованиями самостоя#
тельно. Выделите время на грамотное определение требований, чтобы их измен#
чивость как можно слабее повлияла на конструирование.
Каким бы проект ни был — формальным или неформаль#
Перекрестная ссылка О подхоным, — если требования нестабильны, рассматривайте ра#
дах к изменениям требований
боту над ними как отдельный проект. Оцените время, нуж#
см. подраздел «Что делать при
изменении требований во вреное для выполнения оставшейся части проекта, после завер#
мя конструирования програмшения работы над требованиями. Это разумно: трудно ожи#
мы?» раздела 3.4.
дать, что вы сможете составить график работы до того, как
будете знать, что создаете. Представьте, что вас хотят нанять
для строительства дома. Заказчик говорит: «Сколько будет стоить ваша работа?»
Вы обоснованно спрашиваете: «Что мне нужно построить?», на что заказчик от#
вечает: «Я не могу сказать вам, но сколько это будет стоить?» Что вы сделаете?
Попрощаетесь с заказчиком и отправитесь домой.
Ясно, что клиенты не попросят вас предъявить им счет, пока не расскажут, какой
дом надо построить. Им не нужно, чтобы вы пришли с досками, молотком и гвоз#
дями и начали тратить их деньги до того, как архитектор завершит работу над
чертежами. Однако люди, как правило, разбираются в создании ПО хуже, чем в
лестницах и дверных проемах, поэтому ваши клиенты могут не сразу понять, почему
вы хотите сделать выработку требований отдельным проектом. Возможно, вам
придется объяснить им причины этого.
Выделяя время на проектирование архитектуры ПО, поступайте так же, как и при
выработке требований. Если над данным типом ПО вы еще не работали, выдели#
те больше времени. Убедитесь, что время на создание качественной архитектуры
будет выделено не в ущерб другим этапам. Если нужно, сделайте отдельным про#
ектом и работу над архитектурой.

Дополнительные ресурсы
Ниже я привел список ресурсов, посвященных работе над
требованиями.

Выработка требований

http://cc2e.com/0344

http://cc2e.com/0351

В следующих книгах вы найдете гораздо более подробную
информацию о выработке требований.
Wiegers, Karl. Software Requirements, 2d ed. Redmond, WA: Microsoft Press, 2003.
В этом практическом руководстве описываются все детали выработки требований,
в том числе сбор информации о требованиях, их анализ, составление специфи#
кации требований, проверка требований и управление ими.
Robertson, Suzanne and James Robertson. Mastering the Requirements Process. Reading,
MA: Addison#Wesley, 1999. Хорошая альтернатива книге Карла Вигерса, ориенти#
рованная на более подготовленных специалистов по выработке требований.
Gilb, Tom. Competitive Engineering. Reading, MA: Addison#Wesley,
2004. В этой книге рассматривается язык требований Гил#

http://cc2e.com/0358

54

ЧАСТЬ I

Основы разработки ПО

ба, известный как «Planguage». Кроме того, в ней описывается специфический
подход Гилба к разработке требований, проектированию, оценке проектирования
и эволюционному управлению проектом. Загрузить книгу можно с Web#сайта Тома
Гилба по адресу www.gilb.com.
IEEE Std 830%1998. IEEE Recommended Practice for Software Requirements Specifications.
Los Alamitos, CA: IEEE Computer Society Press. Этот документ представляет собой
руководство IEEE#ANSI по созданию спецификаций требований к ПО. В нем опи#
сываются элементы, которые следует включать в документ спецификации, и рас#
сматриваются некоторые альтернативные варианты.
Abran, Alain, et al. Swebok: Guide to the Software Engineering Body
of Knowledge. Los Alamitos, CA: IEEE Computer Society Press,
2001. В этом руководстве приведено подробное описание
выработки требований к ПО. Загрузить его можно с Web#сайта www.swebok.org.
http://cc2e.com/0365

Ниже указаны хорошие альтернативы названным книгам.
Lauesen, Soren. Software Requirements: Styles and Techniques. Boston, MA: Addison#Wesley,
2002.
Kovitz, Benjamin L. Practical Software Requirements: A Manual of Content and Style.
Manning Publications Company, 1998.
Cockburn, Alistair. Writing Effective Use Cases. Boston, MA: Addison#Wesley, 2000.

Разработка архитектуры
http://cc2e.com/0372

В последние несколько лет было опубликовано много книг,
посвященных разработке архитектуры ПО. Одними из луч#
ших я считаю следующие.

Bass, Len, Paul Clements, and Rick Kazman. Software Architecture in Practice, 2d ed. Boston,
MA: Addison#Wesley, 2003.
Buschman, Frank, et al. Pattern%Oriented Software Architecture, Volume 1: A System of
Patterns. New York, NY: John Wiley & Sons, 1996.
Clements, Paul, ed. Documenting Software Architectures: Views and Beyond. Boston, MA:
Addison#Wesley, 2003.
Clements, Paul, Rick Kazman, and Mark Klein. Evaluating Software Architectures: Meth%
ods and Case Studies. Boston, MA: Addison#Wesley, 2002.
Fowler, Martin. «Patterns of Enterprise Application Architecture». Boston, MA: Addison#
Wesley, 2002.
Jacobson, Ivar, Grady Booch, and James Rumbaugh. The Unified Software Development
Process. Reading, MA: Addison#Wesley, 1999.
IEEE Std 1471%2000. Recommended Practice for Architectural Description of Software%
Intensive Systems. Los Alamitos, CA: IEEE Computer Society Press. Этот документ явля#
ется руководством IEEE#ANSI по созданию спецификаций архитектуры ПО.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

55

Общие подходы к разработке ПО
Издано много книг, посвященных разным подходам к выпол#
нению программных проектов. В одних рассматриваются бо#
лее последовательные подходы, в других — более итеративные.

http://cc2e.com/0379

McConnell, Steve. Software Project Survival Guide. Redmond, WA: Microsoft Press, 1998.
В этой книге рассмотрен один конкретный способ выполнения проекта, подчер#
кивающий обдуманное заблаговременное планирование, выработку требований
и работу над архитектурой, за которыми следует тщательное выполнение проек#
та. Такой подход обеспечивает долговременную предсказуемость финансовых и
временных затрат, позволяет создавать высококачественное ПО и характеризу#
ется умеренной гибкостью.
Kruchten, Philippe. The Rational Unified Process: An Introduction, 2d ed. Reading, MA:
Addison#Wesley, 2000. В этой книге представлен «архитектурно#центрический и оп#
ределяемый моделью использования» подход к выполнению проектов. Как и в
«Software Project Survival Guide», здесь особое внимание уделяется предваритель#
ным действиям, обеспечивающим высокую долговременную предсказуемость фи#
нансовых и временных затрат, умеренную гибкость работы и способствуют со#
зданию высококачественного ПО. В некоторых аспектах этот подход сложнее, чем
описанные в «Software Project Survival Guide» и «Extreme Programming Explained:
Embrace Change».
Jacobson, Ivar, Grady Booch and James Rumbaugh. The Unified Software Development
Process. Reading, MA: Addison#Wesley, 1999. Здесь представлено более глубокое об#
суждение тем, рассматриваемых в «The Rational Unified Process: An Introduction»,
2d ed.
Beck, Kent. Extreme Programming Explained: Embrace Change. Reading, MA: Addison#
Wesley, 2000. Бек описывает высокоитеративный подход, который фокусируется
на итеративной разработке требований к приложению и его проектов в сочета#
нии с конструированием. Подход «экстремального программирования» обладает
невысокой долговременной предсказуемостью, но обеспечивает высокую гибкость.
Gilb, Tom. Principles of Software Engineering Management. Wokingham, England: Addison#
Wesley, 1988. Подход Гилба предусматривает исследование главных вопросов пла#
нирования, выработки требований и проектирования архитектуры на ранних
этапах проекта и последующую непрерывную адаптацию планов проекта по мере
прогресса. Этот подход характеризуется долговременной предсказуемостью и
высокой гибкостью, а создаваемые на его основе приложения отличаются высо#
ким качеством. Он сложнее подходов, описанные в «Software Project Survival Guide»
и «Extreme Programming Explained: Embrace Change».
McConnell, Steve. Rapid Development. Redmond, WA: Microsoft Press, 1996. В этой книге
описан инструментальный подход к планированию проекта. Используя представ#
ленные в книге инструменты, опытный специалист по планированию проектов
сможет создать план, прекрасно адаптированный к уникальным особенностям
проекта.
Boehm, Barry and Richard Turner. Balancing Agility and Discipline: A Guide for the Per%
plexed. Boston, MA: Addison#Wesley, 2003. В этой книге исследуется контраст меж#
ду гибкой разработкой и разработкой, основанной на планировании. Особенно

56

ЧАСТЬ I

Основы разработки ПО

интересны четыре раздела главы 3: «A Typical Day using PSP/TSP», «A Typical Day
using Extreme Programming», «A Crisis Day using PSP/TSP» и «A Crisis Day using Extreme
Programming». Глава 5 посвящена использованию рискованных подходов с целью
уравновешивания гибкости, что может служить руководством по выбору между
гибким методом или методом, основанным на планировании. В главе 6 приводится
хорошо сбалансированная перспектива. Приложение Е включает подробные опыт#
ные данные о гибких методах разработки.
Larman, Craig. Agile and Iterative Development: A Manager’s Guide. Boston, MA: Add#
ison Wesley, 2004. Это основанное на тщательных исследованиях введение в гиб#
кие эволюционные стили разработки включает обзор подходов Scrum, Extreme
Programming, Unified Process и Evo.

Контрольный список: предварительные условия
 Установили ли вы тип проекта, над которым работаете, и адаптировали ли вы к нему свой подход?
 Достаточно ли хорошо определены и достаточно ли стабильны требования для начала конструирования? (См. контрольный список
вопросов о требованиях).
 Достаточно ли хорошо определена архитектура для начала конструирования? (См. контрольный список вопросов об архитектуре).
 Рассмотрели ли вы другие, уникальные для конкретного проекта факторы
риска, чтобы они не снижали эффективность конструирования?

http://cc2e.com/0386

Ключевые моменты
 Главной целью подготовки к конструированию является снижение риска. Убе#

дитесь, что проводимая вами подготовка снижает риск, а не повышает его.
 Если вы хотите разрабатывать высококачественное ПО, внимание к качеству

должно быть частью процесса разработки ПО с начала до конца. Внимание к
качеству в начале процесса оказывает наибольшее влияние на итоговое каче#
ство приложения.
 Одним из аспектов профессии программиста является объяснение руководи#

телям и коллегам процесса разработки ПО, в том числе важности адекватной
подготовки к программированию.
 Предварительные условия конструирования в большой степени зависят от типа

проекта, над которым вы работаете: многие проекты призывают к использо#
ванию высокоитеративного подхода, другие — более последовательного.
 При отсутствии грамотного определения проблемы вы можете на этапе кон#

струирования потратить силы на решение неверной проблемы.
 Если не проведена адекватная выработка требований, вы можете упустить важ#

ные детали проблемы. Изменения требований после конструирования обхо#
дятся в 20–100 раз дороже, чем на предыдущих этапах, поэтому перед нача#
лом программирования обязательно убедитесь в правильности требований.

ГЛАВА 3 Семь раз отмерь, один раз отрежь: предварительные условия

57

 Если не проведено адекватное проектирование архитектуры, во время конст#

руирования вы можете решать верную проблему неверным способом. По мере
написания кода для неверной архитектуры цена изменений архитектуры воз#
растает, так что перед началом программирования вы должны проверить и
правильность архитектуры.
 Выбор подхода к конструированию должен определяться принятым подходом

к предварительным условиям конструирования.

58

ЧАСТЬ I

Г Л А В А

Основы разработки ПО

4

Основные решения, которые
приходится принимать
при конструировании

Содержание
http://cc2e.com/0489

 4.1. Выбор языка программирования
 4.2. Конвенции программирования
 4.3. Волны развития технологий
 4.4. Выбор основных методик конструирования

Связанные темы
 Предварительные условия: глава 3
 Определение типа ПО, над которым вы работаете: раздел 3.2
 Влияние размера программы на ее конструирование: глава 27
 Управление конструированием: глава 28
 Проектирование ПО: главы 5–9

Как только вы убедились, что адекватный фундамент для конструирования про#
граммы создан, фокусом подготовки становятся решения, более специфичные для
конструирования. В главе 3 мы обсудили программные эквиваленты чертежей и
разрешений на конструирование. Как правило, у программистов нет особого кон#
троля над этими подготовительными действиями, поэтому главной темой главы
3 была оценка того, с чем приходится работать в начале конструирования. Эта
глава посвящена тем аспектам подготовки, за которые прямо или косвенно отве#
чают отдельные программисты и технические руководители проекта. Мы рассмот#
рим выбор специфических инструментов и непосредственную подготовку к ра#
боте над приложением.
Если вы думаете, что уже достаточно знаете о подготовке к конструированию,
можете сразу перейти к главе 5.

ГЛАВА 4 Основные решения, которые приходится принимать при конструировании

4.1.

59

Выбор языка программирования

Избавляя разум от всей ненужной работы, хорошая нотация позволяет со%
средоточиться на более сложных проблемах и в конечном счете повышает
интеллект человечества. До появления арабской нотации умножение было
весьма сложным, а деление даже целых чисел требовало усилий ведущих ма%
тематиков. Возможно, ничто в современном мире не смогло бы удивить гре%
ческого математика сильнее, чем то, что большинство европейцев умеют
делить крупные числа. Это показалось бы ему абсолютно невозможным…
Легкость выполнения операций над десятичными дробями — почти сверхъ%
естественный результат постепенного обнаружения отличной нотации.
Альфред Норт Уайтхед (Alfred North Whitehead)
Язык программирования, на котором будет реализована система, заслуживает
большого внимания, так как вы будете погружены в него с начала конструирова#
ния программы до самого конца.
Исследования показали, что выбор языка программирования несколькими спо#
собами влияет на производительность труда программистов и качество создава#
емого ими кода.
Если язык хорошо знаком программистам, они работают более производительно.
Данные, полученные при помощи модели оценки Cocomo II, показывают, что про#
граммисты, использующие язык, с которым они работали три года или более, при#
мерно на 30% более продуктивны, чем программисты, обладающие аналогичным
опытом, но для которых язык является новым (Boehm et al., 2000). В более раннем
исследовании, проведенном в IBM, было обнаружено, что программисты, облада#
ющие богатым опытом использования языка программирования, были более чем
втрое производительнее программистов, имеющих минимальный опыт (Walston and
Felix, 1977). (Различия результатов двух исследований объясняются тем, что в мо#
дели Cocomo II более тщательно изолируется влияние отдельных факторов.)
Программмисты, использующие языки высокого уровня, достигают бо#
лее высокой производительности и создают более качественный код, чем
программисты, работающие с языками низкого уровня. Утверждается, что
при работе с такими языками, как C++, Java, Smalltalk и Visual Basic, производи#
тельность труда программистов, а также надежность, простота и понятность про#
грамм в 5–15 раз выше, чем при использовании низкоуровневых языков, таких
как ассемблер и C (Brooks, 1987; Jones, 1998; Boehm, 2000). Избавившись от необ#
ходимости проводить праздничную церемонию каждый раз, когда оператор язы#
ка C делает то, что было задумано, вы сэкономите время. Более того, высокоуров#
невые языки выразительнее низкоуровневых. Каждая строка кода выполняет боль#
ший объем работы. В табл. 4#1 указано типичное отношение функциональности
команд некоторых языков к функциональности операторов языка C. Показатель,
превышающий 1, означает, что строка кода на указанном языке выполняет боль#
ше работы, чем строка кода на C.

60

ЧАСТЬ I

Основы разработки ПО

Табл. 4-1. Сравнение функциональности операторов высокоуровневых языков
с функциональностью операторов C
Язык

Функциональность операторов в сравнении с языком C

C

1

C++

2,5

Fortran 95

2

Java

2,5

Perl

6

Python

6

Smalltalk

6

Microsoft Visual Basic

4,5

Источники: «Estimating Software Costs» (Jones, 1998), «Software Cost Estimation with
Cocomo II» (Boehm, 2000) и «An Empirical Comparison of Seven Programming Languages»
(Prechelt, 2000).

Некоторые языки лучше выражают концепции программирования, чем другие.
Здесь уместно провести параллель между естественными языками — скажем, ан#
глийским — и языками программирования, такими как Java и C++. Изучая есте#
ственные языки, лингвисты Сапир и Уорф (Sapir and Whorf) высказали предполо#
жение, что способность к размышлению над определенными идеями связана с
выразительной силой языка. Согласно гипотезе Сапира#Уорфа способность чело#
века к обдумыванию определенных мыслей зависит от знания слов, при помощи
которых можно выразить эту мысль. Если вы не знаете слов, то не сможете выра#
зить мысль и, возможно, даже сформулировать ее (Whorf, 1956).
Программисты испытывают аналогичное влияние языков программирования.
«Слова», которые язык предоставляет программисту для выражения мыслей, не#
сомненно, влияют на способ их выражения, а возможно, даже определяют, какие
мысли можно выразить на данном языке.
За доказательствами влияния, оказываемого языками программирования на мыш#
ление программистов, далеко ходить не надо. Типичная история такова: «Мы пи#
сали новую систему на C++, но большинство наших программистов не имели
особого опыта работы на C++. Раньше они использовали Fortran. Они писали код,
который компилировался на C++, но на самом деле это был замаскированный
Fortran. В итоге они заставили C++ эмулировать недостатки языка Fortran (такие
как операторы goto и глобальные данные) и проигнорировали богатый набор
объектно#ориентированных возможностей C++». Данный феномен наблюдается
в отрасли уже много лет (Hanson, 1984; Yourdon, 1986a).

Описания языков
История разработки некоторых языков и их общие возможности довольно инте#
ресны. Ниже приведены описания языков, наиболее популярных в настоящее время.

Ada
Высокоуровневый язык общего назначения, основанный на языке Pascal. Разра#
ботанный под патронажем Минобороны США, он особенно хорошо подходит для

ГЛАВА 4 Основные решения, которые приходится принимать при конструировании

61

создания встроенных систем и систем, работающих в реальном времени. В языке
Ada особое внимание уделяется абстракции данных и сокрытию информации, а
также проводится различие между открытыми и закрытыми частями каждого класса
и пакета. Название «Ada» было присвоено языку в честь Ады Лавлейс (Ada Lovelace)
— женщины#математика, которую считают первым программистом в мире. Сегодня
язык Ada используется преимущественно для разработки военных, космических
и авиационных систем.

Ассемблер
Низкоуровневый язык, каждая команда которого соответствует одной команде
компьютера. Вследствие этого ассемблер специфичен для отдельных процессо#
ров — например, для конкретных процессоров Intel или Motorola. Ассемблер счи#
тается языком второго поколения. Большинство программистов избегают его и
используют, только если к быстродействию или компактности кода программы
предъявляются повышенные требования.

C
Среднеуровневый язык общего назначения, первоначально тесно связанный с ОС
UNIX. Некоторые свойства (структурированные данные, структурированная управ#
ляющая логика, машинная независимость и богатый набор операторов) делают
его похожим на высокоуровневый язык. Язык C также называют «портируемым
языком ассемблера», поскольку он не строго типизирован, поощряет применение
указателей и адресов и поддерживает некоторые низкоуровневые возможности,
такие как побитовые операции.
Язык C, разработанный в 1970#х компанией Bell Labs, предназаначался для сис#
тем DEC PDP#11. На C были написаны ОС, компилятор C и приложения UNIX для
систем DEC PDP#11. В 1988 г. для систематизации C был издан стандарт ANSI, ко#
торый в 1999 г. был пересмотрен. В 1980#х и 1990#х гг. язык C был стандартом
«де#факто» в области разработки программ для микрокомпьютеров и рабочих стан#
ций.

C++
Этот объектно#ориентированный язык был разработан на базе C в компании Bell
Labs в 1980#х. Совместимый с языком C, он поддерживает классы, полиморфизм,
обработку исключений, шаблоны и обеспечивает более надежную проверку ти#
пов, чем C. Кроме того, он предоставляет разработчикам богатую и эффективную
стандартную библиотеку.

C#
Эта комбинация объектно#ориентированного языка общего назначения и среды
программирования разработана в Microsoft. C# имеет синтаксис, похожий на син#
таксис C, C++ и Java, и включает богатый инструментарий, помогающий разраба#
тывать приложения на платформах Microsoft.

62

ЧАСТЬ I

Основы разработки ПО

Cobol
Напоминает английский язык и был разработан в 1959–1961 гг. для нужд Мин#
обороны США. Cobol служит преимущественно для разработки бизнес#приложе#
ний и до сих пор является одним из самых популярных языков, уступая лишь Visual
Basic (Feiman and Driver, 2002). По мере развития языка в нем была реализована
поддержка дополнительных математических функций и ряда объектно#ориенти#
рованных возможностей. Аббревиатура «Cobol» расшифровывается как «COmmon
Business#Oriented Language» (универсальный язык, ориентированный на коммер#
ческие задачи).

Fortran
В этом первом высокоуровневом языке программирования были представлены
концепции переменных и высокоуровневых циклов. Название расшифровывает#
ся как «FORmula TRANslation» (транслятор формул). Разработанный в 1950#х, Fortran
претерпел несколько значительных ревизий: так, в 1977 г. была разработана вер#
сия Fortran 77, в которой была реализована поддержка блочных операторов if#then#
else и манипуляций над символьными строками. В Fortran 90 были включены сред#
ства работы с пользовательскими типами данных, указателями, классами, а также
богатый набор функций для работы с массивами. Fortran применяется преимуще#
ственно для разработки научных и инженерных приложений.

Java
Синтаксис этого объектно#ориентированного языка, разработанного Sun Micro#
systems, Inc., напоминает C и C++. Java — платформенно#независимый язык: ис#
ходный код Java сначала преобразуется в байт#код, который может выполняться
на любой платформе в среде, известной как «виртуальная машина». Java широко
используется для создания Web#приложений.

JavaScript
Этот интерпретируемый язык сценариев мало чем связан с Java. Чаще всего его
используют для создания кода, выполняющегося на клиентской стороне, напри#
мер, для разработки несложных функций и интерактивных приложений для Web#
страниц.

Perl
Этот язык обработки строк основан на C и нескольких утилитах ОС UNIX. Perl часто
используется для решения задач системного администрирования, таких как со#
здание сценариев сборки программ, а также для генерации и обработки отчетов.
Кроме того, на нем создают Web#приложения, такие как Slashdot. Аббревиатура
«Perl» расшифровывается как «Practical Extraction and Report Language» (практи#
ческий язык извлечений и отчетов).

PHP
Этот язык с открытым исходным кодом предназначен для разработки сценариев
и имеет простой синтаксис, похожий на синтаксис языков Perl, JavaScript, C и
оболочки Bourne Shell. PHP поддерживается всеми основными ОС и служит для

ГЛАВА 4 Основные решения, которые приходится принимать при конструировании

63

создания интерактивных функций, выполняющихся на стороне сервера. PHP#код
может быть встроен в Web#страницы для получения доступа к БД и отображения
содержащейся в ней информации. Аббревиатура «PHP» первоначально расшиф#
ровывалась как «Personal Home Page», но теперь означает «PHP: Hypertext Processor».

Python
Этот интерпретируемый интерактивный объектно#ориентированный язык под#
держивает множество сред. Чаще всего его используют для написания сценариев
и небольших Web#приложений, однако он поддерживает и некоторые средства,
помогающие создавать более крупные программы.

SQL
SQL (Structured Query Language, язык структурированных запросов) «де#факто» яв#
ляется стандартным языком выполнения запросов, обновлений реляционнных БД
и управления ими. В отличие от других языков, описанных в этом разделе, SQL
является «декларативным языком», т. е. определяет не последовательность, а резуль#
тат выполнения некоторых операций.

Visual Basic
Basic (Beginner’s All#purpose Symbolic Instruction Code, универсальная система сим#
волического кодирования для начинающих) — это высокоуровневый язык, первая
версия которого была разработана в Дартмутском колледже в 1960#х. Visual Basic
— это высокоуровневая объектно#ориентированная версия Basic, предназначенная
для визуального программирования. Изначально Visual Basic был разработан в
Microsoft для создания приложений Microsoft Windows. Позднее в нем была реали#
зована поддержка настройки Microsoft Office и других приложений для настольных
ПК, создания Web#приложений и других программ. По оценкам экспертов в самом
начале первого десятилетия XXI века Visual Basic являлся самым популярным язы#
ком среди профессиональных разработчиков (Feiman and Driver, 2002).

4.2.

Конвенции программирования

В высококачественном приложении должна быть очевидна
Перекрестная ссылка Подробсвязь между концептуальной целостностью архитектуры и
нее о силе конвенций см. разее низкоуровневой реализацией. Реализация должна соот#
делы 11.3–11.5.
ветствовать высокоуровневой архитектуре и обладать внут#
ренней согласованностью. В этом и заключается смысл принципов конструиро#
вания, определяющих конвенции именования переменных, классов, методов, а
также форматирования кода и оформления комментариев.
При разработке сложной программы архитектурные принципы вносят в программу
структурный баланс, а принципы конструирования — низкоуровневую гармонию,
при наличии которой каждый класс воспринимается как неотъемлемая часть об#
щего плана. Любая крупная программа требует применения контролирующей
структуры, унифицирующей аспекты языка программирования. Красота крупной
структуры частично заключается в том, как в ее отдельных компонентах выраже#
ны особенности архитектуры. Без унификации ваша программа будет смесью

64

ЧАСТЬ I

Основы разработки ПО

небрежных вариаций стиля, заставляющих прилагать дополнительные усилия
только для того, чтобы понять различия в стиле кодирования, которых вполне
можно было избежать. Одно из условий успешного программирования — устра#
нение ненужных вариаций, позволяющее сосредоточиться на действительно не#
обходимых вариациях. См. об этом подраздел «Главный технический императив
разработки ПО: управление сложностью» раздела 5.2.
Что, если у вас есть отличный план создания картины, но одну ее часть вы реши#
те писать в классическом стиле, другую в импрессионистском, а третью в кубист#
ском? Как бы упорно вы ни следовали своему грандиозному плану, картина не будет
концептуально целостной. Она будет похожа на коллаж. Программа тоже должна
обладать низкоуровневой целостностью.
Перед началом конструирования сформулируйте конвенции программи#
рования. Детали конвенций кодирования относятся к такому низкому
уровню, что после написания программы их почти невозможно изменить.
В оставшейся части книги я еще не раз затрону конвенции кодирования.

4.3.

Волны развития технологий

Я видел, как взошла звезда ПК, в то время как звезда мэйнфреймов опустилась за
горизонт. Я видел, как консольные программы были вытеснены программами с
GUI. Я также видел, как традиционные программы уступили главную роль Web#
приложениям. Могу предположить, что, когда вы будете читать эту книгу, будут
бурно развиваться некоторые новые технологии, а Web#программирование в его
современном (2004) виде начнет отходить на второй план. В соответствии с эти#
ми технологическими циклами, или волнами, изменяются и методики програм#
мирования.
В зрелых технологических средах — таких как среда Web#программирования в
середине 2000#х — нам доступны вседостоинства богатой инфраструктуры раз#
работки ПО. Такие среды предоставляют широкий выбор языков программиро#
вания, мощные средства поиска ошибок, эффективные инструменты отладки и
надежные автоматизированные средства оптимизации производительности при#
ложений. Компиляторы почти не содержат ошибок. Инструменты хорошо опи#
саны в документации производителей, в книгах и статьях сторонних фирм и на
многочисленных Web#сайтах. Инструменты интегрированы, благодаря чему вы
можете разрабатывать UI, модули работы с БД, составления отчетов и бизнес#ло#
гики в одной среде. Решения проблем можно легко найти в ответах на «часто за#
даваемые вопросы». Кроме того, доступны разнообразные услуги консультантов
и программы тренинга.
В ранних средах — таких как Web#программирование в середине 1990#х — ситу#
ация противоположная. Языков программирования мало, при этом они часто полны
ошибок и плохо документированы. Вместо написания нового кода программис#
ты тратят массу времени только на то, чтобы разобраться в особенностях языка.
Бесчисленные часы уходят на борьбу с ошибками в языках, ОС и других инстру#
ментах. Инструменты программирования часто примитивны. Отладчиков может
не быть вообще, а об оптимизаторах компиляторов программистам приходится

ГЛАВА 4 Основные решения, которые приходится принимать при конструировании

65

лишь мечтать. Производители часто выпускают новые версии компиляторов, при
этом каждая новая версия отказывается поддерживать значительные части ваше#
го кода. Инструменты не интегрированы, из#за чего UI, модули работы с БД, со#
ставления отчетов и бизнес#логики приходится разрабатывать при помощи раз#
ных средств. Из#за плохой совместимости инструментов и частого появления новых
компиляторов и библиотек программисты тратят много усилий только на под#
держание работоспособности имеющейся инфраструктуры. При возникновении
проблем в Интернете можно найти кое#какую документацию, но она не отлича#
ется достоверностью и полнотой.
Вам может показаться, что я рекомендую избегать программирования в ранних
средах, но это не так. В ранних средах были разработаны программы, давшие
начало некоторым из самых инновационных приложений, такие как Turbo Pascal,
Lotus 123, Microsoft Word и браузер Mosaic. Я просто хочу сказать, что от стадии
развития технологии зависит то, как будет протекать ваша работа. В зрелой сре#
де вы можете посвящать большую часть дня постепенной реализации новой функ#
циональности. Работая в ранней среде, исходите из того, что вам придется тра#
тить много времени на выяснение недокументированных возможностей выбран#
ного языка программирования, отладку ошибок, которые в итоге окажутся дефек#
тами библиотек, проверку того, что написанный код будет работать с новой вер#
сией библиотеки какого#нибудь производителя и т. д.
При работе в примитивной среде методики программирования, описанные в этой
книге, могут оказаться еще более полезными, чем в зрелых средах. Как сказал Дэвид
Грайс (Gries, 1981), подход к программированию не должен определяться исполь#
зуемыми инструментами. В связи с этим он проводит различие между програм#
мированием на языке (programming in language) и программированием с исполь%
зованием языка (programming into language). Разработчики, программирующие «на»
языке, ограничивают свое мышление конструкциями, непосредственно поддер#
живаемых языком. Если предоставляемые языком средства примитивны, мысли
программистов будут столь же примитивными.
Разработчики, программирующие «с использованием» языка, сначала решают, какие
мысли они хотят выразить, после чего определяют, как выразить их при помощи
конкретного языка.

Пример программирования с использованием языка
Разрабатывая программу на Visual Basic, который тогда находился на раннем эта#
пе развития, я с огорчением обнаружил, что язык не поддерживает встроенных
способов разделения бизнес#логики, кода GUI и кода работы с БД. Я знал, что, если
буду невнимателен, со временем некоторые из моих «форм» Visual Basic включат
в себя код бизнес#логики, другие — код доступа к БД, а остальные не будут содер#
жать ни того, ни другого — в итоге я не смогу вспомнить, какая форма за что
отвечает. Я только что завершил работу над проектом C++, в котором разделение
кода было выполнено плохо, и не хотел еще раз наступать на те же грабли.
Поэтому я принял конвенцию, в соответствии с которой файлам .frm (файлам
формы) дозволялось только извлекать данные из БД и сохранять их обратно, но
не передавать эти данные другим частям программы. Все формы поддерживали

66

ЧАСТЬ I

Основы разработки ПО

метод IsFormCompleted(), который сообщал вызвавшему его методу, сохранила ли
активная форма свои данные. IsFormCompleted() был единственным открытым
методом, который могли иметь формы. Код форм также не мог включать никакой
бизнес#логики. Весь остальной код, в том числе проверяющий корректность вво#
димых в форму данных, должен был содержаться в ассоциированном файле .bas.
Visual Basic не поощрял такого подхода. Он поощрял программистов включать в
файл .frm максимальный объем кода, и это отнюдь не облегчало реализацию вза#
имодействия межу файлами .frm и .bas.
Принятая мной конвенция была очень проста, но по мере развития проекта я
обнаружил, что она помогла мне избежать многих случаев, в которых мне при#
шлось бы писать неестественный код. Так, мне пришлось бы загружать формы, но
держать их скрытыми, чтобы можно было вызвать реализованные в них методы
проверки корректности данных, или мне пришлось бы копировать код форм в
другие места программы и сопровождать этот параллельный код. Кроме того,
конвенция IsFormCompleted() позволила все упростить. Все формы работали оди#
наково, поэтому я мог не предполагать семантику IsFormCompleted() — вызовы
этого метода всегда имели одинаковый смысл.
Visual Basic не поддерживал такой подход непосредственно, но простая конвен#
ция программирования — программирование с использованием языка — позво#
лила мне реализовать отсутствующую в то время структуру языка и помогла
упростить проект до приемлемого уровня.
Понимание различия между программированием на языке и програм
ми рованием с использованием языка — важнейшее условие понима#
ния этой книги. Большинство важных принципов программирования
зависит не от конкретных языков, а от способа их использования. Если язык не
поддерживает нужные конструкции или имеет другие недостатки, попробуйте
их компенсировать. Создайте свои конвенции кодирования, стандарты, библио#
теки классов и другие средства.

4.4.

Выбор основных методик конструирования

При подготовке к конструированию следует решить, какие из доступных эффек#
тивных методик вы будете использовать. Некоторые проекты предусматривают пар#
ное программирование и предварительное создание тестов, тогда как другие —
индивидуальное программирование и проведение формальных инспекций. Обе
комбинации методик могут быть удачными, но при их выборе следует учитывать
специфические особенности проекта.
Специфические методики конструирования, которые должны быть осознанно
приняты или отвергнуты, указаны ниже. В оставшейся части книги эти методики
будут описаны подробнее.

ГЛАВА 4 Основные решения, которые приходится принимать при конструировании

Контрольный список: основные методики
конструирования

67

http://cc2e.com/0496

Кодирование
 Решили ли вы, какая часть проекта приложения будет разработана предварительно, а какая во время написания кода?
 Выбрали ли вы конвенции именования программных элементов, оформления комментариев и форматирования кода?
 Выбрали ли вы специфические методики кодирования, определяемые архитектурой приложения? Определили ли вы, как будут обрабатываться ошибки, как будут решаться проблемы, связанные с безопасностью, какие конвенции
будут использоваться при разработке интерфейсов классов, каким стандартам должен будет отвечать повторно используемый код, сколько внимания нужно
будет уделять быстродействию приложения при кодировании и т. д.?
 Определили ли вы стадию развития используемой технологии и адаптировали ли к ней свой подход? Если это необходимо, определились ли вы с
тем, как будете программировать с использованием языка, вместо того чтобы
ограничиваться программированием на нем?
Работа в группе
 Определили ли вы процедуру интеграции? Иначе говоря, какие специфические действия программист должен будет выполнить перед включением
своего кода в исходный код всего проекта?
 Будут ли программисты программировать парами, индивидуально или эти
подходы будут скомбинированы?
Гарантия качества
Перекрестная ссылка Гарантия
 Должны ли будут программисты разработать тесты для
качества рассматривается в гласвоего кода до написания самого кода?
ве 20.
 Должны ли будут программисты разработать блочные
тесты для своего кода?
 Должны ли будут программисты перед включением своего кода в исходный
код всего проекта проанализировать его в отладчике?
 Должны ли будут программисты выполнить интеграционное тестирование
своего кода до его включения в исходный код проекта?
 Будут ли программисты выполнять взаимные обзоры или инспекцию кода?
Инструменты
Перекрестная ссылка Об инст Выбрали ли вы инструмент управления версиями?
рументах программирования см.
 Выбрали ли вы язык, версию языка и версию компиляглаву 30.
тора?
 Выбрали ли вы платформу программирования (такую как
J2EE или Microsoft .NET) или явно решили не использовать ее?
 Приняли ли вы решение о том, можно ли будет использовать нестандартные возможности языка?
 Определили ли вы другие средства, которые будете применять: редактор,
инструмент рефакторинга, платформу для тестирования, модуль проверки
синтаксиса и т. д.? Приобрели ли вы их?

68

ЧАСТЬ I

Основы разработки ПО

Ключевые моменты
 Каждый язык программирования имеет достоинства и недостатки. Вы долж#

ны знать отдельные достоинства и недостатки используемого языка.
 Определите конвенции программирования до начала программирования.

Позднее адаптировать к ним код станет почти невозможно.
 Методик конструирования слишком много, чтобы использовать все в одном

проекте. Тщательно выбирайте методики, наиболее подходящие для вашего про#
екта.
 Спросите себя, являются ли используемые вами методики программирования

ответом на выбранный язык программирования или их выбор был определен
языком. Помните, что программировать следует с использованием языка, а не
на языке.
 Эффективность конкретных подходов и даже возможность их применения за#

висит от стадии развития соответствующей технологии. Определите стадию раз#
вития используемой технологии и адаптируйте к ней свои планы и ожидания.

ГЛАВА 4 Основные решения, которые приходится принимать при конструировании

Часть II

ВЫСОКОКАЧЕСТВЕННЫЙ
КОД



Глава 5. Проектирование при конструировании



Глава 6. Классы



Глава 7. Высококачественные методы



Глава 8. Защитное программирование



Глава 9. Процесс программирования с псевдокодом

69

70

ЧАСТЬ II Высококачественный код

ГЛ А В А

5

Проектирование
при конструировании

Содержание
http://cc2e.com/0578

 5.1. Проблемы, связанные с проектированием ПО
 5.2. Основные концепции проектирования
 5.3. Компоненты проектирования: эвристические принципы
 5.4. Методики проектирования
 5.5. Комментарии по поводу популярных методологий

Связанные темы
 Разработка архитектуры ПО: раздел 3.5
 Классы: глава 6
 Характеристики высококачественных методов: глава 7
 Защитное программирование: глава 8
 Рефакторинг: глава 24
 Зависимость конструирования от объема программы: глава 27

Некоторые программисты могут заявить, что проектирование не связано с конструированием, но при работе над небольшими проектами конструирование
часто включает другие процессы, в том числе проектирование. В некоторых более крупных проектах формальная архитектура может давать ответы только на
во#просы системного уровня, при этом значительная часть проектирования может
быть намеренно оставлена на этап конструирования. В других крупных проектах
проектирование может быть проведено в таком объеме, что кодирование становится почти механическим, однако это случается редко — официально или нет,
программисты обычно сами проектируют некоторые фрагменты программы.
Перекрестная ссылка Об уровнях формальности, требуемой
при работе над крупными и небольшими проектами, см. главу 27.

В случае небольших неформальных проектов значительная
часть проектирования выполняется за клавиатурой. «Проектирование» может выражаться в простом написании интерфейса класса на псевдокоде до разработки его деталей.
Оно может выражаться в рисовании диаграмм отношений

ГЛАВА 5 Проектирование при конструировании

71

нескольких классов перед написанием их кода. Оно может выражаться в обсуждении оптимального шаблона проектирования вместе с коллегой. Какую бы форму
проектирование ни принимало, от тщательного его выполнения выигрывают
проекты любого масштаба, и, рассматривая проектирование как явный процесс,
вы извлечете из него максимальную выгоду.
Проектирование — очень обширная тема, поэтому в данной главе мы рассмотрим
только несколько ее аспектов. Эффективность проектирования классов или методов во многом определяется архитектурой системы, поэтому убедитесь, что вы
выполнили предварительные условия, связанные с разработкой архитектуры (см.
раздел 3.5). Еще больший объем проектирования выполняется на уровне отдельных
классов и методов, что мы обсудим в главах 6 и 7.
Если вы уже хорошо знакомы с проектированием ПО, можете только бегло просмотреть основные моменты раздела 5.1, посвященного проблемам проектирования, и раздела 5.3, в котором обсуждаются основные эвристические принципы
проектирования.

5.1.

Проблемы, связанные
с проектированием ПО

Под «проектированием ПО» понимают разработку или изоПерекрестная ссылка О разлибретение схемы преобразования спецификации приложения
чии между эвристическим и дев готовое приложение. Проектирование — это тот процесс,
терминированным процессами
который связывает выработку требований с кодированием
см. главу 2.
и отладкой. Структура удачного высокоуровневого проекта
приложения может успешно охватывать целый ряд более
низкоуровневых проектов. Хорошее проектирование полезно при работе над небольшими приложениями и просто необходимо при работе над крупными.
Однако с проектированием связано множество проблем — их#то мы и обсудим.

Проектирование — «грязная» проблема
Хорст Риттел и Мелвин Веббер определили «грязную» проблему как проблему, которую можно ясно определить только
путем полного или частичного решения (Rittel and Webber,
1973). По сути данный парадокс подразумевает, что проблему нужно «решить» один раз, чтобы получить ее ясное
определение, а затем еще раз для создания работоспособного
решения. Этот процесс уже несколько десятилетий неразрывно связан с разработкой ПО (Peters and Tripp, 1976).
Одним драматическим примером подобной грязной проблемы является проектирование первого варианта моста
Tacoma Narrows. В то время главным соображением при
проектировании мостов было обеспечение прочности, адекватной планируемой нагрузке. В случае моста Tacoma Narrows оказалось, что ветер вызывает непредвиденные волнообразные гармонические колебания моста из стороны в

Образ разработчика, проектирующего программу рациональным безошибочным способом
на основе ясно сформулированных требований, совершенно
нереалистичен. Никакая система
так никогда не разрабатывалась
и, наверное, не будет разрабатываться. Даже примеры разработки небольших программ,
встречающиеся в учебниках,
нереалистичны. Авторы перепроверяют и улучшают их до тех
пор, пока не продемонстрируют
нам то, что они хотели бы получить, а не то, что получается
на самом деле.
Дэвид Парнас и Пол Клементс (David Parnas and
Paul Clements)

72

ЧАСТЬ II Высококачественный код

сторону. В один ветреный день 1940 г. колебания неконтролируемо усилились, и
часть моста обрушилась (рис. 5#1).
Это наглядный пример грязной проблемы: до разрушения моста инженеры не
знали, что аэродинамика играет такую большую роль. Только построив мост (решив проблему), они смогли обнаружить дополнительный аспект проблемы, что
позволило им возвести новый мост, действующий и поныне.

Рис. 5'1. Мост Tacoma Narrows — пример грязной проблемы

Одно из главных отличий программ, которые вы разрабатывали в институте, от
программ, которые разрабатываете теперь, став профессиональным программистом, в том, что проблемы проектирования, решаемые институтскими программами, редко бывают грязными, если вообще бывают таковыми. В институте
задания по программированию составлены так, чтобы вы по кратчайшему пути
двигались от начала решения к его результату. Преподавателя, который дает
студентам задания и свободно изменяет их по завершении проектирования и
даже перед сдачей готовых программ, вероятно, облили бы дегтем и вываляли в
перьях. Однако в мире профессионального программирования такие изменения
происходят ежедневно.

Проектирование — неряшливый процесс
(даже если оно приводит к аккуратному результату)
Завершенный проект приложения должен выглядеть хорошо организованным и
ясным, но процесс разработки этого проекта далеко не так аккуратен, как конечный результат.

ГЛАВА 5 Проектирование при конструировании

73

Проектирование неряшливо потому, что вы выполняете
Дополнительные сведения См.
много неверных действий и попадаете во множество туобсуждение этой точки зрения
пиков, т. е. совершаете массу ошибок. В действительности
в статье «A Ratio nal Design
Process: How and Why to Fake It»
ошибки являются сутью проектирования: дешевле допустить
(Parnas and Clements, 1986).
ошибки и исправить проект программы, чем найти их после
кодирования и исправлять готовый код. Проектирование
неряшливо потому, что удачное решение часто лишь чуть#чуть отличается от
неудачного.
Проектирование неряшливо еще и потому, что трудно
узнать, когда проект «достаточно хорош». Какого уровня
детализации достаточно? Какую часть проектирования выполнить с использованием формальной нотации, а какую
— прямо за клавиатурой? Когда проектирование считать
завершенным? Улучшать проект программы можно постоянно, поэтому чаще всего на последний вопрос отвечают:
«Когда у вас вышло время».

Перекрестная ссылка Лучший
ответ на этот вопрос см. в подразделе «Какую степень проектирования можно считать
достаточной?» раздела 5.4.

Проектирование связано с определением
компромиссов и приоритетов
В идеальном мире все системы обладали бы бесконечным быстродействием, не
предъявляли никаких требований к подсистеме хранения данных, давали нулевую
нагрузку на сеть, никогда не содержали никаких ошибок и создавались без всяких
затрат. Однако в реальном мире один из важнейших аспектов работы проектировщика — анализ конкурирующих характеристик проекта и достижение баланса
между ними. Если быстрота отклика системы важнее, чем минимизация времени
разработки, проектировщик выберет один вариант. Если во главе угла быстрота
разработки, оптимальным может оказаться другой вариант проекта.

Проектирование подразумевает ограничение возможностей
Проектирование предполагает не только обеспечение возможностей, но и их
ограничение. Если бы люди, создавая физические структуры, обладали бесконечным объемом времени и ресурсов, мы увидели бы на улицах невероятно странные
здания с диковинными башенками и сотнями комнат на отдельных этажах. При
отсутствии целенаправленно заданных ограничений ПО может оказаться именно
таким. Ограниченные объемы ресурсов при конструировании зданий требуют
упрощения решения, что в итоге приводит к его улучшению. Проектирование ПО
в этом смысле ничем не отличается.

Проектирование — недетерминированный процесс
Если вы попросите трех человек спроектировать одну и ту же программу, они
вполне могут разработать три совершенно разных, но вполне приемлемых проекта. Как правило, спроектировать компьютерную программу можно десятками
разных способов.

74

ЧАСТЬ II Высококачественный код

Проектирование — эвристический процесс
Так как проектирование не детерминировано, методы проектирования
чаще всего являются эвристическими методами, т. е. «практическими пра#
вилами» или «способами, которые могут сработать», а не воспроизводимыми процессами, которые всегда приводят к предсказуемым результатам. Проектирование — метод проб и ошибок. Инструменты или методы проектирования,
оказавшиеся эффективными в одном случае, в другой ситуации могут оказаться куда
менее эффективными. Универсальных методик проектирования не существует.

Проектирование — постепенный процесс
http://cc2e.com/0539

Дополнительные сведения ПО
— не единственный тип структур, изменяющихся с течением
времени. Физические структуры
также развиваются; см. об этом
книгу «How Buildings Learn»
(Brand, 1995).

5.2.

Можно довольно удачно обобщить названные аспекты проектирования, сказав, что проектирование — «постепенный»
процесс. Проекты приложений не возникают в умах разработчиков сразу в готовом виде. Они развиваются и улучшаются
в ходе обзоров, неформальных обсуждений, написания кода
и выполнения его ревизий.
Практически во всех случаях проект несколько меняется
во время первоначальной разработки системы и еще больше — при ее модернизации. Степень, в которой изменение
выгодно или приемлемо, зависит от особенностей создаваемого ПО.

Основные концепции проектирования

Успешное проектирование ПО требует понимания нескольких важных концепций.
Здесь мы обсудим роль сложности при проектировании, желательные характеристики проектов и уровни проектирования.

Главный Технический Императив Разработки ПО: управление сложностью
Перекрестная ссылка О влиянии
сложности на другие аспекты программирования см. раздел 34.1.

Чтобы лучше понять важность управления сложностью, обратимся к известной работе Фреда Брукса «No Silver Bullets:
Essence and Accidents of Software Engineering» (Brooks, 1987).

Существенные и несущественные проблемы
Брукс утверждает, что сложность разработки ПО объясняется существенными и
несущественными проблемами. Используя два этих термина, Брукс опирается на
философскую традицию, уходящую корнями к Аристотелю. В философии существенными называют свойства, которыми объект должен обладать, чтобы быть именно
этим объектом. Автомобиль должен иметь двигатель, колеса и двери — если объект
не обладает каким#нибудь из этих существенных свойств, это не автомобиль.
Несущественными (акцидентными) свойствами называют свойства, которыми
объект обладает в силу случайности, — свойства, не влияющие на его суть. Так,
автомобиль может иметь четырехцилиндровый двигатель с турбонаддувом, восьмицилиндровый или любой другой и все же являться автомобилем. Тип двигателя

ГЛАВА 5 Проектирование при конструировании

75

и колес, число дверей — все это несущественные свойства. Можете также думать
о них как о второстепенных, произвольных, необязательных и случайных.
Брукс замечает, что главные несущественные проблемы разПерекрестная ссылка В ранних
работки ПО уже давно решены. Например, несущественные
средах несущественные проблепроблемы, связанные с неудобным синтаксисом языков
мы проявляются сильнее, чем в
зрелых (см. раздел 4.3).
программирования, постепенно утратили свою значимость
по мере эволюции языков. Несущественные проблемы, связанные с неинтерактивностью компьютеров, исчезли, когда
на смену ОС, работающим в пакетном режиме, пришли системы с разделением
времени. Среды интегрированной разработки избавили программистов от проблем, обусловленных плохим взаимодействием инструментов.
В то же время Брукс утверждает, что решение оставшихся существенных проблем
разработки ПО будет более медленным. Это объясняется тем, что разработка
программ по своей сути требует анализа всех деталей крайне сложного набора
взаимосвязанных концепций. Причиной существенных проблем является необходимость анализа сложного неорганизованного реального мира, точного и
полного определения зависимостей и исключений, проектирования абсолютно,
но никак не приблизительно верных решений и т. д. Даже если б мы смогли
придумать язык программирования, основанный на той же терминологии, что
и требующая решения проблема реального мира, программирование все равно
осталось бы сложным из#за необходимости точного определения принципов
функционирования мира. По мере того как разработчики ПО берутся за решение
все более серьезных проблем реального мира, им приходится анализировать все
более сложные взаимодействия между сущностями, что в свою очередь приводит
к повышению существенной сложности программных решений.
Источник всех этих существенных проблем — сложность как несущественная,
так и существенная.

Важность управления сложностью
Программные проекты редко терпят крах по техническим
Есть два способа разработки
причинам. Чаще всего провал объясняется неадекватной
проекта приложения: сделать
выработкой требований, неудачным планированием или
его настолько простым, чтобы
было очевидно, что в нем нет
неэффективным управлением. Если же провал обусловлен
недостатков, или сделать его
все#таки преимущественно технической причиной, очень
таким сложным, чтобы в нем не
часто ею оказывается неконтролируемая сложность. Иначе
было очевидных недостатков.
говоря, приложение стало таким сложным, что разработчики
Ч. Э. Р. Хоар (C. A. R. Hoare)
перестали по#настоящему понимать, что же оно делает. Если
работа над проектом достигает момента, после которого уже
никто не может полностью понять, как изменение одного фрагмента программы
повлияет на другие фрагменты, прогресс прекращается.
Управление сложностью — самый важный технический аспект разработки
ПО. По#моему, управление сложностью настолько важно, что оно долж#
но быть Главным Техническим Императивом Разработки ПО.
Сложность — не новинка в мире разработки ПО. Один из пионеров информатики Эдсгер Дейкстра обращал внимание на то, что компьютерные технологии —

76

ЧАСТЬ II Высококачественный код

единственная отрасль, заставляющая человеческий разум охватывать диапазон,
простирающийся от отдельных битов до нескольких сотен мегабайт информации,
что соответствует отношению 1 к 109, или разнице в девять порядков (Dijkstra,
1989). Такое гигантское отношение просто ошеломляет. Дейкстра выразил это так:
«По сравнению с числом семантических уровней средняя математическая теория
кажется почти плоской. Создавая потребность в глубоких концептуальных иерархиях, компьютерные технологии бросают нам абсолютно новый интеллектуальный
вызов, не имеющий прецедентов в истории». Разумеется, за прошедшее с 1989 г.
время сложность ПО только выросла, и сегодня отношение Дейкстры вполне
может характеризоваться 15 порядками.
Одним из симптомов того, что
вы погрязли в чрезмерной сложности, является упрямое применение метода, нерелевантность
которого очевидна по крайней
мере любому внешнему наблюдателю. При этом вы уподобляетесь человеку, который при поломке автомобиля в силу своей
некомпетентности не находит
ничего лучшего, чем заменить
воду в радиаторе и выбросить
окурки из пепельниц.
Ф. Дж. Плоджер
(P. J. Plauger)

Дейкстра пишет, что ни один человек не обладает интеллектом, способным вместить все детали современной
компьютерной программы (Dijkstra, 1972), поэтому нам
— разработчикам ПО — не следует пытаться охватить всю
программу сразу. Вместо этого мы должны попытаться организовать программы так, чтобы можно было безопасно
работать с их отдельными фрагментами по очереди. Целью
этого является минимизация объема программы, о котором нужно думать в конкретный момент времени. Можете
считать это своеобразным умственным жонглированием:
чем больше умственных шаров программа заставляет поддерживать в воздухе, тем выше вероятность того, что вы
уроните один из них и допустите ошибку при проектировании или кодировании.

На уровне архитектуры ПО сложность проблемы можно
снизить, разделив систему на подсистемы. Несколько несложных фрагментов
информации понять проще, чем один сложный. В разбиении сложной проблемы
на простые фрагменты и заключается цель всех методик проектирования ПО. Чем
более независимы подсистемы, тем безопаснее сосредоточиться на одном аспекте сложности в конкретный момент времени. Грамотно определенные объекты
разделяют аспекты проблемы так, чтобы вы могли решать их по очереди. Пакеты
обеспечивают такое же преимущество на более высоком уровне агрегации.
Стремление к краткости методов программы помогает снизить нагрузку на интеллект. Этому же способствует написание программы в терминах проблемной
области, а не низкоуровневых деталей реализации, а также работа на самом высоком уровне абстракции.
Суть сказанного в том, что программисты, компенсирующие изначальные ограничения человеческого ума, пишут более понятный и содержащий меньшее число
ошибок код.

Как бороться со сложностью?
Чаще всего причинами неэффективности являются:
 сложное решение простой проблемы;
 простое, но неверное решение сложной проблемы;
 неадекватное сложное решение сложной проблемы.

ГЛАВА 5 Проектирование при конструировании

77

Как указал Дейкстра, сложность современного ПО обусловлена самой его природой, поэтому, как бы вы ни старались, вы все равно столкнетесь со сложностью,
присущей самой проблеме реального мира. Исходя из этого, можно предложить
двойственный подход к управлению сложностью:
 старайтесь свести к минимуму объем существенной сложности, с ко#
торым придется работать в каждый конкретный момент времени;
 сдерживайте необязательный рост несущественной сложности.

Как только вы поймете, что все остальные технические цели разработки ПО вторичны по отношению к управлению сложностью, многие принципы проектирования окажутся простыми.

Желательные характеристики проекта
Высококачественные проекты программ имеют несколько
общих характеристик. Если вы сумеете достичь всех этих
целей, ваш проект на самом деле будет очень хорош. Некоторые цели противоречат другим, но это и есть одна
из задач проектирования — объединение конкурирующих
целей в удачном наборе компромиссов. Некоторые аспекты качества проекта — надежность, производительность и
т. д. — описывают и качество программы, тогда как другие
являются внутренними характеристиками проекта.
Вот список таких внутренних характеристик проекта.

Работая над проблемой, я никогда не думаю о красоте. Я
думаю только о решении проблемы. Но если полученное решение некрасиво, я знаю, что
оно неверно.
Р. Бакминстер Фуллер
(R. Buckminster Fuller)
Перекрестная ссылка Эти характеристики связаны с общими
атрибутами качества ПО (см.
раздел 20.1).

Минимальная сложность В силу только что описанных
причин главной целью проектирования должна быть минимизация сложности. Избегайте создания «хитроумных»
проектов: как правило, их трудно понять. Вместо этого создавайте «простые» и «понятные» проекты. Если при работе над отдельным фрагментом программы проект не позволяет безопасно игнорировать большинство
остальных фрагментов, он неудачен.
Простота сопровождения Проектируя приложение, не забывайте о программистах, которые будут его сопровождать. Постоянно представляйте себе вопросы,
которые будут возникать у них при взгляде на создаваемый вами код. Думайте о
таких программистах как о своей аудитории и проектируйте систему так, чтобы
ее работа была очевидной.

Слабое сопряжение Слабое сопряжение (loose coupling) предполагает сведение
к минимуму числа соединений между разными частями программы. Для проектирования классов с минимальным числом взаимосвязей используйте принципы
адекватной абстракции интерфейсов, инкапсуляцию и сокрытие информации.
Это позволит максимально облегчить интеграцию, тестирование и сопровождение программы.
Расширяемость Расширяемостью системы называют свойство, позволяющее
улучшать систему, не нарушая ее основной структуры. Изменение одного фрагмента системы не должно влиять на ее другие фрагменты. Внесение наиболее
вероятных изменений должно требовать наименьших усилий.

78

ЧАСТЬ II Высококачественный код

Возможность повторного использования Проектируйте систему так, чтобы
ее фрагменты можно было повторно использовать в других системах.
Высокий коэффициент объединения по входу При высоком коэффициенте
объединения по входу (fan#in) к конкретному классу обращается большое число
других классов. Это значит, что система предусматривает интенсивное использование вспомогательных низкоуровневых классов.
Низкий или средний коэффициент разветвления по выходу Это означает,
что конкретный класс обращается к малому или среднему числу других классов.
Высокий коэффициент разветвления по выходу (fan#out) (более семи) говорит
о том, что класс использует большое число других классов и, возможно, слишком
сложен. Ученые обнаружили, что низкий коэффициент разветвления по выходу
выгоден как в случае вызова методов из метода, так и в случае вызова методов из
класса (Card and Glass, 1990; Basili, Briand, and Melo, 1996).
Портируемость Проектируйте систему так, чтобы ее можно было легко адаптировать к другой среде.
Минимальная, но полная функциональность Этот аспект подразумевает отсутствие в системе лишних частей (Wirth, 1995; McConnell, 1997). Вольтер говорил,
что книга закончена не тогда, когда в нее больше нечего добавить, а когда из нее
ничего нельзя выбросить. При разработке ПО это верно вдвойне, потому что дополнительный код необходимо разработать, проанализировать, протестировать, а
также пересматривать при изменении других фрагментов программы. Кроме того,
в будущих версиях приложения придется поддерживать обратную совместимость
с дополнительным кодом. Опасайтесь вопроса: «Эту функцию реализовать легко
— почему бы этого не сделать?»
Стратификация Под стратификацией понимают разделение уровней декомпозиции, позволяющее изучить систему на любом отдельном уровне и получить при
этом согласованное представление. Проектируйте систему так, чтобы ее можно
было изучать на отдельных уровнях, игнорируя другие уровни.
Например, если вы создаете современную систему, которая должна использовать большой объем старого, плохо
спроектированного кода, напишите уровень, отвечающий
за взаимодействие со старым кодом. Спроектируйте этот
уровень так, чтобы он скрывал плохое качество старого кода,
предоставляя более новым уровням согласованный набор сервисов. Пусть остальные части системы работают с этими классами вместо старого кода. Такой подход
сулит два преимущества: 1) он изолирует плохой код и 2) если вы когда#нибудь
решите выбросить старый код или выполнить его рефакторинг, вам не придется
изменять новый код за исключением промежуточного уровня.

Перекрестная ссылка О работе
со старыми системами см. раздел 24.5.

Перекрестная ссылка Об особенно полезном типе стратификации — применении шаблонов
проектирования — см. подраздел «Старайтесь использовать
популярные шаблоны проектирования» раздела 5.3.

Соответствие стандартным методикам Чем экзотичнее система, тем сложнее будет другим программистам
понять ее. Попытайтесь придать всей системе привычный
для разработчиков облик, применяя стандартные популярные подходы.

ГЛАВА 5 Проектирование при конструировании

79

Уровни проектирования
Проектирование программной системы требует нескольких уровней детальности.
Некоторые методы проектирования используются на всех уровнях, а другие только
на одном#двух (рис. 5#2).

Рис. 5'2. Уровни проектирования программы. Систему (1) следует разделить
на подсистемы (2), подсистемы — на классы (3), а классы — на методы и данные
(4); методы также необходимо спроектировать (5)

Уровень 1: программная система
Первому уровню проектирования соответствует вся система.
Некоторые программисты с системного уровня сразу переходят к проектированию классов, но обычно целесообразно
обдумать более высокоуровневые комбинации классов, такие
как подсистемы или пакеты.

Уровень 2: разделение системы на подсистемы
или пакеты
Главный результат проектирования на этом уровне —
определение основных подсистем. Подсистемы могут быть

Иными словами — и это неизменный принцип, на котором
основан всегалактический успех
всей корпорации, — фундаментальные изъяны конструкции
ее товаров камуфлируются их
внешними изъянами.
Дуглас Адамс
(Douglas Adams)

80

ЧАСТЬ II Высококачественный код

довольно крупными, такими как модуль работы с базами данных, модули GUI,
бизнес#правил или создания отчетов, интерпретатор команд и т. д. Суть проектирования на данном уровне заключается в разделении программы на основные
подсистемы и определении взаимодействий между подсистемами. Обычно этот
уровень нужен при работе над любыми проектами, требующими более нескольких
недель. При проектировании отдельных подсистем можно применять разные подходы: выбирайте тот, который кажется вам оптимальным в каждом конкретном
случае. На рис. 5#2 данный уровень проектирования обозначен цифрой 2.
Особенно важный аспект этого уровня — определение правил взаимодействия подсистем. Если все подсистемы могут взаимодействовать, выгода их разделения исчезает.
Подчеркивайте суть подсистем, ограничивая их взаимодействие между собой.
Допустим, вы определили систему из шести подсистем (рис. 5#3). При отсутствии
каких#либо ограничений в силу второго закона термодинамики энтропия системы
должна увеличиться. Один из способов увеличения энтропии является абсолютно
свободное взаимодействие между подсистемами (рис. 5#4).

Рис. 5'3. Пример системы, включающей шесть подсистем

Рис. 5'4. Возможный результат отсутствия правил, ограничивающих взаимодействие подсистем

Как видите, в итоге все подсистемы начинают напрямую взаимодействовать, что
поднимает несколько важных вопросов:
 в скольких разных частях системы нужно хоть немного разбираться разработчи-

ку, желающему изменить какой#то аспект подсистемы графических операций?

ГЛАВА 5 Проектирование при конструировании

81

 что будет, если вы попытаетесь задействовать данный модуль бизнес#правил в

другой системе?
 что будет, если вы захотите включить в систему новый пользовательский ин-

терфейс (например, интерфейс командной строки, удобный для проведения
тестирования)?
 что произойдет, если вы захотите перенести модуль хранения данных на уда-

ленный компьютер?
Стрелки между подсистемами можно рассматривать как шланги с водой. Если вам
захочется «выдернуть» одну из подсистем, к ней наверняка будут подключены несколько шлангов. Чем больше шлангов вам нужно будет отсоединить и подключить
заново, тем сильнее вы промокнете. Архитектура системы должна быть такой,
чтобы замена подсистем требовала как можно меньше возни со шлангами.
При должной предусмотрительности все эти вопросы можно решить, проделав
немного дополнительной работы. Реализуйте коммуникацию между подсистемами
на основе принципа «необходимого знания», и пусть оно будет действительно необходимым. Помните: проще сначала ограничить взаимодействие, а затем сделать
его более свободным, чем пытаться изолировать подсистемы после написания нескольких сотен вызовов между ними. На рис. 5#5 показано, как несколько правил
коммуникации могут изменить систему, изображенную на рис. 5#4.

Рис. 5'5. Определив несколько правил коммуникации, можно существенно упростить взаимодействие подсистем

Чтобы соединения подсистем были понятными и легкими в сопровождении, старайтесь поддерживать простоту отношений между подсистемами. Самым простым
отношением является то, при котором одна подсистема вызывает методы другой.
Более сложное отношение имеет место, когда одна подсистема содержит классы
другой. Самое сложное отношение — наследование классов одной подсистемы
от классов другой.
Придерживайтесь одного разумного правила: диаграмма системного уровня вроде
той, что показана на рис. 5#5, должна быть ациклическим графом. Иначе говоря,
программа не должна содержать циклических отношений, при которых класс A
использует класс B, класс B использует класс C, а класс C — класс A.
При работе над крупными программами и программными комплексами проектирование на уровне подсистем просто необходимо. Если вам кажется, что ваша

82

ЧАСТЬ II Высококачественный код

программа достаточно мала, чтобы проектирование на уровне подсистем можно
было пропустить, хотя бы примите это решение осознанно.

Часто используемые подсистемы
Некоторые типы подсистем снова и снова используются в разных системах. Ниже
приведены те, что встречаются чаще всего.
Подсистема бизнес'правил Бизнес#правилами называют
законы, директивы, политики и процедуры, реализуемые в
компьютерной системе. Например, в случае системы расчета
заработной платы бизнес#правилами могли бы быть директивы налогового управления, определяющие разнообразные
виды налогов. Дополнительным источником правил могло бы быть соглашение с
профсоюзом, регламентирующее оплату сверхурочной работы, отпуска и т. д. При
создании программы для агентства по страхованию автомобилей правила могут
быть основаны на соответствующих государственных законах.

Перекрестная ссылка Об упрощении бизнес-логики путем ее
выражения в форме таблиц см.
главу 18.

Подсистема пользовательского интерфейса Изоляция компонентов пользовательского интерфейса в отдельной подсистеме позволяет изменять его, не влияя
на остальную программу. Как правило, подсистема пользовательского интерфейса
включает несколько подчиненных подсистем или классов,отвечающих за GUI,
интерфейс командной строки, работу с меню, управление окнами, справочную
систему и т. д.
Подсистема доступа к БД Вы может скрыть детали реализации доступа к
БД, чтобы большая часть программы не нуждалась в знании «грязных» подробностей операций над низкоуровневыми структурами и могла работать с данными
в терминах бизнес#проблемы. Подсистемы, скрывающие детали реализации, обеспечивают важный уровень абстракции, снижающий сложность программы. Они
концентрируют операции над БД в одном месте и снижают вероятность ошибок
при работе с данными, а также позволяют легко изменять структуру БД без изменения большей части программы.
Подсистема изоляции зависимостей от ОС Зависимости от ОС следует
изолировать в подсистеме по той же причине, что и зависимости от оборудования. Если, например, вы разрабатываете программу для Microsoft Windows, зачем
ограничивать себя средой Windows? Изолируйте вызовы Windows в специализированной интерфейсной подсистеме, и если вам позднее захочется перенести
программу на платформу Mac OS или Linux, то придется изменить только эту
подсистему. Интерфейсная подсистема может быть слишком крупной, чтобы вы
могли реализовать ее самостоятельно, однако такие подсистемы уже разработаны
и включены в несколько коммерческих библиотек.

Уровень 3: разделение подсистем на классы
Этот уровень проектирования предполагает определение всех классов системы.
Например, подсистема доступа к БД может быть далее раздеДополнительные сведения Пролена на классы доступа к данным и классы хранения данных,
ектирование БД хорошо рассмоа также метаданные БД. На рис. 5#2 показано, как разделить
трено в книге «Agile Database
Techniques» (Ambler, 2003).
на классы одну из подсистем уровня 2; конечно, три других
подсистемы также следует разделить на классы.

ГЛАВА 5 Проектирование при конструировании

83

Кроме того, на этом уровне следует определить детали взаимодействия каждого
класса с остальными элементами системы, особенно интерфейс класса. В целом
сутью проектирования на данном уровне является декомпозиция подсистем до
такого уровня детальности, который позволит реализовать части подсистем в
форме отдельных классов.
Разделение подсистем на классы обычно требуется во всех проектах, на реализацию которых уйдет более нескольких дней.
В крупных проектах необходимы и второй, и третий уровни
проектирования. При работе над совсем небольшим проектом
второй уровень проектирования можно пропустить.

Перекрестная ссылка Характеристики высококачественных
классов см. в главе 6.

Классы и объекты
Один из важнейших аспектов объектно#ориентированного проектирования —
различие между объектами и классами. Объект — это любая конкретная динамическая сущность, имеющая конкретные значения и атрибуты и существующая в
период выполнения программы. Класс — это статическая сущность, с которой вы
имеете дело, просматривая листинг программы. Например, вы можете объявить
класс Person (человек), имеющий такие атрибуты, как фамилия, возраст, пол и т. д.
В период выполнения вы будете работать с объектами nancy, hank, diane, tony и
т. д. — иначе говоря, со специфическими экземплярами класса. Если вы знакомы
с терминологией БД, различие между классом и объектом аналогично различию
между «схемой» и «экземпляром». Класс можно рассматривать как форму для выпечки булочек, а объекты — как сами булочки. В этой книге термины «класс» и
«объект» используются неформально и более или менее взаимозаменяемо.

Уровень 4: разделение классов на методы
Данный уровень проектирования заключается в разделении каждого класса на
методы. Некоторые методы уже будут определены на уровне 3, при проектировании интерфейсов классов. На уровне 4 вы детально определите закрытые методы
классов. При этом многие методы окажутся простыми, тогда как другие будут
включать иерархии методов и потребуют дополнительного проектирования.
Полное определение методов класса часто позволяет лучше понять его интерфейс,
что может подтолкнуть к соответствующему изменению интерфейса, т. е. к возвращению на уровень 3.
Четвертый уровень декомпозиции и проектирования часто оставляется отдельным
программистам и необходим в любом проекте, требующем более нескольких
часов. Формально выполнять этот этап не обязательно, но хотя бы про себя выполнить его нужно.

Уровень 5: проектирование методов
На этом уровне проектирование заключается в детальном
Перекрестная ссылка О создаопределении функциональности отдельных методов, за что
нии высококачественных методов см. главы 7 и 8.
обычно отвечают отдельные программисты, работающие над
конкретными методами. Данный уровень может включать
такие действия, как написание псевдокода, поиск алгоритмов в книгах, размышление над оптимальной организацией фрагментов метода и написание кода. Этот

ЧАСТЬ II Высококачественный код

84

уровень проектирования выполняется во всех случаях, но не всегда осознанно и
качественно. На рис. 5#2 он отмечен цифрой 5.

5.3.

Компоненты проектирования:
эвристические принципы

Разработчики ПО любят четкие и ясные правила: «Сделай A, B и C, и это обязательно приведет к X, Y и Z». Мы испытываем гордость, когда находим тайные действия, приводящие к желаемым результатам, и сердимся, если команды работают
не так, как описано. Стремление к детерминированному поведению прекрасно
согласуется с детальным программированием, при котором строгое внимание к
деталям может определить успех или провал программы. Однако проектирование
ПО — совсем другая история.
Так как проектирование не является детерминированным, главным аспектом проектирования качественного ПО становится умелое применение набора эффективных эвристических принципов. Ниже мы рассмотрим ряд таких принципов —
подходов, способных привести к удачным решениям. Можете считать эвристические принципы правилами выполнения проб при использовании метода проб и
ошибок. Несомненно, некоторые из них вам уже известны. Каждый из эвристических принципов будет описан в контексте Главного Технического Императива
Разработки ПО — управления сложностью.

Определите объекты реального мира
Прежде всего следует узнать, не
что система выполняет, а над
ЧЕМ она это выполняет!
Бертран Мейер
(Bertrand Meyer)
Перекрестная ссылка О проектировании с использованием
классов см. главу 6.

Первый, и самый популярный, подход к проектированию —
«общепринятый» объектно#ориентированный подход —
основан на определении объектов реального мира и искусственных объектов.
При проектировании с использованием объектов определите:
 объекты и их атрибуты (методы и данные);
 действия, которые могут быть выполнены над каждым
объектом;
 действия, которые каждый объект может выполнять над

другими объектами;
 части каждого объекта, видимые другим объектам, т. е. открытые и закрытые

части;
 открытый интерфейс каждого объекта.

Эти часто повторяющиеся действия не обязательно выполнять в указанном порядке. Помните о важности итерации.
Определите объекты и их атрибуты В основе создания программ обычно
лежат сущности реального мира. Например, система расчета повременной оплаты
может быть основана на таких сущностях, как сотрудники, клиенты, карты учета
времени и счета (рис. 5#6).

ГЛАВА 5 Проектирование при конструировании

85

Рис. 5'6. Эта система расчета оплаты состоит из четырех основных объектов
(пример упрощен)

Определить атрибуты объектов не сложнее, чем сами объекты. Каждый объект имеет
характеристики, релевантные для компьютерной программы. Скажем, в системе
расчета повременной оплаты объект «сотрудник» обладал бы такими атрибутами,
как имя/фамилия, должность и уровень оплаты. С объектом «счет» были бы связаны
такие атрибуты, как сумма, имя/фамилия клиента, дата и т. д.
Объектами системы GUI были бы разнообразные окна, кнопки, шрифты и инструменты рисования. При дальнейшем изучении проблемной области вы можете
прийти к выводу, что установление однозначного соответствия между объектами
программы и объектами реального мира — не самый лучший способ определения
объектов, но для начала он тоже неплох.
Определите действия, которые могут быть выполнены над каждым объектом Объекты могут поддерживать самые разные операции. В нашей системе
расчета оплаты объект «сотрудник» мог бы поддерживать изменение должности
или уровня оплаты, объект «клиент» — изменение реквизитов счета и т. д.
Определите действия, которые каждый объект может выполнять над
другими объектами Суть этого этапа ясна из его названия. Двумя универсальными действиями, которые объекты могут выполнять друг над другом, являются
включение (containment) и наследование. Какие объекты могут включать другие
(какие?) объекты? Какие объекты могут быть унаследованными от других (каких?) объектов? На рис. 5#6 объект «карта учета времени» может включать объект
«сотрудник» и объект «клиент», а объект «счет» может включать карты учета времени. Кроме того, счет может сообщать, что клиент оплатил услуги, а клиент —
оплачивать указанную в счете сумму. Более сложная система включала бы дополнительные взаимодействия.
Определите части каждого объекта, видимые другим объектам Один
из главных аспектов проектирования — определение частей объекта, которые
следует сделать открытыми, и частей, которые следует держать закрытыми. Этого
решения требуют и данные, и методы.

86

ЧАСТЬ II Высококачественный код

Определите интерфейс каждого объекта Для каждого объекта надо определить формальный синтаксический
интерфейс на уровне языка программирования. Данные
и методы, которые объект предоставляет в распоряжение
остальным объектам, называются «открытым интерфейсом».
Части объекта, доступные производным от него объектам,
называются «защищенным интерфейсом» объекта. Проектируя программу, обдумайте интерфейсы обоих типов.

Перекрестная ссылка О классах
и сокрытии информации см.
подраздел «Скрывайте секреты
(к вопросу о сокрытии информации)» раздела 5.3.

Завершая проектирование высокоуровневой объектно#ориентированной организации системы, вы будете использовать два вида итерации: высокоуровневую, направленную на улучшение организации классов, и итерацию на уровне каждого из
определенных классов, направленную на детализацию проекта каждого класса.

Определите согласованные абстракции
Абстракция позволяет задействовать концепцию, игнорируя ее некоторые детали и
работая с разными деталями на разных уровнях. Имея дело с составным объектом,
вы имеете дело с абстракцией. Если вы рассматриваете объект как «дом», а не как
комбинацию стекла, древесины и гвоздей, вы прибегаете к абстракции. Если вы рассматриваете множество домов как «город», вы прибегаете к другой абстракции.
Базовые классы представляют собой абстракции, позволяющие концентрироваться
на общих атрибутах производных классов и игнорировать детали конкретных
классов при работе с базовым классом. Удачный интерфейс класса — это абстракция, позволяющая сосредоточиться на интерфейсе, не беспокоясь о внутренних
механизмах работы класса. Интерфейс грамотно спроектированного метода
обеспечивает такую же выгоду на более низком уровне детальности, а интерфейс
грамотно спроектированного пакета или подсистемы — на более высоком.
С точки зрения сложности, главное достоинство абстракции в том, что она позволяет игнорировать нерелевантные детали. Большинство объектов реального мира
уже является абстракциями некоторого рода. Как я только что сказал, дом — это
абстракция окон, дверей, обшивки, электропроводки, водопроводных труб, изоляционных материалов и конкретного способа их организации. Дверь же — это
абстракция особого вида организации прямоугольного фрагмента некоторого
материала, петель и ручки. А дверную ручку можно считать абстракцией конкретного способа упорядочения медных, никелевых или стальных деталей.
Мы используем абстракции на каждом шагу. Если б, открывая или закрывая дверь,
вы должны были иметь дело с отдельными волокнами древесины, молекулами лака
и стали, вы вряд ли смогли бы войти в дом или выйти из него. Абстракция — один
из главных способов борьбы со сложностью реального мира (рис. 5#7).

ГЛАВА 5 Проектирование при конструировании

87

Рис. 5'7. Абстракция позволяет представить сложную концепцию
в более простой форме

Разработчики ПО иногда создают системы на уровне волокон
древесины и молекул лака и стали, из#за чего такие системы
становятся слишком сложными и плохо поддаются осмыслению. Если программисты не создают более общие абстракции,
разработка системы может завершиться неудачей.

Перекрестная ссылка Об абстракции в контексте проектирования классов см. подраздел «Хорошая абстракция» раздела 6.2.

Благоразумные программисты создают абстракции на уровне интерфейсов методов, интерфейсов классов и интерфейсов пакетов (иначе говоря, на уровне
дверной ручки, уровне двери и на уровне дома), что способствует более быстрому
и безопасному программированию.

Инкапсулируйте детали реализации
Когда абстракция нас покидает, на помощь приходит инкапсуляция. Абстракция
говорит: «Вы можете рассмотреть объект с общей точки зрения». Инкапсуляция
добавляет: «Более того, вы не можете рассмотреть объект с иной точки зрения».
Продолжим нашу аналогию: инкапсуляция позволяет вам смотреть на дом, но не
дает подойти достаточно близко, чтобы узнать, из чего сделана дверь. Инкапсуляция позволяет вам знать о существовании двери, о том, открыта она или заперта,
но при этом вы не можете узнать, из чего она сделана (из дерева, стекловолокна,
стали или другого материала), и уж никак не сможете рассмотреть отдельные
волокна древесины.
Инкапсуляция помогает управлять сложностью, блокируя доступ к ней (рис. 5#8).
В подразделе «Хорошая инкапсуляция» раздела 6.2 инкапсуляция рассматривается
подробнее в контексте проектирования классов.

Рис. 5'8. Инкапсуляция не только представляет сложную концепцию
в более простой форме, но и не позволяет взглянуть на какие бы то ни было
детали сложной концепции. Что видите, то и получите — и не более того!

88

ЧАСТЬ II Высококачественный код

Используйте наследование, если оно упрощает
проектирование
При проектировании ПО часто выясняется, что одни объекты аналогичны другим
за исключением нескольких различий. Так, при создании системы расчета зарплаты
нужно учесть, что одни сотрудники работают полный день, а другие — неполный.
В этом случае наборы данных, ассоциированные с сотрудниками обеих категорий,
будут различаться лишь несколькими аспектами. Объектно#ориентированный
подход позволяет создать общий тип «сотрудник» и определить сотрудников, работающих полный день, как сотрудников общего типа за исключением нескольких
различий. Если операция над объектом «сотрудник» не зависит от его категории,
она выполняется так, как если бы объект был сотрудником общего типа. Если же
операция зависит от типа сотрудника, она выполняется разными способами.
Определение сходств и различий между такими объектами называется «наследованием», потому что отдельные типы сотрудников, работающих полный и неполный
день, наследуют свойства общего типа «сотрудник».
Польза наследования в том, что оно дополняет идею абстракции. Абстракция
позволяет представить объекты с разным уровнем детальности. Если помните,
на одном уровне мы рассматривали дверь как набор определенных типов молекул, на втором — как набор волокон древесины, а на третьем — как что#то, что
защищает нас от воров. Древесина имеет определенные свойства — скажем, вы
можете распилить ее пилой или склеить столярным клеем, — при этом и плинтусы,
и подоконники имеют общие свойства древесины, но вместе с тем и некоторые
специфические свойства.
Наследование упрощает программирование, позволяя создать универсальные
методы для выполнения всего, что основано на общих свойствах дверей, и затем
написать специфические методы для выполнения специфических операций над
конкретными типами дверей. Некоторые операции, такие как Open() или Close(),
будут универсальными для всех дверей: внутренних, входных, стеклянных, стальных — каких угодно. Поддержка языком операций вроде Open() или Close() при
отсутствии информации о конкретном типе двери вплоть до периода выполнения
называется полиморфизмом. Объектно#ориентированные языки, такие как C++,
Java и более поздние версии Microsoft Visual Basic, поддерживают и наследование,
и полиморфизм.
Наследование — одно из самых мощных средств объектно#ориентированного
программирования. При правильном применении оно может принести большую
пользу, однако в обратном случае и ущерб будет немалым. Подробнее см. подраздел «Наследование (отношение «является»)» раздела 6.3.

ГЛАВА 5 Проектирование при конструировании

89

Скрывайте секреты (к вопросу о сокрытии информации)
Сокрытие информации — один из основных принципов и структурного, и объектно#ориентированного проектирования. В первом случае сокрытие информации
лежит в основе идеи «черных ящиков». Во втором оно дает начало концепциям
инкапсуляции и модульности и связано с концепцией абстракции. Сокрытие информации — одна из самых конструктивных идей в мире разработки ПО, и сейчас
мы рассмотрим ее подробнее.
Впервые сокрытие информации было представлено на суд общественности в 1972 г.
Дэвидом Парнасом (David Parnas) в статье «On the Criteria to Be Used in Decomposing
Systems Into Modules (О критериях, используемых при декомпозиции систем на
модули)». С сокрытием информации тесно связана идея «секретов» — аспектов
проектирования и реализации, которые разработчик ПО решает скрыть в каком#то
месте от остальной части программы.
В юбилейном 20#летнем издании книги «Мифический человеко#месяц» Фред Брукс
пришел к выводу, что критика сокрытия информации была одной из ошибок, допущенных им в первом издании книги. «Парнас был прав в отношении сокрытия
информации, а я ошибался», — признал он (Brooks, 1995). Барри Бом сообщил,
что сокрытие информации — мощный метод избавления от повторной работы, и
указал, что оно особенно эффективно в инкрементных средах с высоким уровнем
изменений (Boehm, 1987).
В контексте Главного Технического Императива Разработки ПО сокрытие информации оказывается особенно мощным эвристическим принципом, так как все его
аспекты и даже само название подчеркивают сокрытие сложности.

Секреты и право на личную жизнь
При сокрытии информации каждый класс (пакет, метод) характеризуется аспектами
проектирования или конструирования, которые он скрывает от остальных классов.
Секретом может быть источник вероятных изменений, формат файла, реализация
типа данных или область, изоляция которой требуется для сведения к минимуму
вреда от возможных ошибок. Класс должен скрывать эту информацию и защищать
свое право на «личную жизнь». Небольшие изменения системы могут влиять на несколько методов класса, но не должны распространяться за его интерфейс.
Один из важнейших аспектов проектирования класса — приИнтерфейсы классов должны
нятие решения о том, какие свойства сделать доступными вне
быть полными и минималькласса, а какие оставить секретными. Класс может включать
ными.
25 методов, предоставляя доступ только к пяти из них и исСкотт Мейерс
(Scott Meyers)
пользуя остальные 20 внутренне. Класс может использовать
несколько типов данных, не раскрывая сведений о них. Этот
аспект проектирования классов называют «видимостью», так как он определяет,
какие свойства класса «видимы» или «доступны» извне.
Интерфейс класса должен сообщать как можно меньше о внутренней работе класса.
В этом смысле класс во многом похож на айсберг, большая часть которого скрыта
под водой (рис. 5#9).

90

ЧАСТЬ II Высококачественный код

Рис. 5'9. Хороший интерфейс класса похож на верхушку айсберга:
большую часть класса он оставляет скрытой

Как и любой другой аспект проектирования, разработка интерфейса класса —
итеративный процесс. Если приемлемый интерфейс класса не удается создать с
первого раза, сделайте еще несколько попыток, пока он не стабилизируется. Если
интерфейс не стабилизируется, попробуйте другой подход.

Пример сокрытия информации
Допустим, вы пишете программу, каждый объект которой должен иметь уникальный идентификатор, хранящийся в переменной#члене id. Один подход к проектированию может заключаться в применении целочисленных идентификаторов
и хранении максимального на данный момент идентификатора в глобальной
переменной g_maxId. При создании новых объектов вы можете — скажем, в конструкторе каждого объекта — просто выполнять команду id = ++g_maxId, что
гарантирует уникальность идентификаторов, и требует абсолютно минимального
кода при создании каждого объекта. Разве это может привести к каким#нибудь
неприятностям?
Может. Что, если вы захотите зарезервировать диапазоны идентификаторов для
определенных целей? Что, если для повышения защищенности программы вы захотите назначать идентификаторы в другом порядке? А если вы захотите повторно задействовать идентификаторы уничтоженных объектов? Или включить в программу диагностический тест, проверяющий, не превысило ли число идентификаторов допустимый предел? Если, назначая идентификаторы, вы распространите
команды id = ++g_maxId по всей программе, вам придется изменить каждую из
них. Кроме того, этот подход небезопасен в многопоточной среде.
Способ генерации новых идентификаторов является тем аспектом проектирования,
который следует скрыть. Применив команду ++g_maxId, вы раскроете сведения о
том, что новый идентификатор создается просто путем увеличения переменной
g_maxId. Если же вместо этого вы используете команды id = NewId(), вы скроете

ГЛАВА 5 Проектирование при конструировании

91

информацию о способе создания новых идентификаторов. Сам метод NewId()
может состоять из единственной строки return ( ++g_maxId ) или ее эквивалента, однако, если вы позднее решите зарезервировать определенные диапазоны
идентификаторов для специфических целей или повторно использовать старые
идентификаторы, вам придется изменить только метод NewId(), но не десятки
команд id = NewId(). Какими бы сложными ни были изменения метода NewId(),
они не повлияют ни на какую другую часть программы.
Допустим теперь, что вам понадобилось изменить тип идентификатора с целочисленного на строковый. Если по всей программе у вас разбросаны объявления
вроде int id, метод NewId() не поможет. В этом случае вам тоже придется просмотреть всю программу и внести десятки или сотни изменений.
Итак, тип идентификатора — это тоже секрет, который следует скрыть. Показывая,
что идентификаторы — целые числа, вы поощряете программистов выполнять над
ними такие операции, как >, < и =. Программируя на C++, вы могли бы не объявлять
идентификаторы как int, а назначить им при помощи директивы typedef пользовательский тип IdType соответствующий тому же int. Или же вы могли бы создать
простой класс IdType. Повторю еще раз: сокрытие аспектов проектирования позволяет значительно уменьшить объем кода, затрагиваемого изменениями.
Сокрытие информации полезно на всех уровнях проектирования: от при#
менения именованных констант вместо литералов до создания типов
данных и проектирования классов, методов и подсистем.

Две категории секретов
Связанные с сокрытием информации секреты относятся к двум общим категориям:
 секреты, которые скрывают сложность, позволяя программистам забыть о ней

при работе над остальными частями программы;
 секреты, которые скрывают источники изменений с целью локализации ре-

зультатов возможных изменений.
В число источников сложности входят сложные типы данных, файловые структуры, булевы тесты, запутанные алгоритмы и т. д. Источники изменений будут
описаны немного позднее.

Барьеры, препятствующие сокрытию информации
В некоторых случаях скрыть информацию невозможно,
однако большинство барьеров, препятствующих сокрытию
информации, является умственными и обусловлены привыканием к другим методикам.

Дополнительные сведения Отдельные фрагменты этого раздела взяты из статьи «Designing
Software for Ease of Extension and
Contraction» (Parnas, 1979).

Избыточное распространение информации Зачастую
сокрытию информации препятствует избыточное распространение информации по системе. Так, жесткое кодирование литерала 100 во
многих местах программы децентрализует ссылки на него. Лучше скрыть эту
информацию в одном месте — скажем, при помощи константы MAX_EMPLOYEES,
для изменения значения которой придется изменить только одну строку кода.

92

ЧАСТЬ II Высококачественный код

Еще один пример избыточного распространения информации — распределение
по системе кода взаимодействия с пользователями. При этом в случае изменения
способа взаимодействия — например, при замене графического интерфейса на
интерфейс командной строки — придется изменить почти весь код. Лучше сконцентрировать взаимодействие с пользователями в одном классе (пакете, подсистеме),
который можно было бы изменить, не влияя на всю систему.
В качестве другого примера приведу повсеместное использование глобального элемента данных, такого как массив
данных о сотрудниках, поддерживающий до 1000 элементов.
Если программа будет обращаться к глобальным данным
напрямую, информация о реализации элемента данных —
скажем, то, что это массив, способный включать до 1000
элементов, — распространится по всей программе. Если
программа будет обращаться к данным только через методы доступа, детали реализации будут известны только этим методам.

Перекрестная ссылка О доступе
к глобальным данным при помощи интерфейсов классов см.
подраздел «Используйте методы доступа вместо глобальных
данных» раздела 13.3.

Круговая зависимость Более тонким барьером, мешающим сокрытию информации, является круговая зависимость, когда, например, метод класса A вызывает
метод класса B, а метод класса B — метод класса A.
Избегайте таких зависимостей: они осложняют тестирование системы, не позволяя протестировать ни один из классов, пока не будет реализована хотя бы
часть второго класса.
Ошибочное представление о данных класса как о глобальных данных
Если вы добросовестный программист, возможна еще одна преграда на пути к
эффективному сокрытию информации: вы можете рассматривать данные класса
как глобальные данные, избегая их из#за соответствующих проблем. Всем известно,
что дорога в ад программирования вымощена глобальными переменными, однако
использовать данные класса гораздо безопаснее.
Глобальные данные имеют два главных недостатка: методы, обращающиеся к глобальным данным, не знают о том, что другие методы тоже обращаются к этим
данным, или же методы знают об этом, но не знают, что именно другие методы
делают с глобальными данными. Данные класса этих недостатков не имеют. Непосредственный доступ к данным класса ограничен несколькими методами этого
же класса, которые знают и о том, что другие методы также работают с данными,
и о том, что это за методы.
Конечно, это предполагает, что система включает грамотно спроектированные небольшие классы. Если программа использует огромные классы, включающие десятки
методов, различие между данными класса и глобальными данными стирается, и данные
класса приобретают многие недостатки, характерные для глобальных данных.
Кажущееся снижение производительности Наконец,
отказ от сокрытия информации может объясняться стремлением избежать снижения производительности и на уровне
архитектуры, и на уровне кода. В обоих случаях волноваться
не о чем. При проектировании архитектуры сокрытие информации не конфликтует
с производительностью. Помня и о сокрытии информации, и о производительности, вы сможете достичь обеих целей.

Перекрестная ссылка О повышении производительности на
уровне кода см. главы 25 и 26.

ГЛАВА 5 Проектирование при конструировании

93

Производительность на уровне кода вызывает еще больше беспокойств. Разработчикам кажется, что опосредованный доступ к данным снизит производительность
программы в период выполнения из#за дополнительных затрат на создание объектов, вызовы методов и т. д. Эти волнения преждевременны. Пока вы не оцените
производительность системы и не найдете узкие места, лучшим способом подготовки к повышению производительности на уровне кода является модульное
проектирование. Позже, определив в коде «горячие точки», вы оптимизируете
отдельные классы и методы, не затрагивая остальную часть системы.

Важность сокрытия информации
Сокрытие информации относится к тем немногим теоретическим под#
ходам, польза которых уже долгое время неоспоримо подтверждается на
практике (Boehm, 1987a). Было обнаружено, что крупные программы,
использующие сокрытие информации, вчетверо легче модифицировать, чем программы, его не использующие (Korson and Vaishnavi, 1986). Более того, сокрытие
информации — один из основных принципов и структурного, и объектно#ориентированного проектирования.
Сокрытие информации обладает уникальной эвристической силой, уникальной
способностью подталкивать разработчиков к эффективным проектным решениям.
Традиционное объектно#ориентированное проектирование предоставляет мощные
эвристические средства моделирования мира в терминах объектов, но объектный
подход не помог бы вам догадаться, что идентификатор следует объявить как IdType, а
не как int. Разработчик, использующий объектно#ориентированный подход, спросил
бы: «Рассматривать ли идентификатор как объект?» В зависимости от принятых
в проекте стандартов кодирования утвердительный ответ мог бы означать, что
программист должен написать конструктор, деструктор, операторы копирования
и присваивания, закомментировать все это и сохранить в системе управления
конфигурацией. Но скорее всего программист решил бы: «Нет, не стоит создавать
целый класс ради какого#то идентификатора. Использую просто int».
Смотрите: эффективный вариант проектирования — простое сокрытие типа данных идентификатора — даже не был рассмотрен! Если бы вместо этого разработчик спросил: «Не скрыть ли информацию об идентификаторе?» — он, возможно,
решил бы объявить собственный тип IdType как синоним int. Различие между объектно#ориентированным проектированием и сокрытием информации в этом примере не сводится к простому несоответствию явных правил и предписаний. Принятое в соответствии с принципом сокрытия информации решение прекрасно
согласуется с объектно#ориентированным подходом. Вместо этого различие относится к области эвристики: размышление над сокрытием информации может
указать на такие варианты проектирования, которые при использовании объектно#ориентированного подхода остались бы незамеченными.
Сокрытие информации может пригодиться при проектировании открытого интерфейса класса. Теория и практика проектирования классов во многом расходятся, и многие разработчики, решая, чту включить в открытый интерфейс класса, думают прежде всего об удобном интерфейсе, а это обычно приводит к раскрытию почти всей информации об устройстве класса. Опыт подсказывает мне,

94

ЧАСТЬ II Высококачественный код

что некоторые программисты скорее раскрыли бы все закрытые данные класса,
чем написали 10 дополнительных строк для защиты его секретов.
Вопрос «Что этот класс должен скрывать?» обнажает самую суть проблемы проектирования интерфейса. Если функцию или данные можно включить в открытый
интерфейс класса, не раскрыв его секретов, сделайте это. В противном случае
воздержитесь от такого решения.
Размышление о том, чту скрыть, способствует принятию удачных решений на всех
уровнях проектирования. Оно подталкивает к применению именованных констант
вместо чисел на уровне конструирования, помогает выбирать удачные имена методов классов и их параметров и указывает на грамотные варианты декомпозиции
и реализации взаимодействия классов и подсистем на уровне системы.
Почаще задавайте себе вопрос «Что мне скрыть?», и вы удивитесь, сколь#
ко проблем проектирования растает на ваших глазах

Определите области вероятных изменений
Исследования показали, что всем лучшим проектировщикам
свойственно предвосхищать изменения (Glass, 1995). Обеспечение легкости адаптации программы к возможным изменениям относится к самым сложным аспектам проектирования. Его цель заключается в изоляции нестабильных областей,
позволяющей ограничить следствия изменений одним методом, классом или пакетом. Вот как проходит подготовка к изменениям.

Дополнительные сведения Подход, описанный в этом разделе, взят из статьи «Designing
Software for Ease of Extension and
Contraction» (Parnas, 1979).

1. Определите элементы, изменение которых кажется вероятным. Если
вы выработали адекватные требования, они включают список потенциальных
изменений и оценки вероятности каждого из них. В этом случае определить
вероятные изменения легко. Если требования не описывают потенциальные
изменения, ниже вы найдете список областей, которые меняются чаще всего
независимо от типа проекта.
2. Отделите элементы, изменение которых кажется вероятным. Создайте
отдельный класс для каждого нестабильного компонента, определенного в п. 1,
или разработайте классы, включающие несколько нестабильных компонентов,
изменение которых скорее всего будет одновременным.
3. Изолируйте элементы, изменение которых кажется вероятным. Спроектируйте интерфейсы между классами так, чтобы они не зависели от потенциальных изменений. Спроектируйте интерфейсы так, чтобы изменения
ограничивались только внутренними частями классов. Изменение класса должно
оставаться незаметным для любых других классов. Интерфейс класса должен
защищать его секреты.
Ниже описано несколько областей, изменяющихся чаще всего.
Перекрестная ссылка Один из
самых эффективных способов
предвосхищения изменений —
табличное управление (см. главу 18).

Бизнес'правила Необходимость изменения ПО часто
объясняется изменениями бизнес#правил. Оно и понятно:
конгресс может изменить систему налогообложения, профсоюзы — пересмотреть условия контрактов и т. д. Если вы
соблюдаете принцип сокрытия информации, логика, основанная на этих правилах, не будет распространена на всю

ГЛАВА 5 Проектирование при конструировании

95

программу. Она будет скрыта в одном темном уголке системы, пока не придет время
ее изменить.
Зависимости от оборудования Примерами модулей, зависимых от оборудования, могут служить интерфейсы между программой и разными типами мониторов,
принтеров, клавиатур, дисководов, звуковых плат и сетевых устройств. Изолируйте зависимости от оборудования в отдельной подсистеме или отдельном классе. Это
облегчает адаптацию программы к новой аппаратной среде, а также помогает разрабатывать ПО для нестабильных версий устройств. Вы можете разработать ПО,
моделирующее взаимодействие с конкретным устройством, и создать подсистему
аппаратного интерфейса, использующую эту модель, пока устройство нестабильно или недоступно. Когда устройство будет готово к работе, подсистему интерфейса
можно будет отключить от модели и подключить к устройству.
Ввод'вывод На чуть более высоком в сравнении с аппаратными интерфейсами
уровне проектирования частой областью изменений является ввод#вывод. Если ваше
приложение создает собственные файлы данных, его усложнение вполне может
потребовать изменения формата файлов. Аспекты формата ввода#вывода данных,
относящиеся к пользовательскому уровню, такие как позиционирование и число
полей на странице, их последовательность и т. д., изменяются не менее часто.
В общем, анализ всех внешних интерфейсов на предмет возможных изменений —
благоразумная идея.
Нестандартные возможности языка Большинство версий языков поддерживает нестандартные расширения, облегчающие работу программистов. Расширения — палка о двух концах, потому что в другой среде — будь то другая аппаратная
платформа, реализация языка другим производителем или новая версия языка,
выпущенная тем же производителем, — они могут оказаться недоступны.
Если вы применяете нестандартные расширения языка, скройте работу с ними в
отдельном классе, чтобы его можно было заменить при адаптации приложения к
другой среде. Аналогично, используя библиотечные методы, доступные не во всех
средах, скройте их за интерфейсом, поддерживающим все нужные среды.
Сложные аспекты проектирования и конструирования Скрывайте сложные аспекты проектирования и конструирования, потому что их частенько приходится реализовывать заново. Отделите их и минимизируйте влияние, которое
может оказать их неудачное проектирование или конструирование на остальные
части системы.
Переменные статуса Переменные статуса характеризуют состояние программы и изменяются чаще, чем большинство других видов данных. Так, разработчики, определившие переменную статуса ошибки как булеву переменную, вполне
могут позднее прийти к выводу, что для этого лучше было бы использовать перечисление со значениями ErrorType_None, ErrorType_Warning и ErrorType_Fatal.
Использование переменных статуса можно сделать более гибким и понятным
минимум двумя способами.
 В качестве переменных статуса примените не булевы переменные, а перечис-

ления. Диапазон поддерживаемых переменными статуса состояний часто приходится расширять, что в случае перечисления требует лишь перекомпиляции

96

ЧАСТЬ II Высококачественный код

программы, а не масштабной ревизии всех фрагментов кода, выполняющих
проверку переменной.
 Вместо непосредственной проверки переменной используйте методы досту-

па. Так вы сохраните возможность реализации более сложного механизма
определения состояния. Например, если вы захотите проверять комбинацию
переменной статуса ошибки и переменной текущего функционального состояния, вам будет легко реализовать это, если проверка будет скрыта в методе, и
гораздо сложнее, если механизм проверки будет жестко закодирован во многих местах программы.
Размеры структур данных Объявляя массив из 100 элементов, вы раскрываете информацию, которую никто знать не должен. Защищайте право на личную
жизнь! Сокрытие информации не всегда требует создания целого класса. Иногда
для этого достаточно именованной константы: например, MAX_EMPLOYEES позволяет скрыть число 100.

Предвосхищение изменений разного масштаба
Перекрестная ссылка Рассматриваемый в этом разделе подход к предвосхищению изменений не связан с заблаговременным проектированием или
кодированием (см. подраздел
«Программа содержит код, который может когда-нибудь понадобиться» раздела 24.2).

Дополнительные сведения Это
обсуждение основано на подходе, описанном в статье «On the
design and development of program families» (Parnas, 1976).

Обдумывая потенциальные изменения системы, проектируйте
ее так, чтобы влияние изменений было обратно пропорционально их вероятности. Если вероятность изменения высока, убедитесь, что систему будет легко адаптировать к нему.
С большим влиянием на несколько классов системы можно
смириться лишь в случае крайне маловероятных изменений.
Грамотные проектировщики также принимают во внимание
цену предвосхищения изменений. Если изменение маловероятно, но его легко предугадать, рассмотрите его внимательнее, чем более вероятное изменение, которое трудно спланировать.

Один хороший метод определения областей вероятных
изменений подразумевает, что вы должны сначала определить минимальное подмножество фрагментов программы,
необходимых пользователям. Это подмножество составляет
ядро системы, и его изменения маловероятны. Затем вы определяете минимальные
инкрементные приращения системы. Они могут быть совсем небольшими, даже
тривиальными. Вместе с функциональными изменениями рассматривайте также
качественные изменения программы: обеспечение безопасности в многопоточной
среде, поддержку механизмов локализации и т. д. Эти области потенциальных
улучшений являются потенциальными изменениями системы; спроектируйте эти
области, используя принципы сокрытия информации. Определив ядро в самом
начале, вы поймете, какие компоненты системы на самом деле являются дополнениями, и сможете с этого момента экстраполировать и скрывать аспекты возможных изменений программы.

Поддерживайте сопряжение слабым
Сопряжение характеризует силу связи класса или метода с другими классами или
методами. Наша цель — создать классы и методы, имеющие немногочисленные,
непосредственные, явные и гибкие отношения с другими классами, что еще на-

ГЛАВА 5 Проектирование при конструировании

97

зывают «слабым сопряжением» (loose coupling)». В контекстах классов и методов
концепция сопряжения одна и та же, так что при обсуждении сопряжения буду
называть методы и классы «модулями».
Сопряжение модулей должно быть достаточно слабым, чтобы одни модули могли с легкостью использовать другие. Например, железнодорожные вагоны соединяются с помощью крюков, которые при столкновении двух вагонов защелкиваются. Представьте, как бы все усложнилось, если бы вагоны нужно было соединять при помощи болтов, набора тросов или если бы вы могли соединить между
собой только определенные типы вагонов. Механизм соединения вагонов эффективен потому, что он максимально прост. Соединения между программными модулями также должны быть как можно проще.
Старайтесь создавать модули, слабо зависящие от других модулей. Отношения
модулей должны напоминать отношения деловых партнеров, а не сиамских близнецов. Скажем, метод sin() (синус) сопряжен слабо, так как нужную информацию он получает в форме одного значения — угла в градусах. Метод InitVars
( var1, var2, var3, ..., varN ) сопряжен жестче, поскольку многие детали его работы
становятся известными вызывающему модулю по передаваемым значениям. Два
класса, зависящих от того, как каждый из них использует одну глобальную переменную, сопряжены еще жестче.

Критерии оценки сопряжения
Ниже описаны критерии, позволяющие оценить сопряжение модулей.
Объем Объем связи характеризуетчисло соединений между модулями. Чем их
меньше, тем лучше, поскольку модуль, имеющий более компактный интерфейс,
легче связать с другими модулями. Метод, принимающий один параметр, слабее
сопряжен с вызывающими его модулями, чем метод, принимающий шесть параметров. Класс, имеющий четыре грамотно определенных открытых метода, слабее сопряжен с модулями, которые его используют, чем класс, предоставляющий
37 открытых методов.
Видимость Видимостью называют заметность связи между двумя модулями. Программирование не служба в ЦРУ — никто не похвалит вас за удачную маскировку. Оно больше похоже на рекламу: вам следует делать связи между модулями как
можно более крикливыми. Передача данных посредством списка параметров
формирует очевидную связь, и это удачный вариант. Передача информации другому модулю в глобальных данных является замаскированной и потому неудачной связью. Описание связи, осуществляемой через глобальные данные, в документации делает ее более явной и является чуть более удачным подходом.
Гибкость Гибкость характеризует легкость изменения связи между модулями.
Идеальная связь должна быть как можно гибче. Гибкость частично определяется
другими аспектами связанности, но в то же время отличается от них. Положим, у
вас есть метод LookupVacationBenefit(), определяющий длительность отпуска сотрудника на основании даты его приема на работу и должности. Допустим далее,
что в другом модуле у вас есть объект employee (сотрудник), содержащий, помимо всего прочего, информацию о должности и дате приема на работу, и что этот
модуль передает объект employee в метод LookupVacationBenefit().

98

ЧАСТЬ II Высококачественный код

С точки зрения других критериев, эти два модуля кажутся слабо сопряженными:
связь двух модулей посредством объекта employee очевидна и является единственной. Теперь предположим, что вам нужно использовать модуль LookupVacationBenefit() из третьего модуля, владеющего информацией о дате приема сотрудника
на работу и его должности, но хранит ее не в объекте employee. В этот момент
модуль LookupVacationBenefit() начинает вести себя гораздо менее дружелюбно, не
желая связываться с новым модулем.
Чтобы третий модуль мог обратиться к модулю LookupVacationBenefit(), он должен
знать о существовании класса Employee. Он мог бы подделать объект employee,
используя лишь два поля, но тогда он должен был бы знать внутренние детали
работы метода LookupVacationBenefit(): ему была бы необходима уверенность в том,
что метод LookupVacationBenefit() использует только два этих поля. Такое решение было бы небрежным и безобразным. Второй вариант мог бы заключаться в
таком изменении метода LookupVacationBenefit(), чтобы вместо объекта employee
он принимал должность сотрудника и дату его приема на работу. В обоих случаях первоначальный модуль оказывается на самом деле гораздо менее гибким, чем
казалось сначала.
Возможен и счастливый конец этой истории: недружелюбный модуль сможет
завести друзей, если пожелает быть гибким — если вместо объекта employee он
согласится принимать должность и дату приема сотрудника на работу.
Короче, чем проще вызывать модуль из других модулей, тем слабее он сопряжен,
и это хорошо, потому что такой модуль более гибок и прост в сопровождении.
Создавая структуру программы, делите ее на блоки с учетом их взаимосвязанности. Если бы программа была куском дерева, его следовало бы расщепить параллельно волокнам.

Виды сопряжения
Самые распространенные виды сопряжения описаны ниже.
Простое сопряжение посредством данных'параметров Два модуля сопряжены таким способом, если между ними передаются только элементарные
типы данных, причем передаются через списки параметров. Этот вид сопряжения
нормален и приемлем.
Простое сопряжение посредством объекта Модуль сопряжен с объектом
этим способом, если он создает экземпляр данного объекта. С этим видом сопряжения также все в порядке.
Сопряжение посредством объекта'параметра Два модуля сопряжены друг
с другом объектом#параметром, если Объект 1 требует, чтобы Объект 2 передал
ему Объект 3. Этот вид сопряжения жестче, чем тот вид, при котором Объект 1
требует от Объекта 2 только примитивных типов данных, потому что Объект 2
должен обладать информацией об Объекте 3.
Семантическое сопряжение Самый коварный тип сопряжения имеет место
тогда, когда один модуль использует не какой#то синтаксический элемент другого модуля, а некоторые семантические знания о внутренней работе этого модуля.
Некоторые примеры такого вида сопряжения описаны ниже.

ГЛАВА 5 Проектирование при конструировании

99

 Модуль 1 передает в Модуль 2 управляющий флаг, определяющий дальнейшую

работу Модуля 2. Этот подход подразумевает, что Модуль 1 должен сделать
предположения о внутренней работе Модуля 2, а именно о том, что Модуль
2 собирается делать с управляющим флагом. Если Модуль 2 определяет для
управляющего флага специфический тип данных (перечисление или объект),
этот вид сопряжения, вероятно, будет вполне приемлем.
 Модуль 2 использует глобальные данные после их изменения Модулем 1. При

этом Модуль 2 предполагает, что Модуль 1 был вызван в нужное время и изменил данные так, как нужно Модулю 2.
 Интерфейс Модуля 1 утверждает, что метод Module1. Initialize() должен быть

вызван до метода Module1.Routine(). Модуль 2 знает, что Module1.Routine() как#
то вызывает метод Module1.Initialize(), поэтому он просто создает экземпляр
Модуля 1 и вызывает Module1.Routine() без предварительного вызова метода
Module1.Initialize().
 Модуль 1 передает Объект в Модуль 2. Модуль 1 знает, что Модуль 2 использует

только три метода Объекта из семи, поэтому он инициализирует Объект лишь
частично, только теми данными, что нужны этим трем методам.
 Модуль 1 передает в Модуль 2 Базовый Объект. Модуль 2 знает, что на самом

деле Модуль 1 передал ему Производный Объект, поэтому он приводит тип
Базового Объекта к типу Производного Объекта и вызывает методы, специфические для Производного Объекта.
Семантическое сопряжение опасно тем, что изменение кода в используемом модуле может так нарушить работу использующего модуля, что компилятор этого
не определит. Обычно это приводит к очень тонким проблемам, которые никто
не соотносит с изменениями используемого модуля, что превращает отладку в
сизифов труд.
Суть слабого сопряжения в том, что грамотно спроектированный модуль предо#
ставляет дополнительный уровень абстракции: разработав его, вы можете принимать его как данное. Это снижает общую сложность программы и позволяет сосредоточиваться в каждый момент времени только на одном аспекте. Если для использования модуля нужно учитывать сразу несколько аспектов: механизм внутренней
работы, изменения глобальных данных, неясную функциональность, — сила абстракции исчезает, и модуль перестает облегчать управление сложностью.
Классы и методы — главные интеллектуальные инструменты снижения
сложности. Если они не упрощают вашу работу, они не исполняют свои
обязанности.

Старайтесь использовать популярные шаблоны
проектирования
Шаблоны проектирования — это готовые шаблоны, позвоhttp://cc2e.com/0585
ляющие решать частые проблемы разработки. Конечно, есть
проблемы, требующие совершенно новых решений, но большинство уже встречалось разработчикам, поэтому их можно решить, применяя
проверенные подходы, или шаблоны. В число популярных шаблонов проектирования входят Адаптер, Мост, Декоратор, Фасад, Фабричный метод, Наблюдатель,

100

ЧАСТЬ II Высококачественный код

Одиночка, Стратегия и Шаблонный метод. О шаблонах проектирования см. книгу
«Design Patterns» Эриха Гаммы, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса (Gamma, Helm, Johnson, and Vlissides, 1995).
Шаблоны имеют ряд достоинств, не характерных для полностью самостоятельного
проектирования программы.
Шаблоны снижают сложность, предоставляя готовые абстракции Если
вы скажете: «В этом фрагменте для создания экземпляров производных классов
применяется шаблон “Фабричный метод”», — другие программисты поймут, что
ваш код включает богатый набор взаимодействий и протоколов программирования, специфических для названного шаблона.
Шаблон «Фабричный метод» позволяет создавать экземпляры любого класса, производного от указанного базового класса, причем отдельные производные классы
отслеживаются только самим «Фабричным методом». Обсуждение шаблона «Фабричный метод» см. в разделе «Replace Constructor with Factory Method» (Замена
конструктора на «Фабричный метод») книги «Refactoring» (Fowler, 1999).
Если вы будете использовать шаблоны, другие программисты легко поймут
вы#бранный вами подход к проектированию без подробного обсуждения кода.
Шаблоны снижают число ошибок, стандартизируя детали популярных
решений Проблемы проектирования содержат нюансы, которые полностью
проявляются только после решения проблемы один или два раза (или три, или
четыре, или…). Шаблоны — это стандартизованные способы решения частых
проблем, заключающие мудрость, накопленную за годы попыток решения этих
проблем, и исправления неудачных попыток.
Так что, с концептуальной точки зрения, применение шаблона проектирования
похоже на использование библиотеки кода вместо написания собственного кода.
Многие программисты рано или поздно решают создать собственный вариант
алгоритма быстрой сортировки, но каковы шансы, что его первая версия окажется
безошибочной? Так же и в проектировании: многие проблемы довольно похожи
на уже решенные задачи, и при столкновении с ними изобретать велосипед ни
к чему.
Шаблоны имеют эвристическую ценность, указывая на возможные варианты проектирования Проектировщик, знакомый с популярными шаблонами, может с легкостью перебрать список шаблонов и спросить себя: «Какие из
них соответствуют моей проблеме проектирования?» Перебрать набор известных
вариантов гораздо проще, чем создавать собственное решение с нуля. Кроме того,
код, основанный на популярном шаблоне, будет понятнее, чем код, полностью
разработанный самостоятельно.
Шаблоны упрощают взаимодействие между разработчиками, позволяя
им общаться на более высоком уровне Шаблоны проектирования не только
помогают управлять сложностью, но и способны ускорить обсуждение проектов,
позволяя разработчикам размышлять и делиться мыслями на более высоком уровне.
Если вы скажете: «Не могу решить, какой шаблон следует использовать в данной
ситуации: “Создатель” или “Фабричный метод”», — вы в нескольких словах сообщите очень подробную информацию — конечно, если и вам, и вашему собеседнику
известны эти шаблоны. Представьте, насколько больше времени потребовалось

ГЛАВА 5 Проектирование при конструировании

101

бы для обсуждения деталей кода шаблонов «Создатель» и «Фабричный метод» и
сравнения этих двух подходов.
Если вы еще не сталкивались с шаблонами проектирования, изучите табл. 5#1, где
описаны некоторые из самых популярных шаблонов.

Табл. 5-1.

Популярные шаблоны проектирования

Шаблон

Описание

Абстрактная фабрика
(Abstract Factory)

Поддерживает создание наборов родственных объектов пу#
тем определения вида набора, но не вида каждого отдельно#
го объекта.

Адаптер (Adapter)

Преобразует интерфейс класса в другой интерфейс.

Мост (Bridge)

Создает интерфейс и реализацию, так что их можно изме#
нять независимо друг от друга.

Компоновщик
(Composite)

Состоит из объекта, содержащего дополнительные объекты
такого же типа, позволяя клиентскому коду взаимодейство#
вать с объектом верхнего уровня и не заботиться о деталь#
ных объектах.

Декоратор (Decorator)

Динамически назначает объекту виды ответственности
без создания отдельных подклассов для каждой возможной
конфигурации видов ответственности.

Фасад (Facade)

Предоставляет согласованный интерфейс к коду, который
в противном случае не предоставлял бы согласованного
интерфейса.

Фабричный метод
(Factory Method)

Создает экземпляры классов, производных от конкретного
базового класса, причем отдельные производные классы от#
слеживаются только «Фабричным методом».

Итератор (Iterator)

Этот серверный объект предоставляет доступ к каждому
элементу набора в последовательном порядке.

Наблюдатель (Observer)

Поддерживает синхронизацию нескольких объектов,
при которой объект уведомляет набор связанных объектов
об изменениях любого члена набора.

Одиночка (Singleton)

Предоставляет глобальный доступ к классу, который может
иметь один и только один экземпляр.

Стратегия (Strategy)

Определяет набор динамически взаимозаменяемых
алгоритмов или видов поведения.

Шаблонный метод
(Template Method)

Определяет структуру алгоритма, оставляя некоторые
детали реализации подклассам.

Если раньше вы не встречались с шаблонами проектирования, при взгляде на
табл. 5#1 у вас может возникнуть мысль: «Почти все эти идеи мне уже знакомы».
Этим во многом и объясняется ценность шаблонов проектирования. Они известны
большинству опытных программистов, а присвоение шаблонам запоминающихся
названий позволяет быстро и эффективно делиться мыслями.
С шаблонами связаны две ловушки. Первая — насильственная адаптация кода к
какому#нибудь шаблону. Иногда легкое изменение кода в соответствии с известным шаблоном может сделать код более понятным. Но если адаптация кода к
стандартному шаблону требует слишком крупного изменения, это может привести
к усложнению программы.

102

ЧАСТЬ II Высококачественный код

Вторая — применение шаблона, продиктованное не целесообразностью, а желанием испытать шаблон в деле.
Вообще применение шаблонов проектирования — это эффективный инструмент
управления сложностью. Некоторые хорошие книги по этой теме указаны в конце
главы.

Другие эвристические принципы
В предыдущих разделах были рассмотрены основные эвристические принципы
проектирования ПО. Ниже описаны менее полезные, однако заслуживающие упоминания эвристические принципы.

Стремитесь к максимальной связности
Понятие связности (cohesion) возникло в области структурного проектирования
и обычно обсуждается в том же контексте, что и сопряжение (coupling). Связность
характеризует то, насколько хорошо все методы класса или все фрагменты метода соответствуют главной цели, — иначе говоря, насколько сфокусирован класс.
Классы, состоящие из очень похожих по функциональности блоков, обладают
высокой степенью связности, и наша эвристическая цель состоит в том, чтобы
целостность была как можно выше. Связность — полезный инструмент управления
сложностью, потому что чем лучше код класса соответствует главной цели, тем
проще запомнить все, что код выполняет.
Стремление к связности на уровне методов давно считается полезным эвристическим принципом. На уровне классов эвристический принцип связности во
многом выражен в более общем эвристическом принципе адекватного определения
абстракций, что уже обсуждалось в этой главе и будет еще обсуждаться в главе
6. Абстрагирование полезно и на уровне методов, но в этом случае принципы
абстрагирования и связности более равноправны.

Формируйте иерархии
Иерархия — это многоуровневая структура организации информации, при которой
наиболее общая или абстрактная репрезентация концепции соответствует вершине,
а более детальные специализированные репрезентации — более низким уровням.
При разработке ПО иерархии обнаруживаются, например, в наборах классов и в
последовательностях вызовов методов (уровень 4 на рис. 5#2).
Формирование иерархий уже более 2000 лет является важным средством управления сложными наборами информации. Так, Аристотель использовал иерархию для
организации царства животных. Люди часто организуют сложную информацию
(такую как эта книга) при помощи иерархических схем. Ученые обнаружили, что
люди в целом находят иерархии естественным способом организации сложной
информации. Рисуя сложный объект (скажем, дом), люди рисуют его иерархически. Сначала они рисуют очертания дома, затем окна и двери, а после этого — еще
более подробные детали. Они не рисуют дом по отдельным кирпичам, доскам или
гвоздям (Simon, 1996).
Иерархии помогают в достижении Главного Технического Императива Разработки ПО, позволяя сосредоточиться только на том уровне детальности, который

ГЛАВА 5 Проектирование при конструировании

103

заботит вас в конкретный момент. Иерархия не устраняет детали — она просто
выталкивает их на другой уровень, чтобы вы могли думать о них, когда захотите,
а не все время.

Формализуйте контракты классов
На более детальном уровне полезную информацию можно
Перекрестная ссылка О контракполучить, рассматривая интерфейс каждого класса как контах см. подраздел «Используйте
тракт с остальными частями программы. Обычно контракт
утверждения для документироимеет форму «Если вы обещаете предоставить данные x, y и
вания и проверки предусловий и
постусловий» раздела 8.2.
z и гарантируете, что они будут иметь характеристики a, b и
c, я обязуюсь выполнить операции 1, 2 и 3 с ограничениями
8, 9 и 10». Обещания клиентов классу обычно называются
предусловиями (preconditions), а обязательства класса перед клиентами — постусловиями (postconditions).
Контракты помогают управлять сложностью, потому что хотя бы теоретически
объект может свободно игнорировать любое поведение, не описанное в контракте.
На практике этот вопрос куда сложнее.

Грамотно назначайте сферы ответственности
Еще один эвристический принцип — обдумывание сфер ответственности, которые
следует назначить объектам. Рассмотрение сферы ответственности объекта аналогично вопросу о том, какую информацию он должен скрывать, но мне кажется,
что первый способ может привести к более общим ответам, чем и объясняется
уникальность этого эвристического принципа.

Проектируйте систему для тестирования
На некоторые интересные идеи можно натолкнуться, спросив, как будет выглядеть
система, если спроектировать ее для обеспечения максимальной легкости тестирования. Отделять ли пользовательский интерфейс от остальной части программы,
чтобы протестировать его независимо? Организовывать ли каждую подсистему так,
чтобы минимизировать ее зависимость от других подсистем? Проектирование для
тестирования часто приводит к разработке более формализованных интерфейсов
классов, что обычно выгодно.

Избегайте неудач
Профессор гражданского строительства Генри Петроски в интересной книге
«Design Paradigms: Case Histories of Error and Judgment in Engineering» (Petroski,
1994), посвященной истории неудач в отрасли проектирования мостов, утверждает,
что многие известные мосты рушились из#за чрезмерного внимания к прошлым
успехам и неадекватного рассмотрения возможных причин аварий. Он делает вывод, что аварий вроде крушения моста Tacoma Narrows можно было бы избежать,
если б инженеры тщательно рассматривали возможные причины аварий, а не
просто копировали другие успешные проекты.
Крупные бреши в защите многих известных систем, обнаруженные в прошедшие
годы, заставляют подумать о том, как применить идеи Петроски в области проектирования ПО.

104

ЧАСТЬ II Высококачественный код

Тщательно выбирайте время связывания
Временем связывания (binding time) называют тот момент,
когда переменной присваивается конкретное значение.
Раннее связывание обычно упрощает код, но и снижает его
гибкость. Иногда к полезным идеям проектирования можно
прийти, спросив себя: «Что, если связать эти значения раньше? Что, если связать
их позже? Что, если инициализировать эту таблицу в этом месте кода? Что, если
получить значение этой переменной от пользователя в период выполнения программы?»

Перекрестная ссылка О времени
связывания см. раздел 10.6.

Создайте центральные точки управления
Ф. Дж. Плоджер говорит, что главным его принципом является «Принцип Одного
Верного Места: в программе должно быть Одно Верное Место для поиска нетривиального фрагмента кода и Одно Верное Место для внесения вероятных изменений» (Plauger, 1993). Управление может быть централизовано в классах, методах, макросах препроцессора, файлах, включаемых директивой #include, — даже
именованная константа может быть центральной точкой управления.
Этот принцип также способствует снижению сложности: если какой#то программный элемент встречается в минимальном числе фрагментов, его изменение окажется проще и безопаснее.
Если сомневаетесь, используйте
грубую силу.

Подумайте об использовании грубой силы

Грубая сила — один из мощнейших эвристических инструментов. Не стоит ее недооценивать. Работоспособное решеБатлер Лэмпсон
ние проблемы методом грубой силы лучше, чем элегантное,
(Butler Lampson)
но не работающее решение. Создавать элегантные решения
зачастую долго и сложно. Так, описывая историю разработки
алгоритмов поиска, Дональд Кнут указал, что, хотя первое описание алгоритма
двоичного поиска было опубликовано в 1946 г., алгоритм, правильно обрабатывающий списки всех размеров, был разработан только спустя 16 лет (Knuth, 1998).
Двоичный поиск элегантнее, но и основанный на грубой силе последовательный
поиск часто приемлем.

Рисуйте диаграммы
Диаграммы — еще один мощный эвристический инструмент. Все знают, что лучше
один раз увидеть, чем сто раз услышать. Диаграммы позволяют представить проблему на более высоком уровне абстракции, и никакие описания их не заменят.
Помните: иногда проблему следует рассматривать на детальном уровне, а иногда
целесообразно иметь дело с более общими аспектами.

Поддерживайте модульность проекта системы
Этот принцип подразумевает, что каждый метод или класс должен быть похож
на «черный ящик»: вы знаете, что в него поступает и что из него выходит, но не
знаете, что происходит внутри. Черный ящик имеет такой простой интерфейс
и такую ясную функциональность, что для любых конкретных входных данных
можно точно предсказать соответствующие выходные данные.

ГЛАВА 5 Проектирование при конструировании

105

Концепция модульности связана с сокрытием информации, инкапсуляцией и другими эвристическими принципами проектирования. И все же размышление о том,
как собрать систему из набора черных ящиков иногда приводит к догадкам, к которым сокрытие информации и инкапсуляция привести не могут, так что принцип
модульности заслужил право занять место в вашем арсенале полезных идей.

Резюме эвристических принципов проектирования
Ниже приведен список основных эвристических принципов
проектирования:
 определите объекты реального мира;
 определите согласованные абстракции;
 инкапсулируйте детали реализации;
 используйте наследование, когда это возможно;
 скрывайте секреты (помните про сокрытие информации);
 определите области вероятных изменений;
 поддерживайте сопряжение слабым;

Больше беспокоит то, что программист вполне может выполнить ту же задачу двумя или
тремя способами: иногда неосознанно, но довольно часто
просто ради изменения или же
создания элегантной вариации.
А. Р. Браун и У. А. Сэмпсон
(A. R. Brown and
W. A. Sampson)

 старайтесь использовать популярные шаблоны проекти-

рования.
Следующие эвристические принципы также иногда бывают полезны:
 стремитесь к максимальной связности;
 формируйте иерархии;
 формализуйте контракты классов;
 грамотно назначайте сферы ответственности;
 проектируйте систему для тестирования;
 избегайте неудач;
 тщательно выбирайте время связывания;
 создайте центральные точки управления;
 подумайте об использовании грубой силы;
 рисуйте диаграммы;
 поддерживайте модульность проекта системы.

Советы по использованию эвристических принципов
Подходы к проектированию ПО могут быть основаны на
http://cc2e.com/0592
подходах, применяемых в других областях. Одной из первых
книг, посвященных использованию эвристики при решении
проблем, является «How to Solve It (Как решать задачу)» Д. Полья (Polya, 1957).
Обобщенный подход Полья к решению проблем концентрируется на решении математических задач. Он резюмирован на рис. 5#10 (шрифт оригинала сохранен).

106

ЧАСТЬ II Высококачественный код

Рис. 5'10. Д. Полья разработал подход к решению математических задач, который
полезен и при решении проблем, связанных с проектированием ПО (Polya 1957)

Одним из самых ценных советов, которые можно дать по поводу проектирования ПО, является использование разных подходов. Если проект, разработанный с
помощью UML, неудачен, выразите его на обычном языке. Напишите небольшую
тестовую программу. Попробуйте совершенно другой подход. Подумайте об использовании грубой силы. Продолжайте рисовать эскизы и наброски карандашом,
и мозг последует за рукой. Если ничего не выходит, отложите решение проблемы.

ГЛАВА 5 Проектирование при конструировании

107

Прежде чем возвращаться к работе над ней, прогуляйтесь, подумайте о чем#то
другом. Довольно часто это приводит к более быстрому получению нужного результата, чем простое упорство.
Никто не заставляет вас разработать весь проект за раз. Если натолкнетесь на
препятствие, подумайте, обладаете ли вы информацией, достаточной для решения
всех специфических проблем? Зачем через силу разрабатывать оставшиеся 20%
проекта, если впоследствии они прекрасно станут на свое место? Зачем принимать
неудачные решения, основанные на недостаточной информации, если позже можно
будет принять более подходящие решения? Некоторые разработчики чувствуют
дискомфорт, если не могут создать полный проект программы к окончанию этапа
проектирования, но после создания нескольких удачных программ без заблаговременного решения всех вопросов на этапе проектирования такая ситуация
начинает казаться вполне естественной (Zahniser, 1992; Beck, 2000).

5.4.

Методики проектирования

Предыдущий раздел был посвящен эвристическим принципам, связанным с атрибутами проектов. Иначе говоря, эти принципы характеризуют то, каким должен
быть завершенный проект приложения. В этом разделе мы рассмотрим эвристические методики проектирования — действия, которые часто приводят к хорошим
результатам.

Используйте итерацию
Возможно, у вас были случаи, когда вы так много узнали во время написания программы, что желали бы написать ее заново, опираясь на полученные знания. Этот
же феномен наблюдается и при проектировании, но этап проектирования короче,
тогда как влияние, оказываемое им на последующие этапы, выражено сильнее,
поэтому вы вполне можете выполнить этап проектирования несколько раз.
Проектирование — итеративный процесс. Выйдя из точки А и достигнув
точки Б, не останавливайтесь, а вернитесь в точку А.
Изучая возможные варианты проектирования и пробуя разные подходы,
вы будете рассматривать и высокоуровневые, и низкоуровневые аспекты. Общая
картина, которую вы получаете при работе над высокоуровневыми вопросами,
поможет вам лучше понять низкоуровневые детали. Детали, которые вы узнаете
при работе над низкоуровневыми вопросами, помогут вам создать прочный фундамент для принятия высокоуровневых решений. Некоторые конфликты между
высокоуровневыми и низкоуровневыми соображениями — вполне здоровое явление; это напряжение способствует созданию структуры, более стабильной, чем
структура, полностью созданная «сверху вниз» или «снизу вверх».
Многим программистам — и вообще многим людям — трудно переключаться между
высокоуровневыми и низкоуровневыми точками зрения, но эта способность —
важное условие эффективного проектирования. Занимательные упражнения, позволяющие развить гибкость ума, можно найти в книге «Conceptual Blockbusting»
(Adams, 2001), описанной в разделе «Дополнительные ресурсы» в конце главы.

108

ЧАСТЬ II Высококачественный код

Если первая попытка создания проекта кажется вполне удачной, не останавливайтесь! Вторая попытка почти всегда
оказывается лучше первой, и при каждой попытке вы будете узнавать что#то такое, что поможет вам улучшить общий
проект. Говорят, что, когда Томаса Эдисона, который пытался
создать нить лампочки и испробовал на тот момент уже тысячу разных материалов, спросили, не жалеет ли он о том, что зря потратил время, так ничего и не
обнаружив, Эдисон ответил: «Ни в коей мере. Я обнаружил тысячу вариантов, которые не работают». Во многих случаях, решив проблему при помощи одного
подхода, вы получите знания, которые позволят решить проблему иным, более
эффективным способом.

Перекрестная ссылка Безопасный способ попробовать разные
варианты кода предоставляет
рефакторинг (глава 24).

Разделяй и властвуй
Как указал Эдсгер Дейкстра, никто не обладает умом, способным вместить все
детали сложной программы. То же можно сказать и о проектировании. Разделите
программу на разные области и спроектируйте их по отдельности. Если, работая
над одной из областей, вы попадете в тупик, вспомните про итерацию!
Инкрементное улучшение — мощное средство управления сложностью. Вспомните, как Полья советовал решать математические задачи: поймите задачу, составьте
план решения, осуществите план и оглянитесь назад, чтобы лучше понять, что и
как вы сделали (Polya, 1957).

Нисходящий и восходящий подходы к проектированию
Слова «нисходящий» и «восходящий» могут казаться устаревшими, но они предоставляют много ценной информации об объектно#ориентированных способах
проектирования. Нисходящее (top#down) проектирование начинается на высоком
уровне абстракции. Например, вы сначала определяете базовые классы или другие
неспецифические элементы проекта. По ходу работы вы повышаете уровень детальности и определяете производные классы, сотрудничающие классы и другие
детали.
Восходящее (bottom#up) проектирование начинается со специфики и постепенно
переходит ко все большей общности. Как правило, оно начинается с определения
конкретных объектов, на основе которых затем разрабатываются более общие
объединения объектов и базовые классы.
Некоторые разработчики утверждают, что лучше всего начинать с общего и двигаться по направлению к частному, а другие — что общие принципы проектирования нельзя определить, не обдумав важных деталей. Аргументы обеих сторон
описаны ниже.

Аргументы в пользу нисходящего проектирования
Нисходящее проектирование основано на том факте, что человеческий мозг в
каждый конкретный момент времени может работать только с определенным
объемом информации. Если вы начинаете проектирование с общих классов, выполняя их декомпозицию на более специализированные классы шаг за шагом,
мозгу не приходится иметь дело со слишком большим числом деталей сразу.

ГЛАВА 5 Проектирование при конструировании

109

Процесс «разделяй и властвуй» итеративен в двух аспектах. Во#первых, потому, что,
выполнив один этап декомпозиции, разработчики обычно не останавливаются, а
выполняют еще несколько этапов. Во#вторых, потому, что дело обычно не ограничивается одной попыткой декомпозиции. Вы выполняете декомпозицию одним
способом. На разных этапах декомпозиции у вас будут разные варианты разделения
подсистем, формирования дерева наследования и группирования объектов. Вы
делаете выбор и смотрите, что происходит. Затем вы возвращаетесь, выполняете
декомпозицию иначе и смотрите, что работает лучше. После нескольких попыток
вы получите хорошее представление о том, чту будет работать и почему.
До каких пор продолжать декомпозицию программы? До тех, пока вам не покажется,
что вместо декомпозиции следующего уровня его было бы проще закодировать.
До тех, пока очевидность и простота проекта не станут вас в каком#то смысле
раздражать. В этот момент все готово. Если что#то неясно, продолжите декомпозицию. Если сейчас решение кажется вам хоть чуть#чуть хитрым, для любого, кто
будет работать над ним позднее, оно станет головоломкой.

Аргументы в пользу восходящего проектирования
Иногда нисходящий подход настолько абстрактен, что его трудно начать. Если
вам нужно работать с чем#то более реальным, попробуйте восходящий подход к
проектированию. Спросите себя: «Какие функции эта система должна выполнять?»
Несомненно, вы сможете ответить на этот вопрос. Вы можете определить несколько
низкоуровневых аспектов ответственности, которые можно назначить конкретным
классам. Так, вы можете знать, что система должна форматировать конкретный
отчет, вычислять данные для отчета, центрировать его заголовки, отображать на
экране, печатать на принтере и т. д. Определив несколько низкоуровневых аспектов
ответственности, вы скорее всего почувствуете себя достаточно подготовленным,
чтобы еще раз взглянуть на вершину.
В других случаях основные атрибуты проекта могут быть продиктованы низкоуровневыми факторами, такими как особенности взаимодействия с оборудованием.
Вот некоторые рекомендации, о которых следует помнить при выполнении восходящей композиции:
 спросите себя, какие функции должна выполнять система;
 опираясь на этот вопрос, определите конкретные объекты и их сферы ответ-

ственности;
 определите общие объекты и сгруппируйте их, организовав в подсистемы или

пакеты, с помощью композиции или наследования (выберите самый подходящий вариант);
 поднимитесь на следующий уровень или вернитесь на вершину и попробуйте

еще раз начать нисходящее проектирование.

Никакого конфликта нет
Главное различие между нисходящей и восходящей стратегиями в том, что одна
является стратегией декомпозиции, а вторая — композиции. В первом случае вы
начинаете работу с общей проблемы, разбивая ее на управляемые фрагменты, во
втором вы начинаете с управляемых фрагментов, составляя из них общее реше-

110

ЧАСТЬ II Высококачественный код

ние. Оба подхода имеют достоинства и недостатки, которые следует рассмотреть
в контексте конкретной проблемы.
Сила нисходящего подхода — в простоте. Люди (особенно программисты) прекрасно умеют делить что#то крупное на меньшие компоненты.
Еще одно достоинство в том, что нисходящее проектирование позволяет отложить
работу над деталями конструирования. Изменения аспектов конструирования
(таких как структура файлов или формат отчетов) часто сказываются на всей
системе, поэтому лучше заранее знать, что эти детали следует скрыть в классах
на нижних уровнях иерархии.
Одно достоинство восходящего подхода объясняется тем, что он обычно приводит
к раннему определению вспомогательной функциональности, что способствует
созданию компактного, хорошо факторизованного проекта системы. Если похожая
система уже создавалась, восходящий подход позволяет начать проектирование
новой системы с рассмотрения и повторного использования фрагментов старой
системы.
Восходящая композиция имеет и недостатки: во#первых, ее трудно использовать
без применения других подходов. Большинство людей находят разбиение крупной концепции на меньшие части более легким, чем объединение небольших
концепций в более крупную. Это напоминает старую проблему, связанную с конструкторами: модель кажется готовой, но почему в коробке остались детали? К
счастью, никто не заставляет проектировать программы, применяя исключительно
восходящий подход.
Во#вторых, при восходящем проектировании иногда оказывается, что из исходных
фрагментов создать программу невозможно. Никто не сможет собрать самолет
из кирпичей. Чтобы узнать, какие фрагменты понадобятся на нижних уровнях,
иногда нужно сначала получить общее представление о системе.
Подведем итог: нисходящее проектирование обычно начинается с простого, но
иногда низкоуровневые сложности прорываются на вершину, и это может приводить к усложнению системы, которого можно было избежать. Восходящее проектирование начинается со сложных аспектов, но определение этой сложности на
ранних этапах позволяет лучше спроектировать высокоуровневые классы.... если
к этому моменту сложность не потопит всю систему!
В конечном счете это не конкурирующие стратегии — они дополняют друг друга.
Проектирование — эвристический процесс, а значит, универсальных решений не
существует. Проектирование содержит элементы метода проб и ошибок. Пробуйте
разные подходы, пока не найдете тот, что вас устроит.

Экспериментальное прототипирование
Иногда адекватность конкретного проекта невозможно
оценить, не имея дополнительных сведений о деталях реализации. Вы можете не знать, приемлема ли конкретная организация базы данных, пока не узнаете, будет ли она удовлетворять конкретным
требованиям к производительности. Вы можете не знать, приемлем ли проект
http://cc2e.com/0599

ГЛАВА 5 Проектирование при конструировании

111

конкретной подсистемы, пока не будут выбраны конкретные библиотеки GUI. Это
примеры существенной «грязи» при проектировании ПО: вы не можете полностью
определить проблему проектирования, пока не решите ее хоть частично.
Хорошо известен недорогой способ получить ответы на эти вопросы — экспериментальное прототипирование. В слово «прототипирование» люди вкладывают
разный смысл (McConnell, 1996). В данном контексте оно означает написание
абсолютно минимального объема подлежащего выбрасыванию кода, нужного для
ответа на отдельный вопрос проектирования.
Если разработчики недисциплинированно относятся к написанию абсолютно
минимального объема кода, нужного для ответа на вопрос, прототипирование
работает плохо. Допустим, вопрос проектирования таков: «Может ли выбранная
нами организация базы данных поддерживать нужный объем транзакций?» Для
ответа не нужно писать полноценный код, который можно было бы использовать
в готовой системе. Вы можете даже не знать специфику базы данных. Вам лишь
нужна информация, достаточная для аппроксимации проблемной области: число
таблиц, число элементов в таблицах и т. д. Далее вы можете написать простой прототипный код, использующий таблицы и столбцы с именами вроде Table1, Table2
и Column1, Column2, заполнить таблицы фиктивными данными и протестировать
производительность.
Прототипирование также работает плохо, если задача недостаточно конкретна.
Вопрос «Будет ли эта организация базы данных работать?» недостаточно хорошо
определяет направление прототипирования. В то же время вопрос «Будет ли эта
организация базы данных поддерживать 1000 транзакций в секунду при условиях
X, Y и Z?» предоставляет более прочную основу для прототипирования.
Наконец, еще один фактор риска возникает, если разработчики не рассматривают
код как подлежащий выбрасыванию. Я обнаружил, что люди не могут написать абсолютно минимальный объем кода, нужный для ответа на вопрос, если думают, что
код в конечном счете войдет в итоговую версию системы. Из#за этого они вместо
прототипирования занимаются реализацией системы. Настроившись на то, что,
как только ответ на вопрос будет получен, код будет выброшен, вы сведете этот
риск к минимуму. Избежать этой проблемы можно, если создавать прототипы и
основную программу, используя разные технологии. Вы можете создать прототип
проекта Java на языке Python или смоделировать пользовательский интерфейс в
Microsoft PowerPoint. Если вы все#таки создаете прототипы, используя ту же технологию, пусть имена прототипичных классов и пакетов начинаются с префикса
prototype. Это хотя бы заставит программиста дважды подумать, прежде чем он
решит расширять прототипный код (Stephens, 2003).
При дисциплинированном применении прототипирование — эффективный
способ борьбы с «грязнотой» проектирования. В противном случае оно делает
проектирование еще более грязным.

112

ЧАСТЬ II Высококачественный код

Совместное проектирование
Перекрестная ссылка О совместной разработке ПО см. главу 21

Обычно при проектировании две головы лучше, чем одна,
организованы они формально или неформально. Сотрудничество может принимать любую из следующих форм:

 вы подходите к столу коллеги и просите его обсудить с вами некоторые идеи;
 вы с коллегой идете в конференц#зал и рисуете на доске варианты проекта;
 вы с коллегой садитесь перед клавиатурой и выполняете детальное проекти-

рование на выбранном языке программирования, т. е. вы можете использовать
парное программирование (см. главу 21);
 вы назначаете собрание для обсуждения своих идей с одним или несколькими

коллегами;
 вы назначаете формальную инспекцию, включающую все аспекты, описанные

в главе 21;
 никто не может провести обзор вашей работы, поэтому вы выполняете неко-

торый объем работы, сохраняете ее и возвращаетесь к ней через неделю — вы
забудете достаточно, чтобы самостоятельно провести довольно хороший обзор
своей же работы;
 вы обращаетесь за помощью к людям, не работающим в вашей компании: от-

правляете вопросы в специализированный форум или группу новостей.
Если целью является гарантия качества, тогда по причинам, описанным в главе
21, я рекомендую наиболее структурированную методику обзора — формальные
инспекции. Но если цель состоит в содействии творчеству и увеличении числа
предлагаемых вариантов проекта, а не в простом нахождении ошибок, лучше применять менее структурированные подходы. После выбора определенного варианта
проекта может оказаться уместным переход к более формальным инспекциям, что
определяется конкретной ситуацией.

Какую степень проектирования считать достаточной?
Мы пытаемся решить проблему,
максимально ускоряя процесс
проектирования, чтобы в конце работы над системой у нас
осталось достаточно времени
для нахождения ошибок, допущенных из-за слишком быстрого проектирования.
Гленфорд Майерс
(Glenford Myers)

Иногда перед началом кодирования создается только самый
общий набросок архитектуры. В других случаях группы
создают проекты с таким уровнем подробностей, что кодирование становится практически механическим занятием.
Насколько детально выполнять проектирование до начала
кодирования?
Родственный вопрос заключается в том, насколько формальным делать проект. Нужны ли вам формальные, тщательно
выполненные диаграммы проекта системы или цифровых
снимков нескольких рисунков на доске будет достаточно?

Принятие решения о том, какую часть проектирования выполнять до начала полномасштабного кодирования и насколько формально документировать проект
системы, трудноназвать точной наукой. При этом следует учитывать опыт группы, ожидаемый срок службы системы, желательный уровень надежности, мас#
штаб проекта и число членов группы. Влияние этих факторов на подход к проектированию отражено в табл. 5#2.

ГЛАВА 5 Проектирование при конструировании

Табл. 5-2.

113

Необходимые уровни формальности и детальности проекта
Уровень детальности
проекта, нужный
перед началом
конструирования

Уровень формальности
документации

Члены группы проектирования/
конструирования имеют большой
опыт работы в прикладной области.

Низкий

Низкий

Члены группы проектирования/
конструирования имеют большой
опыт, но плохо знакомы
с прикладной областью.

Средний

Средний

Члены группы проектирования/
конструирования неопытны.

Средний — высокий

Низкий — средний

Для группы проектирования/конст#
руирования характерен средний —
высокий уровень текучести.

Средний



От приложения будет зависеть
безопасность людей.

Высокий

Высокий

Приложение предназначено
для решения ответственных задач.

Средний

Средний — высокий

Проект небольшой.

Низкий

Низкий

Проект крупный.

Средний

Средний

Предполагается, что ПО будет
использоваться недолго
(недели или месяцы).

Низкий

Низкий

Предполагается, что ПО будет
использоваться длительное
время (месяцы или годы).

Средний

Средний

Фактор

При создании конкретной системы могут иметь место сразу несколько факторов,
подталкивающих к разным решениям: скажем, опытная группа может разрабатывать ПО, от которого будет зависеть безопасность людей. Тогда вам, вероятно,
следует отдать предпочтение более высокой детальности и формальности проекта.
Вообще в подобной ситуации нужно оценить роль каждого фактора и решить,
какой из них важнее.
Если разработчики выполняют какую#то часть проектирования индивидуально, то
при снижении проектирования до уровня задачи, которая уже была решена ранее,
или до уровня простого изменения или расширения такой задачи, проектирование,
вероятно, можно прекратить и начать кодирование.
Если я не могу решить, насколько детальным должен быть проект программы
перед началом кодирования, я предпочитаю разрабатывать его более детально.
Самые крупные ошибки проектирования возникали, когда я думал, что выполнил
проектирование в достаточной степени, но позднее оказывалось, что я заблуждался и не учитывал дополнительные проблемы. Иначе говоря, самые серьезные
проблемы проектирования обычно были связаны не с теми областями, которые
я считал сложными и спроектировал неудачно, а с теми, которые я считал легкими и не спроектировал вообще. Мне редко встречаются проекты, страдающие от
чрезмерного проектирования.

114

ЧАСТЬ II Высококачественный код

С другой стороны, иногда я имею дело с проектами, страдающими от слишком объемной проектной документации. Здесь вступает в игру своеобразный закон Грешема1 , и
«механические действия начинают вытеснять творчество»
(Simon, 1965). Чрезмерное внимание к созданию проектной
Джозеф Костелло
документации — хорошее подтверждение этого закона. Я
(Joseph Costello)
бы предпочел, чтобы разработчики тратили 80% усилий на
разработку и анализ различных вариантов проектирования
и 20% — на создание менее изысканной документации, а не наоборот: 20% — на
создание посредственных решений и 80% — на совершенствование документации
не совсем удачных проектов.

Я никогда не встречал человека,
желающего читать 17 000 страниц документации, а если бы
встретил, то убил бы его, чтобы
он не портил генофонд.

Регистрация процесса проектирования
Традиционным способом регистрации проекта является его
описание в формальной проектной документации. Однако
есть масса других способов, эффективных при использовании неформального подхода, при создании небольших систем или когда нужна
«облегченная» методика регистрации проекта:
http://cc2e.com/0506

Плохие новости заключаются в
том, что мы, как нам кажется,
никогда не найдем философского камня. Мы никогда не найдем процесса, позволяющий
проектировать ПО абсолютно
рациональным образом. Но есть
и хорошая новость: мы можем
его подделать.
Дэвид Парнас
и Пол Клементс (David Parnas
and Paul Clements)

Включайте проектную документацию прямо в код
Документируйте основные аспекты проектирования в комментариях — как правило, в заголовках файлов или классов.
Дополнив этот подход использованием утилиты извлечения
документации (такой как JavaDoc), вы позволите программистам, работающим над конкретными фрагментами кода,
с легкостью обращаться к соответствующей проектной документации, и повысите вероятность того, что они будут
поддерживать ее актуальность.

Регистрируйте протоколы обсуждения проекта и
принятые решения при помощи Wiki Сохраняйте письменные протоколы обсуждения проекта в системе Wiki (набор Web#страниц, которые может редактировать каждый член группы при помощи
Web#браузера). Это позволяет автоматизировать регистрацию нужных данных,
хотя и требует дополнительных затрат на набор текста. Кроме того, в Wiki можно хранить полезные цифровые фотографии, ссылки на Web#сайты, содержащие
обоснования принятых решений, документы и другие материалы. Этот метод
особенно полезен, если члены группы отдалены друг от друга.
Пишите резюме дискуссий в форме электронной почты Обсудив проект
системы, поручите кому#нибудь записать резюме беседы — особенно принятые
решения — и отправить его каждому члену группы. Сохраняйте копии писем в
папке, доступной всем участникам проекта.

1

Закон Грешема (Gresham’s Law) — монетарный принцип, названный в честь лорда Томаса
Грешема, управляющего английским монетным двором при королеве Елизавете. Суть закона
Грешема в том, что «худшие» деньги (некачественные или с пониженным содержанием благородного металла) вытесняют «лучшие» деньги из обращения, т. е. более «дешевые» деньги
вытесняют более «дорогие». — Прим. перев.

ГЛАВА 5 Проектирование при конструировании

115

Используйте цифровой фотоаппарат Одним частым барьером, препятствующим документированию проекта, является утомительность рисования проектов
традиционным способом. Однако способы документирования не ограничиваются
вариантами «регистрации проекта с использованием красиво отформатированной
формальной нотации» и «полного отсутствия проектной документации».
Фотографируйте диаграммы, которые разработчики рисуют на доске, и включайте
их в традиционные документы: это гораздо проще, чем рисовать схемы вручную,
но не менее эффективно.
Храните плакаты со схемами проекта Нет закона, утверждающего, что для
документирования проекта нужно использовать стандартные листы бумаги почтового размера. Если вы рисуете диаграммы проектов на больших листах, можете
просто сохранить их в удобном месте или, что еще лучше, развесить на стенах,
чтобы разработчики могли обращаться к ним и обновлять по мере надобности.
Используйте карточки CRC (Class, Responsibility,
http://cc2e.com/0513
Collaborator — класс, ответственность, сотрудничество) Еще один простой вариант документирования
проекта — использовать карточки. Напишите на каждой карточке имя класса,
аспекты его ответственности и имена классов, с которыми он сотрудничает. Продолжайте работать с карточками, пока не будете удовлетворены результатом. В этот
момент вы можете просто сохранить карточки на будущее. Этот способ почти не
требует расходов, не пугает своей сложностью и поощряет взаимодействие членов
группы (Beck, 1991).
Создавайте диаграммы UML с уместным уровнем детальности Одним из
популярных способов создания диаграмм проектов является язык UML (Unified
Modeling Language; унифицированный язык моделирования), стандартизацией которого занимается организация Object Management Group (Fowler, 2004). Пример
UML#диаграммы классов вы уже видели на рис. 5#6. UML предоставляет богатый
набор формализованных репрезентаций для проектирования сущностей и их
отношений. Вы можете использовать неформальные версии UML для анализа и
обсуждения подходов к проектированию. Начните с минимальных набросков и
добавляйте детали, только выбрав конкретный вариант проекта. Так как UML стандартизирован, он позволяет эффективнее обмениваться идеями и может ускорить
процесс рассмотрения вариантов проектов при работе в группе.
Описанные способы работают и в различных комбинациях, так что можете свободно смешивать их, приспосабливая к конкретным проектам и даже разным
областям одного проекта.

5.5.

Комментарии по поводу популярных
методологий

История проектирования ПО отмечена бурными спорами фанатичных сторонников
конфликтующих подходов к проектированию. Когда в начале 1990#х вышло в свет
первое издание этой книги, фанатики проектирования утверждали, что перед началом кодирования нужно расставить все точки над «i» на этапе проектирования.
Эта рекомендация не имела никакого смысла.

116

ЧАСТЬ II Высококачественный код

Люди, описывающие проектирование ПО как дисциплинированный процесс, тратят много
энергии, заставляя всех нас почувствовать себя виноватыми.
Мы никогда не станем достаточно структурированными или
объектно-ориентированными
для достижения нирваны при
жизни. Все мы расплачиваемся
за первородный грех — изучение Basic в особо впечатлительном возрасте. Но я готов спорить, что большинство из нас
проектируют программы лучше,
чем кажется пуристам.

Сейчас, в середине первого десятилетия XXI века, некоторые
«проповедники» утверждают, что проектирование вообще не
требуется. «Крупномасштабное Предварительное Проектирование — это плохо, — говорят они. — Лучше вообще не
выполнять проектирование перед началом кодирования!»
За десять лет маятник сместился от «проектирования всего»
до «проектирования ничего». Но альтернативой Крупномасштабному Предварительному Проектированию является не
отсутствие предварительного проектирования, а Небольшое
Предварительное Проектирование или Достаточное Предварительное Проектирование.

Какой объем проектирования достаточен? Никто не может точно ответить на этот вопрос. Тем не менее можно с
полной уверенностью утверждать, что два варианта обязаФ. Дж. Плоджер
тельно окажутся неудачными: проектирование всех деталей
(P. J. Plauger)
до единой и полное отсутствие проектирования. Крайние
точки зрения всегда ошибочны!
Как сказал Ф. Дж. Плоджер, «чем догматичнее вы будете в отношении методики
проектирования, тем меньше реальных проблем решите» (Plauger, 1993). Рассматривайте проектирование как грязный, неряшливый эвристический процесс. Не
останавливайтесь на первом проекте, который пришел вам в голову. Сотрудничайте. Стремитесь к простоте. Создавайте прототипы, если нужно. Не забывайте
про итерацию. Вы будете довольны своими проектами.

Дополнительные ресурсы
http://cc2e.com/0520

Проектирование ПО описанно во множестве работ. Проблема
в том, чтобы определить, какие из них наиболее полезны.
Позволю себе дать вам некоторые советы.

Общие вопросы проектирования ПО
Weisfeld, Matt. The Object%Oriented Thought Process, 2d ed. — SAMS, 2004 — понятное
введение в объектно#ориентированное программирование. Если вы уже знакомы
с объектно#ориентированным программированием, возможно, вам следует поискать более серьезную книгу, но если вы новичок в этой области, эта книга познакомит вас с фундаментальными объектно#ориентированными концепциями,
такими как объекты, классы, интерфейсы, наследование, полиморфизм, перегрузка, абстрактные классы, агрегация и ассоциация, конструкторы/деструкторы, исключения и т. д.
Riel, Arthur J. Object%Oriented Design Heuristics. — Reading, MA: Addison#Wesley, 1996.
В этой книге основное внимание уделяется проектированию на уровне классов.
Еще она легко читается.
Plauger, P. J. Programming on Purpose: Essays on Software Design. — Englewood Cliffs,
NJ: PTR Prentice Hall, 1993. В этой книге я нашел столько хороших советов по проектированию ПО, сколько во всех остальных прочитанных мной книгах вместе

ГЛАВА 5 Проектирование при конструировании

117

взятых. Плоджер прекрасно разбирается во многих подходах к проектированию,
он прагматичен, и он отличный писатель.
Meyer, Bertrand. Object%Oriented Software Construction, 2d ed. — New York, NY: Prentice
Hall PTR, 1997. Мейер приводит убедительные доводы в защиту чистого объектно#ориентированного программирования.
Raymond, Eric S. The Art of UNIX Programming. — Boston, MA: Addison#Wesley, 2004.
Эта книга — хорошо обоснованный взгляд на проектирование ПО сквозь призму
UNIX. В разделе 1.6 приведено лаконичное объяснение 17 ключевых принципов
проектирования ПО для UNIX.
Larman, Craig. Applying UML and Patterns: An Introduction to Object%Oriented Analysis and
Design and the Unified Process, 2d ed. — Englewood Cliffs, NJ: Prentice Hall, 2001. Это
популярное введение в объектно#ориентированное проектирование в контексте
метода Unified Process. Кроме того, здесь обсуждается объектно#ориентированный
анализ.

Теория проектирования ПО
Parnas, David L., and Paul C. Clements. «A Rational Design Process: How and Why to Fake
It». — IEEE Transactions on Software Engineering SE%12, no. 2 (February 1986): 251–57.
В этой классической статье описывается разрыв между реальным и желательным
процессами проектирования программ. Суть статьи в том, что в действительности
никто никогда не следует рациональному упорядоченному процессу проектирования,
но стремление к этому приводит в итоге к созданию лучших проектов.
Работы, в которых было бы приведено исчерпывающее обсуждение сокрытия
информации, мне неизвестны. В большинстве учебников по разработке ПО оно
обсуждается кратко, часто в контексте объектно#ориентированных подходов. Наверное, до сих пор лучшими материалами по сокрытию информации являются
три статьи, принадлежащие перу Парнаса.
Parnas, David L. «On the Criteria to Be Used in Decomposing Systems into Modules».
— Communications of the ACM 5, no. 12 (December 1972): 1053#58.
Parnas, David L. «Designing Software for Ease of Extension and Contraction». — IEEE
Transactions on Software Engineering SE%5, no. 2 (March 1979): 128#38.
Parnas, David L., Paul C. Clements, and D. M. Weiss. «The Modular Structure of Complex Systems».
— IEEE Transactions on Software Engineering SE%11, no. 3 (March 1985): 259#66.

Шаблоны проектирования
Gamma, Erich, et al. Design Patterns. — Reading, MA: Addison#Wesley, 1995. Очень полезная книга о шаблонах проектирования, написанная «Бандой четырех»1 .
Shalloway, Alan, and James R. Trott. Design Patterns Explained. — Boston, MA: Addison#
Wesley, 2002. Данная книга представляет собой несложное введение в шаблоны
проектирования.

1

«Бандой четырех (Gang of Four)» называют группу авторов, в которую входят Эрих Гамма,
Ричард Хелм, Ральф Джонсон и Джон Влиссидес. — Прим. перев.

118

ЧАСТЬ II Высококачественный код

Проектирование в общем
Adams, James L. Conceptual Blockbusting: A Guide to Better Ideas, 4th ed. — Cambridge,
MA: Perseus Publishing, 2001. Нельзя сказать, что эта книга посвящена непосредственно проектированию ПО, но это не умаляет ее достоинств: она была написана
как учебник по проектированию для студентов инженерного факультета Стэнфордского университета. Даже если вы никогда ничего не проектировали и не
проектируете, в ней вы найдете увлекательное обсуждение творческого мышления
и много упражнений, позволяющих развить мышление, для эффективного проектирования. Кроме того, данная книга включает список литературы, посвященной
проектированию и творческому мышлению, с подробными аннотациями. Если
вам нравится решать проблемы, вам понравится эта книга.
Polya, G. How to Solve It: A New Aspect of Mathematical Method, 2d ed. — Princeton,
NJ: Princeton University Press, 1957. Это обсуждение эвристики и решения проблем концентрируется на математике, но актуально и для разработки ПО. Книга
Полья стала первым трудом, посвященным применению эвристики для решения
математических проблем. В ней проводится четкое различие между небрежной
эвристикой, используемой для обнаружения решений, и более аккуратными методами, которые применяются для представления найденных решений. Читать ее
нелегко, но если вы интересуетесь эвристикой, то в итоге все равно прочитаете ее,
хотите вы того или нет. Полья ясно показывает, что решение проблем не является
детерминированным процессом и что приверженность единственной методологии аналогично ходьбе в кандалах. Когда#то в Microsoft эту книгу выдавали всем
новым программистам.
Michalewicz, Zbigniew, and David B. Fogel. How to Solve It: Modern Heuristics. — Berlin:
Springer#Verlag, 2000. Это обновленный вариант книги Полья, который содержит
некоторые нематематические примеры и менее требователен к читателю.
Simon, Herbert. The Sciences of the Artificial, 3d ed. — Cambridge, MA: MIT Press, 1996.
В этой интересной книге проводится различие между науками, имеющими дело
с естественным миром (биология, геология и т. д.), и науками, изучающими искусственный мир, созданный людьми (бизнес, архитектура и информатика). Затем в ней обсуждаются характеристики наук об искусственном, при этом особое
внимание уделяется проектированию. Книга написана в академическом стиле, и
ее следует прочитать всем, кто решил сделать карьеру в области разработки ПО
или любой другой «искусственной» области.
Glass, Robert L. Software Creativity. — Englewood Cliffs, NJ: Prentice Hall PTR, 1995. Что
в большей степени управляет процессом разработки ПО: теория или практика?
Является ли он преимущественно творческим или преимущественно детерминированным? Какие интеллектуальные качества нужны разработчику ПО? В этой книге
приведено интересное обсуждение природы разработки ПО со специфическим
акцентом на проектировании.
Petroski, Henry. Design Paradigms: Case Histories of Error and Judgment in Engineering.
— Cambridge: Cambridge University Press, 1994. Главная идея этой книги в том, что
анализ прошлых неудач способствует успешному проектированию не в меньшей,
а то и в большей степени, чем исследование прошлых успехов. В подтверждение
своей позиции автор приводит многие факты из области гражданского строительства (особенно проектирования мостов).

ГЛАВА 5 Проектирование при конструировании

119

Стандарты
IEEE Std 1016%1998, Recommended Practice for Software Design Descriptions. Данный
документ содержит стандарт IEEE#ANSI описания проектов ПО, определяющий,
что следует включать в проектную документацию.
IEEE Std 1471%2000. Recommended Practice for Architectural Description of Software Intensive
Systems. Los Alamitos, CA: IEEE Computer Society Press. Этот документ представляет
собой руководство IEEE#ANSI по созданию спецификаций архитектуры ПО.

Контрольный список: проектирование при конструировании
Методики проектирования
http://cc2e.com/0527
 Выполнили ли вы несколько итераций проектирования,
выбрав самую лучшую попытку, а не просто первую?
 Попробовали ли вы выполнить декомпозицию системы несколькими способами с целью нахождения наилучшего варианта?
 Использовали ли вы для решения проблемы и нисходящий, и восходящий
способы проектирования?
 Выполнили ли вы прототипирование сомнительных или плохо известных
частей системы, создав абсолютный минимум подлежащего выбрасыванию
кода, нужного для ответа на отдельные вопросы?
 Был ли выполнен формальный или неформальный обзор вашего проекта
другими разработчиками?
 Довели ли вы проектирование до той точки, в которой реализация проекта
кажется очевидной?
 Выполнили ли вы регистрацию проекта уместными способами, такими как
Wiki, электронная почта, плакаты, цифровые фотографии, UML, карточки
CRC или комментарии в самом коде?
Цели проектирования
 Адекватно ли проект решает проблемы, которые были определены и отложены на этапе разработки архитектуры?
 Разделен ли проект на уровни?
 Удовлетворены ли вы тем, как выполнена декомпозиция программы на подсистемы, пакеты и классы?
 Удовлетворены ли вы тем, как выполнена декомпозиция классов на методы?
 Достигнуто ли минимальное взаимодействие классов между собой?
 Спроектированы ли классы и подсистемы так, чтобы их можно было использовать в других системах?
 Будет ли программа легкой в сопровождении?
 Является ли проект полным, но минимальным? Все ли его части действительно необходимы?
 Подразумевает ли проект использование только стандартных методик?
Смогли ли вы избежать применения экзотических, трудных для понимания
элементов?
 Помогает ли проект в целом минимизировать и несущественную, и существенную сложность?

120

ЧАСТЬ II Высококачественный код

Ключевые моменты
 Главным Техническим Императивом Разработки ПО является управление слож-

ностью. Управлять сложностью будет гораздо легче, если при проектировании
вы будете стремиться к простоте.
 Есть два общих способа достижения простоты: минимизация объема существен-

ной сложности, с которой приходится иметь дело в любой конкретный момент
времени, и подавление необязательного роста несущественной сложности.
 Проектирование — эвристический процесс. Слепое следование какой#либо

единственной методологии подавляет творческое мышление и снижает качество ваших программ.
 Оптимальный процесс проектирования итеративен; чем больше вариантов про-

ектирования вы попробуете, тем удачнее будет ваш окончательный проект.
 Одной из самых полезных концепций проектирования является сокрытие

информации. Вопрос «Что мне скрыть?» устраняет много сложных проблем
проектирования.
 Много полезной и интересной информации о проектировании можно найти

в других книгах. Описанные в этой главе идеи — лишь вершина айсберга.

ГЛАВА 4 Основные решения, которые приходится принимать при конструировании

Г Л А В А

121

6

Классы

Содержание
 6.1. Основы классов: абстрактные типы данных

http://cc2e.com/0665

 6.2. Качественные интерфейсы классов
 6.3. Вопросы проектирования и реализации
 6.4. Разумные причины создания классов
 6.5. Аспекты, специфические для языков
 6.6. Следующий уровень: пакеты классов

Связанные темы
 Проектирование при конструировании: глава 5
 Архитектура ПО: раздел 3.5
 Высококачественные методы: глава 7
 Процесс программирования с псевдокодом: глава 9
 Рефакторинг: глава 24

На заре компьютерной эпохи программисты думали о программировании в тер#
минах операторов. В 1970–80#е о программах стали думать в терминах методов.
В XXI веке мы рассматриваем программирование в терминах классов.
Класс — это набор данных и методов, имеющих общую, целостную, хо#
рошо определенную сферу ответственности. Данные — необязательный
компонент класса: класс может включать только методы, предоставляю#
щие целостный набор услуг. Одним из главных условий эффективного програм#
мирования является максимизация части программы, которую можно игнориро#
вать при работе над конкретными фрагментами кода. Классы — главное средство
достижения этой цели.
Эта глава содержит экстракт советов по созданию высококачественных классов.
Если вы только знакомитесь с концепциями объектно#ориентированного програм#
мирования, она может показаться слишком сложной. Если вы не прочитали главу
5, вернитесь к ней. Затем начните с раздела 6.1, который поможет понять осталь#
ные разделы главы. Если вы уже знакомы с основами классов, можете, просмот#
рев раздел 6.1, начать серьезное чтение с раздела 6.2, в котором обсуждаются

122

ЧАСТЬ II

Высококачественный код

интерфейсы классов. В разделе «Дополнительные ресурсы» в конце главы вы най#
дете список вводных материалов по данной теме, книг более высокого уровня и
ресурсов, специфических для языков программирования.

6.1.

Основы классов: абстрактные типы данных

Абстрактный тип данных (АТД) — это набор, включающий данные и выполняе#
мые над ними операции. Операции описывают данные для остальной части про#
граммы и позволяют их изменять. Слово «данные» используется в выражении «аб#
страктный тип данных» довольно условно. АТД может быть графическое окно со
всеми влияющими на него операциями, файл с файловыми операциями, таблица
страховых тарифов с соответствующими операциями и др.
Перекрестная ссылка Размышление в первую очередь об АТД
и только во вторую о классах
является примером программирования с использованием языка в отличие от программирования на языке (см. разделы 4.3
и 34.4).

Понимание концепции АТД необходимо для понимания
объектно#ориентированного программирования. Не имея
ясного представления об АТД, программисты создают клас#
сы, которые только называются «классами», будучи на самом
деле лишь удобными контейнерами, содержащими наборы
плохо согласующихся друг с другом данных и методов. По#
нимание АТД облегчает создание классов и их изменение
с течением времени.

В книгах по программированию обсуждение АТД традиционно носит математиче#
ский характер. Довольно часто можно встретить высказывания вроде: «АТД можно
понимать как математическую модель с определенным для нее набором операций».
И создается впечатление, что АТД подойдет разве что в качестве снотворного.
Такие сухие объяснения АТД никуда не годятся. АТД удивительны тем, что позво#
ляют работать с сущностями реального мира, а не с низкоуровневыми сущностя#
ми реализации. Благодаря этому вместо вставки узла в связный список можно
добавить ячейку в электронную таблицу, новый тип окна в список типов окон или
очередной пассажирский автомобиль в программу, моделирующую поток движе#
ния. Возможность работать в проблемной области, а не в низкоуровневой облас#
ти реализации программы очень удобна. Используйте ее!

Пример необходимости АТД
Для начала приведем пример ситуации, в которой применение АТД было бы по#
лезным. После этого мы сможем углубиться в подробности.
Допустим, вы пишете программу, управляющую выводом текста на экран с исполь#
зованием разнообразных гарнитур шрифтов, их размеров и атрибутов (например,
«полужирный» и «курсив»). За работу со шрифтами отвечает конкретная часть
программы. При использовании АТД данные — названия гарнитур, размеры и
атрибуты шрифтов — будут объединены в одну группу с обрабатывающими их
методами. Набор данных и методов, служащих одной цели, — это и есть АТД.
Без АТД вам пришлось бы принять специализированный подход к работе со шриф#
тами. Скажем, для выбора шрифта размером 12 пт, которым могли бы соответ#
ствовать 16 пикселов, вы написали бы что#то вроде:

ГЛАВА 6 Классы

123

currentFont.size = 16
Создав набор библиотечных методов, код можно было бы сделать чуть понятнее:

currentFont.size = PointsToPixels( 12 )
Кроме того, атрибуту шрифта можно было бы присвоить более определенное имя,
например:

currentFont.sizeInPixels = PointsToPixels( 12 )
Однако при этом вы не смогли бы включить в программу сразу два поля, опреде#
ляющих размер шрифта: currentFont . sizeInPixels (размер шрифта в пикселах) и
currentFont . sizeInPoints (размер шрифта в пунктах), — потому что тогда структура
currentFont не смогла бы узнать, какое из них использовать. Кроме того, изменяя
размеры шрифтов в нескольких местах, вы распространили бы похожие строки
по всей программе.
Для выбора полужирного начертания вы могли бы написать код, использующий
логическое ИЛИ и шестнадцатеричную константу 0x02:

currentFont.attribute = currentFont.attribute or 0x02
Этот код можно немного улучшить, но лучшее, что вы получите, используя спе#
циализированный подход, будет похоже на:

currentFont.attribute = currentFont.attribute or BOLD
или на что#нибудь такое:

currentFont.bold = True
Как и в случае с размером шрифта, проблема здесь в том, что клиентский код
должен контролировать элементы данных непосредственно, а это ограничивает
число возможных способов применения структуры currentFont.
Такой подход к программированию способствует распространению похожих строк
кода по всей программе.

Преимущества использования АТД
Проблема не в том, что специализированный подход — плохая методика програм#
мирования. Просто вы можете заменить его на лучшую методику, преимущества
которой описаны ниже.
Возможность сокрытия деталей реализации Сокрытие информации о ти#
пах данных шрифта подразумевает, что при необходимости изменения типа дан#
ных вы сможете изменить его в одном месте, не влияя на всю программу. Напри#
мер, если вы не скроете детали реализации в АТД, то при изменении одного вида
представления полужирного шрифта на другой вам придется изменить каждый
фрагмент кода, в котором задается полужирное начертание. Сокрытие информа#
ции защитит остальную часть программы и в тех случаях, если вы решите хра#
нить данные во внешнем хранилище, а не в памяти или переписать все методы,
выполняющие операции над шрифтами, на другом языке.

124

ЧАСТЬ II

Высококачественный код

Ограничение области изменений Если вы захотите разнообразить шрифты
и реализовать для них дополнительные операции (такие как переключение на над#
строчный шрифт, перечеркивание и т. д.), вы сможете изменить один фрагмент
кода, и это не повлияет на остальную часть программы.
Более высокая информативность интерфейса Код currentFont . size = 16 не#
однозначен, так как число 16 может определять размер шрифта и в пикселах, и в
пунктах. Контекст об этом ничего не говорит. Объединение всех похожих опера#
ций в АТД позволяет определить весь интерфейс в терминах пунктов, в терминах
пикселов или четко разделить оба варианта, помогая избежать путаницы.
Легкость оптимизации кода Для повышения быстродействия операций над
шрифтами вы сможете переписать несколько четко определенных методов, а не
блуждать по всей программе.
Легкость проверки кода Нудную проверку правильности команд вида cur%
rentFont . attribute = currentFont . attribute or 0x02 вы сможете заменить более про#
стой проверкой правильности вызовов currentFont.SetBoldOn(). В первом случае мож#
но указать неверное имя структуры, неверное имя поля, неверную операцию (and
вместо or) или неверное значение атрибута (0x20 вместо 0x02). В случае вызова
currentFont . SetBoldOn() ошибкой может быть лишь указание неверного имени мето#
да, так что заметить ее легче.
Удобочитаемость и понятность кода Команду вида currentFont . attribute or
0x02 можно улучшить, заменив 0x02 на BOLD (или что там представляет константа
0x02), но даже после этого по удобочитаемости она не сравнится с вызовом ме#
тода currentFont . SetBoldOn().
Вудфилд, Дансмор и Шен провели исследование, участники которого —
аспиранты и студенты старших курсов факультета информатики — дол#
жны были ответить на вопросы о двух программах: одна была разделена
на восемь методов в функциональном стиле, а вторая — на восемь методов АТД
(Woodfield, Dunsmore, and Shen, 1981). Студенты, отвечавшие на вопросы о вто#
рой программе, получили на 30% более высокие оценки.
Ограничение области использования данных В только что представленных
примерах структуру currentFont нужно изменять непосредственно или передавать
в каждый метод, работающий со шрифтами. При использовании АТД вам не при#
шлось бы ни передавать ее в методы, ни превращать в глобальные данные. АТД
просто включал бы структуру, содержащую данные currentFont. Прямой доступ к
этим данным имели бы лишь методы из состава АТД, но не какие бы то ни было
другие методы.
Возможность работы с сущностями реального мира, а не с низкоуровне'
выми деталями реализации АТД позволяет определить операции над шриф#
тами так, что большая часть программы будет сформулирована исключительно в
терминах шрифтов, а не доступа к массивам, определений структур или значе#
ний True и False.
В нашем случае в АТД можно было бы включить методы:

currentFont.SetSizeInPoints( sizeInPoints )
currentFont.SetSizeInPixels( sizeInPixels )

ГЛАВА 6 Классы

125

currentFont.SetBoldOn()
currentFont.SetBoldOff()
currentFont.SetItalicOn()
currentFont.SetItalicOff()
currentFont.SetTypeFace( faceName )
Эти методы, вероятно, были бы короткими — пожалуй, они напоминали
бы код, приведенный при обсуждении специализированного подхода
к управлению шрифтами. Различие двух подходов в том, что, используя АТД,
вы изолируете операции над шрифтами в наборе методов, который предоставляет
остальным частям программы, работающим с шрифтами, улучшенный уровень аб#
стракции и защищает остальной код от изменений операций над шрифтами.

Другие примеры АТД
Допустим, вы разрабатываете приложение, управляющее системой охлаждения
ядерного реактора. С системой охлаждения можно работать как с АТД, опреде#
лив для нее такие операции:

coolingSystem.GetTemperature()
coolingSystem.SetCirculationRate( rate )
coolingSystem.OpenValve( valveNumber )
coolingSystem.CloseValve( valveNumber )
Конкретная реализация данных операций зависела бы от конкретной среды. Ос#
тальные фрагменты программы взаимодействовали бы с системой охлаждения при
помощи этих методов и могли бы не беспокоиться о внутренних деталях реали#
зации структур данных, их ограничениях, изменениях и т. д.
Вот дополнительные примеры абстрактных типов данных и операций, которые
можно было бы для них определить:
Система регулирования
скорости

Кофемолка

Топливный бак

Задать скорость

Включить

Заполнить бак

Получить текущие параметры

Выключить

Слить топливо

Восстановить предыдущее
значение скорости

Задать скорость

Получить емкость топ#
ливного бака

Отключить систему

Начать перемалывание
кофе

Получить статус топлив
ного бака

Прекратить
перемалывание кофе
Список

Стек

Инициализировать список

Фонарь

Инициализировать стек

Вставить элемент

Включить

Поместить элемент в стек

Удалить элемент

Выключить

Извлечь элемент из стека

Прочитать следующий элемент

Прочитать верхний эле#
мент стека

(см. след. стр.)

126

ЧАСТЬ II

Высококачественный код

Система справочной
информации

Меню

Файл

Добавить раздел

Создать новое меню

Открыть файл

Удалить раздел

Уничтожить меню

Прочитать файл

Задать текущий раздел

Добавить в меню
новый элемент

Записать файл

Отобразить окно
справочной системы

Удалить элемент меню

Установить указатель
файла

Уничтожить окно
справочной системы

Активировать элемент
меню

Закрыть файл

Отобразить указатель
информационных разделов

Деактивировать элемент
меню

Вернуться к предыдущему
разделу

Отобразить меню

Лифт

Скрыть меню

Переместиться на один
этаж вверх

Получить индекс
выбранного элемента
меню

Переместиться на один
этаж вниз

Указатель

Выделить блок памяти

Переместиться на кон#
кретный этаж

Освободить блок памяти

Сообщить текущий номер
этажа

Изменить объем
выделенной памяти

Вернуться на первый этаж

Изучение этих примеров позволяет вывести принципы использования АТД, ко#
торые мы и обсудим.
Представляйте в форме АТД распространенные низкоуровневые типы
данных Обычно при обсуждении АТД речь идет о представлении в форме АТД
популярных низкоуровневых типов данных. Как вы могли заметить по примерам,
в форме АТД можно представить стек, список, очередь и почти любой другой по#
пулярный тип данных.
Спросите себя: «Что представляет этот стек, список или эта очередь?» Если стек
представляет набор сотрудников, рассматривайте АТД как набор сотрудников, а
не как стек. Если список соответствует набору счетов, рассматривайте его как набор
счетов. Если очередь представляет ячейки электронной таблицы, обращайтесь с
ней как с набором ячеек, а не обобщенных элементов. Используйте как можно более
высокий уровень абстракции.
Представляйте в форме АТД часто используемые объекты, такие как
файлы Большинство языков включает несколько АТД, которые известны всем
программистам, но не всегда воспринимаются как АТД. В качестве примера при#
веду операции над файлами. При записи данных на диск ОС избавляет вас от за#
бот, связанных с позиционированием головки чтения/записи, выделением новых
секторов на диске при заполнении старых и интерпретацией непонятных кодов

ГЛАВА 6 Классы

127

ошибок. ОС предоставляет первый уровень абстракции и соответствующие ему
АТД. Высокоуровневые языки предоставляют второй уровень абстракции и АТД
для этого уровня. Высокоуровневые языки скрывают от вас детали генерации
вызовов ОС и работы с буферами данных. Они позволяют рассматривать область
диска как «файл».
АТД можно разделить на уровни аналогичным образом. Хотите использовать АТД
на уровне операций со структурами данных (таких как помещение элементов в
стек и их извлечение) — прекрасно, но поверх него можно создать и другой уро#
вень, соответствующий проблеме реального мира.
Представляйте в форме АТД даже простые элементы Для оправдания ис#
пользования АТД не обязательно иметь гигантский тип данных. Одним из АТД в
нашем списке примеров был фонарь, поддерживающий только две операции:
включение и выключение. Вам может показаться, что создавать для операций «вклю#
чить» и «выключить» отдельные методы слишком расточительно, однако на самом
деле АТД выгодно использовать даже в случае самых простых операций. Представ#
ление фонаря и его операций в форме АТД облегчает понимание и изменение кода,
ограничивает потенциальные следствия изменений методов TurnLightOn() и Turn%
LightOff() и снижает число элементов данных, которые нужно передавать в методы.
Обращайтесь к АТД так, чтобы это не зависело от среды, используемой
для его хранения Допустим, ваша таблица страховых тарифов настолько вели#
ка, что ее нужно всегда хранить на диске. Вы могли бы представить ее как «файл
тарифов (rate file)» и создать такие методы доступа, как RateFile . Read(). Однако,
ссылаясь на таблицу как на файл, вы сообщаете о ней больше информации, чем
следовало бы. Если вы когда#нибудь измените программу так, чтобы таблица хра#
нилась в памяти, а не на диске, код, обращающийся к ней как к файлу, станет не#
корректным и начнет вызывать замешательство. Поэтому старайтесь присваивать
классам и методам доступа имена, не зависящие от способа хранения данных, и
обращайтесь не к конкретным сущностям, а к АТД, таким как таблица страховых
тарифов. В нашем случае класс и метод доступа следовало бы назвать rateTable. Re%
ad() или просто rates . Read().

Работа с несколькими экземплярами данных
при использовании АТД в средах, не являющихся
объектно-ориентированными
Объектно#ориентированные языки автоматически поддерживают работу с несколь#
кими экземплярами АТД. Если вы использовали исключительно объектно#ориен#
тированные среды и вам не приходилось реализовывать поддержку работы с не#
сколькими экземплярами данных, можете положиться на свою удачу! (И перейти
к следующему разделу — «АТД и классы»).
Если вы программируете на C или другом языке, не являющемся объектно#ори#
ентированным, поддержку работы с несколькими экземплярами данных нужно
реализовать вручную. В целом это значит, что вы должны создать для АТД серви#
сы создания и уничтожения экземпляров данных и спроектировать другие сер#
висы АТД так, чтобы они могли работать с несколькими экземплярами.

128

ЧАСТЬ II

Высококачественный код

АТД «шрифт» изначально предлагал такие сервисы:

currentFont.SetSize( sizeInPoints )
currentFont.SetBoldOn()
currentFont.SetBoldOff()
currentFont.SetItalicOn()
currentFont.SetItalicOff()
currentFont.SetTypeFace( faceName )
В среде, не являющейся объектно#ориентированной, эти методы не были бы свя#
заны с классом и выглядели бы так:

SetCurrentFontSize( sizeInPoints )
SetCurrentFontBoldOn()
SetCurrentFontBoldOff()
SetCurrentFontItalicOn()
SetCurrentFontItalicOff()
SetCurrentFontTypeFace( faceName )
Если бы вы хотели работать с несколькими шрифтами одновременно, то должны
были бы создать сервисы создания и удаления экземпляров шрифтов вроде этих:

CreateFont( fontId )
DeleteFont( fontId )
SetCurrentFont( fontId )
Идентификатор шрифта fontId позволяет следить за несколькими шрифтами по
мере их создания и использования. Что касается других операций, то в этом слу#
чае вы можете выбирать один из трех вариантов реализации интерфейса АТД.
 Вариант 1: явно указывать экземпляр данных при каждом обращении к серви#

сам АТД. В этом случае «текущий шрифт (current font)» не требуется. В каждый
метод, работающий со шрифтами, вы передаете fontId. Методы АТД Font сле#
дят за всеми данными шрифта, а клиентский код — лишь за идентификатором
fontId. Этот вариант требует, чтобы каждый метод, работающий со шрифтами,
принимал дополнительный параметр fontId.
 Вариант 2: явно предоставлять данные, используемые сервисами АТД. В дан#

ном случае вы объявляете нужные АТД данные в каждом методе, использую#
щем сервис АТД. Иначе говоря, вы создаете тип данных Font, который переда#
ете в каждый из сервисных методов АТД. Вы должны спроектировать сервис#
ные методы АТД так, чтобы они использовали данные Font, передаваемые в них
при каждом вызове. При этом клиентский код не нуждается в идентификато#
ре шрифта, потому что он следит за данными шрифтов сам. (Хотя данные типа
Font доступны напрямую, к ним надо обращаться только через сервисные ме#
тоды АТД. Это называется поддержанием структуры «в закрытом виде».)
Преимущество этого подхода в том, что сервисным методам АТД не приходится
просматривать информацию о шрифте, опираясь на его идентификатор. Есть и
недостаток: такой способ предоставляет доступ к данным шрифта остальным ча#
стям программы, из#за чего повышается вероятность того, что клиентский код будет
использовать детали реализации АТД, которым следовало бы оставаться скрыты#
ми внутри АТД.

ГЛАВА 6 Классы

129

 Вариант 3: использовать неявные экземпляры (с большой осторожностью). Вы

должны создать новый сервис — скажем, SetCurrentFont ( fontId ), — при вызо#
ве которого заданный экземпляр шрифта делается текущим. После этого все
остальные сервисы используют текущий шрифт, благодаря чему в них не нуж#
но передавать параметр fontId. При разработке простых приложений такой под#
ход может облегчить использование нескольких экземпляров данных. В слож#
ных приложениях подобная зависимость от состояния в масштабе всей сис#
темы подразумевает, что вы должны следить за текущим экземпляром шрифта
во всем коде, вызывающем методы Font; разумеется, сложность программы при
этом повышается. Каким бы ни был размер приложения, всегда можно найти
более удачные альтернативы данному подходу.
Внутри АТД вы можете реализовать работу с несколькими экземплярами данных
как угодно, но вне его при использовании языка, не являющегося объектно#ори#
ентированным, возможны только три указанных варианта.

АТД и классы
Абстрактные типы данных лежат в основе концепции классов. В языках, поддержи#
вающих классы, каждый АТД можно реализовать как отдельный класс. Однако обыч#
но с классами связывают еще две концепции: наследование и полиморфизм. Може#
те рассматривать класс как АТД, поддерживающий наследование и полиморфизм.

6.2.

Качественные интерфейсы классов

Первый и, наверное, самый важный этап разработки высококачественного клас#
са — создание адекватногоинтерфейса. Это подразумевает, что интерфейс дол#
жен представлять хорошую абстракцию, скрывающую детали реализации класса.

Хорошая абстракция
Как я говорил в подразделе «Определите согласованные абстракции» раздела 5.3,
под абстракцией понимается представление сложной операции в упрощенной
форме. Интерфейс класса — это абстракция реализации класса, скрытой за ин#
терфейсом. Интерфейс класса должен предоставлять группу методов, четко согла#
сующихся друг с другом.
Рассмотрим для примера класс «сотрудник». Он может содержать такие данные,
как фамилия сотрудника, адрес, номер телефона и т. д., и предлагать методы ини#
циализации и использования этих данных. Вот как мог бы выглядеть такой класс:

Пример интерфейса, формирующего хорошую абстракцию (C++)
class Employee {
public:
// открытые конструкторы и деструкторы
Employee();
Employee(
FullName name,
String address,
String workPhone,

Перекрестная ссылка Примеры
кода в этой книге отформатированы с использованием конвенции, поддерживающей сходство стилей между несколькими языками. Об этой конвенции
(и разных стилях кодирования)
см. подраздел «Программирование с использованием нескольких языков» раздела 11.4.

130

ЧАСТЬ II

Высококачественный код

String homePhone,
TaxId taxIdNumber,
JobClassification jobClass
);
virtual ~Employee();
// открытые методы
FullName GetName() const;
String GetAddress() const;
String GetWorkPhone() const;
String GetHomePhone() const;
TaxId GetTaxIdNumber() const;
JobClassification GetJobClassification() const;
...
private:
...
};
Внутри этот класс может иметь дополнительные методы и данные, поддержива#
ющие работу этих сервисов, но пользователям класса знать о них не нужно. Пред#
ставляемая интерфейсом этого класса абстракция великолепна, потому что все
методы интерфейса служат единой согласованной цели.
Интерфейс, представляющий плохую абстракцию, содержал бы набор разнород#
ных методов, например:

Пример интерфейса, формирующего
плохую абстракцию (C++)
class Program {
public:
...
// открытые методы
void InitializeCommandStack();
void PushCommand( Command command );
Command PopCommand();
void ShutdownCommandStack();
void InitializeReportFormatting();
void FormatReport( Report report );
void PrintReport( Report report );
void InitializeGlobalData();
void ShutdownGlobalData();
...
private:
...
};
Похоже, этот класс содержит методы работы со стеком команд, форматирования
отчетов, печати отчетов и инициализации глобальных данных. Трудно увидеть связь
между стеком команд, обработкой отчетов и глобальными данными. Интерфейс
такого класса не формирует согласованную абстракцию, и класс обладает плохой

ГЛАВА 6 Классы

131

связностью. В данном случае методы следует реорганизовать в более четкие классы,
интерфейсы которых будут представлять более удачные абстракции.
Если бы эти методы были частью класса Program, для формирования согласован#
ной абстракции их можно было бы изменить так:

Пример интерфейса, формирующего более удачную абстракцию (C++)
class Program {
public:
...
// открытые методы
void InitializeUserInterface();
void ShutDownUserInterface();
void InitializeReports();
void ShutDownReports();
...
private:
...
};
В ходе очистки интерфейса одни его методы были перемещены в более подходя#
щие классы, а другие были преобразованы в закрытые методы, используемые
методом InitializeUserInterface() и другими методами.
Данный способ оценки абстракции класса основан на изучении открытых методов
класса, т. е. его интерфейса. Однако из того, что класс в целом формирует хорошую
абстракцию, вовсе не следует, что его отдельные методы также представляют удач#
ные абстракции. Рекомендации по проектированию методов см. в разделе 7.2.
Чтобы ваши классы имели высококачественные абстрактные интерфейсы, соблю#
дайте при их проектировании следующие принципы.
Выражайте в интерфейсе класса согласованный уровень абстракции
Классы полезно рассматривать как механизмы реализации абстрактных типов дан#
ных, описанных в разделе 6.1. В идеале каждый класс должен быть реализацией
только одного АТД. Если класс реализует более одного АТД или если вам не уда#
ется определить, реализацией какого АТД класс является, самое время реоргани#
зовать класс в один или несколько хорошо определенных АТД.
Так, следующий класс имеет несогласованный интерфейс, потому что формируе#
мый им уровень абстракции непостоянен:

Пример интерфейса, включающего разные
уровни абстракции (C++)
class EmployeeCensus: public ListContainer {
public:
...
// открытые методы

132

ЧАСТЬ II

Высококачественный код

Абстракция, формируемая этими методами, относится к уровню «employee» (сотрудник).

>

void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee );
Абстракция, формируемая этими методами, относится к уровню «list» (список).

>

Employee NextItemInList();
Employee FirstItem();
Employee LastItem();
...
private:
...
};
Этот класс представляет два АТД: Employee и ListContainer (список#контейнер).
Подобные смешанные абстракции часто возникают, когда программист реализу#
ет класс при помощи класса#контейнера или других библиотечных классов и не
скрывает этот факт. Спросите себя, должна ли информация об использовании
класса#контейнера быть частью абстракции. Обычно это является деталью реали#
зации, которую следует скрыть от остальных частей программы, например так:

Пример интерфейса, формирующего согласованную абстракцию (C++)
class EmployeeCensus {
public:
...
// открытые методы
Абстракция, формируемая всеми этими методами, теперь относится к уровню «employee».

>

void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee );
Employee NextEmployee();
Employee FirstEmployee();
Employee LastEmployee();
...
private:
Тот факт, что класс использует библиотеку ListContainer, теперь скрыт.

>

ListContainer m_EmployeeList;
...

};
Программисты могут утверждать, что наследование от ListContainer удобно, потому
что оно поддерживает полиморфизм, позволяя создать внешний метод поиска или
сортировки, принимающий объект ListContainer. Но этот аргумент не проходит
главный тест на уместность наследования: «Используется ли наследование толь#
ко для моделирования отношения „является“?» Наследование класса EmployeeCensus
(каталог личных дел сотрудников) от класса ListContainer означало бы, что Employee%
Census «является» ListContainer, что, очевидно, неверно. Если абстракция объекта
EmployeeCensus заключается в том, что он поддерживает поиск или сортировку,

ГЛАВА 6 Классы

133

эти возможности должны быть явными согласованными частями интерфейса
класса.
Если представить открытые методы класса как люк, предотвращающий попадание
воды в подводную лодку, несогласованные открытые методы — это щели. Вода не
будет протекать через них так быстро, как через открытый люк, но позже лодка все
же потонет. На практике при смешении уровней абстракции именно это и проис#
ходит. По мере изменений программы смешанные уровни абстракции делают ее
все менее и менее понятной, пока в итоге код не станет совсем загадочным.
Убедитесь, что вы понимаете, реализацией какой абстракции
является класс Некоторые классы очень похожи, поэтому при разра#
ботке класса нужно понимать, какую абстракцию должен представлять
его интерфейс. Однажды я работал над программой, которая должна была под#
держивать редактирование информации в табличном формате. Сначала мы хо#
тели использовать простой элемент управления «grid» (сетка), но доступные эле#
менты управления этого типа не позволяли закрашивать ячейки ввода данных в
другой цвет, поэтому мы выбрали элемент управления «spreadsheet» (электронная
таблица), который такую возможность поддерживал.
Элемент управления «электронная таблица» был гораздо сложнее «сетки» и пре#
доставлял около 150 методов в сравнении с 15 методами «сетки». Так как наша цель
заключалась в использовании «сетки», а не «электронной таблицы», мы поручили
одному программисту написать класс#оболочку, который скрывал бы тот факт, что
мы подменили один элемент управления другим. Он поворчал по поводу ненуж#
ных затрат и бюрократии, ушел и вернулся через пару дней с классом#оболочкой,
который честно предоставлял все 150 методов «электронной таблицы».
Но нам было нужно не это — нам требовался интерфейс «сетки», инкапсулирую#
щий тот факт, что за кулисами мы использовали гораздо более сложную «элект#
ронную таблицу». Программисту следовало предоставить доступ только к 15 ме#
тодам «сетки» и еще одному, шестнадцатому методу, поддерживающему закраши#
вание ячеек. Открыв доступ ко всем 150 методам, программист подверг нас риску
того, что после нескольких изменений реализации класса нам в итоге придется
поддерживать все 150 открытых методов. Он не смог обеспечить нужную нам
инкапсуляцию и проделал гораздо больше работы, чем стоило.
В зависимости от конкретных обстоятельств оптимальной абстракцией может
оказаться как «сетка», так и «электронная таблица». Если приходится выбирать между
двумя похожими абстракциями, убедитесь, что выбор правилен.
Предоставляйте методы вместе с противоположными им методами
Большинство операций имеет соответствующие противоположные операции. Если
одна из операций включает свет, вам, вероятно, понадобится и операция, его вы#
ключающая. Если одна операция добавляет элемент в список, элементы скорее всего
нужно будет и удалять. Если одна операция активизирует элемент меню, вторая,
наверное, должна будет его деактивизировать. При проектировании класса про#
верьте каждый открытый метод на предмет того, требуется ли вам его противо#
положность. Создавать противоположные методы, не имея на то причин, не сле#
дует, но проверить их целесообразность нужно.

134

ЧАСТЬ II

Высококачественный код

Убирайте постороннюю информацию в другие классы Иногда вы будете
обнаруживать, что одни методы класса работают с одной половиной данных, а
другие — с другой. Это значит, что вы имеете дело с двумя классами, скрывающи#
мися под маской одного. Разделите их!
По мере возможности делайте интерфейсы программными, а не семан'
тическими Каждый интерфейс состоит из программной и семантической ча#
стей. Первая включает типы данных и другие атрибуты интерфейса, которые могут
быть проверены компилятором. Вторая складывается из предположений об ис#
пользовании интерфейса, которые компилятор проверить не может. Семантический
интерфейс может включать такие соображения, как «Метод А должен быть выз#
ван перед Методом B» или «Метод А вызовет ошибку, если переданный в него Эле#
мент Данных 1 не будет перед этим инициализирован». Семантический интерфейс
следует документировать в комментариях, но вообще интерфейсы должны как
можно меньше зависеть от документации. Любой аспект интерфейса, который не
может быть проверен компилятором, является потенциальным источником оши#
бок. Старайтесь преобразовывать семантические элементы интерфейса в программ#
ные, используя утверждения (assertions) или иными способами.
Опасайтесь нарушения целостности интерфейса при
изменении класса При модификации и расширении клас#
са часто обнаруживается дополнительная нужная функци#
ональность, которая не совсем хорошо соответствует интер#
фейсу первоначального класса, но плохо поддается реализации иным образом. Так,
класс Employee может превратиться во что#нибудь вроде:

Перекрестная ссылка О поддержании качества кода при его
изменении см. главу 24.

Пример интерфейса, изуродованного при сопровождении
программы (C++)
class Employee {
public:
...
// открытые методы
FullName GetName() const;
Address GetAddress() const;
PhoneNumber GetWorkPhone() const;
...
bool IsJobClassificationValid( JobClassification jobClass );
bool IsZipCodeValid( Address address );
bool IsPhoneNumberValid( PhoneNumber phoneNumber );
SqlQuery GetQueryToCreateNewEmployee() const;
SqlQuery GetQueryToModifyEmployee() const;
SqlQuery GetQueryToRetrieveEmployee() const;
...
private:
...
};
То, что начиналось как ясная абстракция, превратилось в смесь почти несогласо#
ванных методов. Между сотрудниками и методами, проверяющими корректность

ГЛАВА 6 Классы

135

почтового индекса, номера телефона или ставки зарплаты (job classification), нет
логической связи. Методы, предоставляющие доступ к деталям SQL#запросов, от#
носятся к гораздо более низкому уровню абстракции, чем класс Employee, нару#
шая общую абстракцию класса.
Не включайте в класс открытые члены, плохо согласующиеся с абстрак'
цией интерфейса Добавляя новый метод в интерфейс класса, всегда спраши#
вайте себя: «Согласуется ли этот метод с абстракцией, формируемой существую#
щим интерфейсом?» Если нет, найдите другой способ внесения изменения, позво#
ляющий сохранить согласованность абстракции.
Рассматривайте абстракцию и связность вместе Понятия абстракции и
связности (cohesion) тесно связаны: интерфейс класса, представляющий хорошую
абстракцию, обычно отличается высокой связностью. И наоборот: классы, имею#
щие высокую связность, обычно представляют хорошие абстракции, хотя эта связь
выражена слабее.
Я обнаружил, что при повышенном внимании к абстракции, формируемой ин#
терфейсом класса, проект класса получается более удачным, чем при концентра#
ции на связности класса. Если вы видите, что класс имеет низкую связность и не
знаете, как это исправить, спросите себя, представляет ли он согласованную аб#
стракцию.

Хорошая инкапсуляция
Как я уже говорил в разделе 5.3, инкапсуляция является бо#
лее строгой концепцией, чем абстракция. Абстракция по#
могает управлять сложностью, предоставляя модели, позво#
ляющие игнорировать детали реализации. Инкапсуляция не
позволяет узнать детали реализации, даже если вы этого
захотите.

Перекрестная ссылка Об инкапсуляции см. подраздел «Инкапсулируйте детали реализации»
раздела 5.3.

Две этих концепции связаны: без инкапсуляции абстракция обычно разрушается.
По своему опыту могу сказать, что вы или имеете и абстракцию, и инкапсуляцию,
или не имеете ни того, ни другого. Промежуточных вариантов нет.
Минимизируйте доступность классов и их членов Ми#
Самым важным отличием хоронимизация доступности — одно из нескольких правил, под#
шо спроектированного модуля
держивающих инкапсуляцию. Если вы не можете понять,
от плохо спроектированного является степень, в которой мокаким делать конкретный метод: открытым, закрытым или
дуль скрывает свои внутренние
защищенным, — некоторые авторы советуют выбирать са#
данные и другие детали реалимый строгий уровень защиты, который работает (Meyers,
зации от других модулей.
1998; Bloch, 2001). По#моему, это прекрасное правило, но
Джошуа Блох (Joshua Bloch)
мне кажется, что еще важнее спросить себя: «Какой вари#
ант лучше всего сохраняет целостность абстракции интер#
фейса?» Если предоставление доступа к методу согласуется с абстракцией, сделайте
его открытым. Если вы не уверены, скрыть больше обычно предпочтительнее, чем
скрыть меньше.
Не делайте данные'члены открытыми Предоставление доступа к данным#
членам нарушает инкапсуляцию и ограничивает контроль над абстракцией. Как

136

ЧАСТЬ II

Высококачественный код

указывает Артур Риэль, класс Point (точка), который предоставляет доступ к дан#
ным:

float x;
float y;
float z;
нарушает инкапсуляцию, потому что клиентский код может свободно делать с
данными Point что угодно, при этом сам класс может даже не узнать об их изме#
нении (Riel, 1996). В то же время класс Point, включающий члены:

float GetX();
float GetY();
float GetZ();
void SetX( float x );
void SetY( float y );
void SetZ( float z );
поддерживает прекрасную инкапсуляцию. Вы не имеете понятия о том, реализо#
ваны ли данные как float x, y и z, хранит ли класс Point эти элементы как double,
преобразуя их в float, или же он хранит их на Луне и получает через спутник.
Не включайте в интерфейс класса закрытые детали реализации Истинная
инкапсуляция не позволяла бы узнать детали реализации вообще. Они были бы
скрыты и в прямом, и в переносном смыслах. Однако популярные языки — в том
числе C++ — требуют, чтобы программисты раскрывали детали реализации в
интерфейсе класса, например:

Пример обнародования деталей реализации класса (C++)
class Employee {
public:
...
Employee(
FullName name,
String address,
String workPhone,
String homePhone,
TaxId taxIdNumber,
JobClassification jobClass
);
...
FullName GetName() const;
String GetAddress() const;
...
private:
Обнародованные детали реализации.

>

String m_Name;
String m_Address;
int m_jobClass;
...
};

ГЛАВА 6 Классы

137

Включение объявлений закрытых членов в заголовочный файл класса может по#
казаться не таким уж и серьезным нарушением, но оно поощряет других програм#
мистов изучать детали реализации. В нашем случае предполагается, что исполь#
зовать адреса в клиентском коде нужно как типы Address, однако, заглянув в заго#
ловочный файл, можно узнать, что адреса хранятся как типы String.
Общий способ решения этой проблемы описал Скотт Мейерс в разделе 34 книги
«Effective C++, 2d ed» (Meyers, 1998). Отделите интерфейс класса от его реализа#
ции, после чего включите в объявление класса указатель на его реализацию, но
не включайте других деталей реализации.

Пример сокрытия деталей реализации класса (C++)
class Employee {
public:
...
Employee( ... );
...
FullName GetName() const;
String GetAddress() const;
...
private:
Детали реализации скрыты при помощи указателя.

>

EmployeeImplementation *m_implementation;

};
Теперь вы можете поместить детали реализации в класс EmployeeImplementation,
который будет доступен только классу Employee, но не использующему этот класс
коду.
Если вы уже написали много кода, не используя этой методики, то можете найти
преобразование кода неоправданным. Что ж, в этом случае, читая код, раскры#
вающий детали реализации, постарайтесь хотя бы сопротивляться соблазну изу#
чить закрытые разделы интерфейсов классов.
Не делайте предположений о клиентах класса Класс следует спроектиро#
вать и реализовать так, чтобы он придерживался контракта, сформулированного
посредством интерфейса. Выразив свои требования в интерфейсе, класс не дол#
жен делать предположений о том, как этот интерфейс будет или не будет исполь#
зоваться. Подобные комментарии указывают на то, что класс требует от своих
клиентов больше, чем следует:

 инициализируйте x, y и z значением 1.0, потому что
 при инициализации значением 0.0 DerivedClass не работает
Избегайте использования дружественных классов Иногда — например, при
реализации шаблона Состояние (State) — дисциплинированное использование
дружественных классов помогает управлять сложностью (Gamma et al., 1995).
Однако обычно дружественные классы нарушают инкапсуляцию. Они увеличивают
объем кода, о котором приходится думать в каждый конкретный момент време#
ни, повышая тем самым сложность программы.

138

ЧАСТЬ II

Высококачественный код

Не делайте метод открытым лишь потому, что он использует только
открытые методы То, что метод использует только открытые методы, не иг#
рает особой роли. Лучше спросите себя, согласуется ли предоставление доступа
к данному методу с абстракцией, формируемой интерфейсом.
Цените легкость чтения кода выше, чем удобство его написания Даже
во время первоначальной разработки программы код приходится читать гораздо
чаще, чем писать. Выгода от подхода, повышающего удобство написания кода за
счет легкости его чтения, обманчива. При разработке интерфейсов классов это
справедливо вдвойне. Даже если метод плохо согласуется с абстракцией интер#
фейса, иногда так и тянет включить его в интерфейс, чтобы облегчить работу над
конкретным клиентом класса. Однако это первый шаг к беде, и о нем лучше даже
не помышлять.
Очень, очень настороженно относитесь к семанти'
ческим нарушениям инкапсуляции Когда#то мне каза#
лось, что, научившись избегать синтаксических ошибок, я
обрету покой. Но вскоре я обнаружил, что это просто от#
Ф. Дж. Плоджер
крыло передо мной дверь в мир совершенно новых оши#
(P. J. Plauger)
бок, большинство которых диагностировать и исправлять
сложнее, чем синтаксические.

Если для понимания того, что
происходит, нужно увидеть реализацию, это не абстракция.

Аналогичные отношения имеют место между синтаксической и семантической
инкапсуляцией. С точки зрения синтаксиса, не совать нос во внутренние дела
другого класса относительно легко: достаточно просто объявить его внутренние
методы и данные закрытыми. Достичь семантической инкапсуляции гораздо слож#
нее. Вот несколько примеров того, как вы можете нарушить инкапсуляцию семан#
тически. Вы можете:
 решить не вызывать метод InitializeOperations() Класса A, потому что метод

PerformFirstOperation() Класса A вызывает его автоматически;
 не вызвать метод database.Connect() перед вызовом метода employee.Retrieve(

database ), потому что знаете, что при отсутствии соединения с БД метод
employee.Retrieve() его установит;
 не вызвать метод Terminate() Класса A, так как знаете, что метод PerformFinal%

Operation() Класса A уже вызвал его;
 использовать указатель или ссылку на Объект B, созданный Объектом A, даже

после выхода Объекта A из области видимости, потому что знаете, что Объект
A хранит Объект B в статическом хранилище, вследствие чего Объект B все
еще будет корректным;
 использовать константу MAXIMUM_ELEMENTS Класса B вместо константы MAXI%

MUM_ELEMENTS Класса A, потому что знаете, что они имеют одинаковые зна#
чения.
Со всеми этими примерами связана одна и та же проблема: зависимость
клиентского кода от закрытой реализации класса, а не от его открытого
интерфейса. Каждый раз, когда вы смотрите на реализацию класса, что#
бы узнать, как его использовать, вы программируете не в соответствии с интер#
фейсом, а сквозь интерфейс в соответствии с реализацией. Программирование

ГЛАВА 6 Классы

139

сквозь интерфейс разрушает инкапсуляцию, а вскоре к ней присоединяется и
абстракция.
Если исключительно по документации интерфейса разобраться с использовани#
ем класса не удается, изучение реализации класса по исходному коду не будет
грамотным решением. Это хорошая инициатива, но плохое решение. Вы посту#
пите правильно, если свяжетесь с автором класса и скажете ему: «Я не могу по#
нять, как использовать этот класс». Автор класса поступит правильно, если не от#
ветит вам, а изучит файл интерфейса, изменит соответствующую документацию,
зарегистрирует файл в общих исходных кодах проекта и скажет: «Посмотрите,
поймете ли вы работу класса сейчас». Желательно, чтобы этот диалог происхо#
дил в самом коде интерфейса: так он будет сохранен для будущих программис#
тов. Если диалог будет происходить исключительно в вашем уме, это внесет тон#
кие семантические зависимости в код клиентов класса. Если же он будет межлич#
ностным, выгоду сможете извлечь только вы, и больше никто — это некрасиво.
Остерегайтесь слишком жесткого сопряжения «Сопряжение» (coupling)
характеризует силу связи между двумя классами. Как правило, чем сопряжение
слабее, тем лучше. Из этого можно вывести несколько общих правил:
 минимизируйте доступность классов и их членов;
 избегайте дружественных классов, потому что они связаны жестко;
 делайте данные базового класса закрытыми, а не защищенными: это ослабля#

ет сопряжение производных классов с базовым;
 не включайте данные#члены в открытый интерфейс класса;
 остерегайтесь семантических нарушений инкапсуляции;
 соблюдайте «Правило Деметры» (см. раздел 6.3).

Сопряжение идет рука об руку с абстракцией и инкапсуляцией. Жесткое сопря#
жение наблюдается при неудачной абстракции или нарушениях инкапсуляции. Если
класс предлагает неполный набор услуг, другие методы могут попытаться прочи#
тать или записать его данные непосредственно. Это открывает класс, превращая
его из черного ящика в прозрачный, и практически устраняет инкапсуляцию.

6.3.

Вопросы проектирования и реализации

Для создания высококачественной программы недостаточно определить удачные
интерфейсы классов — не менее важно грамотно спроектировать и реализовать
внутреннее устройство классов. В этом разделе мы обсудим вопросы, связанные
с включением, наследованием, методами/данными#членами, сопряжением клас#
сов, конструкторами, а также объектами#значениями и объектами#ссылками.

Включение (отношение «содержит»)
Сущность включения (containment) проста: один класс содержит прими#
тивный элемент данных или другой класс. Наследованию в литературе
уделяют гораздо больше внимания, но это объясняется его сложностью
и подверженностью ошибкам, а не тем, что оно лучше включения. Включение —
один из главных инструментов объектно#ориентированного программирования.

140

ЧАСТЬ II

Высококачественный код

Реализуйте с помощью включения отношение «содержит» Включение мож#
но рассматривать как отношение «содержит». Например, объект «сотрудник» мо#
жет «содержать» фамилию, номер телефона, идентификационный номер налого#
плательщика и т. д. Это отношение можно реализовать, сделав фамилию, номер
телефона и номер налогоплательщика данными#членами класса Employee.
В самом крайнем случае реализуйте отношение «содержит» при помощи
закрытого наследования Иногда включение не получается реализовать, делая
один объект членом другого. Некоторые эксперты советуют при этом выполнять
закрытое наследование класса#контейнера от класса, который должен в нем со#
держаться (Meyers, 1998; Sutter, 2000). Главным мотивом такого решения является
предоставление классу#контейнеру доступа к защищенным методам/данным#чле#
нам содержащегося в нем класса. На практике этот подход устанавливает слиш#
ком близкие отношения между дочерним и родительским классом, нарушая ин#
капсуляцию. Обычно это указывает на ошибки проектирования, которые следует
решить иначе, не прибегая к закрытому наследованию.
Настороженно относитесь к классам, содержащим более семи элементов
данных'членов При выполнении других заданий человек может удерживать в
памяти 7±2 дискретных элементов (Miller, 1956). Если класс содержит более семи
элементов данных#членов, подумайте, не разделить ли его на несколько менее
крупных классов (Riel, 1996). Можете ориентироваться на верхнюю границу диа#
пазона «7±2», если данные#члены являются примитивными типами, такими как
целые числа и строки, и на нижнюю, если они являются сложными объектами.

Наследование (отношение «является»)
Наследование подразумевает, что один класс является более специализированным
вариантом другого класса. Цель наследования — создать более простой код, что
достигается путем определения базового класса, идентифицирующего общие эле#
менты двух или более производных классов. Общими элементами могут быть
интерфейсы методов, их реализация, данные#члены или типы данных. Наследо#
вание помогает избегать повторения кода и данных в нескольких местах, цент#
рализуя их в базовом классе.
Планируя использовать наследование, вы должны принять несколько решений.
 Будет ли конкретный метод#член доступен производным классам? Будет ли он

иметь реализацию по умолчанию? Можно ли будет переопределить его реа#
лизацию по умолчанию?
 Будут ли конкретные данные#члены (в том числе переменные, именованные

константы, перечисления и т. д.) доступны производным классам?
Ниже аспекты этих решений обсуждаются подробнее.
Самое важное правило объектно-ориентированного программирования на C++ таково: открытое наследование означает
«является». Запомните это.
Скотт Мейерс
(Scott Meyers)

Реализуйте при помощи открытого наследования
отношение «является» Если программист решает создать
новый класс путем наследования его от существующего
класса, он по сути говорит, что новый класс «является» бо#
лее специализированной версией существующего класса.

ГЛАВА 6 Классы

141

Базовый класс формулирует ожидания и ограничения, которым должен будет
соответствовать производный класс (Meyers, 1998).
Если производный класс не собирается полностью придерживаться контракта,
определенного интерфейсом базового класса, наследование выполнять не стоит.
Попробуйте вместо этого применить включение или внести изменение на более
высоком уровне иерархии наследования.
Проектируйте и документируйте классы с учетом возможности насле'
дования или запретите его Наследование повышает сложность программы,
и в этом смысле оно может быть опасным. Поэтому гуру программирования на
Java Джошуа Блох и сказал: «Проектируйте и документируйте классы с учетом воз#
можности наследования или запретите его». Если при проектировании класса вы
решили, что он не должен поддерживать наследование, не объявляйте его члены
как virtual в случае C++ или overridable в случае Microsoft Visual Basic; если вы про#
граммируете на Java, объявите члены такого класса как final.
Соблюдайте принцип подстановки Лисков (Liskov Substitution Principle, LSP)
Барбара Лисков как#то заявила, что наследование стоит использовать, только если
производный класс действительно «является» более специализированной верси#
ей базового класса (Liskov, 1988). Энди Хант и Дэйв Томас сформулировали LSP
так: «Клиенты должны иметь возможность использования подклассов через ин#
терфейс базового класса, не замечая никаких различий» (Hunt and Thomas, 2000).
Иначе говоря, все методы базового класса должны иметь в каждом производном
классе то же значение.
Если у вас есть базовый класс Account (счет) и производные классы CheckingAccount
(счет до востребования), SavingsAccount (депозитный счет) и AutoLoanAccount (счет
ссуд), то при вызове каких бы то ни было методов класса Account в любом из его
подтипов программист не должен заботиться о подтипе конкретного объекта «счет».
При соблюдении принципа подстановки Лисков наследование — мощное сред#
ство снижения сложности, позволяющее программисту сосредоточиться на общих
атрибутах объекта, не волнуясь об его деталях. Если же программист должен по#
стоянно помнить о семантических различиях реализаций подклассов, наследо#
вание только повышает сложность. Так, в нашем примере программисту пришлось
бы думать: «Если я вызываю метод InterestRate() (процентная ставка) класса Che%
ckingAccount или SavingsAccount, он возвращает процент, который банк выплачи#
вает клиенту, однако метод InterestRate() класса AutoLoanAccount возвращает про#
цент, выплачиваемый клиентом банку, поэтому я должен изменить знак результа#
та». В соответствии с LSP, в данном случае класс AutoLoanAccount не должен быть
производным от класса Account, потому что методы InterestRate() в этих классах
имеют разные семантические значения.
Убедитесь, что вы наследуете только то, что хотите наследовать
Производный класс может наследовать интерфейсы методов#членов, их реализа#
ции или и то, и другое (табл. 6#1).

142

ЧАСТЬ II

Высококачественный код

Табл. 6-1. Разновидности наследуемых методов
Переопределение
метода возможно

Переопределение
метода невозможно

Реализация по умолчанию
имеется

Переопределяемый метод

Непереопределяемый метод.

Реализация по умолчанию
отсутствует

Абстрактный
переопределяемый метод

Этот вариант не использует#
ся (нет смысла в том, чтобы
оставить метод без определе#
ния, не позволив его переоп#
ределить).

Как следует из таблицы, наследуемые методы могут относиться к одной из трех
категорий:
 абстрактный переопределяемый метод: производный класс наследует интер#

фейс метода, но не его реализацию;
 переопределяемый метод: производный класс наследует интерфейс метода и

его реализацию по умолчанию, а также может переопределить эту реализацию;
 непереопределяемый метод: производный класс наследует интерфейс метода

и его реализацию по умолчанию, переопределить которую не может.
Создавая новый класс при помощи наследования, обдумайте тип наследования
каждого метода#члена. Не наследуйте реализацию только потому, что вы насле#
дуете интерфейс, и не наследуйте интерфейс только для того, чтобы унаследовать
реализацию. Если вам нужна реализация класса, но не его интерфейс, используй#
те включение, а не наследование.
Не «переопределяйте» непереопределяемые методы'члены И C++, и Java
позволяют программисту переопределить непереопределяемый метод#член — ну,
или что#то вроде того. Если функция объявлена в базовом классе как private, в
производном классе можно создать функцию с тем же именем. Программист,
изучающий код производного класса, может прийти к ложному выводу, что эта
функция является полиморфной, хотя на самом деле это не так — просто у нее то
же имя. Иначе сформулировать это правило можно так: «Не используйте имена
непереопределяемых методов базового класса в производных классах».
Перемещайте общие интерфейсы, данные и формы поведения на как мож'
но более высокий уровень иерархии наследования Чем ближе интерфейсы,
данные и формы поведения к корню дерева наследования, тем легче производ#
ным классам их использовать. Какой уровень считать слишком высоким? Руковод#
ствуйтесь соображениями абстракции. Если вам кажется, что перемещение ме#
тода на более высокий уровень нарушит абстракцию соответствующего класса, не
делайте этого.
С подозрением относитесь к классам, объекты которых создаются в един'
ственном экземпляре Использование единственного экземпляра класса может
указывать на то, что вы спутали объекты с классами. Подумайте, можно ли про#
сто создать объект вместо нового класса. Можно ли конкретный производный класс
представить только данными, а не отдельным классом? Шаблон Одиночка (Sing#
leton) — примечательное исключение из этого правила.

ГЛАВА 6 Классы

143

С подозрением относитесь к базовым классам, имеющим только один про'
изводный класс Когда я вижу базовый класс, имеющий только один производ#
ный класс, то начинаю подозревать, что какой#то программист «проектировал на#
перед» — пытался предвосхитить будущие потребности, скорее всего не понимая
их в полной мере. Лучший способ подготовки к будущей работе — не проектиро#
вать дополнительные уровни базовых классов, которые «когда#нибудь могут по#
надобиться», а написать максимально ясный, понятный и простой код. Это озна#
чает, что иерархию наследования не надо усложнять без крайней нужды.
С подозрением относитесь к классам, которые переопределяют метод,
оставляя его пустым Как правило, это говорит о неудачном проектировании
базового класса. Допустим, вы создали класс Cat, включающий метод Scratch()
(царапать), но после обнаружили, что некоторые коты лишены когтей и не могут
царапаться. Вы могли бы унаследовать от класса Cat класс ScratchlessCat, переоп#
ределив в нем метод Scratch() так, чтобы он ничего не делал. Однако этот подход
связан с рядом проблем.
 Он нарушает абстракцию (контракт интерфейса) класса Cat, изменяя семан#

тику его интерфейса.
 При расширении на другие производные классы этот подход быстро стано#

вится неуправляемым. Что будет, когда вы найдете кота без хвоста? Или кота,
который не ловит мышей? Или кота, который не пьет молоко? В итоге у вас
могут появиться производные классы вроде ScratchlessTaillessMicelessMilklessCat.
 Код, написанный по этой методике, трудно сопровождать, потому что со вре#

менем поведение производных классов начинает сильно отличаться от интер#
фейсов и форм поведения базовых классов.
Исправлять эту проблему следует не в базовом классе, а в первоначальном классе
Cat. Создайте класс Claws (когти) и включите его в класс Cats. Корень наших бед
— предположение, что все коты царапаются; предложенный способ позволит
устранить причину проблемы, а не бороться с ее следствиями.
Избегайте многоуровневых иерархий наследования Объектно#ориентиро#
ванное программирование поддерживает массу способов управления сложностью.
Но использование любого мощного средства сопряжено с риском, и некоторые
объектно#ориентированные подходы часто повышают сложность вместо того,
чтобы снижать ее.
Артур Риэль в прекрасной книге «Object#Oriented Design Heuristics» (Riel, 1996)
предлагает ограничивать иерархии наследования максимум шестью уровнями. Он
основывает свой совет на «магическом числе 7±2», но мне кажется, что это слиш#
ком оптимистично. Опыт подсказывает мне, что большинству людей трудно удер#
жать в уме более двух или трех уровней наследования сразу. «Магическое число
7±2» скорее характеризует максимально допустимое общее количество подклас%
сов базового класса, а не уровней иерархии наследования.
Создание многоуровневых иерархий наследования значительно повышает число
ошибок (Basili, Briand, and Melo, 1996). Тот, кто занимался отладкой сложной иерар#
хии наследования, знает причину этого. Многоуровневые иерархии повышают
сложность, что диаметрально противоположно цели наследования. Помните про

144

ЧАСТЬ II

Высококачественный код

Главный Технический Императив и убедитесь, что вы используете наследование,
чтобы избежать дублирования кода и минимизировать сложность.
Предпочитайте полиморфизм, а не крупномасштабную проверку типов
Наличие в коде большого числа блоков case может указывать на то, что програм#
му лучше было бы спроектировать, используя наследование, хотя это верно не
всегда. Вот классический пример кода, призывающего к использованию более
объектно#ориентированного подхода:

Пример кода, который следовало бы заменить
вызовом полиморфного метода (C++)
switch ( shape.type ) {
case Shape_Circle:
shape.DrawCircle();
break;
case Shape_Square:
shape.DrawSquare();
break;
...
}
Здесь методы shape. DrawCircle() и shape. DrawSquare() следует заменить на един#
ственный метод shape. Draw(), поддерживающий рисование и окружностей, и
прямоугольников.
С другой стороны, иногда блоки case служат для разделения по#настоящему раз#
ных видов объектов или форм поведения. Так, следующий фрагмент вполне уме#
стен в объектно#ориентированной программе:

Пример кода, который, пожалуй, не следует заменять
вызовом полиморфного метода (C++)
switch ( ui.Command() ) {
case Command_OpenFile:
OpenFile();
break;
case Command_Print:
Print();
break;
case Command_Save:
Save();
break;
case Command_Exit:
ShutDown();
break;
...
}
В данном случае можно было бы создать базовый класс и унаследовать от него ряд
производных классов, выполняющих каждую команду при помощи полиморфно#
го метода DoCommand() (как в шаблоне Команда). Но в подобной простой ситуа#

ГЛАВА 6 Классы

145

ции это неуместно: имя метода DoCommand() было бы настолько туманным, что
почти утратило бы всякий смысл, тогда как блоки case довольно информативны.
Делайте все данные закрытыми, а не защищенными Как говорит Джошуа
Блох, «наследование нарушает инкапсуляцию» (Bloch, 2001). Выполняя наследо#
вание от класса, вы получаете привилегированный доступ к его защищенным
методам и данным. Если производному классу на самом деле нужен доступ к ат#
рибутам базового класса, включите в базовый класс защищенные методы доступа.

Множественное наследование
Наследование — мощный и... довольно опасный инструмент.
В некотором смысле наследование похоже на цепную пилу:
при соблюдении мер предосторожности оно может быть
невероятно полезным, но при неумелом обращении послед#
ствия могут оказаться очень и очень серьезными.

С множественным наследованием в C++ связан один неоспоримый факт: оно открывает ящик
Пандоры, полный проблем, которые просто невозможны при
единичном наследовании.

Если наследование — цепная пила, то множественное на#
Скотт Мейерс
следование — это старинная цепная пила с барахлящим
(Scott Meyers)
мотором, не имеющая предохранителей и не поддержива#
ющая автоматического отключения. Иногда такой инструмент может пригодить#
ся, но большую часть времени его лучше хранить в гараже под замком.
Некоторые эксперты рекомендуют широкое применение множественного насле#
дования (Meyer, 1997), но по опыту могу сказать, что оно полезно главным обра#
зом только при создании «миксинов» — простых классов, позволяющих добавить
ряд свойств в другой класс. Миксины называются так потому, что они позволяют
«подмешать (mix in)» свойства в производные классы. Миксинами могут быть классы
вроде Displayable, Persistent, Serializable или Sortable. Миксины почти всегда явля#
ются абстрактными и не поддерживают создания экземпляров независимо от
других объектов.
Миксины требуют множественного наследования, но пока все миксины по#насто#
ящему независимы друг от друга, вы можете не бояться классической проблемы,
связанной с ромбовидной схемой наследования. Кроме того, «объединяя» атри#
буты, они делают проект системы понятнее. Программисту легче разобраться с
объектом, использующим миксины Displayable и Persistent, а не 11 более конкрет#
ных методов, которые понадобились бы для реализации этих двух свойств в про#
тивном случае.
Похоже, разработчики Java и Visual Basic понимали ценность миксинов, разрешив
множественное наследование интерфейсов, но только единичное наследование
классов. C++ поддерживает множественное наследование и интерфейсов, и реа#
лизации. Используйте множественное наследование, только тщательно рассмот#
рев все альтернативные варианты и проанализировав влияние выбранного под#
хода на сложность и понятность системы.

Почему правил наследования так много?
В этом разделе были описаны многие правила избавления от проблем,
связанных с наследованием. Все эти правила подразумевают, что насле%
дование часто противоречит главному техническому императиву програм%

146

ЧАСТЬ II

Высококачественный код

мирования — управлению сложностью. Ради управления сложностью относитесь к
наследованию с подозрением. Вот как использовать наследование и включение:
Перекрестная ссылка О сложности см. подраздел «Главный
Технический Императив Разработки ПО: управление сложностью» раздела 5.2.

 если несколько классов имеют общие данные, но не фор#
мы поведения, создайте общий объект, который можно было
бы включить во все эти классы;
 если несколько классов имеют общие формы поведения,
но не данные, сделайте эти классы производными от общего
базового класса, определяющего общие методы;

 если несколько классов имеют общие данные и формы поведения, сделайте эти

классы производными от общего базового класса, определяющего общие дан#
ные и методы;
 используйте наследование, если хотите, чтобы интерфейс определялся базо#

вым классом, и включение, если хотите сами контролировать интерфейс.

Методы-члены и данные-члены
Перекрестная ссылка О методах
в общем см. главу 7.

Ниже я даю несколько советов по эффективной реализации
методов#членов и данных#членов.

Включайте в класс как можно меньше методов
В одном исследовании программ на C++ было обнаружено, что большему числу
методов в расчете на один класс соответствует большее число изъянов (Basili, Briand,
and Melo, 1996). Однако важнее оказались другие конкурирующие факторы, в том
числе многоуровневые иерархии наследования, большое число методов, вызыва#
емых из класса, и сильноесопряжение между классами. Разрабатывая класс, стре#
митесь к оптимальному соответствию между этими факторами и минимальным
числом методов.
Блокируйте неявно сгенерированные методы и операторы, которые вам
не нужны Иногда некоторые возможности, такие как создание объекта или при#
сваивание, целесообразно блокировать. Вам может показаться, что сделать это
невозможно, потому что компилятор генерирует эти операции автоматически.
Однако вы можете запретить их использование в клиентском коде, объявив кон#
структор, оператор присваивания или другой метод или оператор как private.
(Создание закрытого конструктора — стандартный способ определения класса#
одиночки, о чем см. ниже.)
Минимизируйте число разных методов, вызываемых классом Одно иссле#
дование показало, что число дефектов в коде класса статистически коррелирует
с общим числом методов, вызываемых классом (Basili, Briand, and Melo, 1996). То
же исследование показало, что число дефектов в коде класса повышается и при
увеличении числа используемых в нем классов. Эти концепции иногда называют
«коэффициентом разветвления по выходу (fan out)».
Избегайте опосредованных вызовов методов других классов Непосред#
ственные связи довольно опасны. Опосредованные связи, такие как account.Con%
tactPerson() . DaytimeContactInfo() . PhoneNumber(), опасны еще больше. В связи с этим
ученые сформулировали «Правило Деметры (Law of Demeter)» (Lieberherr and
Holland, 1989), которое гласит, что Объект A может вызывать любые из собствен#
ных методов. Если он создает Объект B, он может вызывать любые методы Объекта

ГЛАВА 6 Классы

B, но ему не следует вызывать методы объектов, возвраща#
емых Объектом B. В нашем случае это означает, что вызов
account . ContactPerson() приемлем, однако вызова account.%
ContactPerson() .DaytimeContactInfo() следовало бы избежать.
Это упрощенное объяснение — подробнее см. в книгах, ука#
занных в конце главы.
Вообще минимизируйте сотрудничество класса с дру'
гими классами Старайтесь свести к минимуму все следу#
ющие показатели:

147

Дополнительные сведения Хорошее обсуждение «Правила
Деметры» см. в книгах «Pragmatic Programmer» (Hunt and Thomas, 2000), «Applying UML and
Patterns» (Larman, 2001) и «Fundamentals of Object-Oriented Design in UML» (Page-Jones, 2000).

 число видов создаваемых объектов;
 число непосредственно вызываемых методов созданных объектов;
 число вызовов методов, принадлежащих объектам, возвращенным другими

созданными объектами.

Конструкторы
Советы по использованию конструкторов почти не зависят от языка (по крайней
мере это касается C++, Java и Visual Basic). С деструкторами связано больше раз#
личий — см. материалы, указанные в разделе «Дополнительные ресурсы».
Инициализируйте по мере возможности все данные'члены во всех кон'
структорах Инициализация всех данных#членов во всех конструкторах — про#
стой прием защитного программирования.
Создавайте классы'одиночки с помощью закрытого
конструктора Если вы хотите определить класс, позво#
ляющий создать только один объект, скройте все конструк#
торы класса и создайте статический метод GetInstance(), пре#
доставляющий доступ к единственному экземпляру класса:

Дополнительные сведения Аналогичный код, написанный на
C++, был бы очень похож. Подробнее см. раздел 26 книги «More
Effective C++» (Meyers, 1998).

Пример создания класса-одиночки с помощью
закрытого конструктора (Java)
public class MaxId {
// конструкторы и деструкторы
Закрытый конструктор.

>

private MaxId() {
...
}
...
// открытые методы

Открытый метод, предоставляющий доступ к единственному экземпляру класса.

>

public static MaxId GetInstance() {
return m_instance;
}
...
// закрытые члены

148

ЧАСТЬ II

Высококачественный код

Единственный экземпляр класса.

>

private static final MaxId m_instance = new MaxId();
...

}
Закрытый конструктор вызывается только при инициализации статического объек#
та m_instance. Для обращения к классу#одиночке MaxId нужно просто вызвать метод
MaxId . GetInstance().
Если сомневаетесь, выполняйте полное копирование, а не ограниченное
Одним из главных аспектов работы со сложными объектами является выбор типа
их копирования: полного или ограниченного. Полная копия (deep copy) — это
почленная копия данных#членов объекта; ограниченная копия (shallow copy)
обычно просто указывает или ссылается на исходный объект, хотя конкретные
значения «полного» и «ограниченного» копирования могут различаться.
Мотивом создания ограниченных копий обычно бывает повышение быстродей#
ствия программы. Однако создание нескольких копий крупных объектов редко
приводит к заметному снижению быстродействия, хотя и выглядит эстетически
непривлекательно. Полное копирование некоторых объектов действительно мо#
жет снижать быстродействие, но программисты обычно очень плохо определя#
ют, какой код вызывает проблемы (см. главу 25). Повышение сложности едва ли
можно оправдать сомнительным улучшением быстродействия кода, поэтому, если
не доказано обратное, лучше выполнять полное копирование.
Полные копии легче в реализации и сопровождении, чем ограниченные. При
ограниченном копировании нужно написать не только специфический для объекта
код, но и код подсчета ссылок, безопасного сравнения объектов, их безопасного
уничтожения и т. д. Такой код может быть источником ошибок, поэтому без вес#
кой причины создавать его не следует.
Если вы находите, что вам все#таки нужно ограниченное копирование, прекрас#
ное обсуждение этого подхода в контексте C++ см. в разделе 29 книги Скотта
Мейерса «More Effective C++» (Meyers, 1996). В книге Мартина Фаулера «Refactoring»
(Fowler, 1999) описываются специфические действия, нужные для преобразова#
ния ограниченных копий в полные и наоборот [Фаулер называет их объектами#
ссылками (reference objects) и объектами#значениями (value objects)].

6.4.

Разумные причины создания классов

Перекрестная ссылка Причины
создания классов и методов во
многом перекрываются (см.
раздел 7.1).

Если вы верите всему, что читаете, у вас могло сложиться
впечатление, что единственная причина создания класса —
моделирование объектов реального мира. На самом деле это
весьма далеко от истины. Список разумных причин созда#
ния класса приведен ниже.

Перекрестная ссылка Об идентификации объектов реального
мира см. подраздел «Определите объекты реального мира»
раздела 5.3.

Моделирование объектов реального мира Пусть моде#
лирование объектов реального мира — не единственная
причина создания класса, но от этого она не становится
менее хорошей! Создайте класс для каждого объекта реаль#

ГЛАВА 6 Классы

149

ного мира, моделируемого вашей программой. Поместите нужные объекту дан#
ные в класс и создайте сервисные методы, моделирующие поведение объекта.
Примеры подобного моделирования см. в разделе 6.1.
Моделирование абстрактных объектов Другой разумной причиной созда#
ния класса является моделирование абстрактного объекта — объекта, который
не существует в реальном мире, но является абстракцией других конкретных объек#
тов. Прекрасный пример — классический объект Shape (фигура). Объекты Circle
(окружность) и Square (прямоугольник) существуют на самом деле, тогда как класс
Shape — это абстракция конкретных фигур.
В мире программирования редко встречаются готовые абстракции вроде Shape, из#
за чего поиск ясных абстракций усложняется. Процесс извлечения абстракций из
разнообразия сущностей реального мира недетерминирован, и формирование
абстракций может быть основано на разных принципах. Если бы нам не были из#
вестны окружности, прямоугольники, треугольники и другие геометрические фи#
гуры, мы могли бы прийти в итоге к более необычным фигурам, таким как «каба#
чок», «брюква» и «Понтиак Ацтек». Нахождение адекватных абстрактных объектов
— одна из главных проблем объектно#ориентированного проектирования.
Снижение сложности Снижение сложности — самая важная причи#
на создания класса. Создайте класс для сокрытия информации, чтобы о
ней можно было не думать. Конечно, вам придется думать о ней при
написании класса, но после этого вы сможете забыть о деталях и использовать
класс, не зная о его внутренней работе. Другие причины создания классов —
минимизация объема кода, облегчение сопровождения программы и снижение
числа ошибок — тоже хороши, но без абстрагирующей силы классов сложные
программы было бы невозможно охватить умом.
Изоляция сложности Как бы ни проявлялась сложность — в форме запутан#
ных алгоритмов, крупных наборов данных, замысловатых протоколов коммуни#
кации, — она является источником ошибок. При возникновении ошибки ее будет
проще найти, если она будет локализована в классе, а не распределена по всему
коду. Изменения, обусловленные исправлением ошибки, не повлияют на осталь#
ной код, потому что вам придется исправить только один класс. Если вы найдете
более эффективный, простой или надежный алгоритм, им будет легче заменить
старый алгоритм, изолированный в классе. Во время разработки вам будет про#
ще попробовать несколько вариантов проектирования и выбрать наилучший.
Сокрытие деталей реализации Еще одна прекрасная причина создания класса
— сокрытие деталей реализации, и таких сложных, как мудреный способ доступа
к БД, и столь банальных, как отдельный элемент данных, хранимый в форме чис#
ла или строки.
Ограничение влияния изменений Изолируйте области вероятных изменений,
чтобы влияние изменений ограничивалось пределами одного или нескольких
классов. Проектируйте приложение так, чтобы области вероятных изменений
можно было изменить с максимальной легкостью. В число областей вероятных
изменений входят зависимости от оборудования, подсистема ввода/вывода, слож#
ные типы данных и бизнес#правила. Некоторые частые источники изменений

150

ЧАСТЬ II

Высококачественный код

описаны в подразделе «Скрывайте секреты (к вопросу о сокрытии информации)»
раздела 5.3.
Сокрытие глобальных данных Используя глобальные
данные, вы можете скрыть детали их реализации за интер#
фейсом класса. Обращение к глобальным данным через ме#
тоды доступа имеет ряд преимуществ в сравнении с их не#
посредственным использованием. Вы можете менять структуру данных, не изме#
няя программу. Вы можете следить за доступом к данным. Кроме того, использо#
вание методов доступа подталкивает к размышлениям о том, на самом ли деле
данные глобальны; часто оказывается, что «глобальные данные» на самом деле
являются просто данными какого#то объекта.

Перекрестная ссылка О проблемах, связанных с глобальными
данными, см. раздел 13.3.

Упрощение передачи параметров в методы Если вы передаете один пара#
метр в несколько методов, это может указывать на необходимость объединения
этих методов в класс, чтобы они могли использовать параметр как данные объекта.
Упрощение передачи параметров в методы само по себе не цель, но передача
крупных объемов данных наводит на мысль, что другая организация классов могла
бы быть более эффективной.
Создание центральных точек управления Управлять
каждой задачей в одном месте — разумная идея. Управле#
ние может принимать разные формы. Знание числа элемен#
тов таблицы — одна форма. Управление файлами, соедине#
ниями с БД, принтерами и другими устройствами — другая.
Использование одного класса для чтения и записи БД явля#
ется формой централизованного управления. Если БД нужно будет преобразовать
в однородный файл или данные «в памяти», изменения придется внести только в
один класс.

Перекрестная ссылка О сокрытии информации см. подраздел
«Скрывайте секреты (к вопросу о сокрытии информации)»
раздела 5.3.

Идея централизованного управления похожа на сокрытие информации, но она
имеет уникальную эвристическую силу, так что не забудьте добавить ее в свой
инструментарий.
Облегчение повторного использования кода Код, разбитый на грамотно орга#
низованные классы, легче повторно использовать в других программах, чем тот
же код, помещенный в один более крупный класс. Даже если фрагмент вызывает#
ся только из одного места программы и вполне понятен в составе более крупно#
го класса, подумайте, может ли он понадобиться в другой программе. Если да, стоит
поместить его в отдельный класс.
В Лаборатории проектирования ПО NASA были изучены десять проек#
тов, в которых энергично преследовалось повторное использование кода
(McGarry, Waligora, and McDermott, 1989). И при объектно#, и при функ#
ционально#ориентированном подходах разработчикам первоначально не удалось
достичь этой цели, потому что в предыдущих проектах не была создана достаточная
база кода. Впоследствии при работе над функциональными проектами около 35%
кода удалось взять из предыдущих проектов. В проектах, основанных на объект#
но#ориентированном подходе, этот показатель составил 70%. Если благодаря заб#
лаговременному планированию можно избежать написания 70% кода, грех этим
не воспользоваться!

ГЛАВА 6 Классы

Заметьте, что ядро подхода NASA к созданию повторно ис#
пользуемых классов не включает «проектирование для по#
вторного использования». Классы, претендующие на повтор#
ное использование, определяют в NASA в конце проектов.
Все действия по упрощению повторного использования
классов выполняются как специальный проект в конце ос#
новного проекта или как первый этап нового проекта. Этот
подход помогает предотвращать «позолоту» — создание не#
нужной функциональности, только повышающей сложность.

151

Перекрестная ссылка О реализации минимального объема
необходимой функциональности
см. подраздел «Программа содержит код, который может
когда-нибудь понадобиться»
раздела 24.2.

Планирование создания семейства программ Если вы ожидаете, что програм#
му придется изменять, разумно изолировать области предполагаемых изменений
в отдельных классах. После этого вы можете изменять классы, не влияя на остальную
часть программы, или вообще заменить их на абсолютно новые классы. Размыш#
ление о том, как может выглядеть целое семейство программ, а не просто одна
программа, — эффективный эвристический принцип предвосхищения целых
категорий изменений (Parnas, 1976).
Как#то я руководил группой, работавшей над рядом программ, упрощавших заклю#
чение договоров страхования. Мы должны были адаптировать каждую программу
к отдельным тарифам, формату отчетов конкретного клиента и т. д. Однако мно#
гие части программ были похожи: например, классы ввода данных о потенциаль#
ных заказчиках, классы, сохранявшие информацию в БД, классы просмотра тари#
фов и т. д. Мы организовали программу так, чтобы каждая «изменчивая» часть на#
ходилась в отдельном классе. Создание первоначальной программы заняло три
месяца или около того, но зато когда к нам обращался новый клиент, мы просто
переписывали несколько классов и включали их в остальной код. Несколько дней
работы и — вуаля! — специализированное приложение!
Упаковка родственных операций Если создание класса не удается обосновать
сокрытием информации, совместным доступом к данным или обеспечением гиб#
кости программы, вы все же можете упаковать наборы операций в более осмыс#
ленные группы, такие как группы тригонометрических функций, статистических
функций, методов работы со строками, методов манипулирования битами, гра#
фических методов и т. д. Класс — не единственное средство объединения родствен#
ных операций. В зависимости от конкретного языка для этого также можно ис#
пользовать пакеты, пространства имен или заголовочные файлы.
Выполнение специфического вида рефакторинга Создание новых классов
предусматривают многие специфические виды рефакторинга (см. главу 24), та#
кие как разделение одного класса на два, сокрытие делегата, удаление класса#по#
средника и формирование класса#расширения. Создание этих новых классов может
быть мотивировано стремлением к лучшему выполнению какой#либо задачи из
описанных в данном разделе.

Классы, которых следует избегать
Хотя в целом классы очень полезны, работая с ними, вы можете столкнуться с
проблемами. Ниже описаны классы, создавать которые не следует.

152

ЧАСТЬ II

Высококачественный код

Избегайте создания «божественных» классов Избегайте создания классов,
которые все знают и все могут. Если класс извлекает и задает данные других классов
с использованием методов Get() и Set() (т. е. вмешивается в их дела и указывает
им, что делать), спросите себя, не следует ли его функциональность реализовать
в тех классах, а не выделять в божественный класс (Riel, 1996).
Устраняйте нерелевантные классы Если класс имеет
только данные, но не формы поведения, спросите себя, дей#
ствительно ли это класс. Возможно, этот класс следует раз#
жаловать, сделав его данные#члены атрибутами одного или
нескольких других классов.

Перекрестная ссылка Такой вид
класса обычно называют структурой. О структурах см. раздел 13.1.

Избегайте классов, имена которых напоминают глаголы Как правило,
класс, имеющий только формы поведения, но не данные, на самом деле классом
не является. Подумайте о превращении класса вроде DatabaseInitialization() или
StringBuilder() в метод какого#нибудь другого класса.

Резюме причин создания класса
Вот список разумных причин создания класса:
 моделирование объектов реального мира;
 моделирование абстрактных объектов;
 снижение сложности;
 изоляция сложности;
 сокрытие деталей реализации;
 ограничение влияния изменений;
 сокрытие глобальных данных;
 упрощение передачи параметров в методы;
 создание центральных точек управления;
 облегчение повторного использования кода;
 планирование создания семейства программ;
 упаковка родственных операцией;
 выполнение специфического вида рефакторинга.

6.5.

Аспекты, специфические для языков

Использование классов в разных языках программирования имеет интересные
различия. Рассмотрим, например, переопределение метода#члена в производном
классе при реализации полиморфизма. В Java все методы переопределяемы по
умолчанию, а чтобы в производном классе метод нельзя было переопределить, его
нужно объявить как final. В C++ методы по умолчанию непереопределяемы. Чтобы
сделать метод переопределяемым, его нужно объявить в базовом классе как virtual.
В Visual Basic переопределяемый метод должен быть объявлен в базовом классе как
overridable, а в производном классе нужно использовать ключевое слово overrides.

ГЛАВА 6 Классы

153

Вот некоторые другие аспекты классов, во многом зависящие от языка:
 поведение переопределенных конструкторов и деструкторов в дереве насле#

дования;
 поведение конструкторов и деструкторов при обработке исключений;
 важность конструкторов по умолчанию (конструкторов без аргументов);
 время вызова деструктора или метода финализации;
 целесообразность переопределения встроенных операторов языка, в том числе

операторов присваивания и сравнения;
 управление памятью при создании и уничтожении объектов или при их объяв#

лении и выходе из области видимости.
Подробно эти вопросы мы рассматривать не будем, но в разделе «Дополнительные
ресурсы» я указал несколько хороших книг, посвященных конкретным языкам.

6.6.

Следующий уровень: пакеты классов

В настоящее время использование классов — лучший спо#
Перекрестная ссылка О разлисоб достижения модульности. Однако модульность — обшир#
чии между классами и пакетами
ная тема, и она никак не ограничивается классами. В по#
см. также подраздел «Уровни
проектирования» раздела 5.2.
следние десятилетия отрасль разработки ПО развивалась во
многом благодаря увеличению агрегаций, с которыми нам
приходится работать. Первой агрегацией были операторы, что при сравнении с
машинными командами казалось в то время большим достижением. Затем появи#
лись методы, а позднее придуманы классы.
Ясно, что мы могли бы лучше выполнять абстракцию и инкапсуляцию, если бы
имели эффективные средства агрегации групп объектов. Java поддерживает па#
кеты, а язык Ada поддерживал их уже десять лет назад. Если используемый вами
язык не поддерживает пакеты непосредственно, вы можете создать собственные
версии пакетов, подкрепив их стандартами программирования, такими как:
 конвенции именования, проводящие различие между классами, которые мож#

но применять вне пакета, и классами, предназначенными только для закрыто#
го использования;
 конвенции именования, конвенции организации кода (структура проекта) или

и те, и другие конвенции, определяющие принадлежность каждого класса к тому
или иному пакету;
 правила, определяющие возможность использования конкретных пакетов дру#

гими пакетами, в том числе возможность наследования, включения или того и
другого.
Это еще один удачный пример различия между программированием на языке и
программированием с использованием языка (см. раздел 34.4).

154

ЧАСТЬ II

Высококачественный код

Контрольный список: качество классов
http://cc2e.com/0672

Абстрактные типы данных
 Обдумали ли вы классы программы как абстрактные типы
данных, оценив их интерфейсы с этой точки зрения?

Абстракция
 Имеет ли класс главную цель?
 Удачное ли имя присвоено классу? Описывает ли оно главную цель класса?
 Формирует ли интерфейс класса согласованную абстракцию?
 Ясно ли интерфейс описывает использование класса?
Достаточно ли абстрактен интерфейс, чтобы вы могли не думать о реализации класса? Можно ли рассматривать класс как «черный ящик»?
Достаточно ли полон набор сервисов класса, чтобы другие классы могли
не обращаться к его внутренним данным?
Исключена ли из класса нерелевантная информация?
Обдумали ли вы разделение класса на классы-компоненты? Разделен ли
он на максимально возможное число компонентов?
Сохраняется ли целостность интерфейса при изменении класса?

Перекрестная ссылка Этот контрольный список позволяет
определить качество классов.
Об этапах создания класса см.
контрольный список «Процесс
программирования с псевдокодом» в главе 9.







Инкапсуляция
 Сделаны ли члены класса минимально доступными?
 Избегает ли класс предоставления доступа к своим данным-членам?
 Скрывает ли класс детали реализации от других классов в максимально
возможной степени, допускаемой языком программирования?
 Избегает ли класс предположений о своих клиентах, в том числе о производных классах?
 Независим ли класс от других классов? Слабо ли он связан?
Наследование
 Используется ли наследование только для моделирования отношения «является», т. е. придерживаются ли производные классы принципа подстановки
Лисков?
 Описана ли в документации класса стратегия наследования?
 Избегают ли производные классы «переопределения» непереопределяемых
методов?
 Перемещены ли общие интерфейсы, данные и формы поведения как можно ближе к корню дерева наследования?
 Не слишком ли много уровней включают иерархии наследования?
 Все ли данные — члены базового класса сделаны закрытыми, а не защищенными?
Другие вопросы реализации
 Класс содержит около семи элементов данных-членов или меньше?
 Минимально ли число встречающихся в классе непосредственных и опосредованных вызовов методов других классов?
 Сведено ли к минимуму сотрудничество класса с другими классами?
 Все ли данные-члены инициализируются в конструкторе?

ГЛАВА 6 Классы

155

 Спроектирован ли класс для использования полного, а не ограниченного

копирования, если нет убедительной причины создания ограниченных копий?

Аспекты, специфические для языков
 Изучили ли вы особенности работы с классами, характерные для выбранного языка программирования?

Дополнительные ресурсы
Классы в общем
Meyer, Bertrand. Object%Oriented Software Construction, 2d ed. —
http://cc2e.com/0679
New York, NY: Prentice Hall PTR, 1997. В этой книге Мейер
рассматривает абстрактные типы данных и объясняет, как они
формируют основу классов. В главах 14–16 подробно обсуждается наследование.
В главе 15 Мейер приводит довод в пользу множественного наследования.
Riel, Arthur J. Object%Oriented Design Heuristics. — Reading, MA: Addison#Wesley, 1996.
Эта книга включает множество советов по улучшению проектирования, относя#
щихся большей частью к уровню классов. Я избегал ее несколько лет, потому что
она казалась слишком большой — воистину сапожник без сапог! Однако основ#
ная часть книги занимает только около 200 страниц. Книга написана доступным
и занимательным языком, а ее содержание сжато и практично.

C++
Meyers, Scott. Effective C++: 50 Specific Ways to Improve Your
Programs and Designs, 2d ed. — Reading, MA: Addison#Wesley,
1998.

http://cc2e.com/0686

Meyers, Scott. More Effective C++: 35 New Ways to Improve Your Programs and Designs.
— Reading, MA: Addison#Wesley, 1996. Обе книги Мейерса являются канонически#
ми для программистов на C++. Они очень интересны и позволяют приобрести глу#
бокие знания некоторых нюансов C++.

Java
Bloch, Joshua. Effective Java Programming Language Guide. —
Boston, MA: Addison#Wesley, 2001. В книге Блоха можно найти
много полезных советов по Java, а также описания более
общих объектно#ориентированных подходов.

http://cc2e.com/0693

Visual Basic
Ниже указаны книги, в которых хорошо рассмотрена работа
с классами в контексте Visual Basic.

http://cc2e.com/0600

Foxall, James. Practical Standards for Microsoft Visual Basic .NET.
— Redmond, WA: Microsoft Press, 2003.
Cornell, Gary, and Jonathan Morrison. Programming VB .NET: A Guide for Experienced
Programmers. — Berkeley, CA: Apress, 2002.
Barwell, Fred, et al. Professional VB.NET, 2d ed. — Wrox, 2002.

156

ЧАСТЬ II

Высококачественный код

Ключевые моменты
 Интерфейс класса должен формировать согласованную абстракцию. Многие

проблемы объясняются нарушением одного этого принципа.
 Интерфейс класса должен что#то скрывать — особенности взаимодействия с

системой, аспекты проектирования или детали реализации.
 Включение обычно предпочтительнее, чем наследование, если только вы не

моделируете отношение «является».
 Наследование — полезный инструмент, но оно повышает сложность, что про#

тиворечит Главному Техническому Императиву Разработки ПО, которым явля#
ется управление сложностью.
 Классы — главное средство управления сложностью. Уделите их проектирова#

нию столько времени, сколько нужно для достижения этой цели.

ГЛАВА 7 Высококачественные методы

Г Л А В А

157

7

Высококачественные
методы

Содержание
 7.1. Разумные причины создания методов

http://cc2e.com/0778

 7.2. Проектирование на уровне методов
 7.3. Удачные имена методов
 7.4. Насколько объемным может быть метод?
 7.5. Советы по использованию параметров методов
 7.6. Отдельные соображения по использованию функций
 7.7. Методы#макросы и встраиваемые методы

Связанные темы
 Этапы конструирования методов: раздел 9.3
 Классы: глава 6
 Общие методики проектирования: глава 5
 Архитектура ПО: раздел 3.5

В главе 6 мы подробно рассмотрели создание классов. В этой главе мы обратим
внимание на методы и характеристики, отличающие хорошие методы от плохих.
Если вам хотелось бы сначала разобраться в вопросах, влияющих на проектиро#
вание методов, прочитайте главу 5 и потом вернитесь к этой главе. Некоторые
важные атрибуты высококачественных методов обсуждаются также в главе 8. Если
вас больше интересуют этапы создания методов и классов, см. главу 9.
Прежде чем перейти к деталям, определим два базовых термина. Что такое «метод»?
Метод — это отдельная функция или процедура, выполняющая одну задачу. В раз#
личных языках методы могут называться по#разному, но их суть от этого не меня#
ется. Иногда макросы C и C++ также полезно рассматривать как методы. Многие
советы по созданию высококачественных методов относятся и к макросам.
Что такое высококачественный метод? На этот вопрос ответить сложнее. Возможно,
лучше всего просто показать, что не является высококачественным методом. Вот
пример низкокачественного метода:

158

ЧАСТЬ II

Высококачественный код

Пример низкокачественного
метода (C++)
void HandleStuff( CORP_DATA & inputRec, int crntQtr, EMP_DATA empRec,
double & estimRevenue, double ytdRevenue, int screenX, int screenY,
COLOR_TYPE & newColor, COLOR_TYPE & prevColor, StatusType & status,
int expenseType )
{
int i;
for ( i = 0; i < 100; i++ ) {
inputRec.revenue[i] = 0;
inputRec.expense[i] = corpExpense[ crntQtr ][ i ];
}
UpdateCorpDatabase( empRec );
estimRevenue = ytdRevenue * 4.0 / (double) crntQtr;
newColor = prevColor;
status = SUCCESS;
if ( expenseType == 1 ) {
for ( i = 0; i < 12; i++ )
profit[i] = revenue[i]  expense.type1[i];
}
else if ( expenseType == 2 ) {
profit[i] = revenue[i]  expense.type2[i];
}
else if ( expenseType == 3 )
profit[i] = revenue[i]  expense.type3[i];
}
Что тут не так? Подскажу: вы должны найти минимум 10 недостатков. Составив
свой список, сравните его с моим.
 Неудачное имя: HandleStuff() ничего не говорит о роли метода.
 Метод недокументирован (вопрос документирования не ограничивается отдель#

ными методами и обсуждается в главе 32).
 Метод плохо форматирован. Физическая организация кода почти не дает пред#

ставления о его логической организации. Стратегии форматирования исполь#
зуются непоследовательно: сравните стили операторов if с условиями
expenseType == 2 и expenseType == 3 (о форматировании см. главу 31).
 Входная переменная inputRec внутри метода изменяется. Если это входная пе#

ременная, изменять ее нежелательно (в случае C++ ее следовало бы объявить
как const). Если изменение значения предусмотрено, переменную не стоило
называть inputRec.
 Метод читает и изменяет глобальные переменные: читает corpExpense и изме#

няет profit. Взаимодействие этого метода с другими следовало бы сделать бо#
лее непосредственным, без использования глобальных переменных.
 Цель метода размыта. Он инициализирует ряд переменных, записывает дан#

ные в БД, выполняет вычисления — все эти действия не кажутся связанными
между собой. Метод должен иметь одну четко определенную цель.

ГЛАВА 7 Высококачественные методы

159

 Метод не защищен от получения плохих данных. Если переменная crntQtr равна

0, выражение ytdRevenue * 4.0 / (double) crntQtr вызывает ошибку деления на 0.
 Метод использует несколько «магических» чисел: 100, 4.0, 12, 2 и 3 (о магичес#

ких числах см. раздел 12.1).
 Параметры screenX и screenY внутри метода не используются.
 Параметр prevColor передается в метод неверно: он передается по ссылке (&),

но значение ему внутри метода не присваивается.
 Метод принимает слишком много параметров. Как правило, чтобы параметры

можно было охватить умом, их должно быть не более 7 — этот метод прини#
мает 11. Параметры представлены таким неудобочитаемым образом, что боль#
шинство разработчиков даже не попытаются внимательно изучить их или хотя
бы подсчитать.
 Параметры метода плохо упорядочены и не документированы (об упорядоче#

нии параметров см. эту главу, о документировании — главу 32).
Если не считать сами компьютеры, методы — величайшее
изобретение в области компьютерных наук. Методы облег#
чают чтение и понимание программ в большей степени, чем
любая другая возможность любого языка программирова#
ния, и оскорблять столь заслуженных в мире программиро#
вания деятелей таким кодом, что был приведен выше, —
настоящее преступление.

http://cc2e.com/0799

Перекрестная ссылка Классы
также претендуют на роль величайшего изобретения в области информатики. Об эффективном использовании классов
см. главу 6.

Кроме того, методы — самый эффективный способ умень#
шения объема и повышения быстродействия программ.
Представьте, насколько объемнее были бы ваши програм#
мы, если б вместо каждого вызова метода нужно было вставить соответствующий
код. Представьте, насколько сложнее было бы оптимизировать код, если бы он был
распространен по всей программе, а не локализован в одном методе. Програм#
мирование, каким мы его знаем сегодня, оказалось бы без методов невозможным.
«Хорошо, — скажете вы. — Я уже знаю, что методы очень полезны и постоянно
их использую. Чего ж вы от меня хотите?»
Я хочу, чтобы вы поняли, что есть много веских причин, а также правильных и
неправильных способов создания методов. Будучи студентом факультета инфор#
матики, я думал, что главная причина создания методов — предотвращение дуб#
лирования кода. Во вводном учебнике, по которому я учился, полезность методов
обосновывалась тем, что предотвращение дублирования кода делает программу
более простой в разработке, отладке, документировании и сопровождении. Точ#
ка. Если не считать синтаксические детали использования параметров и локаль#
ных переменных, на этом обсуждение методов в той книге заканчивалось. Такое
объяснение теории и практики использования методов нельзя считать ни удач#
ным, ни полным. В следующих разделах я постараюсь это исправить.

160

ЧАСТЬ II

7.1.

Разумные причины создания методов

Высококачественный код

Ниже я привел список причин создания метода. Они несколько перекрываются и
не исключают одна другую.
Снижение сложности Самая важная причина создания метода —
снижение сложности программы. Создайте метод для сокрытия инфор#
мации, чтобы о ней можно было не думать. Конечно, при написании
метода думать о ней придется, но после этого вы сможете забыть о деталях и ис#
пользовать метод, не зная о его внутренней работе. Другие причины создания
методов — минимизация объема кода, облегчение сопровождения программы и
снижение числа ошибок — также хороши, но без абстрагирующей силы методов
сложные программы было бы невозможно охватить умом.
Одним из признаков того, что метод следует разделить, является глубокая вложен#
ность внутренних циклов или условных операторов. Упростите такой метод, вы#
делив вложенную часть в отдельный метод.
Формирование понятной промежуточной абстракции Выделение фраг#
мента кода в удачно названный метод — один из лучших способов документиро#
вания его цели. Вместо того, чтобы работать с фрагментами вида:

if ( node NULL ) then
while ( node.next NULL ) do
node = node.next
leafName = node.name
end while
else
leafName = ””
end if
вы можете иметь дело с чем#нибудь вроде:

leafName = GetLeafName( node )
Новый метод так прост, что для документирования достаточно присвоить ему
удачное имя. В сравнении с первоначальными восемью строками кода имя мето#
да формирует абстракцию более высокого уровня, что облегчает чтение и пони#
мание кода, а также снижает его сложность.
Предотвращение дублирования кода Несомненно, самая популярная причина
создания метода — желание избежать дублирования кода. Действительно, вклю#
чение похожего кода в два метода указывает на ошибку декомпозиции. Уберите
повторяющийся фрагмент из обоих методов, поместите его общую версию в ба#
зовый класс и создайте два специализированных метода в подклассах. Вы также
можете выделить общий код в отдельный метод и вызвать его из двух первона#
чальных методов. В результате программа станет компактнее. Изменять ее станет
проще, так как в случае чего вам нужно будет изменить только один метод. Код
станет надежнее, потому что для его проверки нужно будет проанализировать
только один фрагмент. Изменения будут реже приводить к ошибкам, поскольку
вы не сможете по невнимательности внести в идентичные фрагменты програм#
мы чуть различающиеся изменения.

ГЛАВА 7 Высококачественные методы

161

Поддержка наследования Переопределить небольшой грамотно организован#
ный метод легче, чем длинный и плохо спроектированный. Кроме того, стремле#
ние к простоте переопределяемых методов уменьшает вероятность ошибок при
реализации подклассов.
Сокрытие очередности действий Скрывать очередность обработки событий
— разумная идея. Например, если программа обычно сначала вызывает метод,
запрашивающий информацию у пользователя, а после этого — метод, читающий
вспомогательные данные из файла, никакой из этих двух методов не должен за#
висеть от порядка их выполнения. В качестве другого примера можно привести
две строки кода, первая из которых читает верхний элемент стека, а вторая умень#
шает переменную stackTop. Вместо того чтобы распространять такой код по всей
системе, скройте предположение о необходимом порядке выполнения двух опе#
раций, поместив две эти строки в метод PopStack().
Сокрытие операций над указателями Операции над указателями не отли#
чаются удобочитаемостью и часто являются источником ошибок. Изолировав та#
кие операции в методах, вы сможете сосредоточиться на их сути, а не на меха#
низме манипуляций над указателями. Кроме того, выполнение операций над ука#
зателями в одном месте облегчает проверку правильности кода. Если же вы най#
дете более эффективный тип данных, чем указатели, изменения затронут лишь
несколько методов.
Улучшение портируемости Использование методов изолирует непортируе#
мый код, явно определяя фрагменты, которые придется изменить при портиро#
вании приложения. В число непортируемых аспектов входят нестандартные воз#
можности языка, зависимости от оборудования и операционной системы и т. д.
Упрощение сложных булевых проверок Понимание сложных булевых проверок
редко требуется для понимания пути выполнения программы. Поместив такую про#
верку в метод, вы сможете упростить код, потому что (1) детали проверки будут скрыты
и (2) описательное имя метода позволит лучше охарактеризовать суть проверки.
Создание отдельного метода для проверки подчеркивает ее значимость. Это мо#
тивирует программистов сделать детали проверки внутри метода более удобочи#
таемыми. В результате и основной путь выполнения кода, и сама проверка стано#
вятся более понятными. Упрощение булевых проверок является примером сни#
жения сложности, которого мы уже не раз касались.
Повышение быстродействия Методы позволяют выполнять оптимизацию кода
в одном месте, а не в нескольких. Они облегчают профилирование кода, направ#
ленное на определение неэффективных фрагментов. Если код централизован в
методе, его оптимизация повысит быстродействие всех фрагментов, в которых этот
метод вызывается как непосредственно, так и косвенно, а реализация метода на
более эффективном языке или с применением улучшенного алгоритма окажется
более выгодной.
Для уменьшения объема других методов? Нет. При на#
личии стольких разумных причин создания методов эта не
нужна. На самом деле для решения некоторых задач лучше
использовать один крупный метод (об оптимальном размере
метода см. раздел 7.4).

Перекрестная ссылка О сокрытии информации см. подраздел
«Скрывайте секреты (к вопросу о сокрытии информации)»
раздела 5.3.

162

ЧАСТЬ II

Высококачественный код

Операция кажется слишком простой,
чтобы создавать для нее метод
Один из главных ментальных барьеров, препятствующих созданию эф#
фективных методов, — нежелание создавать простой метод для простой
цели. Создание метода для двух или трех строк кода может показаться
пальбой из пушки по воробьям, но опыт свидетельствует о том, что небольшие
методы могут быть чрезвычайно полезны.
Небольшие методы обеспечивают несколько преимуществ, и одно из них — об#
легчение чтения кода. Так, однажды я обнаружил следующую строку примерно в
десятке мест программы:

Пример вычисления (псевдокод)
points = deviceUnits * ( POINTS_PER_INCH / DeviceUnitsPerInch() )
Наверняка это не самая сложная строка кода в вашей жизни. Большинство людей
в итоге поняло бы, что она преобразует некоторую величину, выраженную в ап#
паратных единицах, в соответствующее число точек, а кроме того, что каждая из
десятка строк делает одно и то же. Однако эти фрагменты можно было сделать
еще более ясными, поэтому я создал метод с выразительным именем, выполняю#
щий преобразование в одном месте:

Пример вычисления, преобразованного в функцию (псевдокод)
Function DeviceUnitsToPoints ( deviceUnits Integer ): Integer
DeviceUnitsToPoints = deviceUnits *
( POINTS_PER_INCH / DeviceUnitsPerInch() )
End Function
В результате все десять первоначальных фрагментов стали выглядеть примерно
так:

Пример вызова функции (псевдокод)
points = DeviceUnitsToPoints( deviceUnits )
Эта строка более понятна и даже кажется очевидной.
Данный пример позволяет назвать еще одну причину создания отдельных мето#
дов для простых операций: дело в том, что простые операции имеют свойство
усложняться с течением времени. После того как я написал метод DeviceUnits%
Perlnch(), оказалось, что в определенных условиях при активности определенных
устройств он возвращает 0. Для предотвращения деления на 0 мне пришлось на#
писать еще три строки кода:

Пример кода, расширяющегося при сопровождении программы (псевдокод)
Function DeviceUnitsToPoints( deviceUnits: Integer ) Integer;
if ( DeviceUnitsPerInch() 0 )
DeviceUnitsToPoints = deviceUnits *
( POINTS_PER_INCH / DeviceUnitsPerInch() )

ГЛАВА 7 Высококачественные методы

163

else
DeviceUnitsToPoints = 0
end if
End Function
Если бы в коде по#прежнему использовалась первоначальная строка, мне пришлось
бы повторить проверку десять раз, добавив в общей сложности 30 строк кода.
Создание простого метода позволило уменьшить это число до 3.

Резюме причин создания методов
Вот список разумных причин создания методов:
 снижение сложности;
 формирование понятной промежуточной абстракции;
 предотвращение дублирования кода;
 поддержка наследования;
 сокрытие очередности действий;
 сокрытие операций над указателями;
 улучшение портируемости;
 упрощение сложных булевых проверок;
 повышение быстродействия.

Кроме того, разумными причинами создания методов можно считать многие из
причин создания классов:
 изоляция сложности;
 сокрытие деталей реализации;
 ограничение влияния изменений;
 сокрытие глобальных данных;
 создание центральных точек управления;
 облегчение повторного использования кода;
 выполнение специфического вида рефакторинга.

7.2.

Проектирование на уровне методов

Идею связности впервые представили Уэйн Стивенс, Гленфорд Майерс и Ларри
Константайн (Stevens, Myers, and Constantine, 1974). На уровне проектирования
классов ее практически вытеснили более современные концепции, такие как аб#
стракция и инкапсуляция, однако на уровне проектирования отдельных методов
эвристический принцип связности по#прежнему полезен.
В случае методов связность характеризует соответствие
Перекрестная ссылка О связновыполняемых в методе операций единой цели. Некоторые
сти см. подраздел «Стремитесь
к максимальной связности»
программисты предпочитают использовать термин «сила»
раздела 5.3.
(strength): насколько сильно связаны операции в методе? На#
пример, метод Cosine() (косинус) имеет одну четко опреде#
ленную цель и потому обладает прекрасной связностью. Метод CosineAndTan()
(косинус и тангенс) имеет меньшуюсвязность, потому что он выполняет сразу

164

ЧАСТЬ II

Высококачественный код

две функции. Наша цель в том, чтобы каждый метод эффективно решал одну за#
дачу и больше ничего не делал.
Вознаграждением будет более высокая надежность кода. В одном иссле#
довании 450 методов было обнаружено, что дефекты отсутствовали в 50%
методов, обладающих высокой связностью, и только в 18% методов с
низкой связностью (Card, Church, and Agresti, 1986). Другое исследование 450
методов (это просто совпадение, хотя и весьма необычное) показало, что в срав#
нении с методами, имеющими самое низкое отношение «сопряжение/связность»
(coupling#to#cohesion), методы с максимальным отношением «сопряжение/связ#
ность» содержали в 7 раз больше ошибок, а исправление этих методов было в 20
раз более дорогим (Selby and Basili, 1991).
Обсуждение связности обычно касается нескольких ее уровней. Понять эти кон#
цепции важнее, чем запомнить специфические термины. Используйте концепции
как средства, помогающие сделать методы максимально связными.
Функциональная связность — самый сильный и лучший вид связности; она име#
ет место, когда метод выполняет одну и только одну операцию. Примерами мето#
дов, обладающих высокой связностью, являются методы sin() (синус), GetCusto%
merName() (получить фамилию заказчика), EraseFile() (удалить файл), Calculate%
LoanPayment() (вычислить плату за кредит) и AgeFromBirthdate() (определить воз#
раст по дате рождения). Конечно, такая оценка связности предполагает, что эти
методы соответствуют своим именам — иначе они имеют неудачные имена, а об
их связности нельзя сказать ничего определенного.
Ниже описаны другие виды связности, которые обычно считаются менее эффек#
тивными.
 Последовательная связность (sequential cohesion) наблюдается в том случае,

когда метод содержит операции, которые обязательно выполняются в опре#
деленном порядке, используют данные предыдущих этапов и не формируют в
целом единую функцию.
Примером метода с последовательной связностью является метод, вычисляю#
щий по дате рождения возраст сотрудника и срок до его ухода на пенсию. Если
метод вычисляет возраст и затем использует этот результат для нахождения
срока до ухода сотрудника на пенсию, он имеет последовательную связность.
Если метод находит возраст сотрудника, после чего в абсолютно другом вы#
числении определяет срок до ухода на пенсию, применяя те же данные о дате
рождения, он имеет только коммуникационную связность.
Как сделать такой метод функционально связным? Создать два отдельных ме#
тода: метод, вычисляющий по дате рождения возраст сотрудника, и метод,
определяющий по дате рождения срок до ухода сотрудника на пенсию. Вто#
рой метод мог бы вызывать метод нахождения возраста. Оба этих метода име#
ли бы функциональную связность. Другие методы могли бы вызывать любой
из них или оба.
 Коммуникационная связность (communicational cohesion) имеет место, когда вы#

полняемые в методе операции используют одни и те же данные и не связаны между
собой иным образом. Если метод печатает отчет, после чего заново инициализи#

ГЛАВА 7 Высококачественные методы

165

рует переданные в него данные, он имеет коммуникационную связность: две опе#
рации объединяет только то, что они обращаются к одним и тем же данным.
Чтобы повысить связность этого метода, выполняйте повторную инициализа#
цию данных около места их создания, которое не должно находиться в мето#
де печати отчета. Разделите операции на два метода: первый будет печатать
отчет, а второй — выполнять повторную инициализацию данных неподалеку
от кода, создающего или изменяющего данные. Вызовите оба этих метода вместо
первоначального метода, имевшего коммуникационную связность.
 Временная связность (temporal cohesion) наблюдается, когда операции объе#

диняются в метод на том основании, что все они выполняются в один интер#
вал времени. Типичные примеры — методы Startup() (запуск программы) Comp%
leteNewEmployee() (прием нового сотрудника на работу) и Shutdown() (завер#
шение программы). Временную связность порой считают неприемлемой, по#
скольку иногда она связана с плохими методиками программирования, таки#
ми как включение слишком разнообразного кода в метод Startup().
Для устранения этой проблемы рассматривайте методы с временной связнос#
тью как способы организации других событий. Так, метод Startup() мог бы читать
конфигурационный файл, инициализировать вспомогательный файл, настра#
ивать менеджер памяти и выводить первоначальное окно программы. Чтобы
сделать метод с временной связностью максимально эффективным, не выпол#
няйте в нем конкретных операций непосредственно, а вызывайте для их вы#
полнения другие методы. Тогда всем будет ясно, что суть метода — согласова#
ние действий, а не их выполнение.
Этот пример поднимает вопрос выбора имени, описывающего такой метод с
адекватным уровнем абстракции. Вы могли бы назвать метод ReadConfigFileIn%
itScratchFileEtc() (прочитать конфигурационный файл, инициализировать вспо#
могательный файл и т. д.), но из этого следовало бы, что он имеет только слу#
чайную связность. Если же вы назовете метод Startup(), будет очевидно, что он
имеет одну цель и поэтому обладает функциональной связностью.
Остальные виды связности обычно неприемлемы. Они приводят к созданию плохо
организованного кода, который трудно отлаживать и изменять. Метод с плохой
связностью лучше переписать, чем тратить время и средства на поиск проблем.
Однако знание того, чего следует избегать, может пригодиться, поэтому ниже я
привел описания плохих видов связности.
 Процедурная связность (procedural cohesion) имеет место, когда операции в

методе выполняются в определенном порядке. В качестве примера можно при#
вести метод, получающий фамилию сотрудника, затем его адрес, а после это#
го номер телефона. Порядок этих операций важен только потому, что он со#
ответствует порядку, в котором пользователя просят ввести данные. Остальные
данные о сотруднике получает другой метод. В данном случае операции вы#
полняются в определенном порядке и не объединены больше ничем, поэтому
метод имеет процедурную связность.
Для достижения лучшей связности поместите разные операции в отдельные
методы. Сделайте так, чтобы вызывающий метод решал одну задачу, причем пол#
ностью: пусть он соответствует имени GetEmployee() (получить данные о со#

166

ЧАСТЬ II

Высококачественный код

труднике), а не GetFirstPartOfEmployeeData() (получить первую часть данных о
сотруднике). Вероятно, при этом придется изменить и методы, получающие
остальные данные. Довольно часто достижение функциональной связности
требует изменения двух или более первоначальных методов.
 Логическая связность (logical cohesion) имеет место, когда метод включает

несколько операций, а выбор выполняемой операции осуществляется на ос#
нове передаваемого в метод управляющего флага. Этот вид связности называ#
ется логическим потому, что операции метода объединены только управляю#
щей «логикой» метода: крупным оператором if или рядом блоков case. Какой#
нибудь другой по#настоящему «логической» связи между операциями нет. По#
скольку определяющим атрибутом логической связности является отсутствие
отношений между операциями, возможно, лучше было бы назвать ее «нелогич#
ной связностью».
В качестве примера такого метода можно привести метод InputAll(), принима#
ющий в зависимости от полученного флага фамилии клиентов, данные карт
учета времени сотрудников или инвентаризационные данные. Другие приме#
ры — методы ComputeAll(), EditAll(), PrintAll() и SaveAll(). Главная проблема с ними
в том, что передавать флаг для управления работой метода нецелесообразно.
Вместо метода, выполняющего одну из трех операций в зависимости от полу#
ченного флага, лучше создать три метода, выполняющих по одной операции.
Если операции используют некоторый одинаковый код или общие данные, код
следует переместить в метод более низкого уровня, а методы упаковать в класс.
Перекрестная ссылка Связность
такого метода может быть удовлетворительной, однако при
этом возникает один вопрос
проектирования более высокого уровня: использовать ли операторы case вместо полиморфного метода? См. также подраздел «Замена условных операторов (особенно многочисленных
блоков case) на вызов полиморфного метода» раздела 24.3.

Однако логически связный метод вполне приемлем, если его
код состоит исключительно из ряда операторов if или case
и вызовов других методов. Если единственная роль метода
— координация выполнения команд и сам он не выполня#
ет действий, это обычно удачное проектное решение. Такие
методы еще называют «обработчиками событий». Обработ#
чики часто используются в интерактивных средах, таких как
Apple Macintosh, Microsoft Windows и других средах с GUI.
 При случайной связности (coincidental cohesion) каких#

либо ясных отношений между выполняемыми в методе опе#
рациями нет. Этот вариант можно еще называть «отсутстви#
ем связности» или «хаотичной связностью». Низкокачествен#
ный метод C++, приведенный в начале этой главы, имеет случайную связность.
Случайную связность трудно преобразовать в более приемлемый вид связнос#
ти — как правило, методы со случайной связностью нужно проектировать и
реализовать заново.
Никакой из этих терминов не является магическим или священным. Изу#
чайте идеи, а не терминологию. Стремитесь создавать методы с функци#
ональной связностью — это возможно почти всегда.

ГЛАВА 7 Высококачественные методы

7.3.

167

Удачные имена методов

Имя метода должно ясно описывать все, что он делает. Со#
веты по выбору удачных имен методов приведены ниже.

Перекрестная ссылка Об именовании переменных см. главу 11.

Описывайте все, что метод выполняет Опишите в
имени метода все выходные данные и все побочные эффекты. Если метод вычис#
ляет сумму показателей в отчете и открывает выходной файл, имя ComputeReport%
Totals() не будет адекватным. ComputeReportTotalsAndOpenOutputFile() — имя адек#
ватное, но слишком длинное и несуразное. Создавая методы с побочными эффек#
тами, вы получите много длинных несуразных имен. Выход из этого положения
— не использование менее описательных имен, а создание ясных методов без по#
бочных эффектов.
Избегайте невыразительных и неоднозначных глаголов Некоторые глаголы
могут описывать практически любое действие. Имена вроде HandleCalculation(),
PerformServices(), OutputUser(), ProcessInput() и DealWithOutput() не говорят о ра#
боте методов почти ничего. В лучшем случае по этим именам можно догадаться,
что методы имеют какое#то отношение к вычислениям, сервисам, пользователям,
вводу и выводу соответственно. Исключением было бы использование глагола
«handle» в специфическом техническом смысле обработки события.
Иногда единственным недостатком метода является невыразительность
его имени; сам метод при этом может быть спроектирован очень хоро#
шо. Если имя HandleOutput() заменить на FormatAndPrintOutput(), роль
метода станет очевидной.
В других случаях невыразительность глагола в имени метода может объясняться
аналогичным поведением метода. Неясная цель — невыразительное имя. Если это
так, лучше всего выполнить реструктуризацию метода и всех родственных мето#
дов, чтобы все они получили более четкие цели и более выразительные имена,
точно их описывающие.
Не используйте для дифференциации имен методов исключитель'
но номера Один разработчик написал весь свой код в форме единствен#
ного объемного метода. Затем он разбил код на фрагменты по 15 строк
и создал методы Part1, Part2 и т. д. После этого он создал один высокоуровневый
метод, вызывающий каждую часть кода. Подобный способ создания и именова#
ния методов глуп до невозможности (и столь же редок, надеюсь). И все же про#
граммисты иногда используют номера для дифференциации таких методов, как
OutputUser, OutputUser1 и OutputUser2. Номера в конце каждого из этих имен ни#
чего не говорят о различиях представляемых методами абстракций, поэтому та#
кие имена нельзя признать удачными.
Не ограничивайте длину имен методов искусственными правилами Ис#
следования показывают, что оптимальная длина имени переменной равняется в
среднем 9–15 символам. Как правило, методы сложнее переменных, поэтому и
адекватные имена методов обычно длиннее. В то же время к именам методов ча#
сто присоединяются имена объектов, что по сути предоставляет методам часть
имени «бесплатно». Главной задачей имени метода следует считать как можно более
ясное и понятное описание сути метода, поэтому имя может иметь любую длину,
удовлетворяющую этой цели.

168

ЧАСТЬ II

Высококачественный код

Для именования функции используйте описание воз'
вращаемого значения Функция возвращает значение, и
это следует должным образом отразить в ее имени. Так,
имена cos(), customerId . Next(), printer . IsReady() и pen . Current%
Color() ясно указывают, что возвращают функции, и потому являются удачными.

Перекрестная ссылка О различии между процедурами и функциями см. раздел 7.6.

Для именования процедуры используйте выразительный глагол, дополняя
его объектом Процедура с функциональной связностью обычно выполняет опе#
рацию над объектом. Имя должно отражать выполняемое процедурой действие и
объект, над которым оно выполняется, что приводит нас к формату «глагол +
объект». Примеры удачных имен процедур — PrintDocument(),CalcMonthlyRevenues(),
CheckOrderInfo() и RepaginateDocument().
В случае объектно#ориентированных языков имя объекта в имя процедуры вклю#
чать не нужно, потому что объекты и так входят в состав вызовов, принимающих
вид document . Print(), orderInfo . Check() и monthlyRevenues . Calc(). Имена вида docu%
ment . PrintDocument() страдают от избыточности и могут стать в производных
классах неверными. Если Check — класс, производный от класса Document, суть вы#
зова check . Print() кажется очевидной: печать чека. В то же время вызов check. Print%
Document() похож на печать записи чековой книжки или ежемесячной выписки
со счета, но никак не чека.
Перекрестная ссылка Похожий
список антонимов, используемых в именах переменных, см.
в подразделе «Антонимы, часто встречающиеся в именах
переменных» раздела 11.1.

add/remove
begin/end
create/destroy
first/last
get/put
get/set

Дисциплинированно используйте антонимы Приме#
нение конвенций именования, подразумевающих использо#
вание антонимов, поддерживает согласованность имен, что
облегчает чтение кода. Антонимы вроде first/last понятны
всем. Пары вроде FileOpen() и _lclose() несимметричны и
вызывают замешательство. Вот некоторые антонимы, попу#
лярные в программировании:

increment/decrement
insert/delete
lock/unlock
min/max
next/previous
old/new

open/close
show/hide
source/target
start/stop
up/down

Определяйте конвенции именования часто используемых операций При
работе над некоторыми системами важно различать разные виды операций. Са#
мым легким и надежным способом определения этих различий часто оказывает#
ся конвенция именования.
В одном из моих проектов каждый объект имел уникальный идентификатор. Мы
не потрудились выработать конвенцию именования методов, возвращающих иден#
тификатор объекта, и в итоге получили такие имена, как:

employee.id.Get()
dependent.GetId()
supervisor()
candidate.id()
Класс Employee предоставлял доступ к объекту id, который в свою очередь вклю#
чал метод Get(). Класс Dependent предоставлял для этой цели метод GetId(). Разра#

ГЛАВА 7 Высококачественные методы

169

ботчик класса Supervisor сделал id значением, возвращаемым по умолчанию. Класс
Candidate предоставлял доступ к объекту id, который по умолчанию возвращал
значение идентификатора. К середине проекта никто из нас уже не мог вспом#
нить, какой из методов предполагалось использовать для того или иного объек#
та, но мы уже написали слишком много кода, чтобы возвращаться назад и все
согласовывать. Поэтому каждому члену группы пришлось тратить лишние усилия
на запоминание несогласованных подробностей синтаксиса получения id из каж#
дого класса. Конвенция именования, определяющая получение id, сделала бы та#
кую неприятность невозможной.

7.4.

Насколько объемным может быть метод?

На пути в Америку пилигримы 1 спорили о лучшей максимальной длине метода.
И вот они прибыли к Плимутскому камню и начали составлять Мейфлауэрское
соглашение. О максимальной длине методов пилигримы так и не договорились,
а так как до подписания соглашения они не могли высадиться на берег, то сда#
лись и не включили этот пункт в соглашение. Результатом стали нескончаемые
дебаты о допустимой длине методов.
В теоретических работах длину метода часто советуют ограничивать числом строк,
помещающихся на экране монитора, или же одной#двумя страницами, что соот#
ветствует примерно 50–150 строкам. Следуя этому правилу, в IBM однажды огра#
ничили методы 50 строками, а в TRW — двумя страницами (McCabe, 1976). Со#
временные программы обычно включают массу очень коротких методов, вызы#
ваемых из нескольких более крупных методов. Однако длинные методы далеки
от вымирания. Незадолго до завершения работы над этой книгой я в течение месяца
посетил двух клиентов. В одном случае программисты боролись с методом, вклю#
чавшим примерно 4000 строк, а во втором пытались укротить метод, содержав#
ший более 12 000 строк!
Длина методов уже давно стала предметом исследований. Некоторые из них
устарели, а другие актуальны и по сей день.
 Базили и Перриконе обнаружили обратную корреляцию между
размером метода и уровнем ошибок: при росте размера методов (вплоть
до 200 строк) число ошибок в расчете на одну строку снижалось (Basili
and Perricone, 1984).
 Другое исследование показало, что с числом ошибок коррелировали структурная

сложность и объем используемых данных, но не размер метода (Shen et al., 1985).
 В исследовании 1986 г. было обнаружено, что небольшой размер методов (32

строки или менее) не коррелировал с меньшими затратами на их разработку
или меньшим числом дефектов (Card, Church, and Agresti, 1986; Card and Glass,

1

Пилигримы (pilgrims) — пассажиры английского судна «Мейфлауэр» («Mayflower»), основатели
Плимутской колонии в Северной Америке, заключившие Мейфлауэрское соглашение (Mayflower
Compact) о создании «гражданской политической организации» для поддержания порядка и
безопасности, «принятия справедливых и обеспечивающих равноправие законов». Плимутский
камень (Plymouth Rock) — по преданию гранитная глыба, на которую ступил первый сошед#
ший с корабля пилигрим в декабре 1620 г. Почитается в США как национальная святыня. — Прим.
перев.

170

ЧАСТЬ II

Высококачественный код

1990). Разработка крупных методов (65 строк или более) в расчете на одну
строку кода была дешевле.
 Опытное изучение 450 методов показало, что небольшие методы (включавшие

менее 143 команд исходного кода с учетом комментариев) содержали на 23%
больше ошибок в расчете на строку кода, чем более крупные методы, но ис#
правление меньших методов было в 2,4 раза менее дорогим (Selby and Basili,
1991).
 Исследования позволили обнаружить, что код требовал минимальных измене#

ний, если методы состояли в среднем из 100–150 строк (Lind and Vairavan, 1989).
 Исследование, проведенное в IBM, показало, что максимальный уровень оши#

бок был характерен для методов, размер которых превышал 500 строк кода.
При дальнейшем увеличении методов уровень ошибок возрастал пропорцио#
нально числу строк (Jones, 1986a).
Так какую же длину методов считать приемлемой в объектно#ориентированных
программах? Многие методы в объектно#ориентированных программах будут ме#
тодами доступа, обычно очень короткими. Время от времени реализация сложно#
го алгоритма будет требовать создания более длинного метода, и тогда методу можно
будет позволить вырасти до 100–200 строк (строкой считается непустая строка
исходного кода, не являющаяся комментарием). Десятилетия исследований гово#
рят о том, что методы такой длины не более подвержены ошибкам, чем методы
меньших размеров. Пусть длину метода определяют не искусственные ограниче#
ния, а такие факторы, как связность метода, глубина вложенности, число перемен#
ных, число точек принятия решений, число комментариев, необходимых для объяс#
нения метода, и другие соображения, связанные со сложностью кода.
Что касается методов, включающих более 200 строк, то к ним следует относиться
настороженно. Ни в одном из исследований, в которых было обнаружено, что более
крупным методам соответствует меньшая стоимость разработки, меньший уровень
ошибок или оба фактора, эта тенденция не усиливалась при увеличении размера
свыше 200 строк, а при превышении этого предела методы неизбежно становят#
ся менее понятными.

7.5.

Советы по использованию параметров
методов

Интерфейсы между методами — один из основных источников ошибок.
В одном часто цитируемом исследовании, проведенном Базили и Пер#
риконе (Basili and Perricone, 1984), было обнаружено, что 39% всех оши#
бок были ошибками внутренних интерфейсов — ошибками коммуникации меж#
ду методами. Вот несколько советов по предотвращению подобных проблем.
Перекрестная ссылка О документировании параметров методов см. подраздел «Комментирование методов» раздела 32.5,
а о форматировании параметров — раздел 31.7.

Передавайте параметры в порядке «входные значения
— изменяемые значения — выходные значения» Вме#
сто упорядочения параметров случайным образом или по
алфавиту указывайте в списке сначала исключительно вход#
ные параметры, затем входные#и#выходные параметры и на#
конец — исключительно выходные параметры. Такой по#

ГЛАВА 7 Высококачественные методы

171

рядок соответствует последовательности выполняемых в методе операций: ввод
данных, их изменение и возврат результата. Вот примеры списков параметров, на#
писанные на языке Ada:

Примеры размещения параметров в порядке «входные значения —
изменяемые значения — выходные значения» (Ada)
procedure InvertMatrix(
В языке Ada ключевые слова in и out поясняют суть входных и выходных параметров.

>

originalMatrix: in Matrix;
resultMatrix: out Matrix

);
...
procedure ChangeSentenceCase(
desiredCase: in StringCase;
sentence: in out Sentence
);
...
procedure PrintPageNumber(
pageNumber: in Integer;
status: out StatusType
);
Такая конвенция упорядочения параметров противоречит конвенции библиотек
C, предполагающей указание изменяемого параметра в первую очередь. Конвен#
ция «входные значения — изменяемые значения — выходные значения» кажется
мне более разумной, но, даже если вы будете согласованно упорядочивать пара#
метры любым иначе, вы окажете услугу программистам, которым придется читать
ваш код.
Подумайте о создании собственных ключевых слов in и out В отличие от
Ada другие языки не поддерживают ключевые слова in и out. Однако даже в этом
случае вы скорее всего сможете создать их с помощью препроцессора:

Пример определения собственных ключевых слов In и Out (C++)
#define IN
#define OUT
void InvertMatrix(
IN Matrix originalMatrix,
OUT Matrix *resultMatrix
);
...
void ChangeSentenceCase(
IN StringCase desiredCase,
IN OUT Sentence *sentenceToEdit
);
...

172

ЧАСТЬ II

Высококачественный код

void PrintPageNumber(
IN int pageNumber,
OUT StatusType &status
);
В данном примере ключевые слова#макросы IN и OUT используются для докумен#
тирования. Чтобы значение параметра можно было изменить в вызванном мето#
де, параметр все же нужно передавать по указателю или по ссылке.
Прежде чем принять этот подход, обдумайте два его важных недостатка. Собствен#
ные ключевые слова IN и OUT окажутся незнакомыми большинству программис#
тов, которые будут читать ваш код. Расширяя язык таким образом, делайте это
согласованно, лучше всего в масштабе всего проекта. Второй недостаток в том,
что компилятор не будет проверять соответствие параметров ключевым словам
IN и OUT, из#за чего вы сможете отметить параметр как IN и все же изменить его
внутри метода. Так вы только введете программиста, читающего ваш код, в за#
блуждение. Обычно для определения исключительно входных параметров лучше
применять ключевое слово const языка C++.
Если несколько методов используют похожие параметры, передавайте
их в согласованном порядке Порядок параметров может как облегчить, так и
затруднить их запоминание. Например, в C прототипы методов fprintf() и printf()
различаются только тем, что fprintf() принимает файл в качестве дополнительно#
го первого аргумента. Похожее отношение наблюдается и между методами fputs()
и puts(), но в fputs() файл передается последним. Это досадное различие только
затрудняет запоминание параметров названных методов.
С другой стороны, методы strncpy() и memcpy() в том же C принимают аргументы
в одинаковом порядке: строка#приемник, строка#источник и максимальное число
копируемых байт. Такое сходство помогает запомнить параметры обоих методов.
Используйте все параметры Если вы передаете параметр в метод, ис#
пользуйте его, в противном случае удалите параметр из интерфейса ме#
тода. Наличие неиспользуемых параметров соответствует более высоко#
му уровню ошибок. Исследования показали, что ошибки отсутствовали в 46% ме#
тодов, не включавших неиспользуемых переменных, и только в 17–29% методов,
содержавших более одной неиспользуемой переменной (Card, Church, and Agresti,
1986).
Это правило допускает одно исключение. При условной компиляции кода из ком#
пиляции могут быть исключены части метода, использующие некоторый параметр.
Опасайтесь этого подхода, но, если вы убеждены, что все правильно, он вполне
допустим. В общем, если у вас есть серьезная причина не использовать параметр,
оставьте его в списке. Если таковой нет, очистите интерфейс метода от примесей.
Передавайте переменные статуса или кода ошибки последними Перемен#
ные статуса и переменные, указывающие на ошибку, следует располагать в спис#
ке параметров последними. Они второстепенны по отношению к главной цели
метода и являются исключительно выходными параметрами, поэтому такая кон#
венция вполне разумна.

ГЛАВА 7 Высококачественные методы

173

Не используйте параметры метода в качестве рабочих переменных Ис#
пользовать передаваемые в метод параметры как рабочие переменные опасно.
Создайте для этой цели локальные переменные. Так, в следующем фрагменте Java#
кода переменная inputVal некорректно служит для хранения промежуточных ре#
зультатов вычислений:

Пример некорректного использования входного параметра (Java)
int Sample( int inputVal ) {
inputVal = inputVal * CurrentMultiplier( inputVal );
inputVal = inputVal + CurrentAdder( inputVal );
...
Переменная inputVal уже не содержит входного значения.

>

return inputVal;

}

В этом фрагменте переменная inputVal вводит в заблуждение, потому что при за#
вершении метода она больше не содержит входного значения; она содержит резуль#
тат вычисления, частично основанного на входном значении, и поэтому ее имя
неудачно. Если позднее вам придется задействовать первоначальное входное зна#
чение в другом месте метода, вы, вероятно, задействуйте переменную inputVal, пред#
полагая, что она содержит первоначальное значение, но это предположение бу#
дет ошибочным.
Можно ли решить эту проблему путем переименования inputVal? Наверное, нет.
Переменной можно было бы присвоить имя вроде workingVal, но такое решение было
бы неполным, так как это имя не говорит о том, что первоначальное значение пе#
ременной передается в метод извне. Вы могли бы присвоить ей нелепое имя input%
ValThatBecomesWorkingVal (входное значение, которое становится рабочим значе#
нием) или сдаться и просто назвать ее x или val, но все эти подходы неудачны.
Лучше избегать настоящих и будущих проблем, используя рабочие переменные
явно, например:

Пример корректного использования входного параметра (Java)
int Sample( int inputVal ) {
int workingVal = inputVal;
workingVal = workingVal * CurrentMultiplier( workingVal );
workingVal = workingVal + CurrentAdder( workingVal );
...
Если первоначальное значение inputVal понадобится здесь или где#то еще, оно все
еще доступно.

...
return workingVal;
}

174

ЧАСТЬ II

Высококачественный код

Создание новой переменной workingVal поясняет роль inputVal и исключает воз#
можность ошибочного использования inputVal в неподходящий момент. (Не рас#
сматривайте это рассуждение как оправдание присвоения переменным имен
inputVal или workingVal. Имена inputVal и workingVal просто ужасны и служат в данном
примере только для пояснения ролей переменных.)
Присвоение входного значения рабочей переменной подчеркивает тот факт, что
значение поступает в метод извне. Кроме того, это исключает возможность слу#
чайного изменения параметров. В C++ ответственность за это можно возложить
на компилятор при помощи ключевого слова const. Отметив параметр как const,
вы не сможете изменить его значение внутри метода.
Документируйте выраженные в интерфейсе предпо'
ложения о параметрах Если вы предполагаете, что пе#
редаваемые в метод данные должны иметь определенные
характеристики, сразу же документируйте эти предположе#
ния. Документирование предположений и в самом методе,
и в местах его вызова нельзя назвать пустой тратой времени. Пишите коммента#
рии, не дожидаясь завершения работы над методом: к тому времени вы многое
забудете. Еще лучше применить утверждения (assertions), позволяющие встроить
предположения в код.

Перекрестная ссылка О связанных с интерфейсами предположениях см. также главу 8, о документировании кода — главу 32.

Какие типы предположений о параметрах следует документировать? Вот какие:
 вид параметров: являются ли они исключительно входными, изменяемыми или

исключительно выходными;
 единицы измерения (дюймы, футы, метры и т. д.);
 смысл кодов статуса и ошибок, если для их представления не используются

перечисления;
 диапазоны допустимых значений;
 специфические значения, которые никогда не должны передаваться в метод.

Ограничивайте число параметров метода примерно семью 7 —
магическое число. Психологические исследования показали, что люди,
как правило, не могут следить более чем за семью элементами инфор#
мации сразу (Miller, 1956). Это открытие используется в огромном числе дисцип#
лин, поэтому резонно предположить, что большинство людей не может удержи#
вать в уме более семи параметров метода одновременно.
На практике возможность ограничения числа параметров
зависит от того, как в выбранном вами языке реализована
поддержка сложных типов данных. Программируя на совре#
менном языке, поддерживающем структурированные дан#
ные, вы можете передать в метод составной тип данных, содержащий 13 полей, и
рассматривать его как один «элемент» данных. При использовании более прими#
тивного языка вам, возможно, придется передать все 13 полей по отдельности.

Перекрестная ссылка Анализ интерфейсов см. в подразделе «Хорошая абстракция» раздела 6.2.

Если вам постоянно приходится передавать в методы слишком большое число
аргументов, ваши методы имеют слишком сильное сопряжение. Проектируйте ме#
тоды или группы методов так, чтобы сопряжение было слабым. Если вы передае#
те одни и те же данные во многие разные методы, сгруппируйте эти методы и дан#
ные в класс.

ГЛАВА 7 Высококачественные методы

175

Подумайте об определении конвенции именования входных, изменяемых
и выходных параметров Если нужно провести различие между входными, из#
меняемыми и выходными параметрами, сформулируйте соответствующую конвен#
цию их именования. Например, вы можете дополнить их префиксами i_, m_ и o_.
Программисты пословоохотливее могут использовать префиксы Input_, Modify_ и
Output_.
Передавайте в метод те переменные или объекты, которые нужны ему
для поддержания абстракции интерфейса Есть два конкурирующих под#
хода к передаче членов объекта в методы. Допустим, у вас есть объект, предостав#
ляющий доступ к данным посредством 10 методов доступа, но вызываемому ме#
тоду нужны лишь три элемента данных объекта.
Сторонники первого подхода утверждают, что в метод следует передать только
три нужных ему элемента. Они считают, что это позволяет поддерживать мини#
мальное сопряжение между методами, способствует пониманию методов, облег#
чает их повторное использование и т. д. Они говорят, что передача всего объекта
в метод нарушает принцип инкапсуляции, позволяя вызванному методу исполь#
зовать все 10 методов доступа.
Сторонники второго подхода утверждают, что следует передать весь объект. Они
говорят, что если вызываемый метод получит доступ к дополнительным членам
объекта, это позволит сохранить стабильность интерфейса метода. Им кажется, что
именно передача трех конкретных элементов нарушает инкапсуляцию, потому что
это указывает на конкретные элементы данных, используемые методом.
Я думаю, что оба этих правила слишком упрощены и не учитывают самого важ#
ного: какую абстракцию формирует интерфейс метода? Если абстракция под#
разумевает, что метод ожидает три конкретных элемента данных, которые по
чистой случайности принадлежат одному объекту, передайте три элемента по
отдельности. Если же абстракция состоит в том, что элементы данных всегда при#
надлежат конкретному объекту, над которым метод должен выполнять ту или иную
операцию, тогда, раскрывая три этих специфических элемента, вы на самом деле
нарушаете абстракцию.
Если при передаче всего объекта вы создаете объект, заполняете его тремя эле#
ментами, нужными методу, а после вызова извлекаете эти элементы из объекта,
значит, вам следует передать в метод только три конкретных элемента, а не весь
объект. (Обычно наличие кода, «подготавливающего» данные перед вызовом ме#
тода или «разбирающего» объект после вызова, — признак неудачного проекти#
рования метода.)
Если же вам часто приходится изменять список параметров метода, при этом
каждый раз параметры относятся к одному и тому же объекту, в метод следует
передавать весь объект, а не конкретные элементы.
Используйте именованные параметры Некоторые языки позволяют явно со#
поставить формальные параметры с фактическими. Это делает применение па#
раметров более ясным и помогает избегать ошибок, обусловленных неправиль#
ным сопоставлением параметров, например:

176

ЧАСТЬ II

Высококачественный код

Пример явной идентификации параметров (Visual Basic)
Private Function Distance3d( _
Объявления формальных параметров.

ByVal xDistance As Coordinate, _
ByVal yDistance As Coordinate, _
ByVal zDistance As Coordinate _

>
)

...
End Function
...
Private Function Velocity( _
ByVal latitude as Coordinate, _
ByVal longitude as Coordinate, _
ByVal elevation as Coordinate _
)
...
Сопоставление фактических параметров с формальными.

>

Distance = Distance3d( xDistance := latitude, yDistance := longitude, _
zDistance := elevation )
...
End Function
Данный подход особенно полезен при использовании длинных списков параметров
одинакового типа, потому что в этом случае вероятность неправильного сопо#
ставления параметров более высока, а компилятор эту ошибку определить не мо#
жет. Во многих средах явное сопоставление параметров может оказаться пальбой
из пушки по воробьям, но в средах, от которых зависит безопасность людей, или
других средах с повышенными требованиями к надежности дополнительный спо#
соб гарантии правильного сопоставления параметров не помешает.
Убедитесь, что фактические параметры соответствуют формаль'
ным Формальные параметры, известные также как «фиктивные параметры» (dum#
my parameters), — это переменные, объявленные в определении метода. Факти#
ческими параметрами называют переменные, константы или выражения, на са#
мом деле передаваемые в метод.
По невнимательности довольно часто передают в метод переменную неверного
типа — например, целое число вместо числа с плавающей запятой. (Эта пробле#
ма характерна только для слабо типизированных языков, таких как C, при исполь#
зовании неполного набора предупреждений компилятора. Строго типизирован#
ные языки, такие как C++ и Java, не имеют этого недостатка.) Если аргументы яв#
ляются исключительно входными, это редко становится проблемой: обычно ком#
пилятор при вызове метода преобразует фактический тип в формальный. Если это
приводит к проблеме, компилятор обычно генерирует предупреждение. Но иногда,
особенно если аргумент является и входным, и выходным, передача аргумента
неверного типа может привести к серьезным последствиям.

ГЛАВА 7 Высококачественные методы

177

Постарайтесь всегда проверять типы аргументов в списках параметров и внима#
тельно изучайте предупреждения компилятора о несоответствии типов параметров.

7.6.

Отдельные соображения
по использованию функций

Современные языки, такие как C++, Java и Visual Basic, поддерживают и функции,
и процедуры. Функция — это метод, возвращающий значение; процедуры значе#
ний не возвращают. В C++ все методы обычно называют «функциями», однако, с
точки зрения семантики, функция, «возвращающая» void, является процедурой.
Различие между функциями и процедурами выражено в семантике не слабее, чем
в синтаксисе, и именно семантике следует уделять наибольшее внимание.

Когда использовать функцию, а когда процедуру?
Пуристы утверждают, что функция должна возвращать только одно значение по#
добно математической функции. В этом случае все функции принимали бы толь#
ко входные параметры и возвращали единственное значение традиционным пу#
тем. Функции всегда назывались бы в соответствии с возвращаемым значением:
sin(), CustomerID(), ScreenHeight() и т. д. Процедуры, с другой стороны, могли бы
принимать входные, изменяемые и выходные параметры в любом количестве.
Довольно часто можно встретить функцию, работающую как процедура, но воз#
вращающую при этом код статуса. Логически она является процедурой, но из#за
возврата значения официально ее следует называть функцией. Так, объект report
мог бы иметь метод FormatOutput(), используемый подобным образом:

if ( report.FormatOutput( formattedReport ) = Success ) then ...
В этом примере метод report.FormatOutput() работает как процедура в том смыс#
ле, что он имеет входной параметр formattedReport, но технически это функция,
потому что сам метод тоже возвращает значение. В защиту этого подхода вы мог#
ли бы сказать, что возвращаемое функцией значение не имеет отношения ни к
главной цели функции (форматированию вывода), ни к имени метода (report.%
FormatOutput()). В этом смысле метод больше похож на процедуру, пусть даже
технически он является функцией. Если возвращаемое значение служит для
определения успеха или неудачи выполнения процедуры согласованно, это не
вызывает замешательства.
Альтернативный подход — создание процедуры, принимающей переменную ста#
туса в качестве явного параметра, например:

report.FormatOutput( formattedReport, outputStatus )
if ( outputStatus = Success ) then ...
Я предпочитаю именно этот вариант, но не потому, что трепетно отношусь к раз#
личию между функциями и процедурами, а потому, что такой код ясно разделяет
вызов метода и проверку переменной статуса. Объединение вызова и проверки в
одной строке увеличивает «плотность» команды, а значит, и ее сложность. Следу#
ющий вариант использования функции также хорош:

178

ЧАСТЬ II

Высококачественный код

outputStatus = report.FormatOutput( formattedReport )
if ( outputStatus = Success ) then ...
Словом, используйте функцию, если основная цель метода — возврат
значения, указанного в имени функции. Иначе применяйте процедуру.

Возврат значения из функции
Использование функции сопряжено с риском того, что функция возвратит некор#
ректное значение. Обычно это объясняется наличием нескольких путей выпол#
нения функции, один из которых не устанавливает возвращаемого значения. Следуя
моим советам, вы сведете этот риск к минимуму.
Проверяйте все возможные пути возврата Создав функцию, проработай#
те в уме каждый возможный путь ее выполнения, дабы убедиться, что функция воз#
вращает значение во всех возможных обстоятельствах. Целесообразно инициа#
лизировать возвращаемое значение в начале функции значением по умолчанию:
это будет страховкой на тот случай, если функция не установит корректное воз#
вращаемое значение.
Не возвращайте ссылки или указатели на локальные данные Как только
выполнение метода завершается и локальные данные выходят из области види#
мости, ссылки и указатели на локальные данные становятся некорректными. Если
объект должен возвращать информацию о своих внутренних данных, пусть он
сохранит ее в форме данных — членов класса. Реализуйте для него функции до#
ступа, возвращающие данные#члены, а не ссылки или указатели на локальные
данные.

7.7.

Методы-макросы и встраиваемые методы

Перекрестная ссылка Даже если
ваш язык не поддерживает препроцессор макросов, вы можете создать собственный препроцессор (см. раздел 30.5).

С методами#макросами связаны некоторые уникальные со#
ображения. Следующие правила и примеры рассматриваются
в контексте препроцессора C++. Если вы используете дру#
гой язык или препроцессор, адаптируйте правила к своей
ситуации.

Разрабатывая макрос, заключайте в скобки все, что
можно Так как макросы и их аргументы расширяются в код, следите за тем, чтобы
они расширялись так, как вам нужно. Одну частую проблему иллюстрирует сле#
дующий макрос:

Пример макроса, который расширяется неверно (C++)
#define Cube( a ) a*a*a
Если вы передадите в этот макрос неатомарное значение a, он выполнит умноже#
ние неверно. Так, выражение Cube( x+1 ) расширится в x+1 * x + 1 * x + 1, что из#за
приоритета операций умножения и сложения приведет к получению ошибочного
результата. Вот улучшенная, но все еще не совсем правильная версия этого макроса:

ГЛАВА 7 Высококачественные методы

179

Пример макроса, который все еще расширяется неверно (C++)
#define Cube( a ) (a)*(a)*(a)
Цель уже близка. Однако, если вы используете макрос Cube() в выражении, включа#
ющем операции с более высоким приоритетом, чем умножение, выражение (a)*(a)*(a)
будет вычислено неверно. Что делать? Заключите в скобки все выражение:

Пример макроса, с которым все в порядке (C++)
#define Cube( a ) ((a)*(a)*(a))
Заключайте макрос, включающий несколько команд, в фигурные скобки
Макрос может включать несколько команд, что может привести к проблемам, если
вы будете рассматривать их как единый блок, например:

Пример неправильного макроса, состоящего
из нескольких команд (C++)
#define LookupEntry( key, index ) \
index = (key  10) / 5; \
index = min( index, MAX_INDEX ); \
index = max( index, MIN_INDEX );
...
for ( entryCount = 0; entryCount < numEntries; entryCount++ )
LookupEntry( entryCount, tableIndex[ entryCount ] );
Этот макрос работает не так, как работал бы обычный метод: единственной час#
тью макроса, выполняемой в цикле for, является первая строка:
index = (key - 10) / 5;
Чтобы устранить эту проблему, заключите макрос в фигурные скобки:

Пример правильного макроса, состоящего из нескольких команд (C++)
#define LookupEntry( key, index ) { \
index = (key  10) / 5; \
index = min( index, MAX_INDEX ); \
index = max( index, MIN_INDEX ); \
}
Замена вызововметодов макросами обычно считается рискованным и малопонят#
ным (короче, плохим) подходом, так что используйте его только при необходи#
мости.
Называйте макросы, расширяющиеся в код подобно методам, так, что'
бы при необходимости их можно было заменить методами Конвенция
именования макросов в C++ подразумевает использование только заглавных букв.
Если же макрос может быть заменен методом, называйте его в соответствии с
конвенцией именования методов. Это позволит вам заменять макросы на методы
и наоборот, не изменяя остального кода.

180

ЧАСТЬ II

Высококачественный код

Следование этой рекомендации связано с риском. Если вы часто используете
операции ++ и –– ради их побочных эффектов (в составе других выражений), то,
принимая макросы за методы, вы столкнетесь с неприятностями. Это еще одна
причина избегать побочных эффектов.

Ограничения использования методов-макросов
Современные языки вроде C++ поддерживают много альтернатив макросам:
 ключевое слово const для объявления констант;
 ключевое слово inline для определения функций, которые будут компилиро#

ваться как встраиваемый код;
 шаблоны для безопасного в плане типов определения стандартных операций,

таких как min, max и т. д.;
 ключевое слово enum для определения перечислений;
 директиву typedef для простых замен одного типа другим.

Бьерн Страуструп, создатель C++, пишет: «Макрос почти всегда указыва#
ет на недостаток языка программирования, программы или программи#
ста… Если вы используете макросы, значит, вам не хватает возможностей
отладчиков, инструментов, генерирующих перекрестные ссылки, средств профи#
лирования и т. д.» (Stroustrup, 1997). Макросы полезны для выполнения условной
компиляции (см. раздел 8.6), но добросовестные программисты обычно исполь#
зуют макросы вместо методов только в крайнем случае.

Встраиваемые методы
Язык C++ поддерживает ключевое слово inline, служащее для определения встра#
иваемых методов. Иначе говоря, программист может разрабатывать код как ме#
тод, но во время компиляции компилятор постарается встроить каждый экземп#
ляр метода прямо в код. Теоретически встраивание методов может повысить бы#
стродействие кода, позволяя избежать затрат, связанных с вызовами методов.
Не злоупотребляйте встраиваемыми методами Встраиваемые методы на#
рушают инкапсуляцию, потому что C++ требует, чтобы программист поместил код
встраиваемого метода в заголовочный файл, доступный остальным программистам.
При встраивании метода каждый его вызов заменяется на полный код метода, что
во всех случаях увеличивает объем кода и само по себе может создать проблемы.
Практическое применение встраивания аналогично применению прочих мето#
дик повышения быстродействия кода: профилируйте код и оценивайте результа#
ты. Если ожидаемое повышение быстродействия не оправдывает забот, связанных
с профилированием, нужным для проверки выгоды, оно не оправдывает и сни#
жения качества кода.

Контрольный список: высококачественные методы
Общие вопросы
 Достаточна ли причина создания метода?
 Все ли части метода, которые целесообразно поместить
в отдельные методы, сделаны отдельными методами?

http://cc2e.com/0792

ГЛАВА 7 Высококачественные методы

 Имеет ли имя процедуры вид «выразительный глагол +







181

Перекрестная ссылка Этот кон-

объект»? Описывает ли имя функции возвращаемое из
трольный список позволяет
нее значение?
определить качество методов.
Вопросы, касающиеся этапов
Описывает ли имя метода все выполняемые в методе
создания метода, приведены в
действия?
контрольном списке «Процесс
Задали ли вы конвенции именования часто выполняеПрограммирования Псевдокода»
мых операций?
(глава 9).
Имеет ли метод высокую функциональную связность?
Решает ли он только одну задачу и хорошо ли он с ней справляется?
Имеют ли методы слабое сопряжение? Являются ли связи метода с другими методами малочисленными, детальными, заметными и гибкими?
Обусловлена ли длина метода его ролью и логикой, а не искусственным
стандартом кодирования?

Передача параметров
 Формирует ли в целом список параметров метода согласованную абстракцию интерфейса?
 Разумно ли упорядочены параметры метода? Соответствует ли их порядок
порядку параметров аналогичных методов?
 Документированы ли выраженные в интерфейсе предположения?
 Метод имеет семь параметров или меньше?
 Все ли входные параметры используются?
 Все ли выходные параметры используются?
 Не используются ли входные параметры в качестве рабочих переменных?
 Если метод является функцией, возвращает ли он корректное значение во
всех возможных случаях?

Ключевые моменты
 Самая важная, но далеко не единственная причина создания методов — улуч#

шение интеллектуальной управляемости программы. Сокращение кода — не
такая уж и важная причина; повышение его удобочитаемости, надежности и
облегчение его изменения куда важнее.
 Иногда огромную выгоду можно извлечь, создав отдельный метод для простой

операции.
 Связность методов можно разделить на несколько видов. Самая лучшая — функ#

циональная — достижима практически всегда.
 Имя метода является признаком его качества. Плохое, но точное имя часто

указывает на плохое проектирование метода. Плохое и неточное имя не опи#
сывает роль метода. Как бы то ни было, плохое имя предполагает, что программу
нужно изменить.
 Функцию следует использовать, только когда главной целью метода является

возврат конкретного значения, описываемого именем функции.
 Добросовестные программисты используют методы#макросы с осторожностью

и только в крайнем случае.

182

ЧАСТЬ II

Г Л А В А

Высококачественный код

8

Защитное
программирование

http://cc2e.com/0861

Содержание
 8.1. Защита программы от неправильных входных данных
 8.2. Утверждения
 8.3. Способы обработки ошибок
 8.4. Исключения
 8.5. Изоляция повреждений, вызванных ошибками
 8.6. Отладочные средства
 8.7. Доля защитного кода в промышленной версии
 8.8. Защита от защитного программирования

Связанные темы
 Сокрытие информации: подраздел «Скрывайте секреты (к вопросу о сокрытии

информации)» раздела 5.3
 Дизайн изменений: подраздел «Определите области вероятных изменений» раз#

дела 5.3
 Архитектура программного обеспечения: раздел 3.5
 Дизайн в проектировании: глава 5
 Отладка: глава 23

Защитное программирование не означает защиту своего кода словами:
«Это так работает!» Его идея совпадает с идеей внимательного вождения,
при котором вы готовы к любым выходкам других водителей: вы не по#
страдаете, даже если они совершат что#то опасное. Вы берете на себя ответствен#
ность за собственную защиту и в тех случаях, когда виноват другой водитель.
В защитном программировании главная идея в том, что если методу передаются
некорректные данные, то его работа не нарушится, даже если эти данные испор#
чены по вине другой программы. Обобщая, можно сказать, что в программах всегда
будут проблемы, программы будут модифицироваться и разумный программист
будет учитывать это при разработке кода.

ГЛАВА 8 Защитное программирование

183

Эта глава рассказывает, как защититься от беспощадного мира неверных данных,
событий, которые «никогда» не могут случиться, и других программистских ошибок.
Если вы опытный программист, можете пропустить следующий раздел про обра#
ботку входных данных и перейти к разделу 8.2, который рассказывает об утверж#
дениях.

8.1.

Защита программы от неправильных
входных данных

Вы, возможно, слышали в школе выражение: «Мусор на входе — мусор на выхо#
де» 1 . Это вариант предостережения потребителю от разработчиков ПО: пусть
пользователь остерегается.
Для промышленного ПО принцип «мусор на входе — мусор на выходе»
не слишком подходит. Хорошая программа никогда не выдает мусор не#
зависимо от того, что у нее было на входе. Вместо этого она использует
принципы: «мусор на входе — ничего на выходе», «мусор на входе — сообщение
об ошибке на выходе» или «мусор на входе не допускается». По сегодняшним стан#
дартам «мусор на входе — мусор на выходе» — признак небрежного, небезопас#
ного кода.
Существует три основных способа обработки входных мусорных данных, пере#
численные далее.
Проверяйте все данные из внешних источников Получив данные из фай#
ла, от пользователя, из сети или любого другого внешнего интерфейса, удосто#
верьтесь, что все значения попадают в допустимый интервал. Проверьте, что чис#
ловые данные имеют разрешенные значения, а строки достаточно коротки, что#
бы их можно было обработать. Если строка должна содержать определенный набор
значений (скажем, идентификатор финансовой транзакции или что#либо подоб#
ное), проконтролируйте, что это значение допустимо в данном случае, если же
нет — отклоните его. Если вы работаете над приложением, требующим соблюде#
ния безопасности, будьте особенно осмотрительны с данными, которые могут
атаковать вашу систему: попыткам переполнения буфера, внедренным SQL#коман#
дам, внедренному HTML# или XML#коду, переполнениям целых чисел, данным,
передаваемым системным вызовам и т. п.
Проверяйте значения всех входных параметров метода Проверка значе#
ний входных параметров метода практически то же самое, что и проверка дан#
ных из внешнего источника, за исключением того, что данные поступают из дру#
гого метода, а не из внешнего интерфейса. В разделе 8.5 вы узнаете, как опреде#
лить, какие методы должны проверять свои входные данные.

1

«Garbage in, garbage out». Возможно, эту их школьную поговорку следует перевести нашей сту#
денческой: «Каков стол, таков и стул». — Прим. перев.

184

ЧАСТЬ II

Высококачественный код

Решите, как обрабатывать неправильные входные данные Что делать, если
вы обнаружили неверный параметр? В зависимости от ситуации вы можете выб#
рать один из дюжины подходов, подробно описанных в разделе 8.3.
Защитное программирование — это полезное дополнение к другим способам
улучшения качества программ, описанным в этой книге. Лучший способ защит#
ного кодирования — изначально не плодить ошибок. Итеративное проектирова#
ние, написание псевдокода и тестов до начала кодирования и низкоуровневая
проверка соответствия проекту — это все, что помогает избежать добавления де#
фектов. Поэтому этим технологиям должен быть дан более высокий приоритет,
чем защитному программированию. К счастью, вы можете использовать защит#
ное программирование в сочетании с ними.
Защита от проблем, кажущихся несущественными, может иметь большее значе#
ние, чем можно подумать (рис. 8#1). В оставшейся части этой главы я расскажу о
проверке данных из внешних источников, проверке входных параметров и об#
работке неправильных входных данных.

Рис. 8'1. Часть плавучего моста Interstate%90 в Сиэтле затонула во время шторма,
потому что резервуары были оставлены открытыми. Они наполнились водой,
и мост стал слишком тяжел, чтобы держаться на плаву. Обеспечение защиты
от мелочей во время проектирования может значить больше, чем кажется

8.2.

Утверждения

Утверждение (assertion) — это код (обычно метод или макрос), используемый во
время разработки, с помощью которого программа проверяет правильность сво#
его выполнения. Если утверждение истинно, то все работает так, как ожидалось.
Если ложно — значит, в коде обнаружена ошибка. Например, если система пред#
полагает, что длина файла с информацией о заказчиках никогда не будет превы#
шать 50 000 записей, программа могла бы содержать утверждение, что число за#
писей меньше или равно 50 000. Пока это число меньше или равно 50 000, утвер#

ГЛАВА 8 Защитное программирование

185

ждение будет хранить молчание. Но как только записей станет больше 50 000, оно
громко провозгласит об ошибке в программе.
Утверждения особенно полезны в больших и сложных программах, а также
в программах, требующих высокой надежности. Они позволяют нам
быстрее выявить несоответствия в интерфейсах, ошибки, вкравшиеся при
изменении кода и т. п.
Обычно утверждение принимает два аргумента: логическое выражение, описыва#
ющее предположение, которое должно быть истинным, и сообщение, выводимое
в противном случае. Вот как будет выглядеть утверждение на языке Java, если пе#
ременная denominator должна быть ненулевой:

Пример утверждения (Java)
assert denominator != 0 : ”denominator is unexpectedly equal to 0.”;
В этом утверждении объявляется, что denominator не должен быть равен 0. Пер#
вый аргумент — denominator != 0 — логическое выражение, принимающее зна#
чение true или false. Второй — это сообщение, выводимое, когда первый аргумент
равен false (т. е. утверждение ложно).
Используйте утверждения, чтобы документировать допущения, сделанные в коде,
и чтобы выявить непредвиденные обстоятельства. Например, утверждения мож#
но применять при проверке таких условий:
 значение входного (выходного) параметра попадает в ожидаемый интервал;
 файл или поток открыт (закрыт), когда метод начинает (заканчивает) выпол#

няться;
 указатель файла или потока находится в начале (конце), когда метод начина#

ет (заканчивает) выполняться;
 файл или поток открыт только для чтения, только для записи или для чтения

и записи;
 значение входной переменной не изменяется в методе;
 указатель ненулевой;
 массив или другой контейнер, передаваемый в метод, может вместить по край#

ней мере X элементов;
 таблица инициализирована для помещения реальных значений;
 контейнер пуст (заполнен), когда метод начинает (заканчивает) выполняться;
 результаты работы сложного, хорошо оптимизированного метода совпадают

с результатами метода более медленного, но написанного яснее.
Разумеется, это только основы, и ваши методы будут содержать много более спе#
цифических допущений, которые вы сможете документировать, используя утвер#
ждения.
Утверждения не предназначены для показа сообщений в промышленной версии
— они в основном применяются при разработке и поддержке. Обычно их добав#
ляют при компиляции кода во время разработки и удаляют при компиляции про#
мышленной версии. В период разработки утверждения выявляют противоречи#
вые допущения, непредвиденные условия, некорректные значения, переданные

186

ЧАСТЬ II

Высококачественный код

методам, и т. п. При компиляции промышленной версии они могут быть удалены
и, таким образом, не повлияют на производительность системы.

Создание собственного механизма утверждений
Перекрестная ссылка Создание
собственной процедуры утверждений — хороший пример программирования «с использованием языка», а не просто программирования «на языке». Подробнее об этих различиях см.
раздел 34.4.

Многие языки программирования, включая C++, Java, и
Microsoft Visual Basic, имеют встроенную поддержку утвер#
ждений. Если ваш язык не поддерживает процедуры утвер#
ждений напрямую, их легко написать. Стандартный макрос
assert языка C++ не предусматривает вывода текстового со#
общения. Вот пример улучшенного макроса ASSERT на C++:

Пример макроса утверждения (C++)

#define ASSERT( condition, message ) {
if ( !(condition) ) {
LogError( ” Assertion failed: ”,
#condition, message );
exit( EXIT_FAILURE );
}
}

\
\
\
\
\
\

Общие принципы использования утверждений
Далее перечислены общие положения по применению утверждений.
Используйте процедуры обработки ошибок для ожидаемых событий и
утверждения для событий, которые происходить не должны Утверждения
проверяют условия событий, которые никогда не должны происходить. Обработчик
ошибок проверяет внештатные события, которые могут и не происходить слишком
часто, но были предусмотрены писавшим код программистом и должны обрабаты#
ваться и в промышленной версии. Обработчик ошибок обычно проверяет некоррек#
тные входные данные, утверждения — ошибки в программе.
Если для обработки аномальной ситуации служит обработчик ошибок, он позво#
лит программе адекватно отреагировать на ошибку. Если же в случае аномальной
ситуации сработало утверждение, для исправления просто отреагировать на ошибку
мало — необходимо изменить исходный код программы, перекомпилировать и
выпустить новую версию ПО.
Будет правильно рассматривать утверждения как выполняемую документацию —
работать программу с их помощью вы не заставите, но вы можете документиро#
вать допущения в коде более активно, чем это делают комментарии языка про#
граммирования.
Старайтесь не помещать выполняемый код в утверждения Если в утвер#
ждении содержится код, возникает возможность удаления этого кода компилято#
ром при отключении утверждений. Допустим, у вас есть следующее утверждение:

ГЛАВА 8 Защитное программирование

Пример опасного использования утверждения (Visual Basic)
Debug.Assert( PerformAction() ) ’ Невозможно выполнить действие.
Проблема здесь в том, что, если вы не компилируете утвер#
ждения, вы не компилируете и код, который выполняет ука#
занное действие. Вместо этого поместите выполняемые
выражения в отдельных строках, присвойте результаты ста#
тусным переменным и проверяйте значения этих перемен#
ных. Вот пример безопасного использования утверждения:

187

Перекрестная ссылка Можете
рассматривать этот случай как
одну из многих проблем, связанных с размещением нескольких
операторов на одной строке.
Другие примеры см. в подразделе «Размещение одного оператора на строке» раздела 31.5.

Пример безопасного использования утверждения (Visual Basic)
actionPerformed = PerformAction()
Debug.Assert( actionPerformed ) ’

Невозможно выполнить действие.

Используйте утверждения для документирования и
проверки предусловий и постусловий Предусловия и
постусловия — это часть подхода к проектированию и раз#
работке программ, известному как «проектирование по кон#
тракту» (Meyer, 1997). При использовании пред# и постусло#
вий каждый метод или класс заключает контракт с остальной

Дополнительные сведения О предусловиях и постусловиях см.
«Object-Oriented Software Construction» (Meyer, 1997).

частью программы.

Предусловия — это соглашения, которые клиентский код, вызывающий метод или класс,
обещает выполнить до вызова метода или создания экземпляра объекта. Предусло#
вия — это обязательства клиентского кода перед кодом, который он вызывает.
Постусловия — это соглашения, которые метод или класс обещает выполнить при
завершении своей работы. Постусловия — это обязательства метода или класса
перед кодом, который их использует.
Утверждения — удобный инструмент для документирования пред# и постусловий.
С этой целью можно использовать и комментарии, но в отличие от них утверж#
дения могут динамически проверять, выполняются ли пред# и постусловия.
В следующем примере утверждения документируют пред# и постусловия в функ#
ции Velocity:

Пример использования утверждений для документирования
пред- и постусловий (Visual Basic)
Private Function Velocity ( _
ByVal latitude As Single, _
ByVal longitude As Single, _
ByVal elevation As Single _
) As Single
’ Предусловия
Debug.Assert ( 90 #define DEBUG
...
#if defined( DEBUG )
// отладочный код
...
#endif
У этой темы несколько вариаций. Вместо простого определения DEBUG вы може#
те присвоить ему значение, а затем проверять именно это значение, а не просто
факт определения символа. Так вы можете различать несколько уровней отладоч#
ного кода. Какой#то код вы хотели бы использовать в программе все время, по#
этому вы окружаете его операторами вроде #if DEBUG > 0. Другой отладочный код
может понадобиться только в специальных целях, и вы можете заключить его в
операторы #if DEBUG == POINTER_ERROR. В других местах вы захотите установить
различные уровни отладки, для чего годятся такие выражения, как #if DEBUG >
LEVEL_A.
Если вы не хотите распространять #if defined() по всему коду, можно написать
макрос препроцессора, выполняющий ту же задачу. Вот его пример:

Пример использования макроса препроцессора
для управления отладочным кодом на C++
#define DEBUG
#if defined( DEBUG )
define DebugCode( code_fragment ) { code_fragment }
else
#define DebugCode( code_fragment )
#endif
...
DebugCode(
Этот код добавляется или удаляется в зависимости от того, определен ли символ DEBUG.

>

statement 1;
statement 2;
...
statement n;

ГЛАВА 8 Защитное программирование

203

);
...
Как и в первом примере применения препроцессора, эта технология может быть
изменена различными способами, что позволит выполнить более изощренные
действия, чем полное включение или исключение отладочного кода.
Напишите собственный препроцессор Если язык не
Перекрестная ссылка О препросодержит препроцессор, то для включения/исключения от#
цессорах и источниках инфорладочного кода довольно легко написать свой. Разработай#
мации об их написании см. подраздел «Препроцессоры» раздете правила для добавления отладочного кода и напишите
ла 30.3.
свой прекомпилятор, следующий этим соглашениям. Скажем,
в Java вы могли бы написать прекомпилятор для обработ#
ки ключевых слов //#BEGIN DEBUG и //#END DEBUG. Напишите сценарий для вы#
зова препроцессора, а затем скомпилируйте полученный после него код. В дол#
госрочной перспективе вы сбережете время. Кроме того, вы не сможете случай#
но скомпилировать необработанный код.
Используйте отладочные заглушки Зачастую для вы#
Перекрестная ссылка О заглушполнения отладочных проверок вы можете вызвать проце#
ках см. раздел 22.5.
дуру. Во время разработки она могла бы выполнять несколь#
ко операций перед тем, как управление вернется вызывающей стороне. В промыш#
ленном коде сложную процедуру можно заменить процедурой#заглушкой, кото#
рая сразу вернет управление или выполнит перед этим пару быстрых операций.
Этот подход лишь немного снижает производительность и является более быст#
рым решением, чем написание собственного препроцессора. Храните и отладоч#
ную, и итоговую версии процедур, и вы сможете быстро переключаться между ними.
Вы можете начать с метода, разработанного для проверки переданных ему указа#
телей:

Пример метода, использующего отладочную заглушку (C++)
void DoSomething(
SOME_TYPE *pointer;
...
) {
// проверка переданных сюда параметров
Это строка вызывает процедуру для проверки указателя.

>

CheckPointer( pointer );
...
}
Во время разработки процедура CheckPointer() будет выполнять полную провер#
ку указателя. Это будет медленно, но эффективно, например, так:

204

ЧАСТЬ II

Высококачественный код

Пример метода, проверяющего указатели во время разработки (C++)
Этот метод проверяет любой переданный ему указатель. Во время разработки ее можно использовать для стольких проверок, сколько вы сможете выдержать.

> void CheckPointer( void *pointer ) {
// выполнить проверку 1 — например, что указатель не равен NULL
// выполнить проверку 2 — например, что какойто его
// обязательный признак действителен
// выполнить проверку 3 — например, что область,
// на которую он указывает, не повреждена
...
// выполнить проверку n—...
}
Когда код готов к эксплуатации, вас, возможно, не устроят накладные расходы,
связанные с такой проверкой указателей. Тогда вы удалите предыдущий код и
добавьте следующий метод:

Пример метода, проверяющего указатели во время эксплуатации (C++)
Эта процедура сразу же возвращает управление.

> void CheckPointer( void *pointer ) {
// никакого кода; просто возврат управления
}
Это отнюдь не исчерпывающий обзор всех способов удаления средств отладки.
Но его должно быть достаточно, чтобы подать вам идею о том, что может рабо#
тать в вашей программной среде.

8.7.

Доля защитного программирования
в промышленной версии

Один из парадоксов защитного программирования состоит в том, что во время
разработки вы бы хотели, чтобы ошибка была заметной: лучше пусть она надое#
дает, чем будет существовать риск ее пропустить. Но во время эксплуатации вы
бы предпочли, чтобы ошибка была как можно более ненавязчивой, чтобы про#
грамма могла элегантно продолжить или прекратить работу. Далее перечислены
основные принципы для определения, какие инструменты защитного програм#
мирования следует оставить в промышленной версии, а какие — убрать.
Оставьте код, которые проверяет существенные ошибки Решите, какие
части программы могут содержать скрытые ошибки, а какие — нет. Скажем, раз#
рабатывая электронную таблицу, вы можете скрывать ошибки, касающиеся обнов#
ления экрана, так как в худшем случае это приведет к неправильному изображе#
нию. А вот в вычислительном модуле скрытых ошибок быть не должно, посколь#
ку такие ошибки могут привести к неверным расчетам в электронной таблице.
Большинство пользователей предпочтут помучиться с некорректным выводом на
экран, чем с неправильным расчетом налогов и аудитом налоговой службы.

ГЛАВА 8 Защитное программирование

205

Удалите код, проверяющий незначительные ошибки Если последствия
ошибки действительно незначительны, удалите код, который ее проверяет. В на#
шем примере вы могли бы удалить код, проверяющий обновление экрана элект#
ронной таблицы. При этом «удалить» значит не физически убрать код, но исполь#
зовать управление версиями, переключатели прекомпилятора или другую техно#
логию для компиляции программы без этого кода. Если занимаемое место несу#
щественно, проверочный код можно оставить, но настроив для ненавязчивой за#
писи сообщений в журнал ошибок.
Удалите код, приводящий к прекращению работы программы Как я уже
говорил, если на стадии разработки ваша программа обнаруживает ошибку, ее надо
сделать позаметнее, чтобы ее могли исправить. Часто наилучшим действием при
выявлении ошибки будет печать диагностического сообщения и прекращение
работы. Это полезно даже для незначительных ошибок.
Во время эксплуатации пользователям нужна возможность сохранения своей ра#
боты, прежде чем программа рухнет. И поэтому они, вероятно, будут согласны
терпеть небольшие отклонения в обмен на поддержание работоспособности
программы на достаточное для сохранения время. Пользователи не приветству#
ют ничего, что приводит к потере результатов их работы, независимо от того,
насколько это помогает отладке и в конце концов улучшает качество продукта.
Если ваша программа содержит отладочный код, способный привести к потере
данных, уберите его из промышленной версии.
Оставьте код, который позволяет аккуратно завершить работу про'
граммы Если программа содержит отладочный код, определяющий потенциально
фатальные ошибки, оставьте его — это позволит элегантно завершить работу. На#
пример, в марсоходе Pathfinder инженеры намеренно оставили часть отладочно#
го кода. Ошибка произошла после того, как Pathfinder совершил посадку. С помо#
щью отладочных средств, оставленных в нем, инженеры из лаборатории реактив#
ных двигателей смогли диагностировать проблему и загрузить исправленный код.
В результате Pathfinder полностью выполнил свою миссию (March, 1999).
Регистрируйте ошибки для отдела технической поддержки Обдумайте
возможность оставить отладочные средства в промышленной версии, но изменить
их поведение на более подходящее. Если вы заполнили ваш код утверждениями,
прекращающими выполнение программы на стадии разработки, на стадии экс#
плуатации можно не удалять их совсем, а настроить процедуру утверждения на
запись сообщений в файл.
Убедитесь, что оставленные сообщения об ошибках дружелюбны Если
вы оставляете в программе внутренние сообщения об ошибках, проверьте, что они
дружественны к пользователю. Пользователь одной из моих первых программ
сообщила мне, что получила сообщение, гласившее: «У тебя неправильно выделе#
на память для указателя, черт возьми!» К счастью для меня, у нее было чувство юмора.
Общепринятым и эффективным подходом является уведомление пользователя о
«внутренней ошибке» и вывод телефона и адреса электронной почты, по которым
о ней можно сообщить.

206

ЧАСТЬ II

8.8.

Защита от защитного программирования

Высококачественный код

Избыток защитного программирования сам по себе созда#
ет проблемы. Если вы проверяете данные, передаваемые
через параметры, во всех возможных местах и всеми воз#
можными способами, ваша программа будет слишком боль#
Марк Твен
шой и медленной. Еще хуже то, что дополнительный код,
необходимый для защитного программирования, увеличивает сложность. Напи#
санный в этих целях код не лишен недостатков, и вы можете находить дефекты в
коде защитного программирования, так же как и в обычном коде, особенно если
вы добавляете его мимоходом. Подумайте, где надо защищаться, и соответствен#
но расставьте приоритеты защитного программирования.

Слишком много чего-либо — это
плохо, но слишком много виски — это просто достаточно.

Контрольный список: защитное программирование
Общие
 Реализована ли в методе защита от некорректных входных данных?
Используете ли вы утверждения для документирования допущений, включая пред- и постусловия?
Используются ли утверждения для документирования только тех условий,
которые никогда не должны происходить?
Определены ли в архитектуре или высокоуровневом проекте системы технологии обработки ошибок?
Указано ли в архитектуре или высокоуровневом проекте системы, чему будет
отдаваться предпочтение при обработке ошибок: устойчивости или корректности?
Построены ли баррикады для изоляции разрушительного эффекта ошибок
и уменьшения объема кода, занятого в обработке ошибок?
Установлены ли отладочные средства таким образом, что их можно активизировать и деактивировать без особых проблем?
Хватает ли защитного кода: не слишком много и не слишком мало?
Использованы ли технологии наступательного программирования, чтобы
затруднить пропуск ошибок на стадии разработки?

http://cc2e.com/0868











Исключения
 Определен ли в проекте стандартизованный подход к обработке исключений?
 Рассмотрены ли альтернативы использованию исключений?
 Обрабатывается ли ошибка по возможности локально или генерируется
нелокальное исключение?
 Возможны ли исключения в конструкторах и деструкторах?
 Генерируются ли исключения в методах на подходящих уровнях абстракции?
 Содержит ли каждое исключение все относящиеся к нему исходные данные?
 Свободен ли код от пустых блоков catch? (Или, если блок catch действительно допустим, задокументировано ли это?)

ГЛАВА 8 Защитное программирование

207

Вопросы безопасности
 Действительно ли код, проверяющий некорректные входные данные, контролирует попытки переполнения буфера, внедрения SQL- и HTML-кода, переполнения целых чисел и других злонамеренных действий?
 Все ли ошибочные коды возврата проверяются?
 Все ли исключения перехватываются?
 Не содержат ли сообщения об ошибках информацию, которая может помочь
злоумышленнику взломать систему?

Дополнительные ресурсы
Просмотрите следующие ресурсы по защитному програм#
мированию.

http://cc2e.com/0875

Безопасность
Howard, Michael, and LeBlanc David. Writing Secure Code, 2d ed. Redmond, WA: Microsoft
Press, 2003. Авторы обсуждают важность вопроса доверия ко входным данным для
безопасности системы. Книга открывает глаза на то, какими многочисленными
способами может быть нарушена работа программы. Некоторые из них связаны
с проектированием, но большинство — нет. Книга охватывает весь диапазон воп#
росов о требованиях к системе, проектировании, кодировании и тестировании.

Утверждения
Maguire, Steve. Writing Solid Code. Redmond, WA: Microsoft Press, 1993. Глава 2 содер#
жит отличное обсуждение вопросов применения утверждений, в том числе несколь#
ко интересных примеров утверждений из широко известных продуктов Microsoft.
Stroustrup, Bjarne. The C++ Programming Language, 3d ed. Reading, MA: Addison#Wesley,
1997. В разделе 24.3.7.2 описано несколько вариантов реализации утверждений в
C++, включая взаимосвязь утверждений и пред# и постусловий.
Meyer, Bertrand. Object%Oriented Software Construction, 2d ed. New York, NY: Prentice Hall
PTR, 1997. Эта книга содержит наиболее полное обсуждение пред# и постусловий.

Исключения
Meyer, Bertrand. Object%Oriented Software Construction, 2d ed. New York, NY: Prentice
Hall PTR, 1997. Глава 12 содержит подробное обсуждение обработки исключений.
Stroustrup, Bjarne. The C++ Programming Language, 3d ed. Reading, MA: Addison#Wesley,
1997. В главе 14 подробно обсуждается обработка исключений на C++. Раздел 14.11
содержит отличное резюме, состоящее из 21 совета по использованию исключе#
ний в C++.
Meyers, Scott. More Effective C++: 35 New Ways to Improve Your Programs and Designs.
Reading, MA: Addison#Wesley, 1996. В темах 9–15 описаны разнообразные нюансы
по обработке исключений в C++.
Arnold, Ken, James Gosling, and David Holmes. The Java Programming Language, 3d
ed. Boston, MA: Addison#Wesley, 2000. Глава 8 содержит обсуждение обработки ис#
ключений на Java.

208

ЧАСТЬ II

Высококачественный код

Bloch, Joshua. Effective Java Programming Language Guide. Boston, MA: Addison#Wesley,
2001. В темах 39–47 описаны нюансы обработки исключений на Java.
Foxall, James. Practical Standards for Microsoft Visual Basic .NET. Redmond, WA: Microsoft
Press, 2003. В главе 10 описана обработка исключений на Visual Basic.

Ключевые моменты
 Промышленный код должен обрабатывать ошибки более изощренно, чем по

принципу «мусор на входе — мусор на выходе».
 С помощью технологии защитного программирования ошибки легче находить,

легче исправлять, и они наносят меньше вреда промышленному коду.
 Утверждения позволяют обнаружить ошибки на ранней стадии, особенно в

больших системах, системах повышенной надежности и в системах с часто
изменяемым кодом.
 Выбор способа обработки некорректных входных данных — это ключевое

решение обработки ошибок, принимаемое на этапе высокоуровневого проек#
тирования.
 Исключения предоставляют возможность обработки ошибок в измерении,

отличном от нормального хода алгоритма. Если они используются с осторож#
ностью, то являются важным дополнением в интеллектуальном инструменталь#
ном наборе программиста. Применять их следует после сравнения с другими
технологиями обработки ошибок.
 Ограничения, применяемые к промышленной версии системы, не обязатель#

но должны относиться и ко времени разработки. Пользуясь этим преимуще#
ством, вы можете добавлять в отладочную версию любой код, помогающий
быстро выявлять ошибки.

ГЛАВА 9 Процесс программирования с псевдокодом

Г Л А В А

209

9

Процесс программирования
с псевдокодом

Содержание
 9.1. Этапы создания классов и методов

http://cc2e.com/0936

 9.2. Псевдокод для профи
 9.3. Конструирование процедур с использованием ППП
 9.4. Альтернативы ППП

Связанные темы
 Создание высококачественных классов: глава 6
 Характеристики высококачественных методов: глава 7
 Проектирование при конструировании: глава 5
 Стиль комментирования: глава 32

Всю эту книгу можно рассматривать как подробное описание процесса програм#
мирования, в результате которого создаются классы и методы, и в данной главе
этапы этого процесса рассматриваются во всех деталях. Здесь описывается «про#
граммирование в малом» — конкретные шаги построения отдельных классов и
составляющих их методов, шаги, неизбежные в проекте любого размера. В этой
главе также рассмотрен процесс программирования с псевдокодом (ППП, Pseu#
docode Programming Process), уменьшающий объем работы по проектированию
и документированию и улучшающий качество и первого, и второго.
Если вы опытный программист, можете пропустить эту главу, но взгляните на обзор
этапов и просмотрите советы по конструированию методов с помощью ППП в
разделе 9.3. Некоторые программисты применяют описываемый процесс на пол#
ную, поскольку он приносит большую выгоду.
ППП — это не единственная процедура создания классов и методов. В разделе 9.4
описаны наиболее популярные альтернативные подходы, включая разработку с
изначальными тестами и проектирование по контракту.

210

ЧАСТЬ II

9.1.

Этапы создания классов и методов

Высококачественный код

К конструированию классов можно подходить по#разному, но обычно это итера#
тивный процесс создания общей структуры класса, создание списка его методов,
их конструирование и проверка класса как единого целого. Создание класса мо#
жет быть запутанным процессом (рис. 9#1), поскольку проектирование как тако#
вое — тоже процесс запутанный (причины описаны в разделе 5.1).

Рис. 9'1. Детали конструирования классов могут различаться,
но обычно порядок действий такой

Этапы создания класса
Ниже перечислены основные этапы создания класса.
Создание общей структуры класса Проектирование класса связано с мно#
жеством отдельных вопросов. Определите функции класса, «секреты», скрытые в
нем и точный уровень абстракции, предоставляемый интерфейсом класса. Опре#
делите, наследуется ли класс от другого класса и могут ли другие классы наследо#
ваться от него. Определите основные открытые методы класса и спроектируйте
все нетривиальные структуры данных, используемые им. Пройдитесь по всем этим
пунктам столько раз, сколько необходимо для создания четкой структуры класса.
Эти и многие другие вопросы подробно обсуждаются в главе 6.
Конструирование всех методов класса Определив на первом этапе основ#
ные функции класса, переходите к конструированию каждого отдельного мето#
да. При конструировании отдельных методов обычно выясняется потребность в
дополнительных методах (как более низкого, так и более высокого уровня), и
вопросы, возникающие при их создании, приводят к необходимости пересмотра
общей структуры класса.
Оценка и тестирование всего класса Обычно каждый метод тестируется при
его создании. После того как класс в целом становится работоспособным, его нужно
пересмотреть и протестировать как единое целое для выявления тех проблем,
которые невозможно определить на уровне отдельных методов.

ГЛАВА 9 Процесс программирования с псевдокодом

211

Этапы построения метода
Многие метода класса, такие как аксессоры или интерфейсы к другим классам, могут
быть простыми и понятными для реализации. Реализация других будет сложнее,
и для их создания требуется систематический подход. Основные действия по со#
зданию метода — проектирование, проверка структуры, кодирование и проверка
кода — обычно выполняются в такой последовательности (рис. 9#2):

Рис. 9'2.

Основные действия по созданию метода

Специалисты разработали множество подходов к созданию методов, но мой лю#
бимый — процесс программирования с псевдокодом — описан в следующем раз#
деле.

9.2.

Псевдокод для профи

Псевдокодом называют неформальную нотацию на естественном языке, описы#
вающую работу алгоритма, метода, класса или программы. «Процесс программи#
рования с псевдокодом» относится к конкретной методике применения псевдо#
кода для эффективного создания кода методов.
Поскольку псевдокод напоминает естественный язык, разумно предположить, что
любая перефразировка ваших мыслей, выполненная на нем, будет иметь одина#
ковый эффект. На практике же вы обнаружите, что некоторые стили псевдокода
предпочтительней других.
 Применяйте формулировки, в точности описывающие отдельные действия.
 Избегайте синтаксических элементов языков программирования. Псевдокод

позволяет проектировать на несколько более высоком уровне, чем код. При#
меняя конструкции языка программирования, вы мыслите на более низком
уровне и теряете преимущества проектирования на высоком уровне, загружая
себя ненужными синтаксическими ограничениями.

212

ЧАСТЬ II

Перекрестная ссылка Об уровнях комментирования см. подраздел «Виды комментариев»
раздела 32.4.

Высококачественный код
 Пишите псевдокод на уровне намерений. Описывайте
назначение подхода, а не то, как этот подход нужно реали#
зовать на выбранном языке программирования.

 Пишите псевдокод на достаточно низком уровне, так что#
бы код из него генерировался практически автоматически.
Если псевдокод написан на слишком высоком уровне, могут возникнуть про#
блемы кодирования. Детализируйте псевдокод до тех пор, пока вам не пока#
жется, что проще было бы написать код.1

Написав псевдокод, вы окружаете его кодом, а псевдокод превращаете в коммен#
тарии программы. Если вы руководствуетесь перечисленными правилами созда#
ния псевдокода, комментарии в вашей программе будут полными и ясными.
Вот пример псевдокода, в котором нарушены практически все перечисленные
правила:

Пример плохого псевдокода
увеличить номер ресурса на 1
выделить структуру dlg посредством malloc
если malloc() возвращает NULL вернуть 1
вызвать OSrsrc_init для инициализации ресурса
*hRsrcPtr = номер ресурса
вернуть 0
Какие намерения описывает этот блок псевдокода? Трудно сказать, поскольку
написан он плохо. Этот так называемый псевдокод плох потому, что включает
конкретику целевого языка программирования: *hRsrcPtr (описание указателя,
специфичное для языка C) и malloc() (функция C). Этот блок псевдокода показы#
вает, как будет написан код, а не описывает его назначение. Он вдается в излиш#
ние подробности: вернет ли процедура 1 или 0. Если посмотреть, можно ли пре#
вратить этот псевдокод в нормальные комментарии, видно, что толку от него мало.
А вот описание тех же действий на гораздо лучшем псевдокоде:

Пример хорошего псевдокода
Отслеживать текущее число используемых ресурсов
Если другой ресурс доступен
Выделить структуру для диалогового окна
Если структура для диалогового окна может быть выделена
Учесть, что используется еще один ресурс
Инициализировать ресурс
Хранить номер ресурса в вызывающей программе
Конец «если»
Конец «если»
Вернуть true, если новый ресурс был создан; иначе вернуть false

1

К сожалению, эти советы хорошо применимы лишь для псевдокода на английском языке или
для случаев использования экзотических языков программирования с конструкциями, осно#
ванными на естественном языке, отличном от английского. — Прим. перев.

ГЛАВА 9 Процесс программирования с псевдокодом

213

Этот псевдокод лучше предыдущего, так как полностью написан на естественном
языке и не использует специфических конструкций целевого языка программи#
рования. В первом примере псевдокод подлежал реализации только на C. Во вто#
ром же псевдокод не накладывает ограничений на используемый язык. Второй блок
также написан на уровне описания намерений. О чем речь во втором блоке? На#
верное, это легче понять, чем в первом блоке.
Хотя второй блок написан напонятном естественном языке, он достаточно то#
чен и подробен, чтобы быть основой программы. Когда предложения этого псев#
докода будут преобразованы в комментарии, они станут хорошим пояснением на#
значения кода.
Вот выгоды, которые вы получите, применяя такой стиль псевдокода.
 Псевдокод упрощает пересмотр конструкции — вам не потребуется вникать в

исходный код.
 Псевдокод поддерживает идею итеративного усовершенствования. Вы начинаете

с высокоуровневой конструкции, уточняете ее до псевдокода, который в свою
очередь преобразуете в исходный код. Такое последовательное усовершенство#
вание, осуществляемое шаг за шагом, позволяет проверять свои проектные
решения по мере перехода на более низкие уровни. В результате вы обнару#
живаете высокоуровневые ошибки на самом верхнем уровне, среднеуровне#
вые — на среднем, а низкоуровневые — на самом нижнем, прежде чем они
создадут проблемы.
 Псевдокод упрощает внесение изменений. Что проще:

Дополнительные сведения О пре-

исправить линию на чертеже или снести стену и сдви#
имуществах внесения изменения
нуть ее на метр в сторону? В программировании эффект
на наименее значимых стадиях
см. книгу Энди Гроува «High Outне столь драматичен в плане физических усилий, но идея
put Management» (Grove, 1983).
та же: несколько строк псевдокода легче исправить, чем
страницу кода. Одна из основ успеха проекта — отловить
ошибку на «наименее значимой стадии», когда для ее исправления требуется
минимум усилий. Поиск ошибки на стадии псевдокода требует гораздо мень#
ше затрат, чем после полного кодирования, тестирования и отладки, так что
есть экономический стимул обнаружить ошибку как можно раньше.
 Псевдокод упрощает комментирование программ. В типичной ситуации вы

сначала пишете код, а затем добавляете комментарии. В ППП предложения
псевдокода становятся комментариями, так что на самом деле их проще оста#
вить, чем удалить.
 Псевдокод сопровождать проще, чем другие виды проектной документации. При

других подходах проектная документация отделена от кода, и внесение в нее
изменений порождает несоответствие. В ППП предложения псевдокода стано#
вятся комментариями программы. Внося изменения в комментарии, вы, таким
образом, поддерживаете в корректном состоянии проектную документацию.
Псевдокод как инструмент проектирования трудно переоценить. Иссле#
дования показали, что программисты предпочитают псевдокод за его воз#
можности упрощать создание программных конструкций, помощь в
определении некорректных проектных решений, простоту документирования и
внесения изменений (Ramsey, Atwood, and Van Doren, 1983). Псевдокод — не един#

214

ЧАСТЬ II

Высококачественный код

ственный инструмент детального проектирования, но наряду с ППП — полезная
вещь в инструментарии программиста. Попробуйте его в деле. В следующем раз#
деле я расскажу как.

9.3.

Конструирование методов
с использованием ППП

В этом разделе описаны этапы конструирования методов, а именно:
 проектирование метода;
 кодирование метода;
 проверка кода;
 наведение глянца;
 повторение предыдущих шагов при необходимости.

Проектирование метода
Определив состав методов класса, приступайте к их проек#
тированию. Допустим, вы хотите написать метод вывода
сообщения об ошибке, основанного на коде ошибки, и на#
звали этот метод ReportErrorMessage(). Вот неформальная
спецификация ReportErrorMessage():

Перекрестная ссылка О других
аспектах проектирования см.
главы с 5 по 8.

ReportErrorMessage() принимает в качестве входного параметра код ошибки и
выводит сообщение об ошибке, соответствующее этому коду. Он отвечает за
обработку недопустимых кодов. Если программа интерактивная, ReportError%
Message() выводит сообщение пользователю. Если она работает в режиме ко#
мандной строки, ReportErrorMessage() заносит сообщение в файл. После выво#
да сообщения ReportErrorMessage() возвращает значение статуса, указывающее,
успешно ли он завершился.
Этот метод используется в качестве примера во всей главе, а в оставшейся части
этого раздела описывается его проектирование.
Перекрестная ссылка О проверке предварительных условий см.
главы 3 и 4.

Проверка предварительных условий Прежде чем что#
либо делать с самой процедурой, убедитесь, что функции
метода четко определены и соответствуют общим проект#
ным решениям.

Определите задачу, решаемую методом Сформулируйте задачу, решаемую
методом настолько детально, чтобы можно было переходить созданию метода. Если
проект высокого уровня достаточно подробен, эта работа уже сделана. Проект
верхнего уровня должен содержать по крайней мере:
 информацию, скрываемую методом;
 входные данные;
 выходные данные;
Перекрестная ссылка О пред- и
постусловиях см. раздел 8.2.

 предусловия, которые гарантированно должны соблюдать#
ся до вызова метода (входные значения находятся в задан#
ном диапазоне, потоки инициализированы, файлы открыты
или закрыты, буферы заполнены или очищены и т. д.);

ГЛАВА 9 Процесс программирования с псевдокодом

215

 постусловия, которые гарантированно должны соблюдаться, прежде чем ме#

тод вернет управление вызывающей программе (выходные значения находят#
ся в заданном диапазоне, потоки инициализированы, файлы открыты или за#
крыты, буферы заполнены или очищены и т. д.).
Вот как это выглядит для метода ReportErrorMessage():
 метод скрывает текст сообщения и текущий метод обработки (интерактивный

или командной строки);
 выполнение каких#либо предусловий не требуется;
 входными данными является код ошибки;
 выходные данные двух видов: сообщение об ошибке и статус, возвращаемый

ReportErrorMessage() вызывающей программе;
 возвращаемый статус должен принимать одно из двух значений: Success или

Failure.
Название метода Вопрос именования метода кажется
Перекрестная ссылка Об именотривиальным, но хорошее название — признак высокого
вании методов см. раздел 7.3.
стиля программирования и дело это непростое. Вообще ме#
тод должен иметь понятное, недвусмысленное имя. Затруднения в выборе имени
метода могут свидетельствовать о том, что его назначение не совсем понятно. Не#
ясное, невыразительное имя метода сродни предвыборным обещаниям политиков.
Вроде бы о чем#то оно говорит, но если задуматься — непонятно о чем. Если мож#
но дать методу более ясное имя, сделайте это. Если невыразительное имя — результат
неясных проектных решений, вернитесь к ним и измените их.
В нашем примере ReportErrorMessage() — вполне недвусмысленное имя. Хорошее имя.
Решите, как тестировать метод В процессе написа#
ния метода думайте о том, как вы будете его тестировать. Это
принесет пользу вам при блочном тестировании и тести#
ровщикам, проводящим независимое тестирование.
В нашем примере входные данные просты, так что можно
планировать тестирование ReportErrorMessage() со всеми
допустимыми кодами ошибок и различными неверными
кодами.

Дополнительные сведения О
различных подходах к конструированию, ориентированных на
предварительное написание тестов см. книгу «Test-Driven Development: By Example» (Beck,
2003).

Исследуйте функциональность, предоставляемую стандартными биб'
лиотеками Основная возможность улучшить качество и производительность
своего кода — повторно использовать имеющийся хороший код. Если метод ка#
жется вам слишком сложным и у вас возникают проблемы с его проектировани#
ем, спросите себя, не реализована ли часть его функциональности в библиотеках
языка, платформы или средства разработки, которые вы применяете. Нет ли нуж#
ного кода в стандартных библиотеках вашей компании? Множество алгоритмов
уже реализовано, протестировано, обсуждено в профессиональной литературе,
пересмотрено и усовершенствовано. Не тратьте время на реализацию готового
алгоритма, по которому написана кандидатская диссертация.
Продумайте обработку ошибок Подумайте обо всем плохом, что может слу#
читься с вашим методом. Подумайте о плохих входных данных, недопустимых
значениях, возвращаемых другими методами, и т. д.

216

ЧАСТЬ II

Высококачественный код

Методы могут обрабатывать ошибки разными способами, и вам нужно четко опре#
делиться с одним из них. Если стратегию обработки ошибок определяет архитек#
тура программы, вы можете просто следовать этой стратегии. В других случаях вам
следует решить, какой подход будет оптимален в данном конкретном случае.
Думайте об эффективности В зависимости от ситуации вы можете подхо#
дить к эффективности одним из двух способов. В первом случае — в подавляю#
щем большинстве систем — эффективность некритична. При этом убедитесь, что
интерфейс метода достаточно абстрагирован, а код читабелен и при необходи#
мости вы сможете его легко усовершенствовать. При хорошей инкапсуляции вы
сможете заменить медленные, ресурсоемкие конструкции языка высокого уров#
ня более эффективным алгоритмом или реализацией на быстром, компактном язы#
ке низкого уровня, не затронув при этом другие методы.
Во втором случае — в незначительном числе систем — про#
изводительность критична. Проблемы производительности
могут быть связаны с недостатком соединений с базой дан#
ных, ограниченной памятью, малым количеством доступных дескрипторов и дру#
гими ресурсами. Архитектура должна указывать, сколько ресурсов каждому мето#
ду (или классу) может быть предоставлено и как быстро он должен выполнять свои
операции.

Перекрестная ссылка Об эффективности см. главы 25 и 26.

Как правило, не стоит тратить много усилий на оптимизацию отдельных мето#
дов. Эффективность в основном определяется конструкцией высокого уровня.
Обычно микрооптимизация выполняется, только когда закончена вся программа
и выясняется, что высокоуровневая конструкция исчерпала свои возможности
обеспечить нужную производительность. Не теряйте время на вылизывание от#
дельных методов, пока не выяснится, что это необходимо.
Исследуйте алгоритмы и типы данных Если доступные стандартные биб#
лиотеки не предоставляют нужной функциональности, имеет смысл исследовать
литературу с описанием алгоритмов. Если вы нашли подходящий готовый алго#
ритм, корректно адаптируйте его к применяемому вами языку программирования.
Пишите псевдокод У вас не будет сложностей, если вы прошли все предыду#
щие этапы, основное назначение которых в том, чтобы у вас сложилось четкое
понимание того, что нужно писать.
Перекрестная ссылка Это обсуждение предполагает, что при
создании псевдокода метода
применялись правильные способы проектирования (см. главу 5).

Закончив предыдущие этапы, можно приступать к написа#
нию высокоуровневого псевдокода. Запускайте редактор
кода или интегрированную среду разработки и пишите
псевдокод, который станет основой исходного текста про#
граммы.

Начните с основных моментов, а затем детализируйте их.
Самая главная часть метода — заголовок#комментарий, описывающий действия
метода, так что начните с краткой формулировки назначения метода. Написание
этой формулировки поможет вам прояснить ваше понимание метода. Если вы
испытываете затруднения при написании этого обобщенного комментария, вам,
видимо, следует лучше разобраться с ролью этого метода. Вот пример краткого
заголовка#комментария метода:

ГЛАВА 9 Процесс программирования с псевдокодом

217

Пример заголовка метода
Этот метод выводит сообщение об ошибке на основании кода ошибки, получаемого от
вызывающей программы. Способ вывода сообщения зависит от режима работы, который он
определяет сам. Он возвращает значение, указывающее на успешное завершение или сбой.
Написав общий комментарий, добавьте высокоуровневый псевдокод:

Пример псевдокода метода
Этот метод выводит сообщение об ошибке на основании кода ошибки, получаемого от
вызывающей программы. Способ вывода сообщения зависит от режима работы, который он
определяет сам. Он возвращает значение, указывающее на успешное завершение или сбой.
Установить статус по умолчанию в “сбой”.
Найти сообщение, соответствующее коду ошибки.
Если код ошибки корректен
Если работа в интерактивном режиме, вывести сообщение
и указать успешный статус.
Если работа в режиме командной строки, запротоколировать
сообщение об ошибке и указать успешный статус.
Если код ошибки некорректен, информировать пользователя
об обнаружении внутренней ошибки.
Вернуть статус
Еще раз: этот псевдокод достаточно высокого уровня, он не содержит конструк#
ций языка программирования, а объясняет последовательность действий на ес#
тественном языке.
Продумайте применение данных К структуре данных
Перекрестная ссылка Об исможно подходить с разных позиций. В нашем примере дан#
пользовании переменных см.
ные простые, и манипуляция над ними не является суще#
главы 10–13.
ственной частью метода. В противном случае важно проду#
мать основные фрагменты данных до построения логики метода. Когда вы буде#
те строить логику метода, структуры основных типов данных окажутся весьма
полезны.
Проверьте псевдокод Написав псевдокод и спроектиро#
вав данные, уделите минутку просмотру написанного. Заду#
майтесь, как бы вы объяснили это кому#то другому.

Перекрестная ссылка О методиках обзоров см. главу 21.

Попросите кого#нибудь прочитать написанное или выслушать ваше объяснение.
Вам может показаться глупым просить коллегу посмотреть на какие#то 11 строк
псевдокода, но результат вас удивит. Псевдокод более явно обозначит ваши оши#
бочные намерения, чем код на языке программирования. К тому же люди охот#
ней просматривают несколько строк псевдокода своих коллег, чем 35 строк про#
граммы на C++ или Java.

218

ЧАСТЬ II

Высококачественный код

Убедитесь, что вы имеете четкое представление о том, что и как делает метод. Если
вы не понимаете его концептуально, на уровне псевдокода, какой же тогда у вас
шанс разобраться в нем на уровне языка программирования? Если его не пони#
маете вы, кто его поймет?
Опишите несколько идей псевдокодом и выберите луч'
шую (пройдите по циклу) Прежде чем кодировать, ре#
ализуйте как можно больше своих идей в псевдокоде. При#
ступив к кодированию, вы эмоционально вовлекаетесь в этот
процесс, и вам труднее отказаться от плохого проекта и начать заново.

Перекрестная ссылка Об итерациях см. раздел 34.8.

Общая идея: раз за разом проходиться по псевдокоду, пока каждое его предложе#
ние не станет настолько простым, что под ним можно будет вставить строку про#
граммы, а псевдокод оставить в качестве документации. Часть псевдокода, напи#
санного при первых проходах, может оказаться достаточно высокоуровневой и
потребовать дальнейшей декомпозиции. Не забывайте это сделать. Если вам не
понятно, как закодировать какой#то фрагмент, продолжайте работать с псевдо#
кодом, пока это не прояснится. Продолжайте уточнение и декомпозицию, пока
это не будет выглядеть как напрасная трата времени по сравнению с написанием
настоящего кода.

Кодирование метода
Спроектировав метод, приступайте к его конструированию. Конструирование
можно производить в стандартном порядке, а при необходимости отступить от
него (рис. 9#3).

Рис. 9'3. Вы пройдете все эти этапы, но не обязательно именно
в такой последовательности

Объявление метода Напишите интерфейсный оператор метода: объявление
функции на C++, метода на Java, функции или подпрограммы на Microsoft Visual
Basic и т. д. в зависимости от применяемого языка. Превратите существующий

ГЛАВА 9 Процесс программирования с псевдокодом

219

заголовочный комментарий в комментарий соответствующего языка и оставьте
его над уже написанным псевдокодом. Вот интерфейсный оператор и заголовок
нашего примера на C++:

Пример интерфейса метода и заголовка, добавленных к псевдокоду (C++)
Это заголовочный комментарий, превращенный в комментарий C++.

> /* Этот метод выводит сообщение об ошибке на основании кода ошибки, получаемого от
вызывающей программы. Способ вывода сообщения зависит от режима работы, который он
определяет сам. Он возвращает значение, указывающее на успешное завершение или сбой.
*/
Это интерфейсный оператор.

> Status ReportErrorMessage(
ErrorCode errorToReport
)
Установить статус по умолчанию в “сбой”.
Найти сообщение, соответствующее коду ошибки.
Если код ошибки корректен
Если работа в интерактивном режиме, вывести сообщение
и указать успешный статус.
Если работа в режиме командной строки, запротоколировать
сообщение об ошибке и указать успешный статус.
Если код ошибки некорректен, информировать пользователя
об обнаружении внутренней ошибки.
Вернуть статус.
Изменение псевдокода на высокоуровневые комментарии Добавим первый
и последний оператор: { и } на C++ и превратим псевдокод в комментарии:

Пример первого и последнего оператора вокруг псевдокода (C++)
/* Этот метод выводит сообщение об ошибке на основании кода ошибки, получаемого от
вызывающей программы. Способ вывода сообщения зависит от режима работы, который он
определяет сам. Он возвращает значение, указывающее на успешное завершение или сбой.
*/
Status ReportErrorMessage(
ErrorCode errorToReport
) {
С этого места предложения псевдокода заменены на комментарии C++.

>

// Установить статус по умолчанию в “сбой”.
// Найти сообщение, соответствующее коду ошибки.
// Если код ошибки корректен
// Если работа в интерактивном режиме, вывести сообщение
// и указать успешный статус.

220

ЧАСТЬ II

Высококачественный код

// Если работа в режиме командной строки, запротоколировать
// сообщение об ошибке и указать успешный статус.
// Если код ошибки некорректен, информировать пользователя
// об обнаружении внутренней ошибки.
// Вернуть статус.
}
Теперь роль метода очевидна. Проектные работы закончены, и вы без всякого кода
видите, как работает метод.
Напишите код под каждым комментарием Добавьте
код под каждой строкой комментария. Это напоминает на#
писание курсовой работы: сначала вы пишете план, а затем,
под каждым его пунктом, — абзац текста. Каждый коммен#
тарий соответствует блоку или абзацу кода. Длина абзаца
кода, как и длина абзаца литературного текста, зависит от
высказываемой мысли, а его качество — от понимания автором сути.

Перекрестная ссылка В этом
случае подходит литературная
метафора, о которой см. подраздел «Литературная метафора: написание кода» раздела 2.3.

Пример добавления кода к комментариям (C++)
/* Этот метод выводит сообщение об ошибке на основании кода ошибки, получаемого от
вызывающей программы. Способ вывода сообщения зависит от режима работы, который он
определяет сам. Он возвращает значение, указывающее на успешное завершение или сбой.
*/
Status ReportErrorMessage(
ErrorCode errorToReport
) {
// Установить статус по умолчанию в “сбой”.
Добавленный код.

>

Status errorMessageStatus = Status_Failure;
// Найти сообщение, соответствующее коду ошибки.
Новая переменная errorMessage.

>

Message errorMessage = LookupErrorMessage( errorToReport );
// Если код ошибки корректен.
// Если работа в интерактивном режиме, вывести сообщение
// и указать успешный статус.
// Если работа в режиме командной строки, запротоколировать
// сообщение об ошибке и указать успешный статус.
// Если код ошибки некорректен, информировать пользователя
// об обнаружении внутренней ошибки.
// Вернуть статус.
}

ГЛАВА 9 Процесс программирования с псевдокодом

221

Это только начало написания кода. Поскольку используется переменная error%
Message, ее нужно объявить. Если вы вносите комментарии после написания кода,
двух строк комментария на две строки кода почти всегда будет достаточно. При
данном же подходе важно семантическое содержание комментариев, а не число
строк кода, к которым они относятся. Комментарии уже есть и описывают дей#
ствия кода, так что оставьте их все.
Далее нужно добавить код ко всем оставшимся комментариям:

Пример законченного метода, созданного посредством
Процесса Программирования Псевдокода (C++)
/* Этот метод выводит сообщение об ошибке на основании кода ошибки, получаемого от
вызывающей программы. Способ вывода сообщения зависит от режима работы, который он
определяет сам. Он возвращает значение, указывающее на успешное завершение или сбой.
*/
Status ReportErrorMessage(
ErrorCode errorToReport
) {
// Установить статус по умолчанию в “сбой”.
Status errorMessageStatus = Status_Failure;
// Найти сообщение, соответствующее коду ошибки.
Message errorMessage = LookupErrorMessage( errorToReport );
// Если код ошибки корректен.
Отсюда начинаем добавлять код для каждого комментария.

>

if ( errorMessage.ValidCode() ) {
// Определяем метод обработки.
ProcessingMethod errorProcessingMethod = CurrentProcessingMethod();
// Если работа в интерактивном режиме, вывести сообщение
// и указать успешный статус.
if ( errorProcessingMethod == ProcessingMethod_Interactive ) {
DisplayInteractiveMessage( errorMessage.Text() );
errorMessageStatus = Status_Success;
}
// Если работа в режиме командной строки, запротоколировать
// сообщение об ошибке и указать успешный статус.
Этот код — хороший кандидат стать новым методом: DisplayCommandLineMessage().

>

else if ( errorProcessingMethod == ProcessingMethod_CommandLine ) {
CommandLine messageLog;
if ( messageLog.Status() == CommandLineStatus_Ok ) {
messageLog.AddToMessageQueue( errorMessage.Text() );
messageLog.FlushMessageQueue();
errorMessageStatus = Status_Success;
}

222

ЧАСТЬ II

Высококачественный код

Эти код и комментарий новые и являются результатом развертывания оператора if.

>

else {
// Не можем ничего делать, так как процедура
// сама занимается обработкой ошибки.
}
Это тоже новый код и комментарий.

>

else {
// Не можем ничего делать, так как процедура
// сама занимается обработкой ошибки.
}
}
// Если код ошибки некорректен, извещаем пользователя
// об обнаружении внутренней ошибки.
else {
DisplayInteractiveMessage(
“Internal Error: Invalid error code in ReportErrorMessage()”
);
}
// Вернуть статус.
return errorMessageStatus;
}
К каждому комментарию добавлена одна или несколько строк кода. Каждый блок
кода выражает некоторое намерение, описанное комментариями. Все перемен#
ные объявлены и определены рядом с местом их первого использования. Каждый
комментарий обычно разворачивается в 2–10 строк кода.
Теперь вернемся к спецификации и псевдокоду. Первоначальная спецификация
из пяти предложений превратилась в 15 строк псевдокода, которые в свою оче#
редь развернуты в метод размером в страницу. Хотя спецификация и была доста#
точно подробной, создание метода потребовало проектировочных работ при
написании псевдокода и кодировании. Это низкоуровневое проектирование и есть
одна из причин, по которой «кодирование» является нетривиальной задачей.
Проверьте, не нужна ли дальнейшая декомпозиция кода В некоторых слу#
чаях вы увидите, что код, соответствующий одной изначальной строке псевдоко#
да, существенно разросся. В таких ситуациях следует предпринять одно из следу#
ющих действий.
 Преобразуйте код, соответствующий комментарию, в но#
вый метод. Дайте методу имя и напишите код вызова этого
метода. Если вы правильно применяли ППП, имя метода вы
легко придумаете на основе псевдокода. Закончив работу с изначальным ко#
дом, переходите к вновь созданным методам и применяйте ППП к ним.

Перекрестная ссылка О рефакторинге см. главу 24.

 Применяйте ППП рекурсивно. Вместо того чтобы писать несколько десятков

строк кода для одной строки псевдокода, разбейте эту строку псевдокода на
несколько предложений и для каждой из них напишите код.

ГЛАВА 9 Процесс программирования с псевдокодом

223

Проверка кода
Третий шаг после проектирования и реализации метода — его проверка. Все
ошибки, которые вы пропустите на этом этапе, вы сможете обнаружить лишь при
позднейшем тестировании, что обойдется вам дороже.
Ошибка может не проявиться до окончательного кодирова#
Перекрестная ссылка О поиске
ния по нескольким причинам. Ошибка в псевдокоде может
ошибок при построении архистать заметнее при детальной реализации. Конструкция,
тектуры и выработке требований
см. главу 3.
выглядящая элегантно в псевдокоде, на языке программиро#
вания может стать топорной. Проработка детальной реали#
зации может выявить ошибку архитектуры, проекта или требований. Наконец, код
может содержать самые банальные ошибки программирования — никто не совер#
шенен! По всем этим причинам пересмотрите код, прежде чем двигаться дальше.
Умозрительно проверьте ошибки в методе Первая формальная проверка
метода — умозрительная. Мысленно выполните все ветви метода. Сделать это не#
просто, что и является одной из причин писать короткие методы. Проверьте все
возможные ветви и исключительные условия. Проделайте это сами и с коллегами.
Одно из основных различий между любителями и профессиональными
программистами — различие, появляющееся при переходе от суеверия к
пониманию. Под суеверием здесь я понимаю не иллюзию, что програм#
ма выдает больше ошибок в полнолуние, а замену «прочувствования» программы
ее пониманием. Если вы часто обнаруживаете, что подозреваете компилятор или
аппаратные средства в ошибке, вы в плену суеверий. Давнишние исследования по#
казали, что только около 5% всех ошибок связано с аппаратурой, компиляторами
или ОС (Ostrand and Weyuker, 1984). Сейчас этот процент, видимо, еще меньше.
Программист, достигший сферы понимания, обращает внимание прежде всего на
свое творение, являющееся потенциальным источником 95% ошибок. Нужно знать
роль каждой строки своей программы. Ничто не может называться верным толь#
ко потому, что выглядит работоспособным. Если вы не знаете, почему это рабо#
тает, вероятно, оно и не работает на самом деле.
Итог: работающий метод — это еще не все. Если вы не знаете, как он ра#
ботает, изучайте его, обсуждайте его, экспериментируйте с альтернатив#
ными вариантами, пока не добьетесь понимания.
Компиляция метода Проверив метод, скомпилируйте его. Может показаться
неэффективным так долго откладывать компиляцию. Вероятно, вы уменьшите себе
работу, скомпилировав метод ранее и позволив компилятору проверить необъяв#
ленные переменные, обнаружить конфликты имен и т. д.
Между тем, отложив трансляцию на более поздний срок, вы получите ряд преиму#
ществ. Основная причина в том, что, когда вы компилируете новый код, в вас
начинают тикать внутренние часики. После первой трансляции появляется мысль:
«Еще всего одна компиляция, и дело сделано». Этот синдром «еще всего одной
компиляции» подвигает вас к скороспелым, чреватым ошибками изменениям,
которые в долгосрочном плане увеличивают общее время работы. Не спешите и
не компилируйте программу, пока не будете уверены, что она верна.

224

ЧАСТЬ II

Высококачественный код

Вот несколько советов по эффективной компиляции.
 Установите наивысший уровень предупреждений компилятора. Вы можете от#

ловить изрядное число ошибок, лишь позволив компилятору их обнаруживать.
 Применяйте проверяющие средства. Встроенные проверяющие средства ком#

пиляторов могут быть дополнены внешними, такими как lint для С. Даже не#
компилируемый код, скажем, HTML и JavaScript, можно проверить соответству#
ющими утилитами.
 Выясните причину всех сообщений об ошибках и предупреждений. Подумай#

те, что сообщение говорит о вашем коде. Многие предупреждения зачастую
указывают на низкое качество кода, и вам следует попытаться понять смысл
каждого предупреждения. На практике предупреждения, которые вы видите раз
за разом, вызывают одну из двух реакций: вы или не обращаете на них внима#
ния, и они скрывают от вашего взгляда более важные предупреждения, или они
попросту вас раздражают. Как правило, проще и безболезненней переписать
код, решив проблему, вызывающую предупреждения, и, таким образом, изба#
виться от них.
Перекрестная ссылка Подробности см. в главе 22 и подразделе «Создание лесов для тестирования отдельных классов»
раздела 22.5.

Пройдите по коду отладчиком Скомпилировав метод,
запустите его в отладчике и пройдите по каждой строке кода.
Убедитесь, что каждая строка выполняется так, как вы ожи#
даете. Следуя этому простому совету, вы сможете найти мно#
го ошибок.

Протестируйте код Протестируйте код, используя те#
стовые примеры, которые вы запланировали или уже создали при разработке
метода. Возможно, вы создали леса для поддержки тестирования, т. е. код, поддер#
живающий методы при тестировании и не включаемый в конечный продукт. Леса
могут представлять собой методы — тестовые сбруи, которые вызывают ваш ме#
тод с тестовыми данными, или заглушки, вызываемые вашим методом.
Удалите ошибки из метода Обнаруженные ошибки
нужно удалить. Если к этому моменту ваш метод работает
нестабильно, велика вероятность, что он таким и останет#
ся. Обнаружив непонятное поведение метода, начните все заново. Не пытайтесь
доводить его до ума — перепишите его. Изощренные переделки обычно говорят
о неполном понимании и гарантируют возникновение ошибок как сейчас, так и
в будущем. Полное перепроектирование нестабильного метода полностью оправ#
данно. Мало что сравнится по эффективности с переписыванием проблемного
метода — вы позабудете о бывших ошибках.

Перекрестная ссылка Подробности см. в главе 23.

Наведение глянца
Проверив код, оцените его с учетом общих критериев, описанных в этой книге.
Чтобы гарантировать соответствие качества метода высоким стандартам, сделай#
те следующее.
 Проверьте интерфейс метода. Убедитесь, что применяются все входные и вы#

ходные данные и используются все параметры (см. раздел 7.5).

ГЛАВА 9 Процесс программирования с псевдокодом

225

 Проверьте общее качество конструкции. Убедитесь, что метод выполняет един#

ственную задачу и делает это хорошо, имея в виду его слабое сопряжение с
другими методами и проектирование в соответствии с методикой защитного
программирования (см. главу 7).
 Проверьте переменные метода: корректность их именования, неиспользуемые

объекты, необъявленные переменные, неверно инициализированные объек#
ты и т. д. (см. главы 10–13).
 Проверьте логику метода. Проанализируйте наличие ошибок занижения/завы#

шения на 1, некорректной вложенности и утечки ресурсов (см. главы 14–19).
 Проверьте форматирование метода. Убедитесь в корректном использовании

пробелов для структурирования метода, выражений и списка параметров (см.
главу 31).
 Проверьте документирование метода. Убедитесь в корректности псевдокода,

переведенного в комментарии. Проверьте описание алгоритма, документиро#
вание интерфейса, неочевидных зависимостей и нестандартных подходов (см.
главу 32).
 Удалите лишние комментарии. Иногда комментарии, полученные из псевдо#

кода, являются избыточными, особенно когда ППП применяется рекурсивно
и комментарии лишь описывают вызов метода, назначение которого и так
понятно из его имени.

Повторите нужное число раз
Если качество метода неудовлетворительное, вернитесь к псевдокоду. Создание
высококачественного ПО — итеративный процесс, так что без колебаний повто#
ряйте весь цикл конструирования вновь и вновь.

9.4.

Альтернативы ППП

Для меня ППП — идеальная методика создания классов и методов. Другие специ#
алисты рекомендуют иные подходы. Вы можете применять их как альтернативу
или дополнение ППП.
Разработка с изначальными тестами Это популярный стиль разработки, при
котором тестовые задания пишутся до самого кода (см. раздел 22.2). Есть хорошая
книга Кента Бека на эту тему — «Test#Driven Development: By Example» (Beck, 2003).
Рефакторинг Рефакторинг — это подход к разработке с усовершенствовани#
ем кода посредством последовательности семантически корректных преобразо#
ваний. Программисты пользуются шаблонами плохого кода или «запахами» (smells)
для выявления разделов кода, подлежащих усовершенствованию. Этот подход по#
дробно описан в главе 24, а также в книге Мартина Фаулера «Refactoring: Improving
the Design of Existing Code» (Fowler, 1999).
Проектирование по контракту Это подход предполагает, что каждый метод
имеет пред# и постусловия (см. раздел 8.2). Лучший источник информации на эту
тему — книга Бертрана Мейера «Object#Oriented Software Construction» (Meyer, 1997).

226

ЧАСТЬ II

Высококачественный код

Бессистемное программирование Некоторые программисты лепят программу
как попало, а не используют тот или иной систематический подход, например ППП.
Если вы не понимаете, что делать дальше, это признак того, что надо переходить
на ППП. Не было ли у вас такого, чтобы вы забывали написать часть класса или
метода? Вряд ли такое могло случиться при применении ППП. Если вы глядите
на экран и не знаете, с чего начать, пора начать ППП, который сделает вашу про#
граммистскую долю проще.

http://cc2e.com/0943

Контрольный список: Процесс Программирования
с Псевдокодом

 Проверили ли вы, что удовлетворяются предварительные условия?
Перекрестная ссылка Назначе Определена ли проблема, которую решает класс?
ние этого списка — проверить,
 Достаточно ли понятна высокоуровневая конструкция,
применяете ли вы правильные
методики при создании меточтобы дать классам и методам адекватные имена?
дов. Контрольный список по
 Продумали ли вы тестирование класса и каждого из его
качеству методов как таковых
методов?
см. в главе 7.
 Рассматривали ли вы эффективность с позиции стабильных интерфейсов и понятной реализации или с позиции
соответствия ресурсам и бюджету?
 Проанализировали ли вы стандартные и другие библиотеки на предмет
наличия подходящих методов и компонентов?
 Просмотрели ли вы литературу в поисках полезных алгоритмов?
 Проектировали ли вы каждый метод с использованием подробного псевдокода?
 Проверили ли вы псевдокод умозрительно? Легко ли его понять?
 Уделили ли вы внимание предупреждениям, которые указывают на необходимость перепроектирования (использование глобальных данных, операции,
которые лучше перенести в другой класс или метод и т. д.)?
 Точно ли вы перевели псевдокод в код?
 Рекурсивно ли вы применяли ППП, разбивая методы на более мелкие при
необходимости?
 Документировали ли вы принимаемые допущения?
 Убрали ли вы избыточные комментарии?
 Проделали ли вы несколько итераций или остановились после первой?
 Вполне ли вы понимаете свой код? Легко ли в нем разобраться?

Ключевые моменты
 Конструирование классов и методов — процесс итеративный. Особенности,

замечаемые при конструировании отдельных методов, заставляют возвращаться
к проектированию класса.
 Написание хорошего псевдокода предполагает употребление понятного есте#

ственного языка без специфических особенностей конкретного языка програм#
мирования, а также формулировок на уровне намерений (описания сути кон#
струкции, а не способов ее работы).

ГЛАВА 9 Процесс программирования с псевдокодом

227

 Процесс Программирования с Псевдокодом — полезный инструмент деталь#

ного проектирования, упрощающий кодирование. Псевдокод транслируется
непосредственно в комментарии, гарантируя их адекватность и полезность.
 Не останавливайтесь на первой придуманной вами конструкции — испробуй#

те несколько подходов и выберите лучший, прежде чем писать код.
 Проверяйте свою работу на каждом шаге и просите об этом других. При этом

вы отловите ошибки на наименее дорогостоящем уровне, когда вы вложили в
работу меньше усилий.

ГЛАВА 10 Общие принципы использования переменных

Часть III

ПЕРЕМЕННЫЕ



Глава 10. Общие принципы использования переменных



Глава 11. Сила имен переменных



Глава 12. Основные типы данных



Глава 13. Нестандартные типы данных

229

230

ЧАСТЬ III

Г Л А В А

Переменные

1 0

Общие принципы
использования переменных

http://cc2e.com/1085

Содержание
 10.1. Что вы знаете о данных?
 10.2. Грамотное объявление переменных
 10.3. Принципы инициализации переменных
 10.4. Область видимости
 10.5. Персистентность
 10.6. Время связывания
 10.7. Связь между типами данных и управляющими

структурами
 10.8. Единственность цели каждой переменной

Связанные темы
 Именование переменных: глава 11
 Фундаментальные типы данных: глава 12
 Редкие типы данных: глава 13
 Размещение объявлений данных: одноименный подраздел раздела 31.5
 Документирование переменных: подраздел «Комментирование объявлений

данных» раздела 32.5
Если при конструировании приходится заполнять небольшие пробелы в требо#
ваниях и архитектуре, это нормально и даже желательно. Проектирование про#
граммы вплоть до микроскопических деталей было бы неэффективным. В этой
главе рассматривается один из низкоуровневых вопросов конструирования —
использование переменных.
Эта глава будет особенно полезна опытным программистам. Довольно часто мы
применяем рискованные подходы, не имея полного представления об альтерна#
тивах, а после используем их в силу привычки. Особый интерес для опытных
программистов могут представлять разделы 10.6 и 10.8, посвященные соответствен#

ГЛАВА 10 Общие принципы использования переменных

231

но времени связывания и использованию переменных только с одной целью. Если
вы не знаете, насколько «опытным» программистом вы являетесь, пройдите «Тест
на знание типов данных» в разделе 10.1 и узнайте.
В этой главе я буду понимать под «переменными» и объекты, и встроенные типы
данных, такие как целые числа и массивы. «Типами данных» я, как правило, буду
называть встроенные типы данных, а просто «данными» — и встроенные типы
данных, и объекты.

10.1. Что вы знаете о данных?
Создавая данные, вы должны в первую очередь понять, какие именно
данные вам нужны. Обширные знания о разных типах данных — важней#
ший компонент в инструментарии любого программиста. Введения в типы
данных в этой книге вы не найдете, но «Тест на знание данных» поможет вам
определить, сколько еще вам нужно о них узнать.

Тест на знание данных
Поставьте себе 1 балл за каждый термин, который вам известен. Если термин ка#
жется знакомым, но вы не уверены в его значении, поставьте себе 0,5 балла. Вы#
полнив тест, просуммируйте баллы и оцените результат по приведенному ниже
описанию.
______ abstract data type
(абстрактный тип данных)

______ literal (литерал)

______ array (массив)

______ local variable (локальная
переменная)

______ bitmap (растровое изображение)

______ lookup table (таблица поиска)

______ boolean variable (булева переменная)

______ member data (данные#члены)

______ B#tree (B#дерево)

______ pointer (указатель)

______ character variable
(символьная переменная)

______ private (закрытый)

______ container class (класс#контейнер)

______ retroactive synapse (ретроактивный
синапс)

______ double precision (двойная точность)

______ referential integrity (целостность
ссылочных данных)

______ elongated stream (удлиненный поток) ______ stack (стек)
______ enumerated type (перечисление)

______ string (строка)

______ floating point (число
с плавающей точкой)

______ structured variable
(структурная переменная)

______ heap (куча)

______ tree (дерево)

______ index (индекс)

______ typedef (синоним типа)

______ integer (целое число)

______ union (объединение)

______ linked list (связанный список)

______ value chain (цепочка
начисления стоимости)

______ named constant
(именованная константа)

______ variant (универсальный тип)
______ Общее число баллов

232

ЧАСТЬ III

Переменные

Интерпретировать результаты можно примерно так.
0–14

Вы — начинающий программист; возможно, вы учитесь на первом курсе ин#
ститута или самостоятельно изучаете свой первый язык программирования.
Вы можете многое узнать, прочитав одну из книг, перечисленных в следующем
подразделе. Вернувшись к этой части книги потом, вы извлечете больше
пользы, потому что этот материал адресован более опытным программистам.

15–19

Вы — программист среднего уровня или опытный программист, который мно#
гое забыл. Из книг, указанных чуть ниже, вы также сможете извлечь выгоду,
хотя многие концепции будут вам знакомы.

20–24

Вы — эксперт в программировании. Вероятно, на вашей полке уже стоят книги,
указанные ниже.

25–29

Вы знаете о типах данных больше, чем я. Может, напишете собственную книгу
(пришлите мне экземпляр!)?

30–32

Вы — тщеславный мошенник. Термины «удлиненный поток», «ретроактивный
синапс» и «цепочка начисления стоимости» не имеют никакого отношения
к типам данных — я их выдумал! Прочитайте раздел «Профессиональная чест#
ность» в главе 33!

Дополнительные ресурсы
Хорошими источниками информации о типах данных являются следующие книги:
Cormen, H. Thomas, Charles E. Leiserson, Ronald L. Rivest. Introduction to Algorithms.
New York, NY: McGraw Hill. 1990.
Sedgewick, Robert. Algorithms in C++, Parts 1%4, 3d ed. Boston, MA: Addison#Wesley, 1998.
Sedgewick, Robert. Algorithms in C++, Part 5, 3d ed. Boston, MA: Addison#Wesley, 2002.

10.2. Грамотное объявление переменных
Перекрестная ссылка О форматировании объявлений переменных см. одноименный подраздел раздела 31.5, а о документировании — подраздел «Комментирование объявлений данных» раздела 32.5.

В этом разделе описаны способы оптимизации объявления
переменных. Строго говоря, это не такая уж и крупная за#
дача, и вы могли бы подумать, что она не заслуживает соб#
ственного раздела. И все же создавать переменные прихо#
дится очень часто, и приобретение правильных привычек
поможет вам сэкономить время и исключить ненужные
разочарования.

Неявные объявления
Некоторые языки поддерживают неявное объявление переменных. Так, если, про#
граммируя на Microsoft Visual Basic, вы попытаетесь использовать необъявленную
переменную, компилятор может автоматически объявить ее для вас (это зависит
от параметров компилятора).
Неявное объявление переменных — одна из самых опасных возможностей язы#
ка. Если вы программировали на Visual Basic, то знаете, как жаль времени, потра#
ченного на поиск причины неправильного значения acctNo, если в итоге обнару#
живается, что вы по ошибке вызвали переменную acctNum, которая была иници#
ализирована нулем. Если язык не заставляет объявлять переменные, подобную
ошибку допустить очень легко.

ГЛАВА 10 Общие принципы использования переменных

233

Если язык требует объявления переменных, для столкновения с данной
проблемой нужно сделать две ошибки: во#первых, использовать в теле
метода и acctNum, и acctNo, ну, а во#вторых, объявить в методе обе эти
переменные. Такую ошибку допустить сложнее, что практическиустраняет про#
блему похожих имен переменных. По сути языки, требующие явного объявления
переменных, заставляют более внимательно использовать данные, что является
одним из важнейших достоинств таких языков. А если язык поддерживает неяв#
ные объявления? Несколько советов я привел ниже.
Отключите неявные объявления Некоторые компиляторы позволяют запре#
тить неявные объявления. Например, в Visual Basic для этого служит директива
Option Explicit On, которая заставляет объявлять все используемые переменные.
Объявляйте все переменные Печатая имя новой переменной, объявите ее, даже
если компилятор этого не требует. Пусть не от всех, но от некоторых ошибок это
вас избавит.
Используйте конвенции именования Чтобы не исполь#
зовать две переменные там, где предполагается одна, задайте
конвенцию употребления популярных суффиксов в именах
переменных. Скажем, конвенция может требовать примене#
ния директивы Option Explicit On и суффикса No.

Перекрестная ссылка О стандартизации сокращений имен см.
подраздел «Общие советы по
сокращению имен» раздела 11.6.

Проверяйте имена переменных Просматривайте список перекрестных ссы#
лок, сгенерированный компилятором или утилитой. Многие компиляторы состав#
ляют список всех переменных метода, что, например, позволяет узнать о неумыш#
ленном применении двух переменных с похожими именами. Кроме того, компи#
ляторы могут указывать на объявленные, но неиспользованные переменные.

10.3. Принципы инициализации переменных
Неверная инициализация данных — один из самых плодородных источ#
ников ошибок в программировании. Эффективные способы предотвра#
щения проблем с инициализацией могут значительно ускорить отладку.
При неверной инициализации проблемы объясняются тем, что переменная име#
ет не то первоначальное значение, которое вы ожидаете. Это может случиться по
одной из следующих причин.
 Переменной не было присвоено значения. Она имеет то

случайное значение, которое находилось в соответству#
ющих ячейках памяти при запуске программы.
 Значение переменной устарело. Когда#то переменной

было присвоено значение, но оно утратило свою акту#
альность.

Перекрестная ссылка О тестировании, основанном на шаблонах инициализации данных и их
использования, см. подраздел
«Тестирование, основанное на
потоках данных» раздела 22.3.

 Одним частям переменной были присвоены значения, а другим нет.

Последняя причина имеет несколько вариаций. Вы можете инициализировать
несколько членов объекта, но не все. Вы можете забыть выделить память и ини#
циализировать «переменную», на которую указывает неинициализированный ука#
затель. При этом на самом деле вы занесете некоторое значение в случайный блок
памяти. Им может оказаться область памяти, содержащая данные. Им может ока#

234

ЧАСТЬ III

Переменные

заться область памяти, содержащая код. В этом блоке может находиться фрагмент
ОС. Проблема с указателями может проявляться совершенно неожиданным обра#
зом, изменяющимся от случая к случаю, поэтому найти такие ошибки сложнее,
чем любые другие.
Ниже описаны способы предотвращения проблем, связанных с инициализацией.
Инициализируйте каждую переменную при ее объявлении Инициализация
переменных при их объявлении — простая методика защитного программиро#
вания и хорошая страховка от ошибок инициализации. Так, следующий код по#
зволяет гарантировать, что инициализация массива studentGrades будет выполняться
при каждом вызове метода, содержащего массив:

Пример инициализации массива при его объявлении (C++)
float studentGrades[ MAX_STUDENTS ] = { 0.0 };
Инициализируйте каждую переменную там, где она
используется в первый раз Visual Basic и некоторые
другие языки не позволяют инициализировать переменные
при их объявлении. В результате код может принимать вид,
при котором сначала выполняется объявление нескольких
переменных, а потом эти переменные инициализируются — и то и другое про#
исходит вдали от места фактического использования переменных в первый раз:

Перекрестная ссылка Проверка
входных параметров является
еще одной формой защитного
программирования (см. главу 8).

Пример плохой инициализации
переменных (Visual Basic)
‘ объявление всех переменных
Dim accountIndex As Integer
Dim total As Double
Dim done As Boolean
’ инициализация всех переменных
accountIndex = 0
total = 0.0
done = False
...
’ использование переменной accountIndex
...
’ использование переменной total
...
’ использование переменной done
While Not done
...
Лучше инициализировать каждую переменную как можно ближе к месту первого
обращения к ней:

ГЛАВА 10 Общие принципы использования переменных

235

Пример хорошей инициализации переменных (Visual Basic)
Dim accountIndex As Integer
accountIndex = 0
’ использование переменной accountIndex
...
Dim total As Double
Переменная total объявляется и инициализируется непосредственно перед ее использованием.

>total = 0.0
’ использование переменной total
...
Dim done As Boolean
Переменная done также объявляется и инициализируется непосредственно перед ее использованием.

> done = False
’ использование переменной done
While Not done
...
Второй вариант лучше первого по нескольким причинам. Пока выполнение пер#
вого примера дойдет до фрагмента, в котором используется переменная done, она
может оказаться измененной. Даже если при написании программы это не так, нельзя
гарантировать, что этого не произойдет после нескольких ее изменений. Кроме того,
в первом примере все переменные инициализируются в одном месте, из#за чего
создается впечатление, что все они используются на протяжении всего метода, тогда
как на самом деле переменная done вызывается только в его конце. Наконец, в ре#
зультате изменений программы (которые неизбежно придется вносить, и не толь#
ко при отладке) код, использующий переменную done, может оказаться заключен#
ным в цикл, при этом переменную каждый раз нужно будет инициализировать за#
ново. Во второй пример в этом случае придется внести лишь небольшое измене#
ние. Первый пример слабее защищен от досадных ошибок инициализации.
Два этих фрагмента иллюстрируют Принцип Близости: груп#
пируйте связанные действия вместе. Этот принцип предпо#
лагает также близость комментариев к описываемому ими
коду, близость кода настройки цикла к самому циклу, груп#
пировку команд в линейных участках программы и т. д.

Перекрестная ссылка О группировке связанных действий см.
раздел 10.4.

В идеальном случае сразу объявляйте и определяйте каждую переменную
непосредственно перед первым обращением к ней При объявлении пере#
менной вы указываете ее тип. При определении вы присваиваете ей конкретное
значение. Если язык позволяет (к таким языкам относятся, например, C++ и Java),
переменную следует объявлять и определять перед фрагментом, в котором она
используется впервые. В идеале каждую переменную следует определять при ее
объявлении:

236

ЧАСТЬ III

Переменные

Пример хорошей инициализации переменных (Java)
int accountIndex = 0;
// использование переменной accountIndex
...
Переменная total инициализируется непосредственно перед ее использованием.

> double total = 0.0;
// использование переменной total
...
Переменная done также инициализируется непосредственно перед ее использованием.

> boolean done = false;
// использование переменной done
while ( ! done ) {
...
Объявляйте переменные по мере возможности как final
или const Объявив переменную как final в Java или const
в C++, вы можете предотвратить изменение ее значения пос#
ле инициализации. Ключевые слова final и const полезны для
определения констант класса, исключительно входных параметров и любых ло#
кальных переменных, значения которых должны оставаться неизменными после
инициализации.

Перекрестная ссылка О группировке связанных действий см.
также раздел 14.2.

Уделяйте особое внимание счетчикам и аккумуляторам Переменные i, j,
k, sum и total часто играют роль счетчиков или аккумуляторов. Нередко програм#
мисты забывают обнулить счетчик или аккумулятор перед его использованием в
очередной раз.
Инициализируйте данные'члены класса в его конструкторе Подобно пе#
ременным метода, которые следует инициализировать при вызове каждого мето#
да, данные класса следует инициализировать в его конструкторе. Если в конструк#
торе выделяется память, в деструкторе ее следует освободить.
Проверяйте необходимость повторной инициализации Спросите себя, нуж#
но ли будет когда#нибудь инициализировать переменную повторно: например, для
применения в цикле или для переустановки ее значения между вызовами метода.
Если да, убедитесь, что команда инициализации входит в повторяющийся фраг#
мент кода.
Инициализируйте именованные константы один раз; переменные иници'
ализируйте в исполняемом коде Если переменные служат для имитации име#
нованных констант, вполне допустимо инициализировать их один раз при запуске
программы. Инициализируйте их в методе Startup(). Истинные переменные ини#
циализируйте в исполняемом коде неподалеку от места их вызова. Очень часто
метод, который первоначально применялся один раз, после изменения програм#
мы вызывается многократно. Переменные, которые инициализируются в методе
Startup() уровня программы, не будут инициализироваться повторно.

ГЛАВА 10 Общие принципы использования переменных

237

Настройте компилятор так, чтобы он автоматически инициализиро'
вал все переменные Если ваш компилятор поддерживает такую возможность,
заставьте его автоматически инициализировать все переменные. Однако, полагаясь
на компилятор, вы можете столкнуться с проблемами при переносе кода на другой
компьютер или при использовании другого компилятора. Документируйте исполь#
зование параметров компилятора — без такой документации предположения, осно#
ванные на конкретных параметрах компилятора, определить очень трудно.
Внимательно изучайте предупреждения компилятора Многие компиля#
торы предупреждают об использовании неинициализированных переменных.
Проверяйте корректность входных параметров
Это еще один эффективный способ предотвращения ошибок
инициализации. Прежде чем присвоить входные значения
чему#либо, убедитесь, что они допустимы.

Перекрестная ссылка О проверке
входных параметров см. главу 8,
преимущественно раздел 8.1.

Используйте утилиту проверки доступа к памяти для обнаружения не'
верно инициализированных указателей Некоторые ОС сами следят за кор#
ректностью обращений к памяти, выполняемых при помощи указателей, другие ос#
тавляют вас на произвол судьбы. Тогда можно приобрести инструмент проверки
доступа к памяти и проконтролировать использование указателей в своей программе.
Инициализируйте рабочую память при запуске программы Инициализа#
ция рабочей памяти известным значением облегчает поиск ошибок инициализа#
ции. Этого позволяют достичь описанные ниже подходы.
 Вы можете использовать специализированную утилиту для заполнения памя#

ти определенным значением перед запуском программы. Для некоторых це#
лей хорошо подходит значение 0, потому что оно гарантирует, что неиници#
ализированные указатели будут указывать на нижнюю область памяти, благо#
даря чему их будет относительно легко найти. В случае процессоров с архи#
тектурой Intel целесообразно заполнить память значением 0xCC, потому что
оно соответствует машинному коду команды точки прерывания; если вы запу#
стите код в отладчике и попытаетесь выполнить данные, а не код, вы потонете
в точках прерывания. Еще одно достоинство значения 0xCC в том, что его легко
заметить в дампах памяти; кроме того, оно редко используется. По этим же
причинам Брайан Керниган и Роб Пайк предлагают заполнять память констан#
той 0xDEADBEEF 1 (Kernighan and Pike, 1999).
 Если вы применяете утилиту заполнения памяти, можете время от времени

изменять используемое ей значение. «Встряхивание» программы иногда позво#
ляет обнаружить проблемы, которые остаются скрытыми, если среда никогда
не изменяется.
 Вы можете сделать так, чтобы программа инициализировала свою рабочую

память при запуске. Цель заполнения памяти до запуска программы — обна#
ружение дефектов, тогда как цель этого подхода — их сокрытие. Заполняя
рабочую память каждый раз одинаковым значением, вы сможете гарантиро#
вать, что программа не будет зависеть от случайных изменений начальной
конфигурации памяти.
1

Букв. «мертвая корова». — Прим. перев.

238

ЧАСТЬ III

Переменные

10.4. Область видимости
Область видимости можно понимать как «известность» переменной в програм#
ме. Областью видимости называют фрагмент программы, в котором переменная
известна и может быть использована. Переменная с ограниченной или неболь#
шой областью видимости известна только в небольшом фрагменте программы:
в качестве примера можно привести индекс, используемый в теле одного неболь#
шого цикла. Переменная с большой областью видимости известна во многих местах
программы: примером может служить таблица с данными о сотрудниках, исполь#
зуемая по всей программе.
В разных языках реализованы разные подходы к области видимости. В некото#
рых примитивных языках все переменные глобальны. В этом случае вы не имее#
те контроля над областью видимости переменных, что создает много проблем.
В C++ и похожих языках переменная может иметь область видимости, соответ#
ствующую блоку (фрагменту кода, заключенному в фигурные скобки), методу, классу
(возможно, и производным от него классам) или всей программе. В Java и C#
переменная может также иметь область видимости, соответствующую пакету или
пространству имен (набору классов).
Ниже я привел ряд советов, относящихся к области видимости.

Локализуйте обращения к переменным
Код, расположенный между обращениями к переменной, является «окном уязви#
мости». Чем больше это окно, тем выше вероятность, что в его пределах будет
добавлен новый код, искажающий значение переменной, и тем труднее следить
за значением переменной при чтении кода. Поэтому обращения к переменной
всегда целесообразно локализовать, группируя их вместе.
Идея локализации обращений к переменным самоочевидна, однако она допуска#
ет и формальную оценку. Одним из методов оценки степени сгруппированности
обращений к переменной является определение «интервала» (span) между обра#
щениями, например:

Пример определения интервалов между обращениями
к переменным (Java)
a
b
c
a

=
=
=
=

0;
0;
0;
b + c;

В данном случае между первым и вторым обращениями к a находятся две строки
кода, поэтому и интервал равен 2. Между двумя обращениями к b — одна строка,
что дает нам интервал, равный 1, ну а интервал между обращениями к c равен 0.
Вот еще один пример:

ГЛАВА 10 Общие принципы использования переменных

239

Пример интервалов, равных 1 и 0 (Java)
a
b
c
b
b

=
=
=
=
=

0;
0;
0;
a + 1;
b / c;

В этом примере между первым и вторым обращениями к b —
одна строка кода, а между вторым и третьим обращениями
строк нет, поэтому интервалы равны соответственно 1 и 0.

Дополнительные сведения Об
интервалах между обращениями к переменным см. работу
«Software Engineering Metrics
and Models» (Conte, Dunsmore,
and Shen, 1986).

Средний интервал вычисляется путем усреднения отдельных
интервалов. Так, во втором примере средний интервал между
обращениями к b равен (1+0)/2, или 0,5. Локализовав обра#
щения к переменным, вы позволите программисту, который будет читать ваш код,
сосредоточиваться на меньшем фрагменте программы в каждый конкретный мо#
мент времени. Если обращения будут распределены по большему фрагменту кода,
уследить за ними будет сложнее. Таким образом, главное преимущество локали#
зации обращений к переменным в том, что оно облегчает чтение программы.

Делайте время жизни переменных как можно короче
С интервалом между обращениями к переменной тесно связано «время жизни»
переменной — общее число строк, на протяжении которых переменная исполь#
зуется. Жизнь переменной начинается при первом обращении к ней, а заканчи#
вается при последнем.
В отличие от интервала время жизни переменной не зависит от числа обраще#
ний к ней между первым и последним обращениями. Если переменная в первый
раз вызывается в строке 1, а в последний — в строке 25, ее время жизни равно 25
строкам. Если переменная используется только в этих двух строках, средний ин#
тервал между обращениями к ней — 23 строки. Если бы между строками 1 и 25
переменная вызывалась в каждой строке, она имела бы средний интервал, равный
0, но время ее жизни по#прежнему равнялось бы 25 строкам. Связь интервалов меж#
ду обращениями к переменной и времени ее жизни пояснена на рис. 10#1.
Как и интервал между обращениями к переменной, время ее жизни желательно
делать как можно короче. Преимущество в обоих случаях одинаково: это умень#
шает окно уязвимости, снижая вероятность неверного или неумышленного изме#
нения переменной между действительно нужными обращениями к ней.
Второе преимущество короткого срока жизни: оно позволяет получить верное
представление о коде. Если переменная изменяется в строке 10 и вызывается в
строке 45, само пространство между двумя обращениями подразумевает, что пе#
ременная используется также между строками 10 и 45. Если переменная изменя#
ется в строке 44 и вызывается в строке 45, других обращений к ней между этими
строками быть не может, что позволяет вам сосредоточиться на меньшем фраг#
менте кода.

240

ЧАСТЬ III

Переменные

Рис. 10'1. «Длительное время жизни» подразумевает, что переменная используется
в крупном фрагменте кода. При «коротком времени жизни» переменная используется
лишь в небольшом фрагменте. «Интервал между обращениями» к переменной
характеризует, насколько тесно сгруппированы обращения к переменной

Короткое время жизни снижает вероятность ошибок инициализации. По мере
изменения программы линейные участки кода имеют тенденцию превращаться
в циклы, при этом программисты часто забывают про инициализацию перемен#
ных, выполненную вдали от цикла. Поддерживая код инициализации и код цикла
в непосредственной близости, вы снизите вероятность того, что изменения при#
ведут к ошибкам инициализации.
Кроме того, короткое время жизни облегчает чтение кода. Чем меньше строк кода
нужно удерживать в уме в каждый конкретный момент времени, тем проще по#
нять код. К тому же при небольшом времени жизни на экране помещаются сразу
все обращения к переменной, что облегчает редактирование и отладку.
Наконец, короткое время жизни облегчает разделение крупного метода на мень#
шие. Если обращения к переменным сгруппированы в небольшом фрагменте, его
проще выделить в отдельный метод.

Оценка времени жизни переменной
Время жизни переменной можно формализовать, подсчитав число строк между пер#
вым и последним обращениями к ней (с учетом первой и последней строк). В следу#
ющем примере каждая из переменных обладает слишком долгим временем жизни:

Пример слишком долгого времени жизни переменных (Java)
1
2
3

// инициализация каждой переменной
recordIndex = 0;
total = 0;

ГЛАВА 10 Общие принципы использования переменных

241

4

done = false;
...
26 while ( recordIndex < recordCount ) {
27 ...
Последнее обращение к переменной recordIndex.

> 28
64

recordIndex = recordIndex + 1;
...
while ( !done ) {
...

Последнее обращение к переменной total.

>69

if ( total > projectedTotal ) {

Последнее обращение к переменной done.

> 70

done = true;

Времена жизни переменных:

recordIndex (строка 28  строка 2 + 1) = 27
total (строка 69  строка 3 + 1) = 67
done (строка 70  строка 4 + 1) = 67
Среднее время жизни (27 + 67 + 67) / 3 »54
Следующий пример аналогичен предыдущему, только теперь обращения к пере#
менным сгруппированы более тесно:

Пример хорошего, короткого времени жизни переменных (Java)
...
Инициализация переменной recordIndex ранее выполнялась в строке 3.

>25 recordIndex = 0;
26 while ( recordIndex < recordCount ) {
27 ...
28
recordIndex = recordIndex + 1;
...
Инициализация переменных total и done ранее выполнялась в строках 4 и 5.

> 62 total = 0;
63
64
69
70

done = false;
while ( !done ) {
...
if ( total > projectedTotal ) {
done = true;

Теперь времена жизни переменных равны:

recordIndex (строка 28  строка 25 + 1) = 4
total (строка 69  строка 62 + 1) = 8
done (строка 70  строка 63 + 1) = 8
Среднее время жизни (4 + 8 + 8) / 3 »7

242

ЧАСТЬ III

Дополнительные сведения О
времени жизни переменных см.
работу «Software Engineering Metrics and Models» (Conte, Dunsmore, and Shen, 1986).

Переменные

Интуиция подсказывает, что второй вариант предпочтитель#
нее, так как инициализация переменных выполняется бли#
же к месту их использования. Сравнение среднего времени
жизни переменных — 54 и 7 — подкрепляет этот интуитив#
ный вывод конкретными цифрами.

Какое время жизни считать приемлемым? А что можно сказать об интервале? Конк#
ретных цифр у нас пока нет, но разумно предположить, что и интервал между обра#
щениями к переменной, и время ее жизни следует пытаться свести к минимуму.
Если в этом ключе проанализировать глобальные переменные, окажется, что им
соответствует огромный средний интервал между обращениями и такое же вре#
мя жизни, — это одна из многих обоснованных причин избегать глобальных пе#
ременных.

Общие советы по минимизации области видимости
Ниже даны конкретные рекомендации по минимизации области видимости.
Инициализируйте переменные, используемые в цикле,
непосредственно перед циклом, а не в начале метода,
содержащего цикл Следование этому совету снизит ве#
роятность того, что при изменении цикла вы забудете изме#
нить инициализацию используемых в нем переменных. Если
же цикл будет вложен в новый цикл, переменные будут инициализироваться при
каждой итерации нового цикла, а не только при первой.

Перекрестная ссылка Об инициализации переменных около
места их использования см.
раздел 10.3.

Перекрестная ссылка Об этом
стиле объявления и определения переменных см. подраздел
«В идеальном случае сразу
объявляйте и определяйте каждую переменную непосредственно перед первым обращением к
ней» раздела 10.3.

Не присваивайте переменной значение вплоть до его
использования Вероятно, вы знаете, насколько трудно
бывает найти строку, в которой переменной было присво#
ено ее значение. Чем больше вы сделаете для прояснения
того, где переменная получает свое значение, тем лучше.
Такие языки, как C++ и Java, позволяют инициализировать
переменные следующим образом:

Пример грамотного объявления и инициализации переменных (C++)
int receiptIndex = 0;
float dailyReceipts = TodaysReceipts();
double totalReceipts = TotalReceipts( dailyReceipts );
Перекрестная ссылка О группировке связанных команд см.
также раздел 14.2.

Группируйте связанные команды В следующих фраг#
ментах на примере метода, суммирующего дневную выручку,
показано, как сгруппировать обращения к переменным, что#
бы за ними было проще следить. В первом примере этот

принцип нарушен:

Пример запутанного использования двух наборов переменных (C++)
void SummarizeData(...) {
...

ГЛАВА 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;
 всегда: такими переменными могут быть значения, которые вы храните в БД

между запусками программы, — так, например, в случае интерактивной про#
граммы, позволяющей пользователям настраивать цвета экрана, вы могли бы
хранить эти цвета в файле, считывая их при каждой загрузке программы.
Главная проблема персистентности возникает, когда вы предполагаете, что пере#
менная существует дольше, чем есть на самом деле. Переменная похожа на пакет
молока в холодильнике. Предполагается, что он может храниться неделю. Иног#
да он хранится месяц, и с молоком ничего не происходит, а иногда молоко ски#
сает через пять дней. Переменная может быть столь же непредсказуема. Если вы
попытаетесь использовать значение переменной после окончания нормальной дли#
тельности ее существования, получите ли вы ее прежнее значение? Иногда зна#
чение переменной «скисает», и вы получаете ошибку. В других случаях компиля#
тор оставляет старое значение неизменным, позволяя вам воображать, что все
нормально.
Проблем подобного рода можно избежать.
 Включайте в программу отладочный код или утвержде#

ния для проверки важных переменных на предмет до#
пустимости их значений. Если значения недопустимы,
отображайте предупреждение, рекомендующее присту#
пить к поиску кода неверной инициализации.

Перекрестная ссылка Отладочный код легко включать в методы доступа (см. подраздел
«Преимущества методов доступа» раздела 13.3).

 Завершив работу с переменными, присваивайте им «не#

допустимые значения». Скажем, после освобождения памяти при помощи опе#
ратора delete вы может установить указатель в null.
 Исходите из того, что данные не являются персистентными. Так, если при воз#

врате из метода переменная имеет определенное значение, не предполагайте,
что оно будет таким же при следующем вызове метода. Это правило неакту#
ально, если вы используете специфические возможности языка, гарантирую#
щие неизменность значения переменной, такие как ключевое слово static языков
C++ и Java.
 Выработайте привычку объявлять и инициализировать все данные перед их

использованием. Если видите обращение к данным, но не можете найти по#
близости команду их инициализации, отнеситесь к этому с подозрением!

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) — это набор блоков данных, используемых в оп#
ния команд см. главу 14.
ределенном порядке (рис. 10#2). Если у вас есть пять команд
подряд, обрабатывающих пять разных значений, они формируют последователь#
ность команд. Если бы вам нужно было прочитать из файла последовательные
данные (фамилию сотрудника, номер карточки социального обеспечения, адрес,
телефон и возраст), вы включили бы в код последовательность команд.

248

ЧАСТЬ III

Переменные

Рис. 10'2. Последовательными называются данные, обрабатываемые
в определенном порядке

Селективные данные соответствуют операторам if
и case Вообще селективные данные (selective data) пред#
ставляют собой набор, допускающий использование одно#
го и только одного элемента данных в каждый конкретный
момент времени (рис. 10#3). Соответствующими командами, выполняющими фак#
тический выбор данных, являются операторы if%then%else или case. Так, програм#
ма расчета зарплаты должна была бы выполнять разные действия в зависимости
от того, какой является оплата труда конкретного сотрудника: сдельной или по#
временной. Опять#таки шаблоны кода соответствуют шаблонам данных.

Перекрестная ссылка Об условных операторах см. главу 15.

Рис. 10'3. Селективные данные допускают использование только одного
из нескольких элементов

Итеративные данные соответствуют циклам Ите#
ративные данные (iterative data) представляют собой дан#
ные одного типа, повторяющиеся более одного раза (рис.
10#4). Обычно они хранятся как элементы контейнера, записи файла или элементы
массива. Скажем, вы могли бы хранить в файле список номеров карточек соци#
ального обеспечения, для чтения которого было бы разумно использовать соот#
ветствующий цикл.

Перекрестная ссылка О циклах
см. главу 16.

Рис. 10'4.

Итеративные данные повторяются

Реальные данные могут быть комбинацией последовательных, селективных и
итеративных данных. Для описания сложных видов данных подойдет комбина#
ция простых.

ГЛАВА 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.
 Если переменные невозможно объявить и инициализировать одновременно, вы инициализировали их около места использования
в первый раз?
 Правильно ли инициализируются счетчикии аккумуляторы? Выполняется ли
их повторная инициализация, если она необходима?

ГЛАВА 10 Общие принципы использования переменных

251

 Осуществляется ли правильная повторная инициализация переменных в коде,
который выполняется более одного раза?
 Код компилируется без предупреждений? (И задали ли вы самый строгий
уровень диагностики?)
 Если язык поддерживает неявные объявления переменных, постарались ли
вы предотвратить возможные проблемы?

Другие общие вопросы использования данных
 Все ли переменные имеют как можно меньшую область видимости?
 Являются ли обращения к переменным максимально сгруппированными как
в плане интервала между обращениями, так и в плане общего времени жизни?
 Соответствуют ли управляющие структуры типам данных?
 Все ли объявленные переменные используются?
 Все ли переменные связываются в подходящее время, т. е. соблюдаете ли
вы разумный баланс между гибкостью позднего связывания и соответствующей ему повышенной сложностью?
 Каждая ли переменная имеет одну и только одну цель?
 Не имеют ли какие-нибудь переменные скрытого смысла?

Ключевые моменты
 Неграмотная инициализация данных часто приводит к ошибкам. Описанные

в этой главе способы инициализации позволят избежать проблем, связанных
с неожиданными первоначальными значениями переменных.
 Минимизируйте область видимости каждой переменной. Группируйте обраще#

ния к переменным. Старайтесь делать переменные локальными для методов или
классов. Избегайте глобальных данных.
 Располагайте команды, использующие одни и те же переменные, как можно

ближе друг к другу.
 Раннее связывание ограничивает гибкость, но минимизирует сложность про#

граммы. Позднее связывание повышает гибкость, но за это приходится распла#
чиваться повышенной сложностью.
 Используйте каждую переменную исключительно с одной целью.

252

ЧАСТЬ III

Г Л А В А

Переменные

1 1

Сила имен переменных

http://cc2e.com/1184

Содержание
 11.1. Общие принципы выбора имен переменных
 11.2. Именование конкретных типов данных
 11.3. Сила конвенций именования
 11.4. Неформальные конвенции именования
 11.5. Стандартизированные префиксы
 11.6. Грамотное сокращение имен переменных
 11.7. Имена, которых следует избегать

Связанные темы
 Имена методов: раздел 7.3
 Имена классов: раздел 6.2
 Общие принципы использования переменных: глава 10
 Размещение объявлений данных: одноименный подраздел раздела 31.5
 Документирование переменных: подраздел «Комментирование объявлений

данных» раздела 32.5
Несмотря на всю важность выбора удачных имен для эффективного программи#
рования, я не знаю ни одной книги, в которой эта тема обсуждается хотя бы с ми#
нимально приемлемым уровнем детальности. Многие авторы посвящают пару
абзацев выбору аббревиатур, приводят несколько банальных примеров и ожида#
ют, что вы сами о себе позаботитесь. Я рискую быть обвиненным в противопо#
ложном: вы получите настолько подробные сведения об именовании переменных,
что никогда не сможете использовать их в полном объеме!
Советы, рассматриваемые в этой главе, касаются преимущественно именования
переменных: объектов и элементарных типов данных. Однако их следует учиты#
вать и при именовании классов, пакетов, файлов и других сущностей из мира
программирования. Об именовании методов см. также раздел 7.3.

ГЛАВА 11 Сила имен переменных

253

11.1. Общие принципы выбора
имен переменных
Имя переменной нельзя выбирать, как кличку собаке: опираясь на его вычурность
или звучание. В отличие от собаки и ее клички, которые являются разными сущ#
ностями, переменная и ее имя формируют по идее одну сущность. Поэтому и адек#
ватность переменной во многом определяется ее именем. Выбирайте имена пе#
ременных со всей тщательностью.
В следующем примере переменные названы плохо:

Пример неудачного именования
переменных (Java)
x =
xxx
x =
x =

x
=
x
x

 xx;
fido + SalesTax( fido );
+ LateFee( x1, x ) + xxx;
+ Interest( x1, x );

Что происходит в этом фрагменте кода? Что означают имена x1, xx и xxx? А fido?
Допустим, кто#то сказал вам, что этот код подсчитывает общую сумму предъявля#
емого клиенту счета, опираясь на его долг и стоимость новых покупок. Какую
переменную вы использовали бы для распечатки общей стоимости только новых
покупок?
Взглянув на исправленный вариант того же кода, ответить на этот вопрос куда
проще:

Пример удачного именования переменных (Java)
balance = balance  lastPayment;
monthlyTotal = newPurchases + SalesTax( newPurchases );
balance = balance + LateFee( customerID, balance ) + monthlyTotal;
balance = balance + Interest( customerID, balance );
Из сравнения этих фрагментов можно сделать вывод, что хорошее имя перемен#
ной адекватно ее характеризует, легко читается и хорошо запоминается. Чтобы
облегчить себе достижение этих целей, соблюдайте несколько общих правил.

Самый важный принцип именования переменных
Важнейший принцип именования переменных состоит в том, что имя
должно полно и точно описывать сущность, представляемую перемен#
ной. Один эффективный способ выбора хорошего имени предполагает
формулирование сути переменной в словах. Оптимальным именем переменной
часто оказывается само это высказывание. Благодаря отсутствию загадочных со#
кращений оно удобочитаемо; к тому же оно однозначно. Так как оно является
полным описанием сущности, его нельзя спутать с чем#либо другим. Наконец, такое
имя легко запомнить, потому что оно похоже на исходную концепцию.
Переменную, представляющую число членов олимпийской команды США, мож#
но было бы назвать numberOfPeopleOnTheUsOlympicTeam. Переменную, представ#

254

ЧАСТЬ III

Переменные

ляющую число мест на стадионе, — numberOfSeatsInTheStadium. Для хранения
максимального числа очков, набранных спортсменами какой#то страны в совре#
менной Олимпиаде, можно было бы создать переменную maximumNumberOfPoint%
sInModernOlympics. Переменную, определяющую текущую процентную ставку, лучше
было бы назвать rate или interestRate, а не r или x. Думаю, идею вы поняли.
Обратите внимание на две характеристики этих имен. Во#первых, их легко рас#
шифровать. Фактически их не нужно расшифровывать вообще: их можно просто
прочитать. Ну, а во#вторых, некоторые имена велики — слишком велики, чтобы
быть практичными. Длину имен переменных мы рассмотрим ниже.
Несколько примеров удачных и неудачных имен переменных я привел в табл. 11#1.

Табл. 11-1. Примеры удачных и неудачных имен переменных
Удачные имена,
адекватное описание

Неудачные имена,
неадекватное описание

Сумма, на которую
на данный момент
выписаны чеки

runningTotal, checkTotal

written, ct, checks, CHKTTL,
x, x1, x2

Скорость поезда

velocity, trainVelocity, velocityInMph

velt, v, tv, x, x1, x2, train

Текущая дата

currentDate, todaysDate

Суть переменной

Число строк на странице linesPerPage

cd, current, c, x, x1, x2, date
lpp, lines, l, x, x1, x2

Имена currentDate и todaysDate — хорошие имена, потому что полно и точно
описывают идею «текущей даты». Фактически они составлены из слов с очевид#
ным значением. Программисты иногда упускают из виду обычные слова, которые
порой приводят к самому простому решению. Имена cd и c неудачны потому, что
слишком коротки и «неописательны». Имя current тоже неудачно: оно не говорит,
что именно является «текущим». Имя date кажется хорошим, но в итоге оно ока#
зывается плохим, потому что мы имеем в виду не любую дату, а текущую; само по
себе имя date об этом не говорит. Имена x, x1 и x2 заведомо неудачны: x тради#
ционно представляет неизвестное количество чего#либо, и, если вы не хотите,
чтобы ваши переменные были неизвестными величинами, подумайте о выборе
других имен.
Имена должны быть максимально конкретны. Имена x, temp, i и другие,
достаточно общие для того, чтобы их можно было использовать более
чем с одной целью, не так информативны, как могли бы быть, и обычно
являются плохими.

Ориентация на проблему
Хорошее мнемоническое имя чаще всего описывает проблему, а не ее решение.
Хорошее имя в большей степени выражает что, а не как. Если же имя описывает
некоторый аспект вычислений, а не проблемы, имеет место обратное. Предпочи#
тайте таким именам переменных имена, характеризующие саму проблему.
Запись данных о сотруднике можно было бы назвать inputRec или employeeData.
Имя inputRec — компьютерный термин, выражающий идеи ввода данных и запи#
си. Имя employeeData относится к проблемной области, а не к миру компьюте#
ров. В случае битового поля, определяющего статус принтера, имя bitFlag более

ГЛАВА 11 Сила имен переменных

255

компьютеризировано, чем printerReady, а в случае приложения бухгалтерского учета
calcVal более компьютеризировано, чем sum.

Оптимальная длина имени переменной
Оптимальная длина имени, наверное, лежит где#то между длинами имен x и maxi%
mumNumberOfPointsInModernOlympics. Слишком короткие страдают от недостат#
ка смысла. Проблема с именами вроде x1 и x2 в том, что, даже узнав, что такое x,
вы ничего не сможете сказать об отношении между x1 и x2. Слишком длинные
имена надоедает печатать, к тому же они могут сделать неясной визуальную струк#
туру программы.
Горла, Бенандер и Бенандер обнаружили, что отладка программы требо#
вала меньше всего усилий, если имена переменных состояли в среднем
из 10–16 символов (Gorla, Benander, and Benander, 1990). Отладка про#
грамм с именами, состоящими в среднем из 8–20 символов, была почти столь же
легкой. Это не значит, что следует присваивать всем переменным имена из 9–15
или 10–16 символов, — это значит, что, увидев в своем коде много более корот#
ких имен, вы должны проверить их ясность.
Вопрос адекватности длины имен переменных поясняет табл. 11#2.

Табл. 11-2. Слишком длинные, слишком короткие и оптимальные
имена переменных
Слишком длинные имена:

numberOfPeopleOnTheUsOlympicTeam
numberOfSeatsInTheStadium
maximumNumberOfPointsInModernOlympics

Слишком короткие имена:

n, np, ntm
n, ns, nsisd
m, mp, max, points

То, что надо:

numTeamMembers, teamMemberCount
numSeatsInStadium, seatCount
teamPointsMax, pointsRecord

Имена переменных и область видимости
Всегда ли короткие имена переменных неудачны? Нет, не
Перекрестная ссылка Об облавсегда. Если вы присваиваете переменной короткое имя,
сти видимости см. раздел 10.4.
такое как i, сама длина имени говорит о том, что перемен#
ная является второстепенной и имеет ограниченную область действия.
Программист, читающий код, сможет догадаться, что использование такой пере#
менной ограничивается несколькими строками кода. Присваивая переменной имя
i, вы говорите: «Эта переменная — самый обычный счетчик цикла/индекс масси#
ва, не играющий никакой роли вне этих нескольких строк».
У. Дж. Хансен (W. J. Hansen) обнаружил, что более длинные имена лучше присва#
ивать редко используемым или глобальным переменным, а более короткие —
локальным переменным или переменным, вызываемым в циклах (Shneiderman,
1980). Однако с короткими именами связано много проблем, и некоторые осмот#

256

ЧАСТЬ III

Переменные

рительные программисты, придерживающиеся политики защитного программи#
рования, вообще избегают их.
Дополняйте имена, относящиеся к глобальному пространству имен, спе'
цификаторами Если у вас есть переменные, относящиеся к глобальному про#
странству имен (именованные константы, имена классов и т. д.), подумайте, при#
нять ли конвенцию, разделяющую глобальное пространство имен на части и пре#
дотвращающую конфликты имен. В C++ и C# для разделения глобального простран#
ства имен можно применить ключевое слово namespace:

Пример разделения глобального пространства имен
с помощью ключевого слова namespace (C++)
namespace UserInterfaceSubsystem {
...
// объявления переменных
...
}
namespace DatabaseSubsystem {
...
// объявления переменных
...
}
Если класс Employee объявлен в обоих пространствах имен, вы можете указать
нужное пространство имен, написав UserInterfaceSubsystem::Employee или Data%
baseSubsystem::Employee. В Java с той же целью можно использовать пакеты.
Программируя на языке, не поддерживающем пространства имен или пакеты, вы
все же можете принять конвенции именования для разделения глобального про#
странства имен. Скажем, вы можете дополнить глобальные классы префиксами,
определяющими подсистему. Класс Employee из подсистемы пользовательского
интерфейса можно было бы назвать uiEmployee, а тот же класс из подсистемы
доступа к БД — dbEmployee. Это позволило бы свести к минимуму риск конфлик#
тов в глобальном пространстве имен.

Спецификаторы вычисляемых значений
Многие программы включают переменные, содержащие вычисляемые значения:
суммы, средние величины, максимумы и т. д. Дополняя такое имя спецификатором
вроде Total, Sum, Average, Max, Min, Record, String или Pointer, укажите его в конце имени.
У такого подхода несколько достоинств. Во#первых, при этом самая значимая часть
имени переменной, определяющая наибольшую часть его смысла, располагается
в самом начале имени, из#за чего становится более заметной и читается первой.
Во#вторых, приняв эту конвенцию, вы предотвратите путаницу, возможную при
наличии в одной программе имен totalRevenue и revenueTotal. Эти имена семан#
тически эквивалентны, и конвенция не позволила бы использовать их как разные.
В#третьих, набор имен вроде revenueTotal, expenseTotal, revenueAverage и expen%
seAverage обладает приятной глазу симметрией, тогда как набор имен totalRevenue,

ГЛАВА 11 Сила имен переменных

257

expenseTotal, revenueAverage и averageExpense упорядоченным не кажется. Наконец,
согласованность имен облегчает чтение и сопровождение программы.
Исключение из этого правила — позиция спецификатора Num. При расположе#
нии в начале имени спецификатор Num обозначает общее число: например, num%
Customers — это общее число заказчиков. Если же он указан в конце имени, то
определяет индекс: так, customerNum — это номер текущего заказчика. Другим
признаком данного различия является буква s в конце имени numCustomers. Од#
нако даже в этом случае спецификатор Num очень часто приводит к замешатель#
ству, поэтому лучше всего полностью исключить проблему, применив Count или
Total для обозначения общего числа заказчиков и Index для ссылки на конкретно#
го заказчика. Таким образом, переменные, определяющие общее число заказчи#
ков и номер конкретного заказчика, получили бы имена customerCount и customer%
Index соответственно.

Антонимы, часто встречающиеся в именах переменных
Используйте антонимы последовательно. Это делает код бо#
лее согласованным и облегчает его чтение. Пары вроде begin/
end понять и запомнить легко. Пары, не соответствующие
общепринятым антонимам, запоминаются сложнее и вызы#
вают замешательство. В число антонимов, часто используе#
мых в именах переменных, входят:

Перекрестная ссылка Аналогичный список антонимов, используемых в именах методов, см.
в подразделе «Дисциплинированно используйте антонимы»
раздела 7.3.

 begin/end;
 first/last;
 locked/unlocked;
 min/max;
 next/previous;
 old/new;
 opened/closed;
 visible/invisible;
 source/target;
 source/destination;
 up/down.

11.2. Именование конкретных типов данных
При именовании конкретных типов данных следует руководствоваться не толь#
ко общими, но и специфическими соображениями. Ниже описаны принципы
именования индексов циклов, переменных статуса, временных переменных, бу#
левых переменных, перечислений и именованных констант.

Именование индексов циклов
Принципы именования индексов циклов возникли потому,
что циклы относятся к самым популярным конструкциям.
Как правило, в качестве индексов циклов используют пере#
менные i, j и k:

Перекрестная ссылка О циклах
см. главу 16.

258

ЧАСТЬ III

Переменные

Пример простого имени индекса цикла (Java)
for ( i = firstItem; i < lastItem; i++ ) {
data[ i ] = 0;
}
Если же переменную предполагается использовать вне цикла, ей следует присво#
ить более выразительное имя. Например, переменную, хранящую число записей,
прочитанных из файла, можно было бы назвать recordCount:

Пример удачного описательного имени индекса цикла (Java)
recordCount = 0;
while ( moreScores() ) {
score[ recordCount ] = GetNextScore();
recordCount++;
}
// строки, в которых используется переменная recordCount
...
Если цикл длиннее нескольких строк, смысл переменной i легко забыть, поэтому
в подобной ситуации лучше присвоить индексу цикла более выразительное имя.
Так как код очень часто приходится изменять, модернизировать и копировать в
другие программы, многие опытные программисты вообще не используют име#
на вроде i.
Одна из частых причин увеличения циклов — их вложение в другие циклы. Если
у вас есть несколько вложенных циклов, присвойте индексам более длинные имена,
чтобы сделать код более понятным:

Пример удачного именования индексов вложенных циклов (Java)
for ( teamIndex = 0; teamIndex < teamCount; teamIndex++ ) {
for ( eventIndex = 0; eventIndex < eventCount[ teamIndex ]; eventIndex++ ) {
score[ teamIndex ][ eventIndex ] = 0;
}
}
Тщательный выбор имен индексов циклов позволяет избежать путаницы индек#
сов — использования i вместо j и наоборот. Кроме того, это облегчает понима#
ние операций над массивами: команда score[ teamIndex ][ eventIndex ] более ин#
формативна, чем score[ i ][ j ].
Не присваивайте имена i, j и k ничему, кроме индексов простых циклов: наруше#
ние этой традиции только запутает других программистов. Чтобы избежать по#
добных проблем, просто подумайте о более описательных именах, чем i, j и k.

Именование переменных статуса
Переменные статуса характеризуют состояние программы. Ниже рассмотрен один
принцип их именования.

ГЛАВА 11 Сила имен переменных

259

Старайтесь не присваивать переменной статуса имя flag Наоборот, рас#
сматривайте флаги как переменные статуса. Имя флага не должно включать фраг#
мент flag, потому что он ничего не говорит о сути флага. Ради ясности флагам сле#
дует присваивать выразительные значения, которые лучше сравнивать со значени#
ями перечислений, именованных констант или глобальных переменных, выступа#
ющих в роли именованных констант. Вот примеры неудачных имен флагов:

Примеры загадочных флагов (C++)
if
if
if
if

(
(
(
(

flag ) ...
statusFlag & 0x0F ) ...
printFlag == 16 ) ...
computeFlag == 0 ) ...

flag = 0x1;
statusFlag = 0x80;
printFlag = 16;
computeFlag = 0;
Команды вида statusFlag = 0x80 будет совершенно непонятны, пока вы не пояс#
ните в коде или в документации, что такое statusFlag и 0x80. Вот более понятный
эквивалентный фрагмент:

Примеры более грамотного использования переменных статуса (C++)
if
if
if
if

(
(
(
(

dataReady ) ...
characterType & PRINTABLE_CHAR ) ...
reportType == ReportType_Annual ) ...
recalcNeeded == True ) ...

dataReady = true;
characterType = CONTROL_CHARACTER;
reportType = ReportType_Annual;
recalcNeeded = false;
Очевидно, что команда characterType = CONTROL_CHARACTER выразительнее, чем
statusFlag = 0x80. Аналогично условие if ( reportType == ReportType_Annual ) понятнее,
чем if ( printFlag == 16 ). Второй фрагмент показывает, что данный подход приме#
ним к перечислениям и предопределенным именованным константам. Вот как с
помощью именованных констант и перечислений можно было бы задать исполь#
зуемые в нашем примере значения:

Объявление переменных статуса (C++)
// возможные значения переменной CharacterType
const int LETTER = 0x01;
const int DIGIT = 0x02;
const int PUNCTUATION = 0x04;
const int LINE_DRAW = 0x08;
const int PRINTABLE_CHAR = ( LETTER | DIGIT | PUNCTUATION | LINE_DRAW );

260

ЧАСТЬ III

Переменные

const int CONTROL_CHARACTER = 0x80;
// возможные значения переменной ReportType
enum ReportType {
ReportType_Daily,
ReportType_Monthly,
ReportType_Quarterly,
ReportType_Annual,
ReportType_All
};
Если вам трудно понять какой#то фрагмент кода, подумайте о переименовании
переменных. В отличие от детективных романов код программ не должен содер#
жать загадок. Его нужно просто читать.

Именование временных переменных
Временные переменные служат для хранения промежуточных результатов вычис#
лений и служебных значений программы. Обычно им присваивают имена temp,
x или какие#нибудь другие столь же неопределенные и неописательные имена.
В целом использование временных переменных говорит о том, что программист
еще не полностью понял проблему. Кроме того, с переменными, официально
получившими «временный» статус, программисты обычно обращаются небреж#
нее, чем с другими переменными, что повышает вероятность ошибок.
Относитесь к «временным» переменным с подозрением Часто значение нуж#
но на некоторое время сохранить. Однако в том или ином смысле временными
являются почти все переменные. Называя переменную временной, подумайте, до
конца ли вы понимаете ее реальную роль. Рассмотрим пример:

Пример неинформативного имени «временной» переменной (C++)
// Вычисление корней квадратного уравнения.
// Предполагается, что дискриминант (b^24*a*c) неотрицателен.
temp = sqrt( b^2  4*a*c );
root[0] = ( b + temp ) / ( 2 * a );
root[1] = ( b  temp ) / ( 2 * a );
Значение выражения sqrt( b^2 % 4 * a * c ) вполне разумно сохранить в перемен#
ной, особенно если учесть, что оно используется позднее. Но имя temp ничего не
говорит о роли переменной. Лучше поступить так:

Пример замены «временной» переменной на реальную переменную (C++)
// Вычисление корней квадратного уравнения.
// Предполагается, что дискриминант (b^24*a*c) неотрицателен.
discriminant = sqrt( b^2  4*a*c );
root[0] = ( b + discriminant ) / ( 2 * a );
root[1] = ( b  discriminant ) / ( 2 * a );
По сути это тот же код, только в нем использована переменная с точным описа#
тельным именем.

ГЛАВА 11 Сила имен переменных

261

Именование булевых переменных
Ниже я привел ряд рекомендаций по именованию булевых переменных.
Помните типичные имена булевых переменных
полезные имена булевых переменных.

Вот некоторые наиболее

 done Используйте переменную done как признак завершения цикла или дру#

гой операции. Присвойте ей false до выполнения действия и установите ее в
true после его завершения.
 error Используйте переменную error как признак ошибки. Присвойте ей зна#

чение false, если все в порядке, и true в противном случае.
 found Используйте переменную found для определения того, обнаружено ли

некоторое значение. Установите ее в false, если значение не обнаружено, и в
true, как только значение найдено. Используйте переменную found при поис#
ке значения в массиве, идентификатора сотрудника в файле, определенного
чека в списке чеков и т. д.
 success или ok Используйте переменную success или ok как признак успешно#

го завершения операции. Присвойте ей false, если операция завершилась не#
удачей, и true, если операция выполнена успешно. Если можете, замените имя
success на более определенное, ясно определяющее смысл «успеха». Если под
«успехом» понимается завершение обработки данных, можете назвать перемен#
ную processingComplete. Если «успех» подразумевает обнаружение конкретно#
го значения, можете использовать переменную found.
Присваивайте булевым переменным имена, подразумевающие значение
true или false Имена вроде done и success — хорошие имена булевых перемен#
ных, потому что они предполагают использование только значений true или false:
что#то может быть или выполнено, или не выполнено, операция может завершиться
или успехом, или неудачей. С другой стороны, имена вроде status и sourceFile не
годятся, так как при этом значения true или false не имеют ясного смысла. Какой
вывод можно сделать, если переменной status задано true? Означает ли это, что
что#то имеет статус? Все имеет статус. Означает ли это, что что#то имеет статус
«все в порядке»? Означает ли значение false, что никакое действие не было выпол#
нено неверно? Если переменная имеет имя status, ничего определенного на сей
счет сказать нельзя.
Поэтому имя status лучше заменить на имя вроде error или statusOK, а имя source%
File — на sourceFileAvailable, sourceFileFound или подобное имя, соответствующее
сути переменной.
Некоторые программисты любят дополнять имена булевых переменных префик#
сом is. В результате имя переменной превращается в вопрос: isdone? isError? isFound?
isProcessingComplete? Ответ на этот вопрос сразу становится и значением перемен#
ной. Достоинство этого подхода в том, что он исключает использование неопре#
деленных имен: вопрос isStatus? не имеет никакого смысла. Однако в то же время
он затрудняет чтение логических выражений: например, условие if ( isFound ) менее
понятно, чем if ( found ).

262

ЧАСТЬ III

Переменные

Используйте утвердительные имена булевых переменных Имена, основан#
ные на отрицании (такие как notFound, notdone и notSuccessful), при выполнении
над переменной операции отрицания становятся куда менее понятны, например:

if not notFound
Подобные имена следует заменить на found, done и processingComplete, выполняя
отрицание переменных в случае надобности. Так что для проверки нужного зна#
чения вы использовали бы выражение found, а не not notFound.

Именование перечислений
Перекрестная ссылка О перечислениях см. раздел 12.6.

Принадлежность переменных к тому или иному перечисле#
нию можно пояснить, дополнив их имена префиксами, та#
кими как Color_, Planet_ или Month_:

Пример дополнения элементов перечислений префиксами (Visual Basic)
Public Enum Color
Color_Red
Color_Green
Color_Blue
End Enum
Public Enum Planet
Planet_Earth
Planet_Mars
Planet_Venus
End Enum
Public Enum Month
Month_January
Month_February
...
Month_December
End Enum
Кроме того, сами перечисления (Color, Planet или Month) можно идентифициро#
вать разными способами: например, используя в их именах только заглавные буквы
или дополняя их имена префиксами (e_Color, e_Planet или e_Month). Кое#кто мог
бы сказать, что перечисление по сути является типом, определяемым пользовате#
лем, поэтому имена перечислений надо форматировать так же, как имена клас#
сов и других пользовательских типов. С другой стороны, члены перечислений
являются константами, поэтому имена перечислений следует форматировать как
имена констант. В этой книге я придерживаюсь конвенции, предусматривающей
применение в именах перечислений букв обоих регистров.
В некоторых языках перечисления рассматриваются скорее как классы, а именам
членов перечисления всегда предшествует имя самого перечисления, например,
Color.Color_Red или Planet.Planet_Earth. Если вы используете подобный язык, по#
вторять префикс не имеет смысла, так что вы можете считать префиксом само
имя перечисления и сократить имена до Color.Red и Planet.Earth.

ГЛАВА 11 Сила имен переменных

263

Именование констант
Имя константы должно характеризовать абстрактную сущ#
Перекрестная ссылка Об именоность, представляемую константой, а не конкретное значе#
ванных константах см. раздел
ние. Имя FIVE — плохое имя константы (независимо от того,
12.7.
имеет ли она значение 5.0). CYCLES_NEEDED — хорошее имя.
CYCLES_NEEDED может иметь значение 5.0, 6.0 и любое другое. Выражение FIVE =
6.0 было бы странным. Аналогично BAKERS_DOZEN — плохое имя константы, а
DONUTS_MAX — вполне подходящее.

11.3. Сила конвенций именования
Программисты предпочитают порой не использовать стандарты и конвенции по
вполне разумной причине. Некоторые стандарты и конвенции, слишком жесткие
и неэффективные, подавляют творчество и снижают качество программы. Это пе#
чально, так как эффективные стандарты — один из мощнейших инструментов. В
этом разделе мы обсудим, почему, когда и как создавать собственные стандарты
именования переменных.

Зачем нужны конвенции?
Конвенции обеспечивают несколько преимуществ.
 Они позволяют больше принимать как данное. Приняв одно общее решение

вместо нескольких более узких, вы сможете сосредоточиться на более важных
аспектах кода.
 Они помогают использовать знания, полученные при работе над предыдущи#

ми проектами. Сходство имен облегчает понимание незнакомых переменных.
 Они ускоряют изучение кода нового проекта. Вместо изучения особенностей кода

Аниты, Джулии и Кристин вы сможете работать с более согласованным кодом.
 Они подавляют «размножение» имен. Не применяя конвенцию именования, вы

легко можете присвоить одной сущности два разных имени. Скажем, вы мо#
жете назвать общее число баллов и pointTotal, и totalPoints. Возможно, при на#
писании программы вам все будет понятно, но другой программист, который
попытается понять такой код позднее, может столкнуться с серьезными про#
блемами.
 Они компенсируют слабости языка. Вы можете использовать конвенции для

имитации именованных констант и перечислений. Конвенции позволяют про#
вести грань между локальными данными, данными класса и глобальными дан#
ными, а также охарактеризовать типы, не поддерживаемые компилятором.
 Они подчеркивают отношения между связанными элементами. Если вы исполь#

зуете данные объектов, компилятор заботится об этом автоматически. Если язык
не поддерживает объекты, вы можете свести этот недостаток к минимуму при
помощи конвенции именования. Так, имена address, phone и name не говорят
о том, что эти переменные связаны между собой. Но если вы решите допол#
нять все переменные, хранящие данные о сотрудниках, префиксом Employee,
эта связь будет ясно выражена в итоговых именах employeeAddress, employeePhone
и employeeName. Конвенции программирования могут устранить недостатки
используемого вами языка.

264

ЧАСТЬ III

Переменные

Суть сказанного в том, что наличие хоть какой#то конвенции обычно пред#
почтительнее, чем ее отсутствие. Конвенция может быть произвольной.
Сила конвенций именования объясняется не конкретными аспектами, а
самим фактом их использования, обеспечивающим структурирование кода и умень#
шающим количество поводов для беспокойства.

Когда следует использовать конвенцию именования?
Непреложных правил на этот счет нет, однако некоторые рекомендации дать
можно. Итак, используйте конвенцию именования, если:
 над проектом работают несколько программистов;
 программу будут изменять и сопровождать другие программисты (что имеет

место почти всегда);
 обзор программы выполняют другие программисты из вашей компании;
 программа так велика, что вы не можете полностью охватить ее умом, а вы#

нуждены рассматривать по частям;
 программа будет использоваться длительное время, из#за чего вам, возможно,

придется вернуться к ней через несколько недель или месяцев;
 прикладная область имеет необычную терминологию и вы хотите стандарти#

зовать применение терминов или аббревиатур в коде.
Использовать конвенции именования всегда выгодно, а мои советы помогут вам
определить оптимальную детальность конвенции для конкретного проекта.

Степень формальности конвенций
Конвенциям именования могут соответствовать разные сте#
пени формальности. Неформальная конвенция может быть
совсем простой: «Используйте выразительные имена». Дру#
гие неформальные конвенции рассматриваются в следую#
щем разделе. В общем, оптимальная степень формальнос#
ти конвенции определяется числом людей, работающих над программой, разме#
ром программы и ожидаемым временем ее использования. При работе над кро#
шечными проектами, подлежащими выбросу, следование строгой конвенции,
наверное, окажется пустой тратой сил. В случае более крупных проектов, реали#
зуемых с участием нескольких программистов, формальная конвенция — важней#
шее средство улучшения удобочитаемости программы.

Перекрестная ссылка О различиях формальности при работе над небольшими и крупными проектами см. главу 27.

11.4. Неформальные конвенции именования
В большинстве проектов используются относительно неформальные конвенции
именования, подобные тем, что описываются в этом разделе.

Конвенция, не зависящая от языка
Ниже приведены некоторые советы по созданию конвенции, не зависящей от языка.
Проведите различие между именами переменных и именами методов
Конвенция, используемая в этой книге, подразумевает, что имена переменных и

ГЛАВА 11 Сила имен переменных

265

объектов начинаются со строчной буквы, а имена методов — с прописной: variable%
Name, но RoutineName().
Проведите различие между классами и объектами Соответствие между име#
нами классов и объектов (или между именами типов и переменных этих типов)
может быть довольно тонким. Некоторые стандартные способы проведения раз#
личия между ними иллюстрирует следующий фрагмент:

Вариант 1: имена типов отличаются от имен переменных
регистром первой буквы
Widget widget;
LongerWidget longerWidget;

Вариант 2: имена типов отличаются от имен переменных регистром всех букв
WIDGET widget;
LONGERWIDGET longerWidget

Вариант 3: имена типов дополняются префиксом «t_»
t_Widget Widget;
t_LongerWidget LongerWidget;

Вариант 4: имена переменных дополняются префиксом «a»
Widget aWidget;
LongerWidget aLongerWidget;

Вариант 5: имена переменных более конкретны, чем имена типов
Widget employeeWidget;
LongerWidget fullEmployeeWidget;
Каждый из этих вариантов имеет свои плюсы и минусы. Вариант 1 часто использу#
ется при программировании на C++, Java и других языках, чувствительных к регист#
ру букв, но некоторые программисты считают, что различать имена только по реги#
стру первой буквы неудобно. Действительно, имена, отличающиеся только регист#
ром первой буквы, имеют слишком малое психологическое и визуальное различие.
Вариант 1 не удастся согласованно использовать при программировании на не#
скольких языках, если хотя бы в одном из них регистр букв не имеет значения.
Так, при компиляции команды Dim widget as Widget компилятор Microsoft Visual
Basic сообщит о синтаксической ошибке, потому что widget и Widget покажутся
ему одним и тем же элементом.
Вариант 2 проводит более очевидное различие между именами типов и перемен#
ных. Однако по историческим причинам в C++ и Java верхний регистр служит для
определения констант, к тому же при разработке программы с использованием
нескольких языков этот подход приводит к тем же проблемам, что и вариант 1.
Вариант 3 поддерживается всеми языками, но некоторым программистам префиксы
не нравятся по эстетическим причинам.
Вариант 4 иногда используют как альтернативу варианту 3, но вместо изменения
имени одного класса он требует модификации имени каждого экземпляра класса.

266

ЧАСТЬ III

Переменные

Вариант 5 заставляет тщательно обдумывать имя каждой переменной. Обычно
результатом этого является более понятный код. Но иногда widget (приспособле#
ние) на самом деле — всего лишь общее «приспособление», и в этих случаях вы
должны будете придумывать менее ясные имена вроде genericWidget, которые,
несомненно, читаются хуже.
Короче, каждый из вариантов связан с компромиссами. В этой книге я использую
вариант 5, потому что он наиболее понятен, если человеку, читающему код, неиз#
вестны конвенции именования.
Идентифицируйте глобальные переменные Одной из частых проблем про#
граммирования является неверное использование глобальных переменных. Если
вы присвоите всем глобальным переменным имена, начинающиеся, скажем, с пре#
фикса g_, программист, увидевший переменную g_RunningTotal, сразу поймет, что
это глобальная переменная, и будет обращаться с ней должным образом.
Идентифицируйте переменные'члены Идентифицируйте данные#члены клас#
са. Ясно покажите, что переменная#член не является ни локальной, ни глобаль#
ной переменной. Идентифицировать переменные#члены класса можно, например,
при помощи префикса m_.
Идентифицируйте определения типов Конвенции именования типов играют
две роли: они явно показывают, что имя является именем типа, и предотвращают
конфликты имен типов и переменных. Для этого вполне годится префикс (суф#
фикс). В C++ для именования типов обычно используют только заглавные буквы:
например, COLOR и MENU. (Это справедливо для имен типов, определяемых с
помощью директив typedef, и имен структур, но не классов.) Однако при этом можно
спутать типы с именованными константами препроцессора. Для предотвращения
путаницы можно дополнять имена типов префиксом t_, что дает нам такие име#
на, как t_Color и t_Menu.
Идентифицируйте именованные константы Именованные константы нуж#
но идентифицировать, чтобы вы могли определить, присваиваете ли вы переменной
значение другой переменной (которое может изменяться) или именованной кон#
станты. В случае Visual Basic эти два варианта можно также спутать с присваива#
нием переменной значения, возвращаемого функцией. Visual Basic не требует
применения скобок при вызове функции, не принимающей параметров, тогда как
в C++ скобки нужно указывать при вызове любой функции.
Одним из подходов к именованию констант является применение префикса, на#
пример c_. Это дает нам такие имена, как c_RecsMax или c_LinesPerPageMax. В случае
C++ и Java конвенция подразумевает использование только заглавных букв без
разделения слов или с разделением слов символами подчеркивания: RECSMAX или
RECS_ MAX и LINESPERPAGEMAX или LINES_PER_PAGE_ MAX.
Идентифицируйте элементы перечислений Элементы перечислений следует
идентифицировать по той же причине, что и именованные константы: чтобы
элемент перечисления можно было легко отличить от переменной, именованной
константы или вызова функции. Стандартный подход предполагает применение
в имени перечисления только заглавных букв или дополнение имени префиксом
e_ или E_; что касается имен элементов, то они дополняются префиксом, осно#
ванным на имени конкретного перечисления, скажем, Color_ или Planet_.

ГЛАВА 11 Сила имен переменных

267

Идентифицируйте неизменяемые параметры, если язык не требует их
явного определения Иногда программисты случайно изменяют входные пара#
метры. C++, Visual Basic и некоторые другие языки заставляют явно указывать, хотите
ли вы, чтобы изменения параметров внутри метода были доступны в остальном
коде. Для этого служат спецификаторы *, & и const в C++ и ByRef/ByVal в Visual Basic.
В случае других языков изменение входной переменной в методе отражается в
остальном коде, хотите вы того или нет. Это особенно верно при передаче объектов.
Например, в Java все объекты передаются в методы «значением», поэтому, пере#
давая объект в метод, будьте готовы к тому, что состояние объекта может изме#
ниться 1 (Arnold, Gosling, Holmes, 2000).
Если, программируя на таком языке, вы следуете конвенции
именования, согласно которой исключительно входные
(неизменяемые) параметры нужно дополнять префиксом
const (или final, или nonmodifiable, или каким#то аналогич#
ным), то, увидев что#то с префиксом const слева от знака
равенства, вы будете знать, что произошла ошибка. Если вы
увидите вызов constMax.SetNewMax( ... ), вы также по префиксу
const поймете, что это ошибка.

Перекрестная ссылка Дополнение языка конвенцией именования, компенсирующей ограничения самого языка, является примером программирования с использованием языка вместо
простого программирования на
языке (см. раздел 34.4).

Форматируйте имена так, чтобы их было легко читать Для повышения
удобочитаемости кода слова в именах переменных часто разделяют заглавными
буквами или символами#разделителями. Например, имя GYMNASTICSPOINTTOTAL
читается хуже, чем gymnasticsPointTotal или gymnastics_point_total. C++, Java, Visual
Basic и другие языки позволяют использовать оба этих подхода.
Старайтесь не смешивать эти способы, так как это осложняет чтение кода. Если
же вы будете согласованно использовать один из подходов, код станет более по#
нятным. Программисты уже давно спорят по поводу того, делать ли заглавной
первую букву имени (TotalPoints или totalPoints), но если все участвующие в про#
екте программисты будут поступать согласованно, подобные мелочи не будут играть
особой роли. В данной книге имена переменных начинаются с буквы нижнего
регистра по той причине, что этот подход принят в языке Java, а также для под#
держания сходства стилей между разными языками.

Конвенции, специфические для конкретных языков
Соблюдайте конвенции именования, принятые в используемом вами языке. Книги
по стилю программирования можно найти почти для любого языка. Советы, отно#
сящиеся к языкам C, C++, Java и Visual Basic, даны в следующих подразделах.

Конвенции C
Конвенции именования, используемые при программировании на C, предпола#
гают, что:
 имена символьных переменных дополняются префиксом c или ch;
 целочисленным индексам присваиваются имена i и j;

1

Значением передается ссылка на объект, который и может быть изменен. — Прим. перев.

268

ЧАСТЬ III

Дополнительные
сведения
Классической книгой о стиле
программирования на C является «C Programming Guidelines»
(Plum, 1984).

Переменные
 имена переменных, хранящих количество чего#либо, до#
полняются префиксом n;
 имена указателей дополняются префиксом p;
 имена строк начинаются с префикса s;

 имена макросов препроцессора включают ТОЛЬКО_ЗАГ%
ЛАВНЫЕ_БУКВЫ; обычно это правило распространяется и на
имена типов, определяемых при помощи директивы typedef;

 имена переменных и методов включают только_строчные_буквы;
 для разделения слов служит символ подчеркивания (_): имена_такого_вида

читаются легче, чем именатакоговида.
Эти правила справедливы для программирования на C в общем, а также для сред
UNIX и Linux, однако в разных средах конвенции имеют свои особенности. Про#
граммисты на C, разрабатывающие программы для Microsoft Windows, предпочи#
тают применять для именования переменных ту или иную форму венгерской
нотации и буквы верхнего и нижнего регистров. Программисты, разрабатываю#
щие ПО для платформы Macintosh, обычно используют для именования методов
смешанный регистр, потому что инструментарий Macintosh и методы ОС были
изначально разработаны в соответствии с интерфейсом Pascal.

Конвенции C++
Дополнительные сведения О
стиле программирования на C++
см. книгу «The Elements of C++
Style» (Misfeldt, Bumgardner, and
Gray, 2004).

С программированием на C++ связаны такие конвенции:
 целочисленным индексам присваиваются имена i и j;
 имена указателей дополняются префиксом p;
 имена констант, типов, определяемых с помощью дирек#
тивы typedef, и макросов препроцессора включают ТОЛЬКО_%
ЗАГЛАВНЫЕ_БУКВЫ;

 имена классов и других типов содержат БуквыОбоихРегистров;
 первое слово в именах переменных и методов начинается со строчной буквы,

а все последующие слова — с заглавной: имяПеременнойИлиМетода;
 символ подчеркивания используется только в именах, состоящих полностью

из заглавных букв, и после некоторых префиксов (например, после префикса,
служащего для идентификации глобальных переменных).
Как и в случае языка C, некоторые аспекты этой конвенции могут зависеть от
конкретной среды.

Конвенции Java
Дополнительные сведения О стиле программирования на Java см.
книгу «The Elements of Java Style,
2d ed.» (Vermeulen et al., 2000).

В отличие от C и C++ конвенции стиля программирования
на Java были сформулированы уже на ранних этапах раз#
вития этого языка:
 i и j — имена целочисленных индексов;

 имена констант включают ТОЛЬКО_ЗАГЛАВНЫЕ_БУКВЫ, а
слова разделяются символами подчеркивания;

ГЛАВА 11 Сила имен переменных

269

 все слова в именах классов и интерфейсов начинаются с заглавной буквы:

ИмяКлассаИлиИнтерфейса;
 в именах переменных и методов с заглавной буквы начинаются все слова, кроме

первого: имяПеременойИлиМетода;
 символ подчеркивания служит разделителем только в именах, полностью со#

стоящих из заглавныхбукв;
 имена методов доступа начинаются с префикса get или set.

Конвенции Visual Basic
Устойчивых конвенций стиля программирования на Visual Basic не существует. Чуть
ниже я приведу один из возможных вариантов.

Программирование с использованием нескольких языков
Если вы программируете, используя несколько языков, сформулируйте конвенцию
именования (а также форматирования, документирования и т. д.) так, чтобы она
способствовала общей согласованности и удобочитаемости кода, даже если для
этого придется отступить от конвенции, принятой в одном из языков.
Например, в этой книге все имена переменных начинаются со строчной буквы,
что соответствует конвенции Java и некоторым, но не всем конвенциям C++. Все
имена методов начинаются с заглавной буквы, что согласуется с конвенцией C++.
Согласно конвенции Java имена методов должны были бы начинаться с буквы
нижнего регистра, но ради общей удобочитаемости я решил независимо от язы#
ка начинать их с заглавной буквы.

Примеры конвенций именования
В стандартных конвенциях, описанных выше, не отражены некоторые важные
аспекты, в том числе область видимости переменных (закрытая, класс или гло#
бальная), различия имен классов и объектов, методов и переменных и т. д.
Советы по именованию могут показаться сложными, если они сконцентрирова#
ны на нескольких страницах. Однако на самом деле они могут быть вполне про#
стыми, и вы можете адаптировать их к своим потребностям. Имена переменных
должны включать информацию трех видов:
 суть переменной (то, что переменная представляет);
 тип данных (именованная константа; элементарная переменная; тип, опреде#

ленный пользователем, или класс);
 область видимости переменной (закрытая, класс, пакет или глобальная область

видимости).
В табл. 11#3, 11#4 и 11#5 описаны конвенции именования для языков C, C++, Java
и Visual Basic, основанные на уже известных вам принципах. Использовать имен#
но эти конвенции не обязательно, однако они помогут вам понять, что может
включать неформальная конвенция именования.

270

ЧАСТЬ III

Переменные

Табл. 11-3. Пример конвенции именования для языков C++ и Java
Сущность

Описание

ClassName

Имена классов начинаются с заглавной буквы и включают
буквы обоих регистров.

TypeName

Имена типов, в том числе перечислений и типов, определяе#
мых при помощи директив typedef, начинаются с заглавной
буквы и включают буквы обоих регистров.

EnumeratedTypes

Кроме предыдущего правила, имена перечислений всегда име#
ют форму множественного числа.

localVariable

Имена локальных переменных начинаются со строчной буквы
и включают буквы обоих регистров. Имя должно характеризо#
вать сущность, представляемую переменной, и не должно зави#
сеть от фактического типа переменной.

routineParameter

Имена параметров методов форматируются так же, как имена
локальных переменных.

RoutineName()

Имена методов включают буквы обоих регистров (об удачных
именах методов см. раздел 7.3).

m_ClassVariable

Имена переменных#членов, доступных только методам класса,
дополняются префиксом m_.

g_GlobalVariable

Имена глобальных переменных дополняются префиксом g_.

CONSTANT

Имена именованных констант включают ТОЛЬКО_ЗАГЛАВ%
НЫЕ_БУКВЫ.

MACRO

Имена макросов включают ТОЛЬКО_ЗАГЛАВНЫЕ_БУКВЫ.

Base_EnumeratedType

Имена элементов перечислений дополняются именем самого
перечисления в единственном числе: Color_Red, Color_Blue.

Табл. 11-4. Пример конвенции именования для языка C
Сущность

Описание

TypeName

Имена типов начинаются с заглавной буквы и включают буквы
обоих регистров.

GlobalRoutineName()

Имена открытых методов включают буквы обоих регистров.

f_FileRoutineName()

Имена методов, видимых в одном модуле (файле), дополняются
префиксом f_.

LocalVariable

Имена локальных переменных включают буквы обоих регист#
ров. Имя должно характеризовать сущность, представляемую
переменной, и не должно зависеть от фактического типа
переменной.

RoutineParameter

Имена параметров методов форматируются так же, как имена
локальных переменных.

f_FileStaticVariable

Имена переменных, видимых в одном модуле (файле), дополня#
ются префиксом f_.

G_GLOBAL_Glo%
balVariable

Имена глобальных переменных дополняются префиксом G_ и
обозначением модуля (файла), в котором определена перемен#
ная; обозначение модуля (файла) включает только заглавные
буквы: SCREEN_Dimensions.

ГЛАВА 11 Сила имен переменных

271

Табл. 11-4. (окончание)
Сущность

Описание

LOCAL_CONSTANT

Имена именованных констант, видимых в одном методе или
модуле (файле), включают только заглавные буквы: ROWS_MAX.

G_GLOBALCONSTANT

Имена глобальных именованных констант включают только
заглавные буквы и дополняются префиксом G_ и обозначением
модуля (файла), в котором определена именованная константа;
обозначение модуля (файла) включает только заглавные буквы:
G_SCREEN_ROWS_MAX.

LOCALMACRO()

Имена макросов, видимых в одном методе или модуле (файле),
включают только заглавные буквы.

G_GLOBAL_MACRO()

Имена глобальных макросов включают только заглавные буквы
и дополняются префиксом G_ и обозначением модуля (файла),
в котором определен макрос; обозначение модуля (файла)
включает только заглавные буквы: G_SCREEN_LOCATION().

Так как Visual Basic безразличен к регистру букв, для различения имен типов и
переменных приходится применять специфические правила (табл. 11#5).

Табл. 11-5. Пример конвенции именования для языка Visual Basic
Сущность

Описание

C_ClassName

Имена классов дополняются префиксом C_, начинаются
с заглавной буквы и включают буквы обоих регистров.

T_TypeName

Имена типов дополняются префиксом T_, начинаются
с заглавной буквы и включают буквы обоих регистров.

T_EnumeratedTypes

Кроме предыдущего правила, имена перечислений всегда
имеют форму множественного числа.

localVariable

Имена локальных переменных начинаются со строчной буквы
и включают буквы обоих регистров. Имя должно характеризо#
вать сущность, представляемую переменной, и не должно зави#
сеть от фактического типа переменной.

routineParameter

Имена параметров методов форматируются так же, как имена
локальных переменных.

RoutineName()

Имена методов включают буквы обоих регистров (об удачных
именах методов см. раздел 7.3).

m_ClassVariable

Имена переменных#членов, доступных только методам класса,
дополняются префиксом m_.

g_GlobalVariable

Имена глобальных переменных дополняются префиксом g_.

CONSTANT

Имена именованных констант включают
ТОЛЬКО_ЗАГЛАВНЫЕ_БУКВЫ.

Base_EnumeratedType

Имена элементов перечислений дополняются именем самого
перечисления в единственном числе: Color_Red, Color_Blue.

272

ЧАСТЬ III

Переменные

11.5. Стандартизованные префиксы
Стандартизация префиксов обеспечивает лаконичный, но
в то же время согласованный и понятный способ именова#
ния данных. Самая известная схема стандартизации префик#
сов — венгерская нотация — представляет собой набор де#
тальных принципов именования переменных и методов
(а не жителей Венгрии!) , который когда#то широко применялся при программи#
ровании для ОС Microsoft Windows. Сейчас венгерскую нотацию используют ред#
ко, но ее суть — создание стандартизованного набора лаконичных точных абб#
ревиатур — от этого не становится менее полезной.

Дополнительные сведения О
венгерской нотации см. статью
«The Hungarian Revolution» (Simonyi and Heller, 1991).

Стандартизованный префикс состоит из двух частей: аббревиатуры типа, опреде#
ленного пользователем (user#defined type, UDT), и семантического префикса.

Аббревиатура типа, определенного пользователем
Аббревиатура UDT обозначает тип объекта или переменной. Как правило, аббреви#
атуры UDT служат для описания таких сущностей, как окна, области экрана и шриф#
ты, но не предопределенных типов данных конкретного языка программирования.
Типы UDT описываются краткими кодами, которые вы создаете и стандартизиру#
ете для конкретной программы. Коды — это мнемонические обозначения, такие
как wn в случае окна и scr в случае области экрана. В табл. 11#6 приведены при#
меры UDT, которые можно было бы задействовать в текстовом редакторе.

Табл. 11-6. Примеры UDT текстового редактора
Аббревиатура UDT

Значение

ch

Символ (тип данных, используемый для представления
символа документа, а не символ C++)

doc

Документ

pa

Абзац (paragraph)

scr

Область экрана

sel

Выбранный текст

wn

Окно

При работе с UDT следует также определить типы данных с именами, соответству#
ющими аббревиатурам UDT. Таким образом, при использовании UDT из табл.
11#6 у вас получились бы подобные объявления данных:

CH
SCR
DOC
PA
PA
WN

chCursorPosition;
scrUserWorkspace;
docActive
firstPaActiveDocument;
lastPaActiveDocument;
wnMain;

Разумеется, аббревиатуры следует создавать для тех UDT, которые чаще всего встре#
чаются в конкретной среде.

ГЛАВА 11 Сила имен переменных

273

Семантические префиксы
Семантические префиксы дополняют аббревиатуры UDT, характеризуя использо#
вание переменной или объекта. В отличие от аббревиатур UDT, зависимых от
конкретного проекта, семантические префиксы являются в некотором смысле
стандартными (табл. 11#7).

Табл. 11-7. Семантические префиксы
Семантический
префикс

Значение

c

Количество (записей, символов и т. д.).

first

Элемент массива, обрабатываемый первым. Префикс first
аналогичен префиксу min, но связан с текущей операцией,
а не с самим массивом.

g

Глобальная переменная.

i

Индекс массива.

last

Элемент массива, обрабатываемый последним. Префикс last
дополняет префикс first.

lim

Верхняя граница обрабатываемого массива. Значение с пре#
фиксом lim уже не является допустимым индексом. Как и last,
префикс lim дополняет префикс first. В отличие от префикса
last, используемого для представления последнего допустимого
элемента, значение с lim выходит за пределы массива.
В общем, lim равняется last + 1.

m

Переменная уровня класса.

max

Индекс последнего элемента массива или другого списка.
Префикс max связан с самим массивом, а не с выполняемыми
над массивом операциями.

min

Индекс первого элемента массива или другого списка.

p

Указатель.

Семантические префиксы включают строчные буквы или буквы обоих регистров
и по мере необходимости объединяются с аббревиатурами UDT и другими семанти#
ческими префиксами. Например, имя переменной, определяющей первый абзац
документа, включило бы аббревиатуру pa, говорящую о том, что это абзац, и пре#
фикс first, показывающий, что это первый абзац. В итоге мы получили бы имя firstPa.
Индекс набора абзацев был бы назван iPa; счетчик или число абзацев — cPa, а
первый и последний абзацы текущего активного документа — firstPaActiveDocument
и lastPaActiveDocument соответственно.

Достоинства стандартизованных префиксов
Стандартизованные префиксы обеспечивают все общие преимущества
конвенций именования, а также некоторые дополнительные. Стандарти#
зация имен снижает число имен элементов программы или класса, ко#
торые нужно помнить.
Стандартизованные префиксы позволяют уточнить имена, которые без этого ча#
сто оказываются неточными. Особенно полезны точные различия между префик#
сами min, first, last и max.

274

ЧАСТЬ III

Переменные

Стандартизованные префиксы делают имена более компактными. Так, переменной,
определяющей число абзацев, можно присвоить имя cpa, а не totalParagraphs. Ин#
декс массива абзацев можно назвать ipa, а не indexParagraphs или paragraphsIndex.
Наконец, стандартизованные префиксы облегчают проверку правильности исполь#
зования абстрактных типов данных, когда компилятор оказывается беспомощным.
Так, выражение paReformat = docReformat скорее всего ошибочно, потому что аб#
бревиатуры pa и doc соответствуют разным UDT.
Главная ловушка при использовании стандартизованных префиксов — отказ от
дополнения префикса выразительным именем переменной. Так, если имя ipa од#
нозначно определяет индекс массива абзацев, есть соблазн не присваивать пере#
менной более выразительное имя, такое как ipaActiveDocument. Помните про удо#
бочитаемость кода и присваивайте переменным описательные имена.

11.6. Грамотное сокращение имен переменных
Стремление к сокращению имен переменных в некотором смысле ста#
ло пережитком. В более старых языках, таких как ассемблер, обычный
Basic и Fortran, имена переменных были ограничены 2–8 символами.
Кроме того, раньше программирование было более тесно связано с математикой,
что побуждало использовать в уравнениях и других выражениях переменные с «ма#
тематическими» именами i, j, k и т. п. C++, Java, Visual Basic и другие современные
языки позволяют создавать имена почти любой длины, поэтому сокращение вы#
разительных имен уже не имеет под собой практически никаких оснований.
Если обстоятельства требуют создания коротких имен, помните, что некоторые
способы сокращения имен лучше других. Удачные короткие имена можно создать,
устранив ненужные слова, выбрав более короткие синонимы и использовав ка#
кую#нибудь из нескольких стратегий сокращения. Целесообразно знать несколь#
ко методик сокращения имен, потому что ни одна из них не является одинаково
эффективной во всех случаях.

Общие советы по сокращению имен
Ниже я привел несколько рекомендаций по сокращению имен. Некоторые из них
противоречат другим, так что не пытайтесь использовать все советы сразу. Итак:
 используйте стандартные аббревиатуры (общепринятые, которые можно найти

в словаре);
 удаляйте все гласные, не являющиеся первыми буквами имен (computer — cmptr,

screen — scrn, apple — appl, integer — intgr);
 удаляйте артикли и союзы, такие как and, or, the и т. д.;
 сохраняйте одну или несколько первых букв каждого слова;
 «обрезайте» слова согласованно: после первой, второй или третьей буквы (вы#

бирайте вариант, уместный в конкретном случае);
 сохраняйте первую и последнюю буквы каждого слова;
 сохраняйте до трех выразительных слов;
 удаляйте бесполезные суффиксы: ing, ed и т. д.

ГЛАВА 11 Сила имен переменных

275

 сохраняйте наиболее выразительный звук каждого слога;
 проверяйте, чтобы смысл имени переменной в ходе сокращения не искажался;
 используйте эти способы, пока не сократите имя каждой переменной до 8–20

символов или до верхнего предела, ограничивающего длину имен в конкрет#
ном языке.

Фонетические аббревиатуры
Некоторые люди сокращают слова, опираясь на их звучание, а не написание.
В результате skating превращается в sk8ing, highlight — в hilite, before — в b4, execute
— в xqt и т. д. Этот способ не отличается понятностью, поэтому я рекомендую забыть
про него. Попробуйте, например, догадаться, что означают имена:

ILV2SK8

XMEQWK

S2DTM8O

NXTC

TRMN8R

Комментарии по поводу сокращения имен
Сокращая имена, вы можете попасть в одну из нескольких ловушек. Ниже описа#
ны некоторые правила, позволяющие их избежать.
Не сокращайте слова только на один символ Напечатать лишний символ
не так уж трудно, и экономия одного символа едва ли может оправдать ухудше#
ние удобочитаемости кода. При этом имена становятся похожими на названия ме#
сяцев в календаре. Нужно очень уж сильно торопиться, чтобы написать «Июн» вме#
сто «Июнь». Обычно после сокращения слов на один символ потом трудно вспом#
нить, действительно ли вы удалили этот символ. Или удаляйте более одного сим#
вола, или пишите все слово.
Сокращайте имена согласованно Всегда используйте один и тот же вариант
сокращения — например, только Num или только No, но не оба варианта. Аналогич#
но не сокращайте слово только в некоторых именах. Если в одних именах вы ис#
пользовали слово Number, не сокращайте его в других именах до Num и наоборот.
Сокращайте имена так, чтобы их можно было произнести Используйте
имена xPos и needsComp, а не xPstn и ndsCmptg. Возьмите на заметку телефонный
тест: если вы не можете прочитать код другому человеку по телефону, присвойте
переменным более членораздельные имена (Kernighan and Plauger, 1978).
Избегайте комбинаций, допускающих неверное прочтение или произно'
шение имени Если вам нужно как#то обозначить конец B, назовите переменную
ENDB, а не BEND. Если вы грамотно разделяете части имен, этот совет вам не пона#
добится, так как сочетания B%END, BEnd или b_end нельзя произнести неправильно.
Обращайтесь к словарю для разрешения конфликтов имен При сокраще#
нии некоторых имен в итоге получается одна и та же аббревиатура. Так, если длина
имени ограничена тремя символами и вам нужно использовать в одной части
программы элементы fired и full revenue disbursal, вы можете по неосторожности
сократить оба варианта до frd.
Предотвратить конфликт имен позволяют синонимы, и тут на помощь приходит
словарь. В нашем примере fired можно было бы заменить на синоним dismissed, а
full revenue disbursal — на complete revenue disbursal. В итоге получаются аббреви#
атуры dsm и crd, что устраняет конфликт имен.

276

ЧАСТЬ III

Переменные

Документируйте очень короткие имена прямо в коде при помощи таб'
лиц Если язык позволяет использовать только очень короткие имена, включай#
те в код таблицу, характеризующую суть переменных. Включайте ее как коммен#
тарий перед соответствующим блоком кода, например:

Пример хорошей таблицы преобразования (Fortran)
C
C
C
C
C
C
C
C
C
C
C
C
C

*******************************************************************
Translation Table
Variable
————
XPOS
YPOS
NDSCMP

Meaning
———
xCoordinate Position (in meters)
YCoordinate Position (in meters)
Needs Computing (=0 if no computation is needed;
=1 if computation is needed)
PTGTTL
Point Grand Total
PTVLMX
Point Value Maximum
PSCRMX
Possible Score Maximum
*****************************************************************

Может показаться, что этот способ устарел, но не далее чем в середине 2003 г. я
работал с клиентом, который использовал программу на языке RPG, включавшую
несколько сотен тысяч строк кода. Длина имен переменных в ней была ограни#
чена 6 символами. Подобные проблемы все еще всплывают время от времени.
Указывайте все сокращения в проектном документе «Стандартные аб'
бревиатуры» Применение аббревиатур сопряжено с двумя распространенны#
ми факторами риска:
 аббревиатура может оказаться непонятной программисту, читающему код;
 программисты могут сократить одно и то же имя по#разному, что вызывает

ненужное замешательство.
Для предотвращения этих потенциальных проблем вы можете создать документ
«Стандартные аббревиатуры», описывающий все аббревиатуры конкретного про#
екта. Сам документ может быть файлом текстового редактора или электронной
таблицей. При работе над очень крупным проектом им может быть база данных.
Этот документ следует зарегистрировать в системе управления версиями и изме#
нять его каждый раз, когда кто#то создает новую аббревиатуру. Элементы доку#
мента должны быть сортированы по полным словам, а не по аббревиатурам.
Может показаться, что эта методика связана с большим объемом дополнительной
работы, но на самом деле она требует небольших усилий на начальном этапе,
предоставляя взамен механизм, помогающий эффективно использовать аббреви#
атуры. Она устраняет первый из двух факторов риска, заставляя документировать
все используемые аббревиатуры. Если программист не может создать новую аб#
бревиатуру, не проверив документ «Стандартные аббревиатуры», не изменив его
и снова не зарегистрировав в системе управления версиями, — это хорошо. Это
значит, что программисты будут создавать аббревиатуры, только когда будут чув#
ствовать, что выгода от их создания стоит того, чтобы преодолеть все барьеры,
связанные с их документированием.

ГЛАВА 11 Сила имен переменных

277

Вероятность создания избыточных аббревиатур также снижается. Программист,
желающий сократить какое#то слово, должен будет проверить документ и ввести
новую аббревиатуру. Если это слово уже было сокращено, программист заметит
это и будет использовать существующую аббревиатуру.
Этот совет иллюстрирует различие между удобством написания кода и
удобством его чтения. Очевидно, что рассмотренный подход делает на#
писание кода менее удобным, однако на протяжении срока службы сис#
темы программисты проводят гораздо больше времени именно за чтением кода,
которое благодаря этому подходу облегчается. Когда вся пыль осядет, вполне может
оказаться, что этот подход облегчил и написание кода.
Помните, что имена создаются в первую очередь для программистов,
читающих код Прочитайте собственный код, который вы не видели минимум
шесть месяцев, и обратите внимание на те имена, суть которых вы не можете понять
с первого взгляда. Измените подходы, подталкивающие к выбору таких имен.

11.7. Имена, которых следует избегать
Ниже я описал некоторые типы имен, которых следует избегать.
Избегайте обманчивых имен или аббревиатур Убедитесь в том, что имя не
является двусмысленным. Скажем, FALSE обычно является противоположностью
TRUE, и использовать такое имя как сокращение фразы «Fig and Almond Season»
было бы глупо.
Избегайте имен, имеющих похожие значения Если есть вероятность, что вы
можете спутать имена двух переменных и это не приведет к ошибке компиляции,
переименуйте обе переменных. Например, пары имен input и inputValue, recordNum
и numRecords или fileNumber и fileIndex так похожи с семантической точки зре#
ния, что если вы будете использовать их в одном фрагменте кода, то сможете легко
их спутать, внеся в код неуловимые ошибки.
Избегайте переменных, имеющих разную суть, но по'
хожие имена Если у вас есть две таких переменных, по#
пытайтесь переименовать одну из них или изменить аб#
бревиатуры. Избегайте имен вроде clientRecs и clientReps. Они
различаются только одной буквой, и это трудно заметить.
Выбирайте имена, различающиеся хотя бы двумя буквами
или первой/последней буквой. Имена clientRecords и cli%
entReports лучше, чем первоначальные имена.

Перекрестная ссылка Технически подобное различие между
сходными именами переменных
называется «психологической
дистанцией» (см. подраздел
«Психологическая дистанция»
раздела 23.4).

Избегайте имен, имеющих похожее звучание, таких как wrap и rap Когда
вы пытаетесь обсудить код с другими людьми, в разговор иногда вмешиваются
омонимы. Так, одним из самых забавных аспектов экстремального программиро#
вания (Beck, 2000) является слишком хитрое использование терминов «Goal Donor»
и «Gold Owner»1 , которые звучат практически одинаково. В итоге разговор может
принять подобный оборот:
1

Что буквально переводится как «донор цели» и «владелец золота». В экстремальном програм#
мировании так называют роли людей, соответственно ставящих перед разработчиками задачу
и финансирующих проект. — Прим. перев.

278

ЧАСТЬ III

Переменные

— Я только что разговаривал с Goal Donor.
— Что ты сказал? «Gold Owner» или «Goal Donor»?
— Я сказал «Goal Donor».
— Что?
— GOAL # # # DONOR!
— Ясно, Goal Donor. Не нужно кричать, черт возьми (Goll’ Darn it).
— Какое еще «золотое кольцо» (Gold Donut)?
Как и в случае непроизносимых аббревиатур, используйте для исключения подоб#
ных ситуаций телефонный тест.
Избегайте имен, включающих цифры Если наличие цифр в именах действи#
тельно имеет смысл, используйте вместо отдельных переменных массив. Если этот
вариант неуместен, цифры в именах еще более неуместны. Например, избегайте
имен file1 и file2 или total1 и total2. Почти всегда можно найти более разумный
способ проведения различия между двумя переменными, чем дополнение их имен
цифрами. В то же время я не могу сказать, что цифры нельзя использовать вооб#
ще. Некоторые сущности реального мира (такие как шоссе 66) изначально вклю#
чают цифры. И все же перед созданием подобного имени подумайте, есть ли луч#
шие варианты.
Избегайте орфографических ошибок Вспомнить правильные имена перемен#
ных и так довольно трудно. Требовать запоминания «правильных» орфографиче#
ских ошибок — это уж слишком. Например, если вы решите сэкономить три бук#
вы и замените слово highlight на hilite, программисту, читающему код, будет нео#
писуемо трудно вспомнить, на что же вы его заменили. На highlite? hilite? hilight?
hilit? jai%a%lai%t? Кто его знает.
Избегайте слов, при написании которых люди часто допускают ошиб'
ки Absense, acummulate, acsend, calender, concieve, defferred, definate, independance,
occassionally, prefered, reciept, superseed и многие другие орфографические ошиб#
ки весьма распространены в англоязычном мире. Список подобных слов можно
найти в большинстве справочников по английскому языку. Не используйте такие
слова в именах переменных.
Проводите различие между именами не только по регистру букв Если вы
программируете на C++ или другом языке, в котором регистр букв играет роль,
вы можете поддастся соблазну сократить понятия fired, final review duty и full revenue
disbursal соответственно до frd, FRD и Frd. Избегайте этого подхода. Хотя эти имена
уникальны, их связь с конкретными значениями произвольна и непонятна. Имя
Frd может с тем же успехом обозначать final review duty, а FRD — full revenue disbursal,
и никакое логическое правило не поможет вам или кому#то другому запомнить,
что есть что.
Избегайте смешения естественных языков Если в проекте участвуют програм#
мисты разных национальностей, обяжите их именовать все элементы программы,
используя один естественный язык. Понять код другого программиста непросто; а
код, написанный на юго#восточном диалекте марсианского языка, — невозможно.
Еще более тонкая проблема связана с наличием разных диалектов английского
языка. Если в проекте участвуют представители разных англоязычных стран, сде#

ГЛАВА 11 Сила имен переменных

279

лайте стандартом тот или иной вариант английского языка, чтобы вам не при#
шлось вспоминать, как называется конкретная переменная: «color» или «colour»,
«check» или «cheque» и т. д.
Избегайте имен стандартных типов, переменных и методов Все языки
программирования имеют зарезервированные и предопределенные имена. Про#
сматривайте время от времени списки таких имен, чтобы не вторгаться во владе#
ния используемого языка. Так, следующий фрагмент вполне допустим при про#
граммировании на PL/I, но написать ТАКОЕ может только идиот со справкой:

if if = then then
then = else;
else else = if;
Не используйте имена, которые совершенно не связаны с тем, что пред'
ставляют переменные Использование имен вроде margaret и pookie практи#
чески гарантирует, что никто другой их не поймет. Не называйте переменные в
честь девушки, жены, любимого сорта пива и т. д., если только девушка, жена или
сорт пива не являются представляемыми в программе «сущностями». Но даже тогда
вы должны понимать, что все в мире изменяется, поэтому имена девушка, жена и
любимыйСортПива гораздо лучше!
Избегайте имен, содержащих символы, которые можно спутать с дру'
гими символами Помните, что некоторые символы выглядят очень похоже. Если
два имени различаются только одним таким символом, вы можете столкнуться с
проблемами. Попробуйте, например, определить, какое из имен является лишним
в каждой тройке:

eyeChartl
TTLCONFUSION
hard2Read
GRANDTOTAL
ttl5

eyeChartI
TTLCONFUSION
hardZRead
GRANDTOTAL
ttlS

eyeChartl
TTLC0NFUSION
hard2Read
6RANDTOTAL
ttlS

В число пар символов, которые трудно различить, входят пары (1 и l), (1 и I),
(. и ,), (0 и O), (2 и Z), (; и :), (S и 5) и (G и 6).
Действительно ли важны подобные детали? Да! Джеральд
Вайнберг пишет, что в 1970#х из#за того, что в команде
FORMAT языка Fortran вместо точки была использована за#
пятая, ученые неверно рассчитали траекторию космичес#
кого корабля и потеряли космический зонд стоимостью 1,6
млрд долларов (Weinberg, 1983).

Контрольный список: именование переменных

Перекрестная ссылка Вопросы,
касающиеся использования данных, приведены в контрольном
списке в главе 10.

http://cc2e.com/1191

Общие принципы именования переменных
 Описывает ли имя представляемую переменной сущность
полно и точно?
 Характеризует ли имя проблему реального мира, а не ее решение на языке
программирования?

280

ЧАСТЬ III

Переменные

 Имеет ли имя длину, достаточную для того, чтобы над ним не нужно было
ломать голову?
 Спецификаторы вычисляемых значений находятся в конце имен?
 Используются ли в именах спецификаторы Count или Index вместо Num?

Именование конкретных видов данных
 Выразительные ли имена присвоены индексам циклов (более ясные, чем i,
j и k, если цикл содержит более одной-двух строк или является вложенным)?
 Всем ли «временным» переменным присвоены выразительные имена?
 Можно ли по именам булевых переменных понять, какой смысл имеют значения «истина» и «ложь»?
 Включают ли имена элементов перечислений префикс или суффикс, определяющий принадлежность элемента к перечислению — например, префикс
Color_ в случае элементов Color_Red, Color_Green, Color_Blue и т. д.?
 Именованные константы названы в соответствии с представляемыми абстрактными сущностями, а не конкретными числами?
Конвенции именования
 Проводит ли конвенция различие между локальными данными, данными
класса и глобальными данными?
 Проводит ли конвенция различие между именами типов, именованных констант, перечислений и переменных?
 Идентифицировали ли вы исключительно входные параметры методов, если
язык не навязывает их идентификацию?
 Постарались ли вы сделать конвенцию как можно более совместимой со
стандартными конвенциями конкретного языка?
 Способствует ли форматирование имен удобству их чтения?
Короткие имена
 Стараетесь ли вы не сокращать имена без необходимости?
 Избегаете ли вы сокращения имен только на одну букву?
 Все ли слова вы сокращаете согласованно?
 Легко ли произнести выбранные имена?
 Избегаете ли вы имен, допускающих неверное прочтение или произношение?
 Документируете ли вы короткие имена при помощи таблиц преобразования?
Распространенные проблемы именования. Избежали ли вы…
 …имен, которые вводят в заблуждение?
 …имен с похожими значениями?
 …имен, различающихся только одним или двумя символами?
 …имен, имеющих похожее звучание?
 …имен, включающих цифры?
 …имен, намеренно написанных с ошибками с целью сокращения?
 …имен, при написании которых люди часто допускают ошибки?
 …имен, конфликтующих с именами методов из стандартных библиотек или
предопределенными именами переменных?
 …совершенно произвольных имен?
 …символов, которые можно спутать с другими символами?

ГЛАВА 11 Сила имен переменных

281

Ключевые моменты
 Выбор хороших имен переменных — одно из главных условий понятности

программы. С отдельными типами переменных — например, с индексами цик#
лов и переменными статуса — связаны свои принципы именования.
 Имена должны быть максимально конкретны. Имена, которые из#за невыра#

зительности или обобщенности можно использовать более чем с одной целью,
обычно являются плохими.
 Конвенции именования позволяют провести различие между локальными дан#

ными, данными класса и глобальными данными, а также между именами ти#
пов, именованных констант, перечислений и переменных.
 Над каким бы проектом вы ни работали, вам следует принять конвенцию име#

нования переменных. При выборе типа конвенции следует учитывать размер
программы и число работающих над ней программистов.
 Современные языки программирования позволяют отказаться от сокращения

имен. Если вы все#таки сокращаете имена, регистрируйте аббревиатуры в сло#
варе проекта или используйте стандартизированные префиксы.
 За чтением кода программисты проводят гораздо больше времени, чем за его

написанием. Выбирайте имена так, чтобы они облегчали чтение кода, пусть даже
за счет удобства его написания.

282

ЧАСТЬ III

Г Л А В А

Переменные

1 2

Основные типы данных

http://cc2e.com/1278

Содержание
 12.1. Числа в общем
 12.2. Целые числа
 12.3. Числа с плавающей запятой
 12.4. Символы и строки
 12.5. Логические переменные
 12.6. Перечислимые типы
 12.7. Именованные константы
 12.8. Массивы
 12.9. Создание собственных типов данных (псевдонимы)

Связанные темы
 Присвоение имен данным: глава 11
 Нестандартные типы данных: глава 13
 Общие вопросы использования переменных: глава 10
 Описание форматов данных: подраздел «Размещение объявлений данных»

раздела 31.5
 Документирование переменных: подраздел «Комментирование объявлений дан#

ных» раздела 32.5
 Создание классов: глава 6

Основные типы данных являются базовыми блоками для построения остальных
типов данных. Эта глава содержит советы по применению чисел вообще, целых
чисел, чисел с плавающей запятой, символов и строк, логических переменных,
перечислимых типов, именованных констант и массивов. В заключительном раз#
деле этой главы рассказано о создании собственных типов данных.
Эта глава содержит рекомендации по предупреждению главных ошибок в приме#
нении основных типов данных. Если вы уже ознакомились с основами, перехо#
дите к концу главы, к обзору перечня допускаемых ошибок, а также к обсужде#
нию нестандартных типов данных в главе 13.

ГЛАВА 12 Основные типы данных

283

12.1. Числа в общем
Далее дано несколько рекомендаций, позволяющих сократить число ошибок при
использовании чисел.
Избегайте «магических чисел» Магические числа — это
Перекрестная ссылка О примеобычные числа, такие как 100 или 47524, которые появля#
нении именованных констант
ются в программе без объяснений. Если вы программируе#
вместо магических чисел см.
раздел 12.7.
те на языке, поддерживающем именованные константы, ис#
пользуйте их вместо магических чисел. Если вы не можете
применить именованные константы, применяйте глобальные переменные, когда
это возможно.
Исключение магических чисел дает три преимущества.
 Изменения можно сделать более надежно. Если вы используете именованные

константы, нет нужды искать каждое из чисел 100, и вы не измените по ошиб#
ке те из этих чисел, что ссылаются на что#либо иное.
 Изменения сделать проще. Когда максимальное число элементов меняется со

100 на 200, используя магические числа, вы должны найти каждое число 100
и изменить его на 200. Если вы используете 100+1 или 100%1, вы также долж#
ны найти и изменить все числа 101 и 99 на 201 и 199 соответственно. При
использовании именованных констант вы просто меняете определение кон#
станты со 100 на 200 в одном месте.
 Ваша программа лучше читается. Конечно, в выражении:

for i = 0 to 99 do ...
можно предположить, что 99 определяет максимальное число элементов. А вот
выражение:

for

i = 0 to MAX_ENTRIES1 do ...

не оставляет на этот счет сомнений. Даже если вы уверены, что это число ни#
когда не изменится, применяя именованные константы, вы получите более
читабельную программу.
Применяйте жестко заданные нули и единицы по необходимости Зна#
чения 0 и 1 используются для инкремента, декремента, а также в начале циклов
при нумерации первого элемента массива. 0 в конструкции:

for i = 0 to CONSTANT do ...
вполне приемлем, так же как 1 в выражении:

total = total + 1
Вот хорошее правило: используйте в программе как константы только 0 и 1, а любые
другие числа определите как литералы с понятными именами.
Ошибки деления на ноль Каждый раз, когда вы пользуетесь символом деления
(/ в большинстве языков), думайте о том, может ли в знаменателе оказаться 0. Если
такая возможность существует, напишите код, предупреждающий появление ошиб#
ки деления на 0.

284

ЧАСТЬ III

Переменные

Выполняйте преобразования типов понятно Убедитесь, что кто#нибудь, чи#
тая вашу программу, поймет преобразования между разными типами данных,
которые в ней встречаются. На языке C++ вы могли бы написать:

y = x + (float) i
а на Microsoft Visual Basic:

y = x + CSng( i )
Эта практика поможет обеспечить однозначность ваших преобразований — раз#
ные компиляторы по#разному конвертируют, а при таком подходе вы гарантиро#
ванно получите то, что ожидали.
Перекрестная ссылка Вариант
этого примера см. в разделе 12.3.

Избегайте сравнений разных типов Если x — число с
плавающей запятой, а i — целое, проверка:

if ( i = x ) then ...
почти гарантированно не сработает. К тому времени, когда компилятор опреде#
лит каждый тип, который он хочет задействовать для сравнения, преобразует один
из типов в другой, произведет ряд округлений и вычислит ответ, вы будете рады,
если ваша программа вообще работает. Сделайте преобразования вручную, так
чтобы компилятор мог сравнить два числа одного и того же типа, и точно знай#
те, что нужно сравнивать.
Обращайте внимание на предупреждения вашего компилято'
ра Многие современные компиляторы сообщают о наличии разных
типов чисел в одном выражении. Обращайте на это внимание! Каждый
программист рано или поздно просит кого#нибудь помочь выследить надоедли#
вую ошибку, а выясняется, что о ней все время предупреждал компилятор. Про#
фессионалы высокого класса пишут свои программы так, чтобы исключить все
предупреждения компилятора. Легче предоставить работу компилятору, чем вы#
полнять ее самому.

12.2. Целые числа
Учитывайте следующие рекомендации при применении целых чисел.
Проверяйте целочисленность операций деления Когда используются целые
числа, выражение 7/10 не равно 0,7. Оно обычно равно 0 или минус бесконечно#
сти, или ближайшему целому, или… ну, вы понимаете. Результат зависит от выб#
ранного языка. Это же относится и к промежуточным результатам. В реальном мире
10 * (7/10) = (10*7) / 10 = 7. Но не в мире целочисленной арифметики. 10 * (7/10)
равно 0, потому что целочисленное деление (7/10) равно 0. Простейший способ
исправить положение — преобразовать его так, чтобы операции деления выпол#
нялись последними: (10*7) / 10.
Проверяйте переполнение целых чисел При выполнении умножения или сло#
жения необходимо принимать во внимание наибольшие возможные значения це#
лых чисел. Для целого числа без знака это обычно 232 –1, а иногда и 216 –1, или 65 535.
Проблема возникает, когда вы умножаете два числа, в результате чего получается

ГЛАВА 12 Основные типы данных

285

число большее, чем максимально возможное целое. Скажем, если вы умножаете
250 * 300, правильным ответом будет 75 000. Но если максимальное целое — 65 535,
то, возможно, из#за переполнения вы получите 9464 (75 000 – 65 536 = 9464). Вот
интервалы значений для часто встречающихся целых типов (табл. 12#1):

Табл. 12-1. Интервалы значений некоторых целых типов
Целый тип

Интервал

8#битный со знаком

От –128 до 127

8#битный без знака

От 0 до 255

16#битный со знаком

От –32 768 до 32 767

16#битный без знака

От 0 до 65 535

32#битный со знаком

От –2 147 483,648 до 2 147 483 647

32#битный без знака

От 0 до 4 294 967 295

64#битный со знаком

От –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807

64#битный без знака

От 0 до 18 446 744 073 709 551 615

Простейший способ предотвращения целочисленного переполнения — просмотр
каждого члена арифметического выражения с целью представить наибольшее
возможное значение, которое он может принимать. Так, если в целочисленном
выражении m = j * k, наибольшим значением для j будет 200, а для k — 25, то мак#
симальным значением для m будет 200 * 25 = 5 000. Для 32#разрядных машин это
вполне допустимо, так как максимальным целым является 2 147 483 647. С другой
стороны, если максимально возможное значение для j — это 200 000, а для k —
100 000, то значение m может достигать 200 000 * 100 000 = 20 000 000 000. Это
уже неприемлемо, так как 20 000 000 000 больше, чем 2 147 483 647. В этом слу#
чае для размещения наибольшего возможного значения m вам придется исполь#
зовать 64#битные целые или числа с плавающей запятой.
Кроме того, учитывайте будущее развитие программы. Если m никогда не будет
больше 5 000 — отлично. Но если ожидается, что m будет постоянно расти на
протяжении нескольких лет, примите это во внимание.
Проверяйте на переполнение промежуточные результаты Число, полу#
чаемое в конце вычислений, — не единственное, о котором следует беспокоить#
ся. Представьте, что у вас есть такой код:

Пример переполнения промежуточных результатов (Java)
int termA = 1000000;
int termB = 1000000;
int product = termA * termB / 1000000;
System.out.println( “( “ + termA + “ * “ + termB + “ ) / 1000000 = “ + product );
Вы можете подумать, что значение Product вычисляется как (100 000*100 000) /
100 000 и поэтому равно 100 000. Но программе приходится вычислять проме#
жуточное значение 100 000*100 000 до того, как будет выполнено деление на
100 000, а это значит, что нужно хранить такое большое число, как
1 000 000 000 000. Угадайте, что получится? Вот результат:

( 1000000 * 1000000 ) / 1000000 = 727

286

ЧАСТЬ III

Переменные

Если значение целых чисел в вашей системе не превышает 2 147 483 647, проме#
жуточный результат слишком велик для целого типа данных. В такой ситуации
промежуточный результат, который должен быть равен 1 000 000 000 000, на са#
мом деле равен 727 379 968, поэтому, когда вы делите его на 100 000, вы получа#
ете #727 вместо 100 000.
Вы можете решить проблему переполнения промежуточных результатов так же,
как и в случае целочисленного переполнения: изменив тип на длинное целое или
число с плавающей запятой.

12.3. Числа с плавающей запятой
Главная особенность применения чисел с плавающей запятой в том, что
многие дробные десятичные числа не могут быть точно представлены
с помощью нулей и единиц, используемых в цифровом компьютере.
В бесконечных десятичных дробях, таких как 1/3 или 1/7, обычно сохраняется
только 7 или 15 цифр после запятой. В моей версии Microsoft Visual Basic 32#бит#
ное представление дроби 1/3 в виде числа с плавающей запятой равно 0,33333330.
То есть точность ограничена 7 цифрами. Такая точность достаточна для большин#
ства случаев, но все же способна иногда вводить в заблуждение.
Вот несколько рекомендаций по использованию чисел с плавающей запятой.
Перекрестная ссылка Книги,
содержащие алгоритмы решения этих проблем, см. в подразделе «Дополнительные ресурсы» раздела 10.1.

Избегайте сложения и вычитания слишком разных по
размеру чисел Для 32#битной переменной с плавающей
запятой сумма 1 000 000,00 + 0,1, вероятно, будет равна
1 000 000,00, так как в 32 битах недостаточно значимых
цифр, чтобы охватить интервал между 1 000 000 и 0,1. Ана#
логично 5 000 000,02 – 5 000 000,01, вероятно, равно 0,0.

Решение? Если вам нужно складывать настолько разные по величине числа, сна#
чала отсортируйте их, а затем складывайте, начиная с самых маленьких значений.
Аналогично, если вам надо сложить бесконечный ряд значений, начните с наи#
меньшего члена, т. е. суммируйте члены в обратном порядке. Это не решит про#
блемы округления, но минимизирует их. Многие алгоритмические книги предла#
гают решения для таких случаев.
Избегайте сравнений на равенство Числа с плавающей
запятой, которые должны быть равны, на самом деле рав#
ны невсегда. Главная проблема в том, что два разных спо#
Аноним
соба получить одно и то же число не всегда приводят к
одинаковому результату. Так, если 10 раз сложить 0,1, то 1,0 получается только в
редких случаях. Следующий пример содержит две переменных (nominal и sum),
которые должны быть равны, но это не так.

1 равен 2 для достаточно больших значений 1.

Пример неправильного сравнения чисел с плавающей точкой (Java)
Переменная nominal — 64-битное вещественное число.

> double nominal = 1.0;
double sum = 0.0;

ГЛАВА 12 Основные типы данных

287

for ( int i = 0; i < 10; i++ ) {
sum вычисляется как 10*0,1. Она должна быть равна 1,0.

>

sum += 0.1;
}
Здесь неправильное сравнение.

> if ( nominal == sum ) {
System.out.println( “Numbers are the same.” );
}
else {
System.out.println( “Numbers are different.” );
}
Как вы, наверное, догадались, программа выводит:

Numbers are different.
Вывод каждого значения sum в цикле for выглядит так:

0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
Таким образом, хорошей идеей будет найти альтернативу операции сравнения на
равенство для чисел с плавающей запятой. Один эффективный подход состоит в
том, чтобы определить приемлемый интервал точности, а затем использовать
логические функции для выяснения, достаточно ли близки сравниваемые значе#
ния. Для этого обычно пишется функция Equals(), которая возвращает true, если
значения попадают в этот интервал, и false — в противном случае. На языке Java
такая функция может выглядеть так:

Пример метода для сравнения чисел с плавающей запятой (Java)
final double ACCEPTABLE_DELTA = 0.00001;
boolean Equals( double Term1, double Term2 ) {
if ( Math.abs( Term1  Term2 ) < ACCEPTABLE_DELTA ) {
return true;
}
else {
return false;
}
}

Перекрестная ссылка Этот пример — доказательство того, что
из каждого правила есть исключения. Переменные здесь содержат цифры в именах. Правило
против использования цифр в
именах переменных см. в разделе 11.7.

288

ЧАСТЬ III

Переменные

Если код в примере «неправильного сравнения чисел с плавающей запятой» из#
менить так, чтобы для сравнения использовался этот метод, новое выражение
получит следующий вид:

if ( Equals( Nominal, Sum ) ) ...
При запуске теста программа выведет сообщение:

Numbers are the same.
В зависимости от требований вашего приложения использование жестко закодиро#
ванного значения ACCEPTABLE_DELTA может быть недопустимо. Возможно, придет#
ся вычислять ACCEPTABLE_DELTA на основании размера двух сравниваемых чисел.
Предупреждайте ошибки округления Проблемы с ошибками округления сход#
ны с проблемами слишком разных по размеру чисел. У них одинаковые причи#
ны и похожие методики решения. Кроме того, далее перечислены способы реше#
ния проблем округления.
 Измените тип переменной на тип с большей точностью. Если вы используете числа

с одинарной точностью, замените их числами с двойной точностью и т. д.
Перекрестная ссылка Как правило, конвертация в тип BCD
минимально влияет на производительность. Если вы озабочены проблемой производительности, см. раздел 25.6.

 Используйте двоично#десятичные переменные (binary
coded decimal, BCD). BCD#числа обычно работают медлен#
нее и требуют больше памяти для хранения, но предотвра#
щают множество ошибок округления. Это особенно важно,
если используемые переменные представляют собой долла#
ры и центы или другие величины, которые должны точно
балансироваться.

 Измените тип с плавающей запятой на целые значения. Это такая самодельная

замена BCD#переменных. Возможно, вам придется использовать 64#битные це#
лые, чтобы получить нужную точность. Этот способ предполагает, что вы сами
будете отслеживать дробные части чисел. Допустим, изначально вы вели учет
денежных сумм, применяя числа с плавающей запятой, при этом центы указы#
вались как дробная часть. Это обычный способ обработки долларов и центов.
Когда вы переключаетесь на целые числа, вам нужно вести учет центов с помо#
щью целых, а долларов — с помощью чисел, кратных 100 центам. Иначе говоря,
вы умножаете сумму в долларах на 100 и храните центы в этой переменной в
интервале от 0 до 99. Такое решение может показаться абсурдным, но оно эф#
фективно и с точки зрения скорости, и с точки зрения точности. Вы можете
упростить эти манипуляции, создав класс DollarsAndCents, скрывающий целое
представление чисел и предоставляющий необходимые числовые операции.
Проверяйте поддержку специальных типов данных в языке и дополнитель'
ных библиотеках Некоторые языки, включая Visual Basic, предоставляют такие
типы данных, как Currency, предназначенные для данных, чувствительных к ошиб#
кам округления. Если ваш язык содержит встроенный тип данных, предоставляю#
щий такую функциональность, используйте его!

ГЛАВА 12 Основные типы данных

289

12.4. Символы и строки
Этот раздел предлагает несколько советов по использованию строк. Первый от#
носится к строкам во всех языках.
Избегайте магических символов и строк Магические
Перекрестная ссылка Вопросы
символы — это литеральные символы (например, 'А'), а ма#
использования магических симгические строки — это литеральные строки (например,
волов и строк аналогичны вопросам применения магических
”Gigamatic Accounting Program”), которые разбросаны по всей
чисел (см. раздел 12.1).
программе. Если ваш язык программирования поддерживает
применение именованных констант, то лучше задействуй#
те их. В противном случае используйте глобальные переменные. Далее перечис#
лено несколько причин, по которым надо избегать литеральных строк.
 Для таких часто встречающихся строк, как имя программы, названия команд,

заголовки отчетов и т. п., вам может понадобиться поменять содержимое. На#
пример, ”Gigamatic Accounting Program” в более поздней версии может изме#
ниться на ”New and Improved! Gigamatic Accounting Program”.
 Все большее значение приобретают международные рынки, и строки, сгруп#

пированные в файле ресурсов переводить гораздо легче, чем раскиданные по
всей программе.
 Строковые литералы обычно занимают много места. Они используются для

меню, сообщений, экранов помощи, форм ввода и т. д. Если их слишком мно#
го, они выходят из#под контроля и вызывают проблемы с памятью. Во многих
системах объем памяти, занимаемый строками, не является причиной для бес#
покойства. Однако при программировании встроенных систем и других при#
ложений, в которых каждый байт на счету, проблему хранения строк легче
решить, если эти строки относительно независимы от кода.
 Символьные и строковые литералы могут быть загадочными. Комментарии или

именованные константы проясняют ваши намерения. В следующем примере
смысл 0x1B неясен. Константа ESCAPE делает значение более понятным.

Пример сравнений с использованием строк (C++)
Плохо!

>if ( input_char == 0x1B ) ...
Лучше!

>if ( input_char == ESCAPE ) ...
Следите за ошибками завышения/занижения на единицу Поскольку подстро#
ки могут индексироваться аналогично массивам, не забывайте об ошибках завы#
шения/занижения на 1, которые приводят к чтению или записи за концом строки.
Узнайте, как ваш язык и система поддерживают Uni'
http://cc2e.com/1285
code В некоторых языках, например в Java, все строки хра#
нятся в формате Unicode. В других — таких, как C и C++ —
работа со строками в Unicode требует применения отдельного набора функций. Пре#
образование между Unicode и другими наборами символов часто необходимо для
взаимодействия со стандартными библиотеками и библиотеками сторонних про#

290

ЧАСТЬ III

Переменные

изводителей. Если часть строк не будет поддерживать Unicode (скажем, в C или C++),
как можно раньше решите, стоит ли вообще использовать символы Unicode. Если
вы решились на это, подумайте, где и когда будете это делать.
Разработайте стратегию интернационализации/локализации в ранний
период жизни программы Вопросы, связанные с интернационализацией, от#
носятся к разряду ключевых. Решите, будут ли все строки храниться во внешних
ресурсах и будет ли создаваться отдельный вариант программы для каждого язы#
ка или конкретный язык будет определяться во время выполнения.
Если вам известно, что нужно поддерживать толь'
ко один алфавит, рассмотрите вариант использова'
ния набора символов ISO 8859 Для приложений, исполь#
зующих только один алфавит (например, английский), которым не надо поддер#
живать несколько языков или какой#либо идеографический язык (такой как пись#
менный китайский), расширенный ASCII#набор стандарта ISO 8859 — хорошая
альтернатива символам Unicode.
http://cc2e.com/1292

Если вам необходимо поддерживать несколько языков, используйте Uni'
code Unicode обеспечивает более полную поддержку международных наборов
символов, чем ISO 8859 или другие стандарты.
Выберите целостную стратегию преобразования строковых типов Если
вы используете несколько строковых типов, общим подходом, помогающим хра#
нить строковые типы в порядке, будет хранение всех строк программы в одном
формате и преобразование их в другой формат как можно ближе к операциям
ввода и вывода.

Строки в языке C
Строковый класс в стандартной библиотеке шаблонов C++ решил большинство
проблем со строками языка C. А тот, кто напрямую работает с C#строками, ниже
узнает о способах избежать часто встречающихся ошибок.
Различайте строковые указатели и символьные массивы Проблемы со
строковыми указателями и символьными массивами возникают из#за способа об#
работки строк в C. Учитывайте различия между ними в двух случаях.
 Относитесь с недоверием к строковым выражениям, содержащим знак равен#

ства. Строковые операции в C практически всегда выполняются с помощью
strcmp(), strcpy(), strlen() и аналогичных функций. Знаки равенства часто сигна#
лизируют о каких#то ошибках в указателях. Присваивание в C не копирует стро#
ковые константы в строковые переменные. Допустим, у нас есть выражение:

StringPtr = “Some Text String”;
В этом случае ”Some Text String” — указатель на литеральную текстовую строку,
и это присваивание просто присвоит указателю StringPtr адрес данной стро#
ки. Операция присваивания не копирует содержимое в StringPtr.
 Используйте соглашения по именованию, чтобы различать переменные —

массивы символов и указатели на строки. Одно из общепринятых соглашений —
использование ps как префикса для обозначения указателя на строку, и ach —
как префикса для символьного массива. И хотя они не всегда ошибочны, от#

ГЛАВА 12 Основные типы данных

291

носитесь все#таки с подозрением к выражениям, включающим переменные с
обоими префиксами.
Объявляйте для строк в стиле C длину, равную КОНСТАНТА+1 В C и C++
ошибки завышения на 1 в C#строках — обычное явление, потому что очень легко
забыть, что строка длины n требует для хранения n + 1 байт, и не выделить место
для нулевого терминатора (байта в конце строки, установленного в 0). Эффектив#
ный способ решения этой проблемы — использовать именованные константы при
объявлении всех строк. Суть в том, что именованные константы применяются всегда
одинаково: Сначала длина строки объявляется как КОНСТАНТА+1, а затем КОНСТАН%
ТА используется для обозначения длины строки во всем остальном коде. Вот пример:

Пример правильных объявлений строк (С)
/* Объявляем строку длиной «константа+1».
Во всех остальных местах программы используем «константа»,
а не «константа +1». */
Эта строка объявлена с длиной NAME_LENGTH +1.

> char name[ NAME_LENGTH + 1 ] = { 0 }; /* Длина строки — NAME_LENGTH */
...
/* Пример 1: Заполняем строку символами ‘A’, используя константу NAME_LENGTH
для определения количества символов ‘A’, которые можно скопировать.
Заметьте: используется NAME_LENGTH, а не NAME_LENGTH + 1. */
В действиях со строкой NAME_LENGTH используется здесь…

>for ( i = 0; i < NAME_LENGTH; i++ )
name[ i ] = ‘A’;
...
/* Пример 2: Копируем другую строку в первую, используя константу
для определения максимальной длины, которую можно копировать. */
…и здесь.

>strncpy( name, some_other_name, NAME_LENGTH );
Если у вас не будет соглашения по этому поводу, иногда вы будете объявлять строки
длиной NAME_LENGTH, а в операциях использовать NAME_ LENGTH%1; а иногда вы будете
объявлять строки длиной NAME_LENGTH+1 и работать с NAME_LENGTH. Каждый раз
при использовании строки вам придется вспоминать, как вы ее объявили.
Если же вы всегда одинаково объявляете строки, думать, как работать с каждой из
них, не надо, и вы избежите ошибок из#за того, что забыли особенность объявле#
ния данной строки. Выработка соглашения минимизирует умственную перегруз#
ку и ошибки при программировании.
Инициализируйте строки нулем во избежание строк
бесконечной длины Язык C определяет конец строки пу#
тем поиска нулевого терминатора — байта в конце строки,
установленного в 0. Какой предполагалась длина строки, зна#

Перекрестная ссылка Подробнее об инициализации данных
см. раздел 10.3.

292

ЧАСТЬ III

Переменные

чения не имеет: C никогда не найдет ее конец, если не найдет нулевой байт. Если
вы забыли поместить нулевой байт в конец строки, строковые операции могут ра#
ботать не так, как вы ожидаете.
Вы можете предупредить появление бесконечных строк двумя способами. Во#пер#
вых, при объявлении инициализируйте символьные массивы 0:

Пример правильного объявления символьного массива (C)
char EventName[ MAX_NAME_LENGTH + 1 ] = { 0 };
Во#вторых, при динамическом создании строк инициализируйте их 0, используя
функцию calloc() вместо malloc(). Функция calloc() выделяет память и инициали#
зирует ее 0. malloc() выделяет память без инициализации, поэтому вы рискуете,
используя память, выделенную с помощью malloc().
Используйте в C массивы символов вместо указате'
лей Если объем занимаемой памяти некритичен (а часто
так и есть), объявляйте все строковые переменные как мас#
сивы символов. Это поможет избежать проблем с указателями, а компилятор бу#
дет выдавать больше предупреждений в случае неправильных действий.

Перекрестная ссылка О массивах см. раздел 12.8.

Используйте strncpy() вместо strcpy() во избежание строк бесконечной
длины Строковые функции в C существуют в опасной и безопасной версиях. Более
опасные функции, такие как strcpy() и strcmp(), продолжают работу до обнаруже#
ния нулевого терминатора. Их более безобидные спутники — strncpy() и strncmp()
— принимают максимальную длину в качестве параметра, так что, даже если строки
будут бесконечными, ваши вызовы функций не зациклятся.

12.5. Логические переменные
Логические или булевы переменные сложно использовать неправильно, а их вдум#
чивое применение сделает вашу программу аккуратней.
Используйте логические переменные для документи'
рования программы Вместо простой проверки логиче#
ского выражения вы можете присвоить его значение пере#
менной, которая сделает смысл теста очевидным. Например,
в этом фрагменте из условия if не ясно, выполняется ли
проверка завершения, ошибочной ситуации или чего#то еще:

Перекрестная ссылка Об использовании комментариев для
документирования программы
см. главу 32.

Перекрестная ссылка Пример
использования логической функции для документирования
программы см. в подразделе
«Упрощение сложных выражений» раздела 19.1.

Пример логического условия, чье назначение неочевидно (Java)
if ( ( elementIndex < 0 ) || ( MAX_ELEMENTS < elementIndex ) ||
( elementIndex == lastElementIndex )
) {
...
}

В следующем фрагменте применение логических переменных делает назначение
if#проверки яснее:

ГЛАВА 12 Основные типы данных

293

Пример логического условия, чье назначение понятно (Java)
finished = ( ( elementIndex < 0 ) || ( MAX_ELEMENTS < elementIndex ) );
repeatedEntry = ( elementIndex == lastElementIndex );
if ( finished || repeatedEntry ) {
...
}
Используйте логические переменные для упрощения сложных условий
Чтобы правильно закодировать сложное условие, часто приходится делать несколько
попыток. Когда через некоторое время нужно модифицировать это условие, быва#
ет сложно разобраться, что же оно проверяет. Логические переменные могут уп#
ростить проверку. В предыдущем примере программа на самом деле проверяет два
условия: завершено ли выполнение метода и выполняется ли этот метод повторно.
Создав логические переменные finished и repeatedEntry, вы упрощаете if#проверку:
теперь ее легче читать, легче изменять, и она меньше подвержена ошибкам.
Вот другой пример сложного условия:

Пример сложного условия (Visual Basic)
If ( ( document.AtEndOfStream() ) And ( Not inputError ) ) And _
( ( MIN_LINES

localNumber( LOCAL_NUMBER_LENGTH ) As String
End Type
...

300

ЧАСТЬ III

Переменные

’ Убедимся, что все символы в телефонном номере — это цифры.
И здесь тоже используется.

>For iDigit = 1 To LOCAL_NUMBER_LENGTH
If ( phoneNumber.localNumber( iDigit ) < “0” ) Or _
( “9” < phoneNumber.localNumber( iDigit ) ) Then
‘ Выполняем обработку ошибок.
...
Это простой пример, но вы вполне можете представить программу, в которой
сведения о длине телефонных номеров требуются во многих местах.
На момент создания программы все работники живут в одной стране, поэтому вам
нужно только семь цифр для их телефонных номеров. По мере расширения ком#
пании ее филиалы открываются в разных странах, и вам понадобятся более длин#
ные телефонные номера. Если вы параметризовали эту длину, вам надо сделать
изменение только в одном месте — в определении именованной константы LOCAL_
NUMBER_LENGTH.
Дополнительные сведения О значении централизованного управления см. стр. 57–60 в книге
«Software Conflict» (Glass, 1991).

Как вы, наверное, поняли, именованные константы делают
сопровождение программы удобнее. Как правило, любая
технология, централизующая управление объектами, подвер#
женными изменениям, — это хороший способ уменьшить
затраты на сопровождение (Glass, 1991).

Избегайте литеральных значений, даже «безопасных»
в следующем цикле означает число 12?

Как вы думаете, что

Пример непонятного кода (Visual Basic)
For i = 1 To 12
profit( i ) = revenue( i ) – expense( i )
Next
Исходя из специфического содержимого кода, можно предположить, что выпол#
няется цикл по 12 месяцам в году. Но вы уверены? Вы поставите на это свое со#
брание «Монти Пайтон»?
В этом случае вам не нужно использовать именованные константы для поддерж#
ки расширяемости: вряд ли число месяцев в году изменится в ближайшем буду#
щем. Но если при написании кода остается хотя бы тень сомнения в его предназ#
начении, развейте ее с помощью хорошо названной именованной константы,
например, так:

Пример более понятного кода (Visual Basic)
For i = 1 To NUM_MONTHS_IN_YEAR
profit( i ) = revenue( i ) – expense( i )
Next
Это уже лучше, но для завершения примера индекс цикла тоже нужно назвать более
информативно:

ГЛАВА 12 Основные типы данных

301

Пример еще более понятного кода (Visual Basic)
For month = 1 To NUM_MONTHS_IN_YEAR
profit( month ) = revenue( month ) – expense( month )
Next
Этот пример выглядит весьма неплохо, но мы можем сделать еще один шаг впе#
ред, применив перечислимый тип:

Пример очень понятного кода (Visual Basic)
For month = Month_January To Month_December
profit( month ) = revenue( month ) – expense( month )
Next
В последнем примере не может возникнуть никаких сомнений относительно
назначения цикла. Даже если вы считаете, что литеральное значение безопасно,
используйте вместо него именованную константу. Фанатично искорените лите#
ралы из вашего кода. С помощью текстового редактора выполните поиск 2, 3, 4, 5,
6, 7, 8 и 9, чтобы убедиться, что вы не используете их случайно.
Имитируйте именованные константы с помощью
Перекрестная ссылка Об имипеременных или классов правильной области види'
тации перечислимых типов см.
подраздел «Если ваш язык не
мости Если ваш язык не поддерживает именованные кон#
поддерживает перечислимые
станты, их можно создать. Подход, аналогичный приведен#
типы» раздела 12.6.
ному выше Java#примеру, имитирующему перечислимые
типы, позволяет получить преимущества использования
именованных констант. Старайтесь применять обычные правила области види#
мости: отдавайте предпочтение локальной, классовой или глобальной области
видимости именно в таком порядке.
Последовательно используйте именованные константы Опасно исполь#
зовать для представления одной сущности именованные константы в одном мес#
те и литералы в другом. Некоторые приемы программирования напрашиваются
на ошибки, а этот просто доставляет вам ошибки на дом. Если значение имено#
ванной константы нужно изменить, вы сделаете это и подумаете, что выполнили
все необходимые изменения. Вы не обратите внимания на жестко закодирован#
ные литералы, и ваша программа будет демонстрировать таинственные дефекты.
Их устранение может потребовать так много усилий, что захочется схватить те#
лефонную трубку и молить о помощи.

12.8. Массивы
Массивы — простейшие и наиболее часто используемые типы структурирован#
ных данных. В некоторых языках это единственный вид структурированных дан#
ных. Массивы состоят из группы элементов одинакового типа, доступ к которым
осуществляется напрямую по индексу.
Убедитесь, что все значения индексов массива не выходят за его
границы Все проблемы с массивами так или иначе связаны с тем, что
доступ к их элементам может осуществляться произвольно. Наиболее часто

302

ЧАСТЬ III

Переменные

возникающая проблема объясняется попыткой доступа к элементу по индексу, вы#
ходящему за пределы массива. В некоторых языках при этом генерируется ошиб#
ка, а в других — получаются причудливые и неожиданные результаты.
Обдумайте применение контейнеров вместо массивов или рассматривай'
те массивы как последовательные структуры Некоторые именитые в ком#
пьютерной науке люди предлагали запретить произвольный доступ к массиву,
заменив его последовательным (Mills and Linger, 1986). Аргументтруют они это тем,
что произвольный доступ к массиву похож на случайные операторы goto в про#
грамме: их применение приводит к неаккуратному, подверженному ошибкам коду,
в корректности которого сложно быть уверенным. Поэтому вместо массивов пред#
лагается использовать множества, стеки и очереди, доступ к элементам которых
выполняется последовательно.
Проведя небольшой эксперимент, Миллз (Mills) и Линджер (Linger) вы#
яснили, что разработанный таким образом проект потребовал исполь#
зования меньшего числа переменных и меньшего числа ссылок на эти
переменные. То есть проект был относительно эффективнее, что привело к со#
зданию более надежного ПО.
Рассмотрите вопрос использования контейнерных классов с последовательным
доступом — наборов, стеков, очередей и т. п. — как альтернативу прежде, чем
выбрать массив.
Проверяйте конечные точки массивов Как бывает по#
лезно продумать применение конечных точек в операторе
цикла, так и вы сможете обнаружить немало ошибок, прове#
рив крайние элементы массивов. Задайтесь вопросом, пра#
вильно ли выполняется доступ к первому элементу массива
или случайно используется элемент перед ним либо после него. А что с последним
элементом? Нет ли в коде ошибки потери единицы? И, наконец, спросите себя, пра#
вильно ли код обращается к элементам в середине массива.

Перекрестная ссылка Вопросы
применения массивов и циклов
имеют много общего. Подробнее о циклах см. главу 16.

В многомерном массиве убедитесь, что его индексы используются в пра'
вильном порядке Очень легко написать Array[ i ][ j ], имея в виду Array[ j ][ i ],
так что не жалейте времени для проверки правильного порядка индексов. Попро#
буйте использовать более значимые имена, чем i и j, когда их назначение не вполне
очевидно.
Остерегайтесь пересечения индексов При использовании вложенных цик#
лов легко написать Array[ j ], имея в виду Array[ i ]. Перемена мест индексов назы#
вается «пересечением индексов» (index cross#talk). Проверьте эту возможность.
Опять же, используйте более значимые имена индексов, чем i и j, чтобы ошибки
пересечения изначально сложнее было совершить.
В языке C для работы с массивами используйте макрос ARRAY_LENGTH()
Вы можете добавить гибкости вашей работе с массивами, определив макрос ARRAY_
LENGTH():

Пример определения макроса ARRAY_LENGTH() на языке C
#define ARRAY_LENGTH( x )

(sizeof(x)/sizeof(x[0]))

ГЛАВА 12 Основные типы данных

303

При выполнении операций над массивами для указания верхней границы исполь#
зуйте макрос ARRAY_LENGTH() вместо именованной константы. Например:

Пример использования макроса ARRAY_LENGTH()
для операций с массивами на языке C
ConsistencyRatios[] =
{ 0.0, 0.0, 0.58, 0.90, 1.12,
1.24, 1.32, 1.41, 1.45, 1.49,
1.51, 1.48, 1.56, 1.57, 1.59 };
...
Вот здесь используется макрос.

>for ( ratioIdx = 0; ratioIdx < ARRAY_LENGTH( ConsistencyRatios ); ratioIdx++ );
...
Этот способ особенно полезен для массивов неопределенного размера, как в этом
примере. Если вы добавляете или удаляете элементы, вам не надо помнить об
изменении именованной константы, определяющей размер массива. Разумеется,
эта технология работает и с массивами заданного размера, но, используя этот
подход, вам не всегда надо будет создавать дополнительные именованные кон#
станты для объявления массивов.

12.9. Создание собственных
типов данных (псевдонимы)
Типы данных, определяемые программистом, — одна из наиболее мощ#
ных возможностей, позволяющих наиболее четко обозначить ваше
понимание программы. Они защищают программу от непредвиденных
изменений и упрощают ее прочтение, и все это — без необходимости проекти#
ровать, разрабатывать или тестировать новые классы. Если вы программируете на
C, C++ или других языках, поддерживающих такие типы, задействуйте это преиму#
щество!
Чтобы оценить возможности создания типов, представьте, что
Перекрестная ссылка Во многих
вы пишете программу для преобразования координат из сис#
случаях лучше создавать класс,
темы x, y, z в широту, долготу и высоту. Вам кажется, что могут
чем простой тип данных. Подробнее см. главу 6.
потребоваться числа с плавающей запятой двойной точнос#
ти, но пока вы абсолютно в этом не уверены, предпочитаете
писать программу, используя числа с одинарной точностью. Вы можете создать но#
вый тип данных специально для координат, применив оператор typedef в C или C++
или его эквивалент в другом языке. Вот как вы определите такой тип в C++:

Пример создания типа (C++)
typedef float Coordinate;

// для координатных переменных

Это определение объявляет новый тип Coordinate, функционально идентичный типу
float. Чтобы задействовать этот тип, вы просто объявляете с ним переменные точ#
но так же, как и с любым предопределенным типом вроде float. Пример:

304

ЧАСТЬ III

Переменные

Пример использования созданного типа (C++)
Routine1( ...
Coordinate
Coordinate
Coordinate
...
}
...

) {
latitude;
longitude;
elevation;

Routine2( ...
Coordinate
Coordinate
Coordinate
...
}

) {
x;
y;
z;

// широта в градусах
// долгота в градусах
// высота в метрах от центра Земли

// координата x в метрах
// координата y в метрах
// координата z в метрах

Здесь все переменные latitude, longitude, elevation, x, y и z объявлены с типом
Coordinate.
Теперь допустим, что программа изменилась и вы выяснили, что все#таки нужны
переменные с двойной точностью. Поскольку вы создали тип специально для
координатных данных, все, что вам нужно изменить, — это определение типа.
И сделать это вам необходимо только в одном месте — в выражении typedef. Вот
как выглядит новое определение типа:

Пример измененного определения типа (C++)
Первоначальный тип float заменен на double.

> typedef double Coordinate; // для координатных переменных
Вот еще один пример — теперь на языке Pascal. Представьте, что вы разрабатыва#
ете систему расчета заработной платы, в которой длина имен работников не пре#
вышает 30 символов. Пользователи сказали вам, что ни у кого нет имени длинней
30 символов. Закодируете ли вы число 30 по всей программе? Если да, то вы дове#
ряете вашим пользователям гораздо больше, чем я — своим. Лучший подход со#
стоит в определении типа для имен работников:

Пример создания типа для имен работников (Pascal)
Type
employeeName = array[ 1..30 ] of char;
Когда речь идет о строке или массиве, обычно разумно определить именованную
константу, содержащую длину строки или массива, а затем задействовать ее в
определении типа. Вы найдете в своей программе много мест, в которых стоит
использовать константу, и это — первое из них. Вот как это выглядит:

Пример лучшего создания типа (Pascal)
Const

ГЛАВА 12 Основные типы данных

305

Вот объявление именованной константы.

>

NAME_LENGTH = 30;
...
Type
Здесь эта именованная константа используется.

>

employeeName = array[ 1..NAME_LENGTH ] of char;
Еще более усовершенствованный пример может комбинировать идею создания
собственных типов с технологией сокрытия информации. Порой сведения, ко#
торые вы хотите скрыть, и есть информация о типе данных.
Пример с координатами на C++ частично удовлетворяет принципу сокрытия ин#
формации. Если вы всегда будете использовать Coordinate вместо float или double,
вы эффективно спрячете исходный тип данных. В C++ это практически все воз#
можное сокрытие информации, которое язык позволяет сделать разработчику. Все
последующие пользователи вашего кода должны соблюдать дисциплину и не смот#
реть на определение Coordinate. C++ предоставляет скорее фигуральную, а не бук#
вальную возможность сокрытия информации.
Другие языки, например Ada, делают шаг вперед и поддерживают буквальное со#
крытие информации. Вот как фрагмент кода для типа Coordinate будет выглядеть
в модуле Ada, где он был объявлен:

Пример сокрытия деталей реализации типа внутри модуля (Ada)
package Transformation is
Это выражение объявляет Coordinate скрытым в данном модуле.

>

type Coordinate is private;
...
Вот как тип Coordinate будет выглядеть в другом модуле, где он используется:

Пример использования типа из другого модуля (Ada)
with Transformation;
...
procedure Routine1(...) ...
latitude: Coordinate;
longitude: Coordinate;
begin
 операторы, использующие широту и долготу
...
end Routine1;
Заметьте: тип Coordinate объявлен в модуле как private. Это значит, что единственная
часть программы, которая знает определение типа Coordinate, — это закрытая часть
модуля Transformation. При групповой разработке проекта вы можете распрост#
ранить только спецификацию модуля, что затруднит программисту, работающе#
му с другим модулем, просмотр исходного типа Coordinate. Информация будет

306

ЧАСТЬ III

Переменные

буквально спрятана. Такие языки, как C++, которые требуют распространять
определение типа Coordinate в заголовочном файле, подрывают идею реального
сокрытия информации.
Следующие примеры иллюстрируют несколько причин для создания собственных
типов.
 Упростить модификацию кода

Сделать новый тип легко, а это дает вам

большую гибкость.
 Избежать излишнего распространения информации

Явная типизация
распространяет сведения о типе данных по всей программе вместо их цент#
рализации в одном месте. Это пример принципа сокрытия информации с це#
лью достижения централизации, (см. раздел 6.2).

В Ada вы можете объявлять типы как type Age is range
0..99. После этого компилятор генерирует проверки времени выполнения, чтобы
удостовериться, что значение любой переменной типа Age всегда попадает в
диапазон 0..99.

 Увеличить надежность

 Замаскировать недостатки языка

Если ваш язык не содержит необходи#
мого предопределенного типа, вы можете создать его сами. Например, в C нет
булева или логического типа. Этот недостаток легко исправить, создав тип:

typedef int Boolean;

Почему приведены примеры создания типов
на языках Pascal и Ada?
Языки Pascal и Ada сейчас подобны динозаврам, а языки, заменившие их, в основном
гораздо практичнее. Однако в области определения простых типов мне кажется,
что C++, Java и Visual Basic представляют случай трех шагов вперед и одного шага
назад. В Ada такое объявление, как:

currentTemperature: INTEGER range 0..212;
содержит важную семантическую информацию, которую объявление:

int temperature;
не содержит. Если посмотреть глубже, то определение:

type Temperature is range 0..212;
...
currentTemperature: Temperature;
позволяет компилятору удостовериться, что currentTemperature присваивается
только другим переменным типа Temperature, и такая дополнительная прослойка
безопасности требует минимального кодирования.
Естественно, программист может создать класс Temperature, чтобы реализовать те
же семантические правила, автоматически предоставляемые в Ada, но между со#
зданием простого типа данных в одну строку и созданием класса дистанция ог#
ромного размера. Зачастую программист будет использовать простой тип данных,
но не станет делать дополнительных усилий для создания класса.

ГЛАВА 12 Основные типы данных

307

Основные принципы создания собственных типов
Имейте в виду следующие принципы, когда решите создавать собственный тип.
Создавайте типы с именами, отражающими их функ'
Перекрестная ссылка В каждом
циональность Избегайте имен типов, которые ссылаются
случае следует решать, не лучна данные, лежащие в основе этих типов. Используйте имена,
ше ли использовать класс, а не
простой тип данных. Подробнее
которые отражают те элементы реальной задачи, которые
см. главу 6.
этот тип представляет. Определения из предыдущих приме#
ров — понятно названные типы для координат и имен ра#
ботников — это реальные сущности. Точно так же вы можете создавать типы для
валюты, кодов платежей, возрастов и т. д., а именно для аспектов действительно
существующих задач.
Будьте осторожны, создавая имена типов, ссылающиеся на предопределенные типы.
Такие имена, как BigInteger или LongString, описывают компьютерные данные, а не
конкретную задачу. Большое преимущество создания собственных типов данных
состоит в том, что добавляется слой, изолирующий программу от языка разработки.
Имена типов, ссылающиеся на типы языка, лежащие в их основе, нарушают эту
изоляцию. Они не дают вам большого преимущества по сравнению с примене#
нием предопределенных типов. Проблемно#ориентированные имена, с другой
стороны, облегчают процесс внесения изменений и предоставляют самодокумен#
тируемые объявления типов.
Избегайте предопределенных типов Если есть хоть малейшая возможность,
что тип может измениться, избегайте применения предопределенных типов вез#
де, кроме определений typedef или type. Легко создать новые функционально#ори#
ентированные типы — менять же данные в программе, использующей жестко за#
кодированные типы, гораздо сложней. Более того, функционально#ориентирован#
ные типы частично документируют объявленные с ними переменные. Объявле#
ние Coordinate x сообщит вам об x гораздо больше, чем объявление float x. Исполь#
зуйте собственные типы везде, где только можно.
Не переопределяйте предопределенные типы Изменение определения стан#
дартного типа может вызвать путаницу. Например, если в вашем языке есть пре#
допределенный тип Integer, не создавайте свой тип с именем Integer. Читающие
ваш код могут забыть, что вы его переопределили, и будут считать, что видят тот
же Integer, который привыкли видеть.
Определите подстановки для переносимости В отличие от совета не изме#
нять определение стандартных типов вы можете создать для этих типов подста#
новки, так что на разных платформах переменные будут представлены одними и
теми же сущностями. Так, вы можете определить тип INT32 и использовать его
вместо int или тип LONG64 вместо long. Изначально единственной разницей между
двумя типами будет применение заглавных букв. Но при переходе на другую плат#
форму вы сможете переопределить варианты с большими буквами так, чтобы они
совпадали с типами для данных аппаратных средств.
Не создавайте типы, которые легко перепутать с предопределенными. Существу#
ет возможность определить INT вместо INT32, но лучше сделать явное различие
между типами, созданными вами, и типами, предоставленными языком програм#
мирования.

308

ЧАСТЬ III

Переменные

Рассмотрите вопрос создания класса вместо использования typedef Прос#
тые операторы typedef позволяют проделать большой путь в сторону сокрытия ин#
формации об исходном типе переменной. Однако иногда вам может потребоваться
дополнительная гибкость и управляемость, которой позволяют добиться классы.
Подробнее см. главу 6.

http://cc2e.com/1206

Перекрестная ссылка Список
вопросов, затрагивающих данные вообще, без подразделения
на конкретные типы, см. в контрольном списке главы 10. Список вопросов по вариантам
именования см. в контрольном
списке главы 11.

Контрольный список: основные данные
Числа в общем
 Не содержит ли код магические числа?
 Предупреждаются ли в коде ошибки деления на ноль?
 Очевидны ли преобразования типов?
 Если переменные двух разных типов используются в одном выражении, будет ли оно вычислено так, как вы это
предполагаете?
 Не происходит ли сравнение переменных разных типов?
 Компилируется ли программа без предупреждений компилятора?

Целые числа
 Работают ли выражения, содержащие целочисленное деление так, как это
предполагалось?
 Предупреждаются ли в целочисленных выражениях проблемы целочисленного переполнения?
Числа с плавающей запятой
 Не содержит ли код операции сложения и вычитания слишком разных по
величине чисел?
 Предупреждаются ли в коде ошибки округления?
 Не выполняется сравнение на равенство чисел с плавающей запятой?
Символы и строки
 Не содержит ли код магических символов и строк?
 Свободны ли операции со строками от ошибки потери единицы?
 Различаются ли в коде на C строковые указатели и массивы символов?
 Соблюдается ли в коде на C соглашение об объявлении строк с длиной
CONSTANT+1?
 Используются ли в C массивы символов вместо указателей там, где это
допустимо?
 Инициализируются ли в C строки с помощью NULL во избежание бесконечных строк?
 Используются ли в коде на C strncpy() вместо strcpy()? А strncat() и strncmp()?
Логические переменные
 Используются ли в программе дополнительные логические переменные для
документирования проверок условия?
 Используются ли в программе дополнительные логические переменные для
упрощения проверок условия?

ГЛАВА 12 Основные типы данных

309

Перечислимые типы
 Используются ли в программе перечислимые типы вместо именованных
констант ради их улучшенной читабельности, надежности и модифицируемости?
 Используются ли перечислимые типы вместо логических переменных, если
все значения переменной не могут быть переданы с помощью true и false?
 Проверяются ли некорректные значения перечислимых типов в условных
операторах?
 Зарезервирован ли первый элемент перечислимого типа как недопустимый?
Перечислимые константы
 Используются ли в программе именованные константы вместо магических
чисел для объявления данных и границ циклов?
 Используются ли именованные константы последовательно, чтобы одно
значение не представлялось в одном месте константой, а в другом — литералом?
Массивы
 Находятся ли все индексы массива в его границах?
 Свободны ли ссылки на массив от ошибок потери единицы?
 Указаны ли все индексы многомерных массивов в правильном порядке?
 В правильном ли порядке используются переменные-индексы во вложенных
циклах, не происходит ли пересечения индексов?
Создание типов
 Используются ли в программе отдельные типы для каждого вида данных,
который может измениться?
 Ориентируются ли имена типов на реальные сущности, которые эти типы
представляют, а не на типы языка программирования?
 Достаточно ли наглядны имена типов, чтобы помочь документированию
объявлений данных?
 Не произошло ли переопределение предопределенных типов?
 Рассматривался ли вопрос создания нового класса вместо простого переопределения типа?

Ключевые моменты
 Работа с определенными типами данных требует запоминания множества

правил для каждого из них. Используйте список контрольных вопросов из этой
главы, чтобы убедиться, что вы учли основные проблемы с ними.
 Создание собственных типов, если ваш язык это позволяет, упрощает модифи#

кацию вашей программы и делает ее более самодокументируемой.
 Прежде чем создавать простой тип с помощью typedef или его эквивалента,

подумайте, не следует ли создать вместо него новый класс.

310

ЧАСТЬ III

Г Л А В А

Переменные

1 3

Нестандартные типы данных

http://cc2e.com/1378

Содержание
 13.1. Структуры
 13.2. Указатели
 13.3. Глобальные данные

Связанные темы
 Фундаментальные типы данных: глава 12
 Защитное программирование: глава 8
 Нестандартные управляющие структуры: глава 17
 Сложность в разработке ПО: раздел 5.2

Некоторые языки программирования поддерживают экзотические виды данных
в дополнение к типам, обсуждавшимся в главе 12. В разделе 13.1 рассказывается,
при каких обстоятельствах вы могли бы использовать структуры вместо классов.
В разделе 13.2 описываются детали использования указателей. Если у вас возни#
кали проблемы с использованием глобальных данных, из раздела 13.3 вы узнае#
те, как их избежать. Если вы думаете, что типы данных, описанные в этой главе,
— это не те типы, о которых вы обычно читаете в современных книгах по объек#
тно#ориентированному программированию, то вы абсолютно правы. Поэтому эта
глава и называется «Нестандартные типы данных».

13.1. Структуры
Термин «структура» относится к типу данных, построенному на основе других
типов. Так как массивы — особый случай, они рассматриваются отдельно в главе
12. В этом разделе обсуждаются структурированные данные, созданные пользо#
вателем: structs в C/C++ и Structures в Microsoft Visual Basic. В Java и C++ классы тоже
иногда выглядят, как структуры (когда они состоят только из открытых членов
данных и не содержат открытые методы).
Чаще всего вы предпочтете создавать классы, а не структуры, чтобы задейство#
вать преимущества закрытости и функциональности, предлагаемой классами, в

ГЛАВА 13 Нестандартные типы данных

311

дополнение к открытым данным, поддерживаемым структурами. Но иногда пря#
мое манипулирование блоками данных может быть полезно.
Используйте структуры для прояснения взаимоотношений между дан'
ными Структуры объединяют группы взаимосвязанных элементов. Иногда труднее
всего понять, какие данные в программе используются вместе. Это похоже на
прогулку по маленькому городку с вопросами о том, кто с кем в родстве. Вы вы#
ясняете, что каждый кому#то кем#то приходится, но вы не уверены в этом, и ни#
когда не можете получить внятного ответа.
Если данные хорошо структурированы, выяснение, что с чем связано, сильно
упрощается. Вот пример данных, которые не были структурированы:

Пример неструктурированных, вводящих в заблуждение переменных (Visual Basic)
name = inputName
address = inputAddress
phone = inputPhone
title = inputTitle
department = inputDepartment
bonus = inputBonus
Так как данные не структурированы, кажется, что эти операторы присваивания
объединены. На самом деле, переменные name, address, и phone относятся кря#
довому служащему, а title, department и bonus — к менеджеру. В этом фрагменте
нет подсказок о том, что используется два вида данных. В следующем фрагменте
применение структур делает взаимоотношения яснее:

Пример более информативных, структурированных переменных (Visual Basic)
employee.name = inputName
employee.address = inputAddress
employee.phone = inputPhone
supervisor.title = inputTitle
supervisor.department = inputDepartment
supervisor.bonus = inputBonus
В этом коде, содержащем структурированные переменные, очевидно, что часть
данных относится к работнику, а остальные — к менеджеру.
Используйте структуры для упрощения операций с блоками данных Вы
можете объединить взаимосвязанные элементы в структуру и выполнять опера#
ции над ней. Проще обрабатывать структуру целиком, чем выполнять те же дей#
ствия над каждым элементом. Это надежней и требует меньше строк кода.
Допустим, у вас есть группа взаимосвязанных элементов данных — скажем, ин#
формация о работнике в базе данных персонала. Если данные не объединены в
структуру, простое копирование всей группы может потребовать большого чис#
ла операторов. Вот пример на Visual Basic:

312

ЧАСТЬ III

Переменные

Пример громоздкого копирования группы (Visual Basic)
newName = oldName
newAddress = oldAddress
newPhone = oldPhone
newSsn = oldSsn
newGender = oldGender
newSalary = oldSalary
Каждый раз, желая передать сведения о работнике, вам приходится иметь дело со
всеми этими операторами. Если вам нужно добавить новый элемент данных, на#
пример numWithholdings , вам придется найти все места, где написаны эти при#
сваивания, и добавить еще одно: newNumWithholdings = oldNumWithholdings.
Представьте, как ужасно будет менять местами данные о двух работниках. Вам не
надо напрягать воображение — вот пример:

Пример утомительного способа
обмена двух групп данных (Visual Basic)
‘ Поменять местами старые и новые данные о работнике.
previousOldName = oldName
previousOldAddress = oldAddress
previousOldPhone = oldPhone
previousOldSsn = oldSsn
previousOldGender = oldGender
previousOldSalary = oldSalary
oldName = newName
oldAddress = newAddress
oldPhone = newPhone
oldSsn = newSsn
oldGender = newGender
oldSalary = newSalary
newName = previousOldName
newAddress = previousOldAddress
newPhone = previousOldPhone
newSsn = previousOldSsn
newGender = previousOldGender
newSalary = previousOldSalary
Более легкое решение проблемы — объявить структурную переменную:

Пример объявления структуры (Visual Basic)
Structure Employee
name As String
address As String
phone As String
ssn As String
gender As String
salary As long

ГЛАВА 13 Нестандартные типы данных

End
Dim
Dim
Dim

313

Structure
newEmployee As Employee
oldEmployee As Employee
previousOldEmployee As Employee

Теперь вы можете поменять элементы в старой и новой структуре с помощью трех
операторов:

Пример более легкого способа обмена двух групп данных (Visual Basic)
previousOldEmployee = oldEmployee
oldEmployee = newEmployee
newEmployee = previousOldEmployee
Если вы хотите добавить новое поле, например numWithholdings, вы просто вно#
сите его в объявление Structure. Ни одно из вышеприведенных выражений не
потребует никаких изменений. C++ и другие языки также содержат подобные
возможности.
Используйте структуры для упрощения списка па'
Перекрестная ссылка О том, как
раметров Сократить список параметров метода позволя#
распределять данные между
ют структурированные переменные. Технология похожа на
методами, см. подраздел «Поддерживайте сопряжение слатолько что продемонстрированную. Вместо того чтобы пе#
бым» раздела 5.3.
редавать параметры по одному, можно объединить взаимо#
связанные элементы в структуру и передать все скопом. Вот
пример утомительного способа передачи группы общих параметров:

Пример громоздкого вызова метода, не использующего структуру (Visual Basic)
HardWayRoutine( name, address, phone, ssn, gender, salary )
А вот пример простого способа вызвать метод с помощью структурированной
переменной, состоящей из параметров первого метода:

Пример элегантного вызова метода, использующего структуру (Visual Basic)
EasyWayRoutine( employee )
Если вы хотите добавить numWithholdings к первому варианту программы, вам
придется прочесать весь код и изменить каждый вызов HardWayRoutine(). Если же
вы добавите элемент к структуре Employee, вам совсем не придется изменять па#
раметры вызова EasyWayRoutine().
Вы можете довести эту идею до крайности, поместив все
Перекрестная ссылка Об опаспеременные вашей программы в одну большую, жирную
ностях передачи слишком больструктуру и передавая ее всюду. Аккуратные программисты
шого объема данных см. подраздел «Поддерживайте сопряизбегают объединения большего количества данных, чем это
жение слабым» раздела 5.3.
необходимо логически. Более того, аккуратисты стараются
не передавать параметры в виде структур, если нужны лишь
одно#два поля из этой структуры — в этом случае передаются указанные поля. Это
аспект вопроса о сокрытии информации: часть данных скрыта в методе, а часть
— от метода. Между методами должна передаваться только та информация, ко#
торую необходимо знать.

314

ЧАСТЬ III

Переменные

Используйте структуры для упрощения сопровождения Так как, приме#
няя структуры, вы группируете взаимосвязанные данные, изменение структуры тре#
бует минимальных исправлений в программе. В большей степени это относится
к участкам кода, не связанным с вносимым изменением логически. Поскольку
изменения часто приводят к ошибкам, то чем меньше изменений, тем меньше
ошибок. Если в структуре Employee есть поле title и вы решаете его удалить, вам
не нужно исправлять ни списки параметров, ни операторы присваивания, исполь#
зующие эту структуру целиком. Конечно, вам придется поправить код, напрямую
работающий со званиями работников, но эти действия связаны с процессом уда#
ления поля title концептуально, и поэтому вы вряд ли о них забудете.
На участках кода, логически не связанных с полем title, преимущество структури#
рования данных еще более очевидно. Иногда программы содержат выражения,
концептуально работающие скорее с набором данных, а не с отдельными компо#
нентами. В этих случаях отдельные элементы, такие как поле title, упоминаются
только потому, что они — часть набора. Эти участки кода не имеют логических
причин работать конкретно с полем title, поэтому при изменении title такие уча#
стки очень легко пропустить. Если же вы используете структуру, все будет нор#
мально, потому что код ссылается на набор взаимосвязанных данных, а не на
каждый элемент индивидуально.

13.2. Указатели
Использование указателей — одна из наиболее подверженных ошибкам
областей программирования. Это привело к тому, что современные язы#
ки, такие как Java, C# и Visual Basic, не предоставляют указатель в каче#
стве типа данных. Применять указатели и так сложно, а правильное применение
требует от вас отличного понимания того, как ваш компилятор управляет распре#
делением памяти. Многие общие проблемы с безопасностью, особенно случаи пе#
реполнения буфера, могут быть сведены к ошибочному использованию указате#
лей. (Howard and LeBlanc, 2003).
Даже если в вашем языке не нужны указатели, их хорошее понимание поможет
вам разобраться, как работает ваш язык программирования. А щедрая доза защит#
ного программирования будет еще полезнее.

Парадигма для понимания указателей
Концептуально каждый указатель состоит из двух частей: области памяти и зна#
ния, как следует интерпретировать содержимое этой области.

Область памяти
Область памяти — это адрес, часто представленный в шестнадцатеричном виде.
В 32#разрядном процессоре адрес будет 32#битным числом, например 0x0001EA40.
Сам по себе указатель содержит только этот адрес. Чтобы обратиться к данным,
на которые этот указатель указывает, надо пойти по этому адресу и как#то интер#
претировать содержимое памяти в этой области. Сам по себе этот участок памя#
ти — просто набор битов. Чтобы он обрел смысл, его надо как#то истолковать.

ГЛАВА 13 Нестандартные типы данных

315

Знание, как интерпретировать содержимое
Информация о том, как интерпретировать содержимое области памяти, предо#
ставляется основным типом указателя. Если указатель указывает на целое число,
то на самом деле это значит, что компилятор интерпретирует область памяти, за#
даваемую указателем, как целое число. Конечно, у вас может быть указатель на целое
число, строку или число с плавающей точкой, которые ссылаются на одну и ту
же область памяти. Но только один из них будет корректно интерпретировать
содержимое этой области.
Говоря об указателях, полезно помнить, что память сама по себе не имеет одно#
значной интерпретации. И только с помощью конкретного типа указателя набор
битов в некоторой области памяти истолковывается как осмысленное значение.
Рис. 13#1. содержит несколько представлений одной и той же области памяти,
интерпретированной разными способами.

Рис. 13'1. Объем памяти, используемый каждым типом данных,
показан двойной линией

В каждом случае на рис. 13#1 указатель ссылается на область памяти, содержащую
шестнадцатеричное значение 0x0A. Количество байт, используемых после 0A, за#
висит от того, как память интерпретируется. Содержимое памяти также зависит
от ее интерпретации. (Еще содержимое зависит и от используемого процессора,
так что не забудьте об этом, если будете пытаться повторить эти результаты на
вашем Cray#десктопе.) Одна и та же необработанная область памяти может быть
представлена как строка, целое число, число с плавающей точкой или иначе —
все зависит от основного типа указателя, ссылающегося на эту область памяти.

316

ЧАСТЬ III

Переменные

Основные советы по использованию указателей
Обычно ошибку легко найти, но трудно исправить. Но это не относится к про#
блемам с указателями. Ошибка в указателе — чаще всего результат того, что он
указывает не туда, куда должен. Когда вы присваиваете значение некорректной
переменной#указателю, вы записываете данные туда, куда не должны записывать.
Это называется «повреждением памяти». Порой оно приводит к ужасным круше#
ниям системы, порой изменяет результаты вычислений в другой части програм#
мы, порой вынуждает программу неожиданно завершить работу метода, а порой
вообще ничего не делает. В последнем случае ошибка в указателе как бомба с
часовым механизмом — она разрушит вашу программу за пять минут до ее пока#
за самому главному заказчику. По симптомам таких ошибок тяжело понять, что
их вызывало. Поэтому самым трудоемким в процессе исправления ошибок указа#
теля является поиск их причины.
Для успешной работы с указателями требуется двухэтапная стратегия.
Во#первых, старайтесь изначально не делать в них ошибок. Проблемы
с указателями так сложно обнаружить, что дополнительные превентив#
ные меры вполне оправданны. Во#вторых, выявляйте ошибки в указателях как
можно быстрее после того, как они закодированы. Симптомы ошибок в указате#
лях настолько изменчивы, что дополнительные меры с целью сделать эти симп#
томы более предсказуемыми, также вполне оправданны. Вот как можно добиться
этих ключевых целей.
Изолируйте операции с указателями в методах или классах Допустим, в
нескольких частях программы используется связный список. Вместо того чтобы
каждый раз обрабатывать его вручную, напишите методы доступа NextLink(), Pre%
viousLink(), InsertLink() и DeleteLink(). Минимизировав количество мест, в которых
выполняется обращение к указателю, вы уменьшите вероятность неосторожных
ошибок, распространяющихся по всей программе, на поиск которых уходит веч#
ность. Поскольку такой код становится относительно независимым от деталей пред#
ставления данных, вы также увеличиваете шансы его повторного использования
в других программах. Написание методов, распределяющих память для указате#
лей, — еще один способ централизовать управление вашими данными.
Выполняйте объявление и определение указателей одновременно При#
своение переменной начального значения рядом с местом ее объявления — как
правило, хорошая практика программирования. Она обладает особой ценностью
при работе с указателями. Вот как не надо делать:

Пример неправильной инициализации
указателя (C++)
Employee *employeePtr;
// много кода
...
employeePtr = new Employee;
Даже если этот код изначально работает правильно, при дальнейших модифика#
циях он подвержен ошибкам, так как существует шанс, что кто#нибудь попробует

ГЛАВА 13 Нестандартные типы данных

317

использовать employeePtr после его объявления, но до инициализации. Вот более
безопасный подход:

Пример правильной инициализации указателя (C++)
// много кода
...
Employee *employeePtr = new Employee;
Удаляйте указатели в той же области действия, где они были созданы
Соблюдайте симметрию при выделении и освобождении памяти для указателей.
Если вы используете указатель в единственном блоке кода, вызывайте new для
выделения памяти и deletе для ее освобождения в том же блоке. Если вы распре#
деляете память внутри метода, освобождайте ее внутри аналогичного метода, а если
в конструкторе объекта — освобождайте в деструкторе этого объекта. Метод,
выделяющий память для указателя, а затем ожидающий, что клиентский код вручную
его освободит, нарушает целостность, что прямиком ведет к ошибкам.
Проверяйте указатели перед их применением Прежде чем использовать ука#
затель в критической части вашей программы, удостоверьтесь, что он указывает
на осмысленную область памяти. Так, если вы ожидаете, что память распределя#
ется между адресами StartData и EndData, у вас должно вызывать подозрение, если
значение указателя меньше, чем StartData, или больше, чем EndData. Вам надо
определить значения StartData и EndData в вашей системе. Эту проверку можно
выполнять автоматически, если обращаться к указателям не напрямую, а через
методы доступа.
Проверяйте переменную, на которую ссылается указатель, перед ее ис'
пользованием Иногда вы можете выполнить корректную проверку значения, на
которое ссылается указатель. Скажем, если вы предполагаете, что он указывает на
целое число от 0 до 1000, значения больше 1000 должны вызывать у вас подозре#
ние. Если указатель ссылается на строку в стиле C++, ее длина свыше 100 симво#
лов также может вызывать недоверие. Эти проверки тоже могут быть выполнены
автоматически при работе с указателями с помощью методов доступа.
Используйте закрепленные признаки для проверки повреждения памяти
«Поле#тэг» (tag field) или «закрепленный признак» (dog tag) — это поле, которое
вы добавляете к структуре исключительно с целью проверки ошибок. Когда вы
выделяете память для переменной, поместите в это поле закрепленного призна#
ка значение, которое должно остаться неизменным. Используя структуру, особенно
освобождая для нее память, проверяйте значение закрепленного признака. Если
это поле не содержит ожидаемого значения, значит, данные были повреждены.
Удаляя указатель, измените значение этого поля. Так вы сможете выявить ошибку,
если случайно попытаетесь освободить этот указатель еще раз. Например, пусть
нам нужно выделить 100 байт памяти:
1. Выделите 104 байта — на 4 байта больше, чем требуется.

318

ЧАСТЬ III

Переменные

2. Укажите в первых четырех байтах значение обязательного признака, а затем
верните указатель на область, следующую за этими четырьмя байтами.

3. Когда понадобится удалить указатель, проверьте значение признака.

4. Если значение признака корректно, присвойте ему 0 или другое значение,
которое ваша программа будет считать недопустимым. Главное, чтобы его
ошибочно не посчитали корректным после освобождения памяти. С той же
целью заполните всю область памяти 0, 0xCC или любым другим неслучайным
значением.
5. В заключение удалите указатель.

Размещение закрепленного признака в начале выделенного блока памяти помо#
жет выявить попытки повторного освобождения блока. При этом вам не нужно
поддерживать список используемых областей памяти. Размещение признака в конце
блока позволит выявить попытки записи за допустимые границы области памя#
ти. Для достижения обеих целей закрепленные признаки можно размещать и в
начале, и в конце блока.
Вы можете использовать этот подход и для дополнительных проверок, предло#
женных ранее, — того, что значение указателя должно быть между адресами Start%
Data и EndData. Однако, чтобы убедиться, что указатель содержит корректный адрес,
вместо проверки возможного диапазона адресов следует проверять наличие это#
го указателя в списке используемых областей памяти.
Если вы проверите поле признака один раз — перед удалением переменной, то
некорректный признак будет означать, что когда#то на протяжении жизни пере#
менной ее содержимое было повреждено. Но чем чаще вы будете проверять этот
признак, тем ближе к источнику проблемы будет обнаружено повреждение.
Добавьте явную избыточность Альтернативой полю признака будет исполь#
зование двух таких полей. Если данные в избыточных полях не совпадают, вы
знаете, что память была повреждена. Этот способ может потребовать большого
количества дополнительного кода, если напрямую манипулировать указателями.
Но если работу с указателями изолировать в методах, то дублировать код придет#
ся лишь в нескольких местах.
Используйте для ясности дополнительные переменные указателей Ни#
когда не экономьте на переменных#указателях. Одну и ту же переменную нельзя
вызвать для разных целей. Особенно это касается переменных#указателей. Довольно
тяжело выяснить, какие действия выполняются со связным списком и без того,
чтобы разбираться, почему одна переменная genericLink используется снова и снова
и куда указывает pointer%>next%>last%>next. Рассмотрим фрагмент:

ГЛАВА 13 Нестандартные типы данных

319

Пример кода традиционной вставки в список нового узла (C++)
void InsertLink(
Node *currentNode,
Node *insertNode
) {
// добавляем “insertNode” после “currentNode”
insertNode>next = currentNode>next;
insertNode>previous = currentNode;
if ( currentNode>next != NULL ) {
Эта строка излишне сложна.

>

currentNode>next>previous = insertNode;
}
currentNode>next = insertNode;
}
Этот традиционный код добавления нового узла в связный список излишне сло#
жен для понимания. В добавлении элемента задействованы три объекта: текущий
узел, узел, в данный момент следующий за текущим, и узел, который надо вста#
вить между ними. Однако в коде явно упомянуты только два объекта: insertNode и
currentNode. Из#за этого вам придется запомнить, что currentNode%>next тоже уча#
ствует в алгоритме. Если вы попробуете изобразить диаграммой, что происходит,
не используя элемент, изначально следующий за currentNode, у вас получится что#
то вроде этого:

Гораздо лучшая диаграмма содержит все три объекта. Она может выглядеть так:

Вот пример кода, который явно упоминает все три объекта, участвующих в алго#
ритме:

Пример более читабельного кода для вставки узла (C++)
void InsertLink(
Node *startNode,
Node *newMiddleNode
) {
// вставляем “newMiddleNode” между “startNode” и “followingNode”
Node *followingNode = startNode>next;
newMiddleNode>next = followingNode;
newMiddleNode>previous = startNode;
if ( followingNode != NULL ) {
followingNode>previous = newMiddleNode;
}
startNode>next = newMiddleNode;
}
Этот код содержит одну дополнительную строку, но без участия выражения current%
Node%>next%>previous из первого фрагмента этот пример легче для понимания.

320

ЧАСТЬ III

Переменные

Упрощайте сложные выражения с указателями Сложные выражения с ис#
пользованием указателей тяжело читать. Если в вашем коде есть выражения вро#
де p%>q%>r%>s.data, подумайте о том человеке, которому придется это читать. Вот
особенно вопиющий пример:

Пример сложного для понимания
выражения с указателем (C++)
for ( rateIndex = 0; rateIndex < numRates; rateIndex++ ) {
netRate[ rateIndex ] = baseRate[ rateIndex ] * rates>discounts>factors>net;
}
Подобные выражения заставляют разбираться в коде, а не читать его. Если в ва#
шей программе есть сложное выражение, присвойте его понятно названной пе#
ременной, чтобы прояснить смысл операции. Вот улучшенная версия примера:

Пример упрощения сложного выражения с указателем (C++)
quantityDiscount = rates>discounts>factors>net;
for ( rateIndex = 0; rateIndex < numRates; rateIndex++ ) {
netRate[ rateIndex ] = baseRate[ rateIndex ] * quantityDiscount;
}
Это упрощение не только позволяет увеличить удобочитаемость, но, возможно, и
повысить производительность, упростив операцию с указателем внутри цикла. Но,
как обычно, улучшение производительности надо измерить
Перекрестная ссылка Такие
до того, как делать на это крупные ставки.
диаграммы, как на рис. 13-2,
могут стать частью внешней
документации вашей программы. О хорошей практике документирования см. главу 32.

Нарисуйте картинку Описание указателей в коде про#
граммы может сбивать с толку. Обычно помогает картинка.
Например, изображение задачи по вставке элемента в связ#
ный список может выглядеть так (рис. 13#2):

Рис. 13'2. Пример рисунка, помогающего осмыслить шаги, необходимые
для изменения связей между элементами

Удаляйте указатели в связных списках в правильном порядке Обычной
проблемой в работе с динамически созданными связными списками является
освобождение сначала первого указателя, после чего становится невозможно

ГЛАВА 13 Нестандартные типы данных

321

получить указатель на следующий узел списка. Чтобы избежать этой проблемы,
перед удалением текущего элемента убедитесь, что у вас есть указатель на следу#
ющий элемент списка.
Выделите «запасной парашют» памяти Если в программе используется ди#
намическая память, необходимо избежать проблемы ее внезапной нехватки, при#
водящей к исчезновению пользовательских данных на бескрайних просторах
оперативной памяти. Один из способов дать вашей программе запас прочности
— заранее выделить «парашют» памяти. Определите, какой объем памяти нужен
программе для сохранения работы, освобождения ресурсов и аккуратного завер#
шения. Зарезервируйте эту память в начале работы программы как запасной па#
рашют и оставьте ее в покое. Когда памяти станет не хватать, раскройте резерв#
ный парашют — освободите эту память и завершите работу программы.
Уничтожайте мусор Ошибки указателей сложно отсле#
живать, потому что момент времени, когда память, адресу#
емая указателем, станет недействительной, не определен.
Иногда содержимое памяти после освобождения указателя
долго еще выглядит корректным. В другой раз ее содержи#
мое изменится сразу.

Дополнительные сведения Отличное обсуждение безопасных
подходов к обработке указателей в языке C см. в книге «Writing Solid Code» (Maguire, 1993).

Вы можете избежать ошибок с освобожденными указателями, записывая мусор в
блоки памяти прямо перед их освобождением. Если вы используете методы дос#
тупа, то это, как и многие другие операции, можно делать автоматически. В C++
при каждом удалении указателя можно делать так:

Пример принудительной записи мусорных данных в освобождаемую память (C++)
memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) );
delete pointer;
Естественно, эта технология требует поддержки списка размеров памяти, выде#
ленной для указателей, которые можно было бы получить функцией MemoryBlock%
Size(). Мы обсудим это позднее.
Устанавливайте указатели null при их удалении или освобождении Из#
вестный тип ошибок указателей — это «висячий указатель» (dangling pointer), т. е.
обращение к нему после вызова функций delete или free. Одна из причин, по ко#
торым ошибки в указателях так сложно обнаружить, в том, что иногда симптомы
ошибки никак не проявляются. Записывая в указатели пустое значение после их
освобождения, вы не измените факт чтения данных, адресуемых висячим указа#
телем. Но вы добьетесь того, что запись данных по этому адресу приведет к ошибке.
Возможно, это будет ужасная, катастрофическая ошибка, но по крайней мере ее
обнаружите вы, а не кто#то другой.
Код, предшествующий операции delete в предыдущем примере, можно дополнить,
чтобы обрабатывать и эту ситуацию:

Пример установки указателя в null после его удаления (C++)
memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) );
delete pointer;
pointer = NULL;

322

ЧАСТЬ III

Переменные

Проверяйте корректность указателя перед его удалением Один из луч#
ших способов обрушить программу — вызвать функции delete() или free() для ука#
зателя, который уже был освобожден. Увы, лишь немногие языки обнаруживают
такой тип ошибок.
Если вы присваиваете освобождаемым указателям пустое значение, то перед ис#
пользованием или повторным удалением указателя вы сможете проверить его на
равенство null. Разумеется, если вы не устанавливаете в null освобождаемые ука#
затели, у вас такой возможности не будет. В связи с этим можно предложить сле#
дующее дополнение к коду удаления указателя:

Пример проверки утверждения о неравенстве
указателя null перед его удалением (C++)
ASSERT( pointer != NULL, “Attempting to delete null pointer.” );
memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) );
delete pointer;
pointer = NULL;
Отслеживайте распределение памяти для указателей Ведите список ука#
зателей, для которых была выделена память. Это позволит вам проверить, нахо#
дится ли указатель в этом списке перед его освобождением. Вот как для этих це#
лей может быть изменен код удаления указателя:

Пример проверки, выделялась ли память для указателя (C++)
ASSERT( pointer != NULL, “Attempting to delete null pointer.” );
if ( IsPointerInList( pointer ) ) {
memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) );
RemovePointerFromList( pointer );
delete pointer;
pointer = NULL;
}
else {
ASSERT( FALSE, “Attempting to delete unallocated pointer.” );
}
Напишите методы'оболочки, чтобы централизовать стратегию борь'
бы с ошибками в указателях Как видно из этого примера, каждый вызов опе#
раторов new и delete может сопровождаться достаточно большим количеством
дополнительного кода. Некоторые технологии, описанные в этом разделе, явля#
ются взаимоисключающими или избыточными, и не хотелось бы использовать
несколько конфликтующих стратегий в одной программе. Например, вам не надо
создавать и проверять обязательные признаки, если вы поддерживаете собствен#
ный список действительных указателей.
Вы можете минимизировать избыточность в программе и уменьшить вероятность
ошибок, написав методы#оболочки для общих операций с указателями. В C++ вы
могли бы использовать следующие методы:
Вызывает new для выделения памяти, добавляет указатель в спи#
сок задействованных указателей и возвращает вновь созданный указатель вы#

 SAFE_NEW

ГЛАВА 13 Нестандартные типы данных

323

зывающей стороне. Он может также проверить, что оператор new не вернул
null (ошибка нехватки памяти). Поскольку это надо сделать только единожды
в этом месте, упрощается процесс обработки ошибок в других частях вашей
программы.
 SAFE_DELETE

Проверяет, находится ли переданный ему указатель в списке
действительных указателей. Если он там есть, метод записывает мусор в адре#
суемую им память, удаляет указатель из списка, вызывает C++#оператор delete
для освобождения памяти и устанавливает указатель в null. Если указатель не
найден в списке, SAFE_DELETE выводит диагностическое сообщение и преры#
вает программу.

Метод SAFE_DELETE, реализованный в виде макроса, может выглядеть так:

Пример добавления оболочки для кода удаления указателя (C++)
#define SAFE_DELETE( pointer ) { \
ASSERT( pointer != NULL, “Attempting to delete null pointer.”); \
if ( IsPointerInList( pointer ) ) { \
memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) ); \
RemovePointerFromList( pointer ); \
delete pointer; \
pointer = NULL; \
} \
else { \
ASSERT( FALSE, “Attempting to delete unallocated pointer.” ); \
} \
}
В C++ этот метод будет освобождать единичные указатели,
поэтому вам придется создать похожий макрос SAFE_DELE%
TE_ARRAY для удаления массивов.

Перекрестная ссылка О планах
по удалению отладочного кода
см. подраздел «Запланируйте
удаление отладочных средств»
раздела 8.6.

Централизовав управление памятью в этих двух методах, вы
также сможете менять поведение SAFE_NEW и SAFE_DELETE
в отладочной и промышленных версиях продукта. Напри#
мер, обнаружив попытку освободить пустой указатель в период разработки, SAFE_
DELETE может остановить программу. Но если это происходит во время эксплуа#
тации, он может просто записать ошибку в журнал и продолжить выполнение.
Вы легко сможете адаптировать эту схему для функций calloc и free в языке C,
а также для других языков, использующих указатели.

Используйте технологию, не основанную на указателях Указатели в це#
лом сложнее для понимания, они подвержены ошибкам и приводят к созданию
машинно#зависимого, непереносимого кода. Если вы можете придумать разумную
альтернативу указателям, избавьте себя от головной боли и возьмите ее за основу.

Указатели в C++
Язык C++ добавил специфические тонкости при работе с указателями и ссылка#
ми. В следующих подразделах описаны основные принципы, применяемые в ра#
боте с указателями на C++.

324

ЧАСТЬ III

Переменные

Осознайте различие между указателями и ссылка'
ми В C++ и указатели (*), и ссылки (&) косвенно ссылают#
ся на объект. Для непосвященных единственной, чисто кос#
метической разницей между ними будет способ обращения
к полю: object%>field или object.field. Наиболее значительным
различием является то, что ссылка обязана всегда ссылаться
на объект, тогда как указатель может быть равен null. Кроме
того, после инициализации ссылки нельзя изменить то, куда она ссылается.

Дополнительные сведения Множество других советов по применению указателей в C++ см.
в «Effective C++», 2d ed. (Meyers,
1998) и «More Effective C++»
(Meyers, 1996).

Используйте указатели для передачи параметров «по ссылке» и констан'
тные ссылки для передачи параметров «по значению» По умолчанию C++
передает в методы аргументы по значению, а не по ссылке. Когда объект переда#
ется по значению, C++ создает копию объекта, и при передаче объекта вызываю#
щей программе вновь создается копия. Для больших объектов такое копирование
может съедать много времени и ресурсов. Следовательно, при передаче объектов
в метод вы обычно стараетесь избегать копирования объектов, а это означает, что
вы хотите передавать их по ссылке, а не по значению.
Однако иногда хотелось бы использовать семантику передачи по значению (т. е.
передаваемый объект должен остаться неизменным) и реализацию передачи па#
раметра по ссылке (т. е. передавать сам объект, а не его копию).
В C++ решением этой проблемы является применение указателей для передачи
по ссылке, и — как ни странно может звучать — «константных ссылок» для пе#
редачи по значению! Приведем пример:

Пример передачи параметров по значению и по ссылке (C++)
void SomeRoutine(
const LARGE_OBJECT &nonmodifiableObject,
LARGE_OBJECT *modifiableObject
);
Дополнительным преимуществом этого подхода является синтаксическое разли#
чие между изменяемыми и неизменяемыми объектами в вызванном методе.
В изменяемых объектах ссылка на элементы будет осуществляться с помощью но#
тации object%>member, тогда как в неизменяемых будет использоваться нотация
object . member.
Недостаток этого подхода состоит в необходимости постоянного применения кон#
стантных ссылок. Хорошим тоном считается использование модификатора const
везде, где это возможно (Meyers, 1998). Поэтому в своем коде вы сможете объяв#
лять передаваемые по значению параметры как константные ссылки. В библио#
течном коде и других неподконтрольных вам местах вы столкнетесь с проблемой
константных параметров. Компромиссной позицией будет все же задавать пара#
метры, предназначенные только для чтения, с помощью ссылок, но не объявлять
их константными. При этом подходе вы не в полной мере реализуете преимуще#
ство проверки компилятором попыток модификации неизменяемых аргументов
метода, однако по крайней мере предоставляете возможность визуального разли#
чения object%>member и object . member.

ГЛАВА 13 Нестандартные типы данных

325

Используйте автоматические указатели auto_ptr Если вы еще не вырабо#
тали привычку использовать указатели auto_ptr, займитесь этим! Удаляя занятую
память автоматически при выходе auto_ptr из области видимости, такие указате#
ли решают множество проблем с утечками памяти, присущих обычным указате#
лям. Книга «More Effective C++» Скотта Мейерса в правиле 9 содержит интересное
обсуждение auto_ptr (Meyers, 1996).
Изучите интеллектуальные указатели Интеллектуальные указатели — это
замена обычных или «тупых» указателей (Meyers, 1996). Они действуют аналогично
обычным, но предоставляют дополнительные возможности по управлению ресур#
сами, операциям копирования, присваивания, создания и удаления объектов. Пе#
речисленные действия характерны для C++. Более полное обсуждение см. в пра#
виле 28 книги «More Effective C++».

Указатели в C
Вот несколько советов по применению указателей, которые в особенности име#
ют отношение к языку C.
Используйте явный тип указателя вместо типа по умолчанию Язык C
позволяет использовать указатели на char или void для любого типа переменной.
Главное, что указатель куда#то указывает, и языку, в общем, не важно, на что именно.
Но если вы используете явные типы для указателей, компилятор может выдавать
предупреждение о несовпадающих типах указателей и некорректных преобразо#
ваниях. Если же явные типы не используются, он этого сделать не сможет. Ста#
райтесь применять конкретные типы где только можно.
Из этого правила следует необходимость явного преобразования типа в тех слу#
чаях, когда нужно его изменить. Так, в этом фрагменте очевидно, что выделяется
память для переменной типа NODE_ PTR:

Пример явного преобразования типа (C)
NodePtr = (NODE_PTR) calloc( 1, sizeof( NODE ) );
Избегайте преобразования типов Этот совет о преобразовании типов не име#
ет ничего общего с учебой в актерской школе или отказом всегда играть негодя#
ев. Он предлагает избегать втискивания переменной одного типа в переменную
другого типа. Такое преобразование выключает способность вашего компилято#
ра проверять несовпадения типов и тем самым пробивает брешь в броне защит#
ного программирования. В программе, требующей многочисленных преобразо#
ваний типов, вероятно, существуют какие#то архитектурные нестыковки, которые
нужно пересмотреть. Попробуйте перепроектировать систему, в противном слу#
чае старайтесь избегать преобразований типов, насколько это возможно.
Следуйте правилу звездочки при передаче параметров Вы можете полу#
чить значение аргумента из функции на языке C, только если в операции присва#
ивания перед этим аргументом была указана звездочка (*). Многие программис#
ты испытывают трудности при определении, когда C позволяет передавать зна#
чение обратно в вызывающий метод. Легко запомнить, что если вы указываете звез#
дочку перед параметром, которому присваиваете значение, то это значение бу#

326

ЧАСТЬ III

Переменные

дет возвращено в вызывающий метод. Независимо от того, сколько звездочек вы
указали в объявлении, для передачи значения в операторе присваивания должна
быть хотя бы одна. Так, в следующем фрагменте значение, присвоенное перемен#
ной parameter, не будет передано в вызывающий метод, потому что операция при#
сваивания не содержит звездочки:

Пример передачи параметра, который не будет работать (C)
void TryToPassBackAValue( int *parameter ) {
parameter = SOME_VALUE;
}
А здесь значение, присвоенное параметру parameter, будет возвращено, потому
что перед parameter указана звездочка:

Пример передачи параметра, который сработает (C)
void TryToPassBackAValue( int *parameter ) {
*parameter = SOME_VALUE;
}
Используйте sizeof() для определения объема памяти, необходимой для раз'
мещения переменной Легче использовать sizeof(), чем выяснять размер типа в
справочнике. Кроме того, sizeof() работает с вашими собственными структурами,
которые в справочнике не описаны. Так как значение вычисляется в момент ком#
пиляции, то sizeof() не влияет на производительность. Кроме того, он переносим:
перекомпиляция в другой среде автоматически изменяет размеры, вычисленные
sizeof(). И еще он прост в сопровождении, поскольку при изменении используе#
мого типа изменится и рассчитываемый размер.

13.3. Глобальные данные
Перекрестная ссылка О различиях между глобальными данными и данными класса, см. подраздел «Ошибочное представление о данных класса как о глобальных данных» раздела 5.3.

Глобальные переменные доступны из любого места програм#
мы. Иногда этот термин небрежно используют для обозна#
чения переменных с более широкой областью видимости,
чем локальные переменные, таких как классовые перемен#
ные, доступные во всех методах класса. Но сама по себе
доступность внутри единственного класса не означает, что
переменная является глобальной.

Наиболее опытные программисты пришли к выводу, что применять глобальные
переменные рискованней, чем локальные. Эти программисты также считают, что
полезней осуществлять доступ к данным с помощью методов.
Даже если применение глобальных переменных не всегда ведет к ошиб#
кам, оно все#таки вряд ли представляет собой хороший способ програм#
мирования.

ГЛАВА 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 и большинство современных языков поддерживают
ванных константах см. раздел 12.7
именованные константы, некоторые языки, такие как Python,
Perl, Awk и язык сценариев UNIX, до сих пор — нет. Вы мо#
жете использовать глобальные переменные как подстановки для именованных кон#
стант, если ваш язык их не поддерживает. Так, вы можете заменить константные
значения 1 и 0 глобальными переменными TRUE и FALSE, установленными в 1 и
0. Или вы можете заменить число 66, используемое как число строк на странице,
переменной LINES_PER_PAGE = 66. Этот подход позволяет упростить дальнейшее
изменение кода, кроме того, его легче читать. Такое упорядоченное применение

330

ЧАСТЬ III

Переменные

глобальных данных — отличный пример программирования с использованием
языка, а не на языке (см. раздел 34.4).
Эмуляция перечислимых типов Вы также можете использовать глобальные
переменные для эмуляции перечислимых типов в таких языках, как Python, кото#
рые напрямую такие типы не поддерживают.
Оптимизация обращений к часто используемым данным Иногда перемен#
ная так часто вызывается, что упоминается в списке параметров каждого метода.
Вместо того чтобы включать ее в каждый список параметров, вы можете сделать
ее глобальной. Однако случаи, когда к переменной обращаются отовсюду, редки.
Обычно она используется ограниченным набором методов. Их вы можете объе#
динить в класс вместе с данными, с которыми они работают. Позднее мы вернем#
ся к этому вопросу.
Исключение бродячих данных Иногда вы передаете данные методу или классу
только для того, чтобы передать в другой метод или класс. Например, у вас может
быть объект#обработчик ошибок, применяемый в каждом методе. Если метод в се#
редине цепочки вызовов не использует этот объект, он называется «бродячим» (tramp
data). Применение глобальных переменных помогает исключить бродячие данные.

Используйте глобальные данные
только как последнее средство
Прежде чем вы решите использовать глобальные данные, рассмотрите следующие
альтернативы.
Начните с объявления всех переменных локальными и делайте их глобаль'
ными только по необходимости Изначально сделайте все переменные локаль#
ными по отношению к конкретным методам. Если выяснится, что они нужны еще
где#то, сделайте их сначала закрытыми или защищенными переменными класса,
прежде чем вы решите сделать их глобальными. Если в конце концов выяснится,
что их придется сделать глобальными, сделайте, но только после того, как в этом
убедитесь. Если вы с самого начала объявите переменную глобальной, вы никог#
да не сделаете ее локальной, но если она сначала будет локальной, вам, возмож#
но, не понадобится делать ее глобальной.
Различайте глобальные переменные и переменные'члены класса Некото#
рые переменные действительно глобальны в том плане, что к ним обращаются
из любого места программы. Другие — на самом деле классовые переменные —
интенсивно используются только некоторымо набором методов. Вполне нормально
обращаться к классовой переменной из нескольких методов сколь угодно интен#
сивно. Если методу вне класса нужно использовать эту переменную, предоставьте
ее значение посредством метода доступа. Не обращайтесь к членам класса напря#
мую (как если бы эти переменные были глобальными), даже если ваш язык про#
граммирования это позволяет. Этот совет равносилен высказыванию «Модуляри#
зируйте! Модуляризируйте! Модуляризируйте!».
Используйте методы доступа Создание методов доступа — основной под#
ход для решения проблем с глобальными данными (см. следующий раздел).

ГЛАВА 13 Нестандартные типы данных

331

Используйте методы доступа вместо глобальных данных
Все, что вы можете сделать с глобальными данными, вы можете сделать
лучше, используя методы доступа. Применение методов доступа — основ#
ная технология реализации абстрактных типов данных и достижения со#
крытия информации. Даже если вы не хотите создавать полноценный абстракт#
ный тип данных, вы все равно можете использовать методы доступа для центра#
лизации управления данными и для защиты от изменений.

Преимущества методов доступа
Использование методов доступа имеет несколько преимуществ.
 Вы получаете централизованный контроль над данными. Если позднее вы об#

наружите более подходящую реализацию структуры, вам не придется изменять
код везде, где она упоминается. Изменения не всколыхнут всю вашу програм#
му — они останутся внутри методов доступа.
 Вы можете быть уверены, что все ссылки на переменную

Перекрестная ссылка Об изоля-

изолированы. Добавляя элемент в стек с помощью таких
ции данных см. раздел 8.5.
выражений, как stack.array[ stack.top ] = newElement, вы
легко можете забыть проверить переполнение стека и допустить серьезную
ошибку. Используя же методы доступа (скажем, PushStack( newElement )), вы
можете написать проверку переполнения стека в методе PushStack(). Провер#
ка будет выполняться автоматически, и вы сможете про нее забыть.
 Вы автоматически получаете главные преимущества со#

Перекрестная ссылка О сокры-

крытия информации. Методы доступа представляют со#
тии информации см. подраздел
бой примеры сокрытия информации, даже если вы и не
«Скрывайте секреты (к вопросу о сокрытии информации)»
намеревались использовать их в этих целях. Вы можете
раздела 5.3.
изменять содержимое метода доступа, не затрагивая
остальную часть программы. Эти методы позволяют вам
отремонтировать интерьер вашего дома, оставив фасад прежним, так что ваши
друзья смогут его узнать.
 Методы доступа легко преобразуются в абстрактные типы данных. Одно из

преимуществ этих методов в том, что вы можете создавать более высокий уро#
вень абстракции, чем при использовании глобальных данных напрямую. На#
пример, вместо кода if lineCount > MAX_LINES вы сможете, используя метод до#
ступа, написать if PageFull(). Это небольшое изменение документирует цель
проверки if lineCount прямо в коде программы. Оно дает небольшой выигрыш
в читабельности, но постоянное внимание к таким деталям и создает разли#
чие между красиво написанным ПО и наскоро слепленным кодом.

Как использовать методы доступа
Здесь представлена краткая версия теории и практики методов доступа: Скройте
данные в классе. Объявите их с помощью ключевого слова static или аналогично#
го, чтобы гарантировать их существование в единственном экземпляре. Напиши#
те методы, позволяющие получать и изменять данные. Потребуйте, чтобы код вне
класса использовал эти методы, а не данные напрямую.
Например, если у вас есть глобальная статусная переменная g_globalStatus, опи#
сывающая общее состояние программы, вы можете создать два метода доступа:

332

ЧАСТЬ III

Переменные

globalStatus . Get() и globalStatus . Set(), каждый из которых делает то, что сказано в
ее названии. Эти методы обращаются к переменной, спрятанной внутри класса,
заменяющего g_globalStatus. Остальная часть программы может получать все пре#
имущества бывшей глобальной переменной, вызывая globalStatus.Get() и global%
Status.Set().
Перекрестная ссылка Ограничение доступа к глобальным переменным, даже если ваш язык
не поддерживает это напрямую,
— пример программирования
с использованием языка, а не
на языке (см. раздел 34.4).

Если язык не поддерживает классы, вы все равно можете
создавать методы доступа для манипуляции глобальными
данными. Однако вам придется устанавливать ограничения
доступа к глобальным данным с помощью стандартов ко#
дирования, а не встроенных средств языка.

Вот несколько основных принципов применения методов
доступа для сокрытия глобальных переменных в языках, не
имеющих встроенной поддержки этого.

Требуйте, чтобы весь код обращался к данным через методы доступа Хо#
рошим соглашением будет начинать имена всех глобальных переменных с пре#
фикса g_, и в дальнейшем требовать, чтобы никакой код не обращался к перемен#
ным с префиксом g_ напрямую, кроме методов доступа к этим переменным. Весь
остальной код работает с этими данными через методы доступа.
Не валите все глобальные данные в одну кучу Если вы сложите все глобаль#
ные данные в одну большую кучу и напишете для них методы доступа, вы решите
проблему глобальных данных, но утратите некоторые преимущества абстрактных
типов данных и сокрытия информации. Раз уж вы пишете методы доступа, обду#
майте, к каким классам принадлежит каждая глобальная переменная, и затем упа#
куйте данные и методы доступа к ним в этот класс.
Управляйте доступом к глобальным переменным с помощьюблокировок
По аналогии с управлением параллельным доступом к многопользовательской ба#
зой данных, блокировка требует, чтобы перед вызовом или обновлением значения
глобальной переменной ее помечали для изменений (check out). После использо#
вания переменную можно освободить (check in). Если пока она занята (т. е. поме#
чена для изменений), другая часть программы попытается к ней обратиться, про#
цедура блокировки выводит сообщение об ошибке или генерирует исключение.
Такое описание механизма блокировок опускает многие
тонкости в написании кода, полностью поддерживающего
параллельное выполнение. По этой причине упрощенные
схемы блокировок вроде этой наиболее полезны на стадии
разработки. Пока схема тщательно не продумана, она ско#
рее всего не будет достаточно надежна для работы в про#
мышленной версии. При вводе программы в эксплуатацию
такой код должен быть заменен на более безопасный и вы#
полняющий более элегантные действия, чем вывод сообщений об ошибках. Так,
при обнаружении ситуации, когда несколько частей программы пытаются забло#
кировать одну и ту же глобальную переменную, он мог бы записать сообщение
об ошибке в файл.

Перекрестная ссылка О планировании различий между рабочей и промышленной версиями
программы см. подраздел «Запланируйте удаление отладочных средств» раздела 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.

Создайте хорошо аннотированный список всех глобальных переменных
Если соглашение по именованию указывает, что данная переменная является гло#
бальной, будет полезно показать, что эта переменная делает. Список глобальных
переменных — один из наиболее полезных инструментов, который может иметь
программист, работающий с вашей программой.
Не храните промежуточных результатов в глобальных переменных Если
вам нужно вычислить новое значение глобальной переменной, присвойте ей окон#
чательный результат в конце вычислений, а не храните в ней результаты проме#
жуточных расчетов.

ГЛАВА 13 Нестандартные типы данных

335

Не считайте, что вы не используете глобальные переменные, поместив
все данные в чудовищный объект и передавая его всюду Размещение всех
возможных данных в одном огромном объекте может формально удовлетворять
принципу отказа от глобальных переменных, но это приводит исключительно к
накладным расходам и не создает преимуществ реальной инкапсуляции. Если вы
используете глобальные данные, делайте это открыто. Не пытайтесь замаскиро#
вать это с помощью объектов, страдающих ожирением.

Дополнительные ресурсы
Далее указаны дополнительные ресурсы, в которых освеща#
ются необычные типы данных:

http://cc2e.com/1385

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/1392

Структуры
 Используете ли вы структуры вместо отдельных переменных для организации и манипуляции группами взаимосвязанных данных?
 Рассматривали ли вы создание класса как альтернативу использованию
структуры?
Глобальные данные
 Действительно ли все переменные объявлены локально или в области видимости класса, если только они не обязательно должны быть глобальными?
 Различаются ли в соглашениях по именованию переменных локальные,
классовые и глобальные данные?
 Документированы ли все глобальные переменные?
 Свободен ли код от псевдоглобальных данных — мамонтообразных объектов, содержащих мешанину из данных, передающихся в каждый метод?
 Используются ли методы доступа вместо глобальных данных?
 Организованы ли данные и методы доступа к ним в классы?
 Предоставляют ли методы доступа уровень абстракции, независимый от
реализации используемого типа данных?
 Находятся ли все методы доступа на одном уровне абстракции?
Указатели
 Изолированы ли операции с указателями в методах?
 Корректны ли обращения к указателям или они могут быть «висячими»?

336

ЧАСТЬ III

Переменные

 Проверяет ли код корректность указателей перед их использованием?
 Проверяется ли корректность переменной, на которую ссылается указатель,
перед ее использованием?
 Присваивается ли указателям пустое значение после их освобождения?
 Использует ли код все необходимые для читабельности переменные-указатели?
 Освобождаются ли указатели в связных списках в правильном порядке?
 Выделяет ли программа «резервный парашют» памяти, чтобы иметь возможность аккуратно завершить выполнение в случае нехватки памяти?
 Используются ли указатели только как последнее средство, когда другие
методы неприменимы?

Ключевые моменты
 Структуры могут помочь сделать программы менее сложными, упростить их

понимание и сопровождение.
 Принимая решение использовать структуру, подумайте, не будет ли класс под#

ходить лучше.
 Работа с указателями чревата ошибками. Обезопасьте себя, используя методы

или классы для доступа к ним и практику защитного программирования.
 Избегайте глобальных переменных не только потому, что они опасны, но и

потому что их можно заменить чем#то лучшим.
 Если вы не можете отказаться от глобальных переменных, работайте с ними

через методы доступа. Эти методы предоставляют все то же и даже больше, что
и глобальные переменные.

ГЛАВА 13 Нестандартные типы данных

Часть IV

ОПЕРАТОРЫ



Глава 14. Организация последовательного кода



Глава 15. Условные операторы



Глава 16. Циклы



Глава 17. Нестандартные управляющие структуры



Глава 18. Табличные методы



Глава 19. Общие вопросы управления

337

338

ЧАСТЬ IV

Г Л А В А

Операторы

1 4

Организация
последовательного кода

http://cc2e.com/1465

Содержание
 14.1. Операторы, следующие в определенном порядке
 14.2. Операторы, следующие в произвольном порядке

Связанные темы
 Общие вопросы управления: глава 19
 Код с условными операторами: глава 15
 Код с операторами цикла: глава 16
 Область видимости переменных и объектов: раздел 10.4

В этой главе мы начнем рассматривать программирование не с точки зрения дан#
ных, а с точки зрения выражений. Глава представляет самую простую управляю#
щую логику программы: размещение выражений и их блоков в последовательном
порядке.
Хотя размещение последовательного кода относительно простая задача, некото#
рые организационные тонкости влияют на качество, корректность, читабельность
и управляемость кода.

14.1. Операторы, следующие
в определенном порядке
Проше всего организовать такие выражения, для которых важен порядок следо#
вания. Вот пример:

Пример выражений, для которых важен порядок следования (Java)
data = ReadData();
results = CalculateResultsFromData( data );
PrintResults( results );

ГЛАВА 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, можно сказать, что, поскольку никакие данные меж#
ду методами не передаются, неизвестно, используют ли эти
методы одни и те же данные. Переписав код так, чтобы происходила передача дан#
ных, вы сообщаете, что порядок выполнения имеет значение. Новый код может
выглядеть, например, так:

Перекрестная ссылка Об использовании методов и их параметров см. главу 5.

Пример данных, которые позволяют предположить порядковую
зависимость (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 )
Данные могут также указывать, что порядок выполнения не имеет значения, как в
этом случае:

ГЛАВА 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 используется в каждом случае между

ГЛАВА 14 Организация последовательного кода

343

первым и последним своим упоминанием. Иначе говоря, вам придется просмот#
реть и осмыслить каждую строку кода в этом фрагменте, чтобы выяснить, как
вычисляется marketingData. И, разумеется, этот пример гораздо проще, чем код,
встречающийся в реальных системах. Вот тот же код, но лучше организованный:

Пример хорошего, последовательного кода, который читается сверху вниз (C++)
MarketingData marketingData;
marketingData.ComputeQuarterly();
marketingData.ComputeAnnual();
marketingData.Print();
SalesData salesData;
salesData.ComputeQuarterly();
salesData.ComputeAnnual();
salesData.Print();
TravelData travelData;
travelData.ComputeQuarterly();
travelData.ComputeAnnual();
travelData.Print();
Этот код лучше по нескольким причинам. Упоминания каж#
Перекрестная ссылка Более
дой переменной располагаются вместе — они «локализова#
формальное определение «живых» переменных см. в подразны». Число строк кода, в которых объекты являются «живы#
деле «Измерение времени жизми», невелико. И, возможно, самое важное: код теперь вы#
ни переменной» раздела 10.4.
глядит так, что его можно разбить на отдельные методы для
данных по маркетингу, продажам и поездкам. Первый фраг#
мент не содержал подсказки, что эта декомпозиция возможна.

Группировка взаимосвязанных выражений
Размещайте взаимосвязанные выражения вместе. Они мо#
гут быть связаны, так как работают с одними и теми же дан#
ными, выполняют схожие задачи или зависят от порядка
выполнения друг друга.

Перекрестная ссылка Если вы
придерживаетесь Процесса Программирования с Псевдокодом,
ваш код будет автоматически группироваться в блоки взаимосвязанных выражений (см. главу 9).

Есть простой способ убедиться, что взаимосвязанные выра#
жения хорошо сгруппированы. Распечатайте текст вашего
метода и обведите рамкой взаимосвязанные выражения. Если они хорошо упоря#
дочены, вы получите картинку, похожую на рис. 14#1, где рамки не перекрываются.

344

ЧАСТЬ IV

Операторы

Рис. 14'1. Если код хорошо организован в группы, то рамки вокруг
взаимосвязанных разделов не перекрываются; они могут быть вложенными
Перекрестная ссылка Об объединении операций над переменными см. раздел 10.4.

Если выражения плохо организованы, вы получите картинку,
похожую на рис. 14#2, где рамки перекрываются. Если выяснится,
что перекрытие происходит, реорганизуйте ваш код, чтобы
взаимосвязанные выражения были лучше сгруппированы.

Рис. 14'2. Если код организован неудачно, то рамки вокруг связанных
разделов пересекаются

После того, как вы сгруппируете взаимосвязанные выражения, может выяснить#
ся, что они сильно связаны между собой, а к предшествующему и последующему
коду не имеют значимого отношения. В этом случае, возможно, следует выделить
эти выражения в отдельный метод.

http://cc2e.com/1472









Контрольный список: организация
последовательного кода

 Способствует ли код выявлению зависимостей между
выражениями?
Способствуют ли имена методов выявлению зависимостей?
Способствуют ли параметры методов выявлению зависимостей?
Описывают ли комментарии такие зависимости, которые иначе не будут
явными?
Используются ли вспомогательные переменные для проверки последовательных действий в критических частях кода?
Возможно ли прочтение кода сверху вниз?
Сгруппированы ли вместе взаимосвязанные выражения?
Перенесены ли относительно независимые группы выражений в отдельные
методы?

ГЛАВА 14 Организация последовательного кода

345

Ключевые моменты
 Главный принцип организации последовательного кода — упорядочение за#

висимостей.
 Зависимости должны быть сделаны явными с помощью хороших имен мето#

дов, списков параметров, комментариев и — если последовательность кода
достаточно критична — с помощью вспомогательных переменных.
 Если порядковые зависимости в коде отсутствуют, старайтесь размещать вза#

имосвязанные выражения как можно ближе друг к другу.

346

ЧАСТЬ IV

Г Л А В А

Операторы

1 5

Условные операторы

http://cc2e.com/1538

Содержание
 15.1. Операторы if
 15.2. Операторы case

Связанные темы
 Укрощение глубокой вложенности: раздел 19.4
 Общие вопросы управления: глава 19
 Код с операторами цикла: глава 16
 Последовательный код: глава 14
 Отношения между типами данных и управляющими структурами: раздел 10.7

Условный оператор управляет выполнением других операторов. Их выполнение
«обусловливают» такие операторы, как if, else, case и switch. Хотя операторы цик#
ла, например while и for, по смыслу тоже можно отнести к условным, обычно их
рассматривают отдельно. Операторы while и for мы обсудим в главе 16.

15.1. Операторы if
В зависимости от выбранного языка программирования вы можете использовать
несколько видов if#операторов. Простейшие из них — if или if%then. Оператор if%
then%else немного сложнее, а наибольшую сложность представляют последователь#
ности if%then%else%if.

Простые операторы if-then
Следуйте этим правилам при написании if#выражений.
Сначала напишите код номинального хода алгоритма, затем
опишите исключительные случаи Пишите код так, чтобы нормаль#
ный путь выполнения был очевиден. Убедитесь, что нестандартные об#
стоятельства не затмевают смысл основного алгоритма. Это важно как с точки
зрения читабельности, так и с точки зрения производительности.
Убедитесь, что при сравнении на равенство ветвление корректно Исполь#
зование > вместо >= или < вместо

break;
...
}
...
} while ( ... );
Большое количество break не обязательно означает ошибку, но их присутствие в
цикле — тревожный сигнал: как канарейка в шахте, задыхающаяся из#за недостатка
воздуха, вместо того чтобы петь.

ГЛАВА 16

Циклы

373

Используйте continue для проверок в начале цикла Хорошим применени#
ем оператора continue будет перемещение операций в конец тела цикла после про#
верки некоторого условия в его начале. Например, если цикл читает записи, от#
брасывает часть из них, а остальные обрабатывает, вы можете поместить подоб#
ную проверку в начало цикла:

Пример относительно безопасного использования continue (псевдокод)
while ( not eof( file ) ) do
read( record, file )
if ( record.Type targetType ) then
continue
— Обрабатываем запись targetType.
...
end while
Такое использование continue позволяет избегать проверок if, что эффективно
уменьшит отступы внутри всего тела цикла. С другой стороны, если continue воз#
никает в середине или конце цикла, используйте вместо него if.
Используйте структуру break с метками, если ваш язык ее поддержи'
вает Java поддерживает помеченные операторы break, что позволяет предотв#
ратить проблемы, приведшие к выходу из строя телефонов в Нью#Йорке. break с
меткой можно использовать для выхода из цикла for, условия if или любого блока
кода, заключенного в скобки (Arnold, Gosling and Holmes, 2000).
Вот возможное решение «нью#йоркской проблемы», переписанное на Java вмес#
то C++, что позволяет использовать break с меткой:

Пример лучшего использования помеченного оператора break
в блоке do-switch-if (Java)
do {
...
switch
...
CALL_CENTER_DOWN:
if () {
...
Назначение помеченного break однозначно.

>

break CALL_CENTER_DOWN;
...
}
...
} while ( ... );
Используйте операторы break и continue очень осторожно Применение
break исключает возможность представления цикла в виде черного ящика. Если
вы ограничиваетесь только одним выражением для управления условием выхода
из цикла, то получаете мощное средство для упрощения циклов. Применение break

374

ЧАСТЬ IV

Операторы

заставляет читателя смотреть внутрь цикла, чтобы разобраться в его управлении.
Это усложняет понимание цикла.
Используйте break только после того, как рассмотрели все альтернативы. Вы не
можете сказать с уверенностью, хороши или плохи конструкции continue и break.
Некоторые ученые утверждают, что это допустимые технологии в структурном
программировании, а некоторые — что нет. Поскольку вы не знаете, правильно
ли применять continue и break вообще, используйте их, но не забывайте, что вы
можете быть неправы. На самом деле это сводится к простому утверждению: если
вы не можете аргументировать применение break или continue, не применяя их.

Проверка граничных точек
При разработке цикла обычно представляют интерес три точки: первая итерация,
случайно выбранная итерация в середине и последняя итерация. Когда вы созда#
ете цикл, мысленно пройдитесь по этим трем точкам и убедитесь, что в цикле нет
ошибки потери единицы. Если цикл содержит какие#то специальные случаи, вы#
полнение которых отличается от первой или последней итерации, проверьте их
тоже. Если цикл производит сложные вычисления, достаньте свой калькулятор и
проверьте их вручную.
Готовность выполнять такой вид проверки — ключевое различие между
квалифицированными и неквалифицированными программистами. Пер#
вые проделывают мысленное моделирование и вычисления вручную,
потому что знают, что эти меры помогут им найти ошибки.
Вторые имеют склонность к случайному экспериментированию, пока не найдут
правдоподобную комбинацию. Если цикл не работает так, как предполагалось,
неумелый программист меняет знак < на rate = table[ ]
totalRate = totalRate + rate
Пример предполагает, что table — это массив, содержащий данные о ставках.
Сначала вам не надо беспокоиться об индексах массива. rate — это переменная, в
которой хранится ставка, выбранная из таблицы ставок. Соответственно totalRate
— переменная, содержащая сумму всех ставок.
Далее добавьте индексы к массиву table:

Шаг 3: Создание цикла изнутри наружу (псевдокод)
rate = table[ census.Age ][ census.Gender ]
totalRate = totalRate + rate
Доступ к элементам массива осуществляется в зависимости от возраста и пола,
поэтому census.Age и census.Gender служат для индексации массива. Пример пред#

ГЛАВА 16

Циклы

379

полагает, что census — это структура, содержащая сведения о людях из рассчиты#
ваемой группы.
Следующий шаг — построение цикла вокруг существующих выражений. Поскольку
цикл должен вычислять ставки для каждого человека из группы, индекс должен
перечислять всех членов группы.

Шаг 4: Создание цикла изнутри наружу (псевдокод)
For person = firstPerson to lastPerson
rate = table[ census.Age, census.Gender ]
totalRate = totalRate + rate
End For
Все, что вы должны сделать, — это поместить цикл for вокруг существующего кода
и добавить к нему пару begin%end. Напоследок убедитесь, что переменные, исполь#
зующие индекс цикла person, написаны правильно. В данном случае переменная
census изменяется вместе с person, поэтому ее следует корректно проиндексировать.

Шаг 5: Создание цикла изнутри наружу (псевдокод)
For person = firstPerson to lastPerson
rate = table[ census[ person ].Age, census[ person ].Gender ]
totalRate = totalRate + rate
End For
И, наконец, напишите необходимую инициализацию. В этом примере нужно ини#
циализировать переменную totalRate.

Последний шаг: Создание цикла изнутри наружу (псевдокод)
totalRate = 0
For person = firstPerson to lastPerson
rate = table[ census[ person ].Age,census[ person ].Gender ]
totalRate = totalRate + rate
End For
Если вы хотите добавить еще один цикл вокруг цикла person, продолжайте таким
же образом. Вы не должны жестко придерживаться этого порядка. Идея в том, чтобы
начать с чего#то определенного, думать только об одной задаче в каждый момент
времени и строить цикл из простых компонентов. Предпринимайте маленькие,
понятные шаги, постепенно обобщая и усложняя цикл. Таким образом, вы мини#
мизируете количество кода, на котором необходимо одновременно сосредоточи#
ваться и, следовательно, уменьшите вероятность ошибки.

16.4. Соответствие между циклами и массивами
Циклы и массивы часто связаны друг с другом. Зачастую цикл
создается для манипуляций с массивами, и счетчики цикла
один к одному соответствуют индексам массива. Так, следу#
ющие индексы циклов for соответствуют индексам массива:

Перекрестная ссылка О соответствии между циклами и массивами см. также раздел 10.7.

380

ЧАСТЬ IV

Операторы

Пример умножения массивов (Java)
for ( int row = 0; row < maxRows; row++ ) {
for ( int column = 0; column < maxCols; column++ ) {
product[ row ][ column ] = a[ row ][ column ] * b[ row ][ column ];
}
}
В языке Java цикл для таких операций с массивами необходим. Но стоит заметить,
что циклические структуры и массивы не обязательно должны использоваться
вместе. Некоторые языки, особенно APL и Fortran 90 и более поздние, предостав#
ляют операции с массивами, исключающие необходимость применять такие циклы,
как только что продемонстрированные. Вот так выглядит фрагмент кода на APL,
выполняющий ту же операцию:

Пример умножения массивов (APL)
product Comparison Compare( int value1, int value2 ) {
if ( value1 < value2 ) {
return Comparison_LessThan;
}
else if ( value1 > value2 ) {
return Comparison_GreaterThan;
}
return Comparison_Equal;
}
Другие примеры не настолько однозначны, что будет проиллюстрировано ниже.
Упрощайте сложную обработку ошибок с помощью сторожевых опера'
торов (досрочных return или exit) Если программа вынуждена проверять боль#
шое количество ошибочных ситуаций перед выполнением номинальных действий,
это может привести к коду очень большой вложенности и замаскировать номи#
нальный вариант. Вот пример такого кода:

Код, скрывающий номинальный вариант (Visual Basic)
If file.validName() Then
If file.Open() Then
If encryptionKey.valid() Then
If file.Decrypt( encryptionKey ) Then
Здесь код номинального варианта.

>

‘ Много кода.
...
End If
End If
End If
End If
Отступ основного кода метода внутри четырех условий if выглядит неэстетично,
особенно если этот код в самом внутреннем блоке if состоит из множества строк.
В таких случаях часто можно упростить логику, если все ошибочные ситуации
проверять сначала, расчистив дорогу для номинального хода алгоритма. Вот как
это может выглядеть:

384

ЧАСТЬ IV

Операторы

Простой код, использующий сторожевые операторы
для прояснения номинального варианта (Visual Basic)
‘ Выполняем инициализацию. При обнаружении ошибок завершаем работу.
If Not file.validName() Then Exit Sub
If Not file.Open() Then Exit Sub
If Not encryptionKey.valid() Then Exit Sub
If Not file.Decrypt( encryptionKey ) Then Exit Sub
’ Много кода.
...
В таком простом примере описанный способ выглядит аккуратным решением, но
промышленный код при обнаружении ошибки часто требует большего количе#
ства служебных операций или действий по очистке ресурсов. Вот более реалис#
тичный пример:

Более реалистичный код, использующий сторожевые операторы
для прояснения номинального варианта (Visual Basic)
‘ Выполняем инициализацию. При обнаружении ошибок завершаем работу.
If Not file.validName() Then
errorStatus = FileError_InvalidFileName
Exit Sub
End If
If Not file.Open() Then
errorStatus = FileError_CantOpenFile
Exit Sub
End If
If Not encryptionKey.valid() Then
errorStatus = FileError_InvalidEncryptionKey
Exit Sub
End If
If Not file.Decrypt( encryptionKey ) Then
errorStatus = FileError_CantDecryptFile
Exit Sub
End If
Здесь код номинального варианта.

> ‘ Много кода.
...
В коде промышленного масштаба использование Exit Sub приводит к написанию
довольно большого количества кода до обработки номинального варианта. Од#
нако Exit Sub позволяет избежать глубокой вложенности, присущей первому при#
меру, и если код первого примера расширить с целью установки значений пере#
менной errorStatus, то вариант с Exit Sub покажется лучшим с точки зрения груп#
пировки взаимосвязанных выражений. Когда вся пыль осядет, подход с Exit Sub
покажется более удобным для чтения и сопровождения, и за небольшую цену.

ГЛАВА 17 Нестандартные управляющие структуры

385

Минимизируйте число возвратов из каждого метода Тяжело понять ло#
гику метода, если при чтении его последних строк вы не подозреваете о возмож#
ности выхода из него где#то вверху. По этой причине используйте операторы воз#
врата благоразумно — только если они улучшают читабельность.

17.2. Рекурсия
При рекурсии метод решает небольшую часть задачи, разбивает задачу на мень#
шие порции и вызывает сам себя для решения каждой из этих порций. Обычно
рекурсию применяют, когда небольшую часть задачи легко решить, а саму задачу
просто разложить на составные части.
Рекурсия не часто бывает необходима, но при аккуратном использова#
нии она позволяет создавать элегантные решения, как в этом примере,
где алгоритм сортировки иллюстрирует отличное применение рекурсии:

Пример алгоритма сортировки, использующего рекурсию (Java)
void QuickSort( int firstIndex, int lastIndex, String [] names ) {
if ( lastIndex > firstIndex ) {
int midPoint = Partition( firstIndex, lastIndex, names );
Здесь выполняются рекурсивные вызовы.

>

QuickSort( firstIndex, midPoint1, names );
QuickSort( midPoint+1, lastIndex, names )
}
}
В этом фрагменте алгоритм сортировки разрезает массив на две части и затем
вызывает сам себя для сортировки каждой половины массива. Когда ему будет
передан участок массива, слишком короткий для сортировки, т. е. когда ( lastIndex
fieldIdx = 1;
while ( ( fieldIdx

field[ fieldType ].ReadAndPrint( fieldName, fileStatus );
}
Помните первоначальные 34 строки псевдокода табличного поиска, содержаще#
го оператор case? Если вы замените оператор case таблицей объектов, то это весь
код, который вам нужен для обеспечения той же функциональности. Невероят#
но, но это также весь код, необходимый для замены всех 20 отдельных методов,
применяемых при логическом подходе. Более того, если описания сообщений
читаются из файла, то новые типы сообщений не потребуют изменений кода, если
только не будут содержать новых типов полей.
Вы можете использовать такой подход в любом объектно#ориентированном язы#
ке. Он менее подвержен ошибкам, легче в сопровождении и эффективнее длин#
ных выражений if, операторов case или огромного количества подклассов.
Сам факт, что проект использует наследование и полиморфизм, не делает его
хорошим проектом. Механический объектно#ориентированный дизайн, описан#
ный в разделе «Объектно#ориентированный подход», потребовал бы такого же
большого объема кода, как и механический функциональный дизайн, а может, и
больше. Такой подход скорее усложнил бы решение, чем упростил. В данном слу#
чае основная суть проектного решения не в объектной и не в функциональной
ориентации, а в использовании хорошо продуманной таблицы поиска.

ГЛАВА 18 Табличные методы

417

Подгонка значений ключа
Во всех трех предыдущих примерах вы могли использовать данные в качестве
ключа для прямого обращения к таблице. То есть можно было указать перемен#
ную messageID как ключ без всяких изменений, переменную month в примере
количества дней в месяцах, а также gender, maritalStatus и smokingStatus в приме#
ре ставок страхования.
Было бы хорошо всегда обращаться к таблице напрямую, потому что это просто
и быстро. Однако не всегда данные для этого годятся. В примере со ставками стра#
хования переменная age не очень удобна в качестве ключа. Первоначальная ло#
гика определяла одну ставку для лиц моложе 18 лет, индивидуальные ставки для
возрастов от 18 до 65 и одну ставку для людей старше 65. Это означает, что для
возрастов от 0 до 17 и от 66 и выше нельзя использовать возраст как ключ напря#
мую, если таблица хранит только один набор ставок для нескольких лет.
Это приводит к обсуждению вопроса подгонки значений ключа в таблице поис#
ка. Подогнать ключ можно несколькими способами.
Продублировать информацию, чтобы использовать ключ напрямую
Один прямолинейный способ заставить age работать ключом в таблице ставок —
продублировать все ставки для лиц, моложе 18, для каждого возраста от 0 до 17, а
затем использовать возраст для прямого обращения к таблице. То же самое мож#
но сделать и для возрастов от 66 лет и старше. Преимущество этого подхода в том,
что структура таблицы остается простой и доступ к данным так же прост. Если
нужно добавить специальное значение ставки для некоторого возраста, меньше#
го 17, вы можете просто изменить табличное значение. Недостаток этого метода
в том, что дублирование приведет к напрасным затратам на хранение избыточ#
ной информации, а также увеличит вероятность появления ошибок в таблице хотя
бы потому, что таблица будет содержать избыточные данные.
Преобразовать ключ, чтобы использовать его напрямую Второй способ
задействовать Age в качестве прямого ключа — применить к переменной Age неко#
торую функцию, которая позволит это делать. В данном случае такая функция дол#
жна преобразовывать все возрасты от 0 до 17 к какому#то одному значению, ска#
жем, 17, а возрасты старше 66 — к другому, например, 66. В данном случае такое
преобразование легко выполнить с помощью функций min() и max(). Так, для со#
здания табличного ключа в диапазоне от 17 до 66 можно использовать выражение:

max( min( 66, Age ), 17 )
Реализация функции трансформации требует хорошего понимания структуры
данных, которые вы хотите применить как ключ, и это не всегда так просто, как
использование функций min() и max(). Допустим, в этом примере ставки меня#
ются через интервалы не в 5 лет, а в 1 год. Если только вы не хотите дублировать
все данные по пять раз, вам придется написать функцию, которая делит Age на 5
и использует методы min() и max().
Изолируйте преобразование ключа в собственном методе Если вам нуж#
но подгонять данные для использования в качестве табличного ключа, помести#
те операции, трансформирующие данные в ключ, в отдельный метод. Его исполь#
зование исключает возможность применения разных преобразований в разных

418

ЧАСТЬ IV

Операторы

местах. Это упростит модификацию при изменении функции преобразования.
Хорошее имя процедуры, такое как KeyFromAge(), также прояснит и задокументи#
рует назначение математических махинаций.
Если ваша среда предоставляет готовые варианты преобразования ключа, исполь#
зуйте их. Например, класс HashMap в языке Java позволяет создавать пары «ключ#
значение».

18.3. Таблицы с индексированным доступом
Иногда простого математического преобразования недостаточно для перехода от
таких данных, как Age к значению ключа. Некоторые из таких случаев подходят
для схем с индексным доступом.
Применяя индексы, вы используете исходные данные для поиска ключа в индексной
таблице, а затем значение из этой таблицы служит для поиска интересующих вас
данных.
Допустим, вы заведуете складом, и у вас около 100 наименований товара. Далее
предположим, что каждый товар имеет четырехзначный номер в диапазоне от 0000
до 9999. В этом случае, если вы захотите задействовать номер товара в качестве
ключа для прямого доступа к таблице, описывающей какой#то признак каждого
товара, вам придется создать индексный массив с 10 000 записей (от 0 до 9999).
Этот массив в основном будет пустым за исключением 100 элементов, соответ#
ствующих номерам товаров на вашем складе. Как показано на рис. 18#4, эти эле#
менты указывают на таблицу с описанием товаров, содержащую гораздо менее
10 000 записей.

Рис. 18'4. В отличие от таблиц с прямым доступом для обращения к таблице
с индексным доступом используется промежуточный индекс

ГЛАВА 18 Табличные методы

419

У схем с индексным доступом два главных преимущества. Во#первых, если каж#
дый элемент главной таблицы поиска имеет большой размер, создание индекс#
ного массива с большим количеством пустых ячеек потребует гораздо меньше
места, чем создание самой таблицы поиска с большим количеством пустых яче#
ек. Пусть, например, элемент основной таблицы занимает 100 байт, а элемент
индексной таблицы — 2 байта. Далее предположим, что главная таблица содер#
жит 100 записей, а данные, необходимые для обращения к ней, могут принимать
10 000 возможных значений. В этом случае выбор осуществляется между исполь#
зованием индексной таблицы с 10 000 записей или только одной главной табли#
цы с 10 000 записей. Если вы используете индекс, общий объем требуемой памя#
ти равен 30 000 байт. Если вы откажетесь от индекса и будете попусту тратить
память в основной таблице, то общий объем используемой памяти составит
1 000 000 байт.
Вторым преимуществом индексного доступа (даже если вам не удастся сэкономить
память с его помощью) является то, что иногда дешевле манипулировать элемен#
тами индекса, чем элементами основной таблицы. Так, если у вас есть таблица с
именами работников, датами их приема на работу и окладами, вы можете создать
один индекс для доступа к таблице по имени работника, второй — для доступа на
основе даты приема, и третий — для доступа на основе размера зарплаты.
И последнее преимущество индексной схемы доступа — общее для всех таблиц
поиска удобство сопровождения. Данные, закодированные в таблицах, легче со#
провождать, чем внедренные в код. Для увеличения гибкости поместите код ин#
дексного доступа в отдельный метод и вызывайте его, когда вам нужно получить
значение ключа на основе номера товара. Когда понадобится изменить таблицу,
вы можете решить изменить схему индексного доступа или даже перейти к дру#
гому способу доступа к таблице поиска. Схему доступа будет легче поменять, если
код доступа по индексу не разбросан по всей программе.

18.4. Таблицы со ступенчатым доступом
Еще один способ табличного доступа — ступенчатый метод. Этот метод не такой
прямой, как индексная структура, но позволяет не терять такого большого объе#
ма памяти.
Основная идея ступенчатой структуры в том, что записи в таблице соответству#
ют некоторому диапазону данных, а не отдельным элементам (рис. 18#5).

Рис. 18'5. Ступенчатый подход классифицирует каждый элемент, определяя
уровень, на котором он наталкивается на «лестницу». «Ступенька», в которую
этот элемент упирается, определяет его категорию

420

ЧАСТЬ IV

Операторы

Например, если вы пишете аттестационную программу, диапазон оценки «B» мо#
жет быть в пределах 75–90%. Вот список оценок, которые вам однажды, возмож#
но, придется запрограммировать:
≥ 90.0%

A

< 90.0%

B

< 75.0%

C

< 65.0%

D

< 50.0%

F

Это ужасный диапазон для табличного поиска, потому что вы не можете напи#
сать простую функцию преобразования данных для соответствия буквам от A до
F. Индексная схема неудобна, так как используются числа с плавающей запятой.
Вы можете предложить конвертировать числа с плавающей запятой в целые, что
для данного случая вполне допустимо, однако в целях иллюстрации этот пример
будет придерживаться чисел с плавающей запятой.
Применяя ступенчатый метод, вы помещаете верхнюю границу каждого диапазо#
на в таблицу, а затем пишете цикл для сравнения количества баллов с этой верх#
ней границей. Обнаружив точку, в которой сумма баллов в первый раз превысит
заданный предел, вы узнаете оценку. Применяя ступенчатую методику, надо сле#
дить за правильной обработкой граничных точек диапазона. Вот пример кода Visual
Basic, присваивающей оценки группе студентов, основываясь на данных этого
примера:

Пример ступенчатого поиска в таблице (Visual Basic)
‘ Подготавливаем данные для таблицы оценок.
Dim rangeLimit() As Double = { 50.0, 65.0, 75.0, 90.0, 100.0 }
Dim grade() As String =
{ ”F”, “D”, “C”, “B”, “A” }
maxGradeLevel = grade.Length – 1
...
’ Присваиваем оценки, основываясь на количестве баллов, набранных студентом.
gradeLevel = 0
studentGrade = “A”
While ( ( studentGrade = “A” ) and ( gradeLevel < maxGradeLevel ) )
If ( studentScore < rangeLimit( gradeLevel ) ) Then
studentGrade = grade( gradeLevel )
End If
gradeLevel = gradeLevel + 1
Wend
Хотя это и простой пример, вы легко можете его обобщить для работы с несколь#
кими студентами или несколькими системами оценок (например, введя разные
оценки для различных уровней сложности выполняемых заданий), а также для
изменения системы оценок.
Преимущество этого подхода перед другими табличными методами в том, что он
хорошо работает с нестандартными данными. Пример с оценками прост с той
точки зрения, что, хотя оценки и присваиваются через неодинаковые промежут#

ГЛАВА 18 Табличные методы

421

ки, сами рассматриваемые числа — «круглые», оканчивающиеся на 5 или 0. Сту#
пенчатый способ столь же хорошо подходит и для данных, не заканчивающихся
на 5 или 0. Вы можете применять эту методику и в статистических задачах для
работы с такими примерами вероятностных распределений, как этот:
Вероятность

Сумма страхового иска

0,458747

$0,00

0,547651

$254,32

0,627764

$514,77

0,776883

$747,82

0,893211

$1 042,65

0,957665

$5 887,55

0,976544

$12 836,98

0,987889

$27 234,12

...

Такие ужасные числа сводят на нет все попытки создать функцию для их точного
преобразования в табличные ключи. В этом случае следует использовать ступен#
чатый метод.
Этот подход также позволяет оценить главные преимущества табличных методов:
гибкость и модифицируемость. Если диапазоны баллов в примере с оценками надо
изменить, программу легко исправить, изменив элементы массива RangeLimit. Вы
легко можете обобщить метод выставления отметок так, чтобы он принимал из#
вне таблицы с отметками и соответствующими граничными значениями баллов.
При выставлении оценок не обязательно использовать баллы, выраженные в про#
центах, их можно поменять на обычные единицы, и это не потребует больших
изменений в программе.
Вот несколько тонкостей, которые надо принимать во внимание, применяя сту#
пенчатый метод.
Следите за граничными точками Убедитесь, что вы учитываете верхнюю гра#
ницу каждого диапазона. Выполните ступенчатый поиск так, чтобы он находил
значения, соответствующие любому интервалу, кроме самого верхнего, а затем за#
дайте несколько элементов, попадающих в этот верхний интервал. Иногда требу#
ется создать искусственное значение для верхней границы последнего интервала.
Не ошибитесь с операциями < или b ) ...
вместо:

while ( done = false ) ...
while ( (a > b) = true ) ...
Использование неявных сравнений уменьшает число элементов, которые придется
помнить при чтении кода, и приближает получаемое выражение к разговорному
английскому. Вот как улучшить стиль предыдущего примера:

Улучшенные примеры неявных проверок True или False (Visual Basic)
Dim printerError As Boolean
Dim reportSelected As ReportType
Dim summarySelected As Boolean
...
If ( Not printerError ) Then InitializePrinter()
If ( printerError ) Then NotifyUserOfError()
If ( reportSelected = ReportType_First ) Then PrintReport()
If ( summarySelected ) Then PrintSummary()
If ( Not printerError ) Then CleanupPrinter()
Если ваш язык не поддерживает логические переменные и
вам приходится их эмулировать, то, вероятно, вы не смо#
жете использовать эту технологию, поскольку искусствен#
ные true и false не всегда могут проверяться в таких выра#
жениях, как while ( not done ).

Перекрестная ссылка О логических переменных см. раздел
12.5.

ГЛАВА 19 Общие вопросы управления

427

Упрощение сложных выражений
Вы можете предпринять несколько описанных далее шагов для упрощения слож#
ных выражений.
Разбивайте сложные проверки на части с помощью новых логических
переменных Вместо создания чудовищных условий с полудюжиной элементов
присвойте значения этих элементов промежуточным переменным, что позволит
выполнять упрощенную проверку.
Размещайте сложные выражения в логических функциях Если какая#то
проверка выполняется неоднократно или отвлекает от основного хода алгорит#
ма программы, поместите ее код в отдельную функцию и проверяйте значение
этой функции. Вот пример сложного условия:

Пример проверки сложного условия (Visual Basic)
If ( ( document.AtEndOfStream ) And ( Not inputError ) ) And _
( ( MIN_LINES

...
}
else {
...с кодом в этом блоке.

>

// Делаем чтото.
...
}
Второй фрагмент кода логически совпадает с первым, но его
легче читать, потому что отрицательное выражение было
изменено на положительное.
В качестве альтернативы вы можете использовать другие
имена переменных, которые изменят истинное значение
условия на противоположное. В приведенном примере вы
можете заменить переменную statusOK на ErrorDetected, ко#
торая будет истиной, когда statusOK будет ложно.

Перекрестная ссылка Рекомендация по составлению положительных логических выражений
иногда идет вразрез с рекомендацией кодировать номинальный вариант в блоке if, а не в
блоке else (см. раздел 15.1).
В этом случае вам нужно взвесить
преимущества каждого подхода и
решить, какой вариант в вашей
ситуации будет наилучшим.

Применяйте теоремы Деморгана для упрощения логи'
ческих проверок с отрицаниями Теоремы Деморгана
позволяют эксплуатировать логическую взаимосвязь меж#
ду некоторым выражением и версией этого выражения, обозначающего то же са#
мое, благодаря использованию двойного отрицания. Рассмотрим фрагмент кода,
содержащий следующее условие:

Пример условия с отрицанием (Java)
if ( !displayOK || !printerOK ) ...
Это условие логически эквивалентно следующему:

Пример после применения теорем Деморгана (Java)
if ( !( displayOK && printerOK ) ) ...
В данном случае вам не надо менять местами блоки if и else — выражения в двух
последних фрагментах кода логически эквивалентны. Для применения теорем
Деморгана к логическому оператору and или or и паре операндов вы инвертиру#
ете оба операнда, меняете местами операторы and и or и инвертируете все выра#
жение целиком. Табл. 19#1 обобщает возможные преобразования в соответствии
с теоремами Деморгана.

430

ЧАСТЬ IV

Операторы

Табл. 19-1. Преобразования логических переменных
в соответствии с теоремами Деморгана
Исходное выражение

Эквивалентное выражение

not A and not B

not ( A or B )

not A and B

not ( A or not B )

A and not B

not ( not A or B )

A and B

not ( not A or not B )

not A or not B*

not ( A and B )

not A or B

not ( A and not B )

A or not B

not ( not A and B )

A or B

not ( not A and not B )

*

Это выражение и используется в примере.

Использование скобок для пояснения
логических выражений
Если у вас есть сложное логическое выражение, не надей#
тесь на порядок вычисления его операндов в языке програм#
мирования — используйте скобки, чтобы сделать ваши на#
мерения понятными. Скобки уменьшают требования, предъ#
являемые к читателю кода, который может и не разобрать#
ся в тонкостях вычисления логических выражений в вашем
языке программирования. Если вы благоразумны, то не будете полагаться на соб#
ственную или чью#то еще способность правильно запомнить приоритет вычис#
лений, особенно если приходится переключаться между двумя или более языка#
ми. Использование скобок не похоже на отправку телеграммы: вы не платите за
каждую букву — дополнительные символы бесплатны.

Перекрестная ссылка Примеры
применения скобок для прояснения других видов выражений
см. в подразделе «Скобки» раздела 31.2.

Вот пример выражения, содержащего слишком мало скобок:

Пример выражения, содержащего слишком мало скобок (Java)
if ( a < b == c == d ) ...
Начнем с того, что это выражение слишком запутано. Оно тем более сбивает с толку,
что не ясно, хотел ли кодировщик проверить условие ( a < b ) == ( c == d ) или ( ( a <
b ) == c ) == d. Следующая версия все равно не идеальна, но скобки все же помогают:

Пример выражения, частично улучшенного с помощью скобок (Java)
if ( ( a < b ) == ( c == d ) ) ...
В этом случае скобки повышают удобство чтения и корректность программы,
поскольку компилятор не истолковал бы первый фрагмент таким способом. Ког#
да сомневаетесь, используйте скобки.
Используйте простой метод подсчета для проверки симметричности
скобок Если у вас возникают проблемы с поиском парных скобок, то вот про#
стой способ подсчета. Начните считать, сказав «ноль». Двигайтесь вдоль выраже#

ГЛАВА 19 Общие вопросы управления

ния слева направо. Встретив открывающую скобку, скажите
«один». Каждый раз при встрече открывающей скобки уве#
личивайте число. А встречая закрывающую скобку, умень#
шайте это число. Если к концу выражения у вас опять по#
лучится 0, то ваши скобки симметричны.

Пример симметричных скобок (Java)
Читаем это выражение.

431

Перекрестная ссылка Многие
текстовые редакторы, ориентированные на программистов,
предоставляют команды для
поиска парных круглых, квадратных и фигурных скобок.
О редакторах для программирования см. подраздел «Редактирование» раздела 30.2.

> if ( ( ( a < b ) == ( c == d ) ) && !done ) ...
| | |

|
2

>0 1 2 3

|
3

| |
2 1

|
0

Произносим эти числа.
В этом примере в конце получился 0, следовательно, скобки симметричны. В сле#
дующем примере количество отрывающих и закрывающих скобок не одинаково:

Пример несимметричных скобок (Java)
Читаем это выражение.

>if ( ( a < b ) == ( c == d ) ) && !done ) ...
| |

>0 1 2

|
1

|
2

| |
1 0

|
1

Произносим эти числа.
Значение 0, полученное до последней закрывающей скобки, подсказывает, что где#
то до этой точки была пропущена скобка. Вы не должны получить 0, не достиг#
нув последней скобки в выражении.
Заключайте в скобки логическое выражение целиком Скобки ничего вам
не стоят, а читабельность улучшают. Привычка заключать в скобки все логичес#
кое выражение целиком — хорошая практика программирования.

Понимание правил вычисления логических выражений
Множество языков содержит неявную управляющую форму, которая начинает
действовать при вычислении логических выражений. Компиляторы некоторых
языков вычисляют каждый элемент логического выражения перед объединением
всех этих элементов и вычисления значения всего выражения. Компиляторы других
используют «короткозамкнутый» (или «ленивый») алгоритм, обрабатывая только
необходимые элементы выражения. Это особенно важно, когда в зависимости от
результатов первой проверки вы можете не захотеть выполнять следующий тест.
Допустим, вы проверяете элементы массива с помощью следующего выражения:

Пример псевдокода неправильной проверки условия
while ( i < MAX_ELEMENTS and item[ i ] 0 ) ...
Если вычисляется выражение целиком, вы получите ошибку при последней ите#
рации цикла. В этом случае переменная i равна maxElements, а значит, выражение
item[ i ] эквивалентно item[ maxElements ], что является недопустимым значением

432

ЧАСТЬ IV

Операторы

индекса. Вы можете возразить, что это не имеет значения, поскольку вы только
обращаетесь к элементу, а не изменяете его. Но это неряшество способно сбить с
толку читателя вашего кода. Во многих средах этот код будет также генерировать
ошибку выполнения или нарушение защиты.
Используя псевдокод, можно реструктурировать данное условие так, чтобы эта
ошибка не возникала:

Пример псевдокода правильно реструктурированной проверки условия
while ( i < MAX_ELEMENTS )
if ( item[ i ] 0 ) then
...
Этот вариант корректен, так как item[ i ] будет вычисляться, только когда i мень#
ше, чем maxElements.
Многие современные языки предоставляют средства, которые изначально предот#
вращают возможность возникновения такой ошибки. Так, C++ использует корот#
козамкнутые вычисления: если значение первого операнда в операции and лож#
но, то второй операнд не вычисляется, потому что полное выражение в любом
случае будет ложным. Иначе говоря, в C++ единственный элемент выражения:

if ( SomethingFalse && SomeCondition ) ...
который будет вычисляться, — это SomethingFalse. Обработка выражения заверша#
ется, поскольку значение SomethingFalse определяется как ложное.
Аналогичное укороченное вычисление будет производиться и для оператора or.
В C++ и Java в выражении:

if ( somethingTrue || someCondition ) ...
вычисляется только somethingTrue. Обработка завершается, как только операнд
somethingTrue определяется как истинный, так как все выражение будет истинным,
если истинна хотя бы одна из его частей. В результате такого способа вычисле#
ния следующее выражение вполне допустимо:

Пример условия, которое работает благодаря короткозамкнутому вычислению (Java)
if ( ( denominator != 0 ) && ( ( item / denominator ) > MIN_VALUE ) ) ...
Если бы это выражение вычислялось целиком, то в случае, когда переменная
denominator равна 0, операция деления во втором операнде генерировала бы
ошибку деления на 0. Но поскольку вторая часть не вычисляется, если значение
первой ложно, то когда denominator равен 0, вторая операция не выполняется, и
ошибка деления на 0 не возникает.
С другой стороны, из#за того что операция && (and) вычисляется слева направо,
следующее логически эквивалентное выражение работать не будет:

ГЛАВА 19 Общие вопросы управления

433

Пример условия, в котором короткозамкнутое вычисление не спасает от ошибки (Java)
if ( ( ( item / denominator ) > MIN_VALUE ) && ( denominator != 0 ) ) ...
Здесь item / denominator вычисляется раньше, чем denominator != 0. Следователь#
но, в этом коде происходит ошибка деления на 0.
Язык Java еще более усложняет эту картину, предоставляя «логические» операто#
ры. Логические операторы & и | языка Java гарантируют, что все элементы будут
вычислены полностью независимо от того, определяется ли истинность или лож#
ность выражения без его полного вычисления. Иначе говоря, в Java такое усло#
вие будет безопасно:

Пример условия, которое работает благодаря
короткозамкнутому (условному) вычислению (Java)
if ( ( denominator != 0 ) && ( ( item / denominator ) > MIN_VALUE ) ) ...
А вот такое — нет:

Пример условия, которое не будет работать, потому что короткозамкнутое
вычисление не гарантируется (Java)
if ( ( denominator != 0 ) & ( ( item / denominator ) > MIN_VALUE ) ) ...
Разные языки используют разные способы вычисления, и случается, что
разработчики языка чересчур свободно обращаются с правилами вычис#
ления выражений, поэтому обратитесь к руководству по вашей версии
языка, чтобы выяснить, как в нем выполняются эти операции. Еще лучше (поскольку
читатель может не обладать вашей сообразительностью) использовать вложенные
условия, проясняющие ваши намерения, и не зависеть от порядка обработки вы#
ражений и короткозамкнутых вычислений.

Упорядочение числовых выражений
в соответствии со значениями на числовой прямой
Организуйте числовые условия так, чтобы они соответствовали порядку точек на
числовой прямой. В общем случае структурируйте выражения так, чтобы сравне#
ния выглядели следующим образом:

MIN_ELEMENTS while ( recordArray.Read( index++ ) ) != recordArray.EmptyRecord() ) {}
Это еще один способ сделать это.
while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) {
;
>}
Создайте для пустых выражений макрос препроцессора или встроенную
функцию DoNothing() Это выражение не делает ничего, кроме бесспорного под#
тверждения того факта, что никакие действия предприниматься не должны. Это
похоже на пометку пустых страниц документа фразами «Эта страница намерен#
но оставлена пустой». На самом деле страница не совсем пустая, но вы знаете, что
ничего другого на ней быть не должно.

438

ЧАСТЬ IV

Операторы

Вот как создать собственный пустой оператор в C++ с помощью #define. (Вы так#
же можете создать inline#функцию, которая дает тот же эффект.)

Пример пустого выражения, выделенного с помощью DoNothing() (C++)
#define DoNothing()
...
while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) {
DoNothing();
}
В дополнение к использованию DoNothing() в пустых циклах while и for можно
задействовать ее в несущественных вариантах оператора switch — добавление
DoNothing() делает очевидным тот факт, что вариант был рассмотрен и никаких
действий предприниматься не должно.
Если ваш язык не поддерживает макросы препроцессора или встроенные функ#
ции, вы можете создать обычный метод DoNothing(), который сразу будет возвра#
щать управление вызывающей стороне.
Подумайте, не будет ли код яснее с непустым телом цикла Большая часть
циклов с пустым телом полагается на побочный эффект в управляющем выраже#
нии цикла. В большинстве случаев код будет читабельнее, если эти побочные дей#
ствия будут выполняться явно, например:

Пример более очевидного цикла с непустым телом (C++)
RecordType record = recordArray.Read( index );
index++;
while ( record != recordArray.EmptyRecord() ) {
record = recordArray.Read( index );
index++;
}
Этот подход требует дополнительной переменной, управляющей циклом, а так#
же большего количества строк, но он делает акцент на простоте программирова#
ния, а не на остроумном использовании побочных эффектов. В промышленном
коде такой акцент предпочтительней.

19.4. Укрощение опасно глубокой вложенности
Чрезмерные отступы (или «вложенность») осуждаются в компьютерной
ли тературе уже на протяжении 25 лет и все еще являются главными обви#
няемыми в создании запутанного кода. В работах Ноума Чомски и Дже#
ральда Вейнберга (Noam Chomsky and Gerald Weinberg) высказывалось предполо#
жение, что немногие люди способны понять более трех уровней вложенных if
(Yourdon, 1986a), и многие исследователи рекомендуют избегать вложенности, пре#
вышающей три или четыре уровня (Myers, 1976; Marca, 1981; Ledgard and Tauer,
1987a). Глубокая вложенность противоречит описанному в главе 5 Главному Тех#
ническому Императиву ПО (управлению сложностью). Это достаточная причина
для отказа от глубокой вложенности.

ГЛАВА 19 Общие вопросы управления

439

Избавиться от глубокой вложенности несложно. Для этого вы можете
переписать проверки условий, выполняемые в блоках if и else, или раз#
бить код на более простые методы.
Упростите вложенные if с помощью повторной проверки части условия
Если вложенность становится слишком глубокой, вы можете уменьшить количе#
ство ее уровней, повторно проверив некоторые условия. Глубина вложенности в
этом примере кода является достаточным основанием для его реструктуризации:

Пример плохого, глубоко вложенного
кода (C++)
if ( inputStatus == InputStatus_Success ) {
// Много кода.
...
if ( printerRoutine != NULL ) {

Перекрестная ссылка Повторная
проверка части условия для
уменьшения сложности аналогична повторному тестированию
статусной переменной. Такой
способ демонстрируется в подразделе «Обработка ошибок и
операторы goto» раздела 17.3.

// Много кода.
...
if ( SetupPage() ) {
// Много кода.
...
if ( AllocMem( &printData ) ) {
// Много кода.
...
}
}
}
}
Этот пример придуман для демонстрации уровней вложенности. Части, обозна#
ченные как // Много кода, подразумевают, что метод содержит достаточно много
строк и простирается на нескольких экранах или нескольких страницах напеча#
танного листинга. Вот как можно видоизменить этот код, используя повторные
проверки, а не вложенность:

Пример кода, милосердно избавленного от вложенности
с помощью повторных проверок (C++)
if ( inputStatus == InputStatus_Success ) {
// Много кода.
...
if ( printerRoutine != NULL ) {
// Много кода.
...
}
}
if ( ( inputStatus == InputStatus_Success ) &&
( printerRoutine != NULL ) && SetupPage() ) {
// Много кода.
...

440

ЧАСТЬ IV

Операторы

if ( AllocMem( &printData ) ) {
// Много кода.
...
}
}
Это чрезвычайно реалистичный пример, так как показывает, что вы не можете
уменьшить уровень вложенности безнаказанно, взамен вам придется формиро#
вать более сложный условия. Однако уменьшение с четырех до двух уровней вло#
женности дает большое улучшение в читабельности, поэтому такой способ стоит
принять во внимание.
Упростите вложенные if с помощью блока с выходом Альтернативой к толь#
ко что описанному подходу будет создание фрагмента кода, который будет вы#
полняться как блок. Если одно из условий в середине блока не выполнится, оста#
ток блока будет пропущен.

Пример использования блока с выходом (C++)
do {
// Начало блока с выходом.
if ( inputStatus != InputStatus_Success ) {
break; // Выходим из блока.
}
// Много кода.
...
if ( printerRoutine == NULL ) {
break; // Выходим из блока.
}
// Много кода.
...
if ( !SetupPage() ) {
break; // Выходим из блока.
}
// Много кода.
...
if ( !AllocMem( &printData ) ) {
break; // Выходим из блока.
}
// Много кода.
...
} while (FALSE); // Конец блока с выходом
Этот способ довольно необычен, поэтому его следует использовать, только если
вся ваша команда разработчиков с ним знакома и он одобрен в качестве подхо#
дящей практики кодирования.
Преобразуйте вложенные if в набор ifthenelse Если вы критически отно#
ситесь к вложенными условиями if, вам будет интересно узнать, что вы можете ре#

ГЛАВА 19 Общие вопросы управления

441

организовать эти конструкции так, чтобы использовать операторы if%then%else
вместо вложенных if. Допустим, у вас есть развесистое дерево решений вроде этого:

Пример заросшего дерева решений (Java)
if ( 10 < quantity ) {
if ( 100 < quantity ) {
if ( 1000 < quantity ) {
discount = 0.10;
}
else {
discount = 0.05;
}
}
else {
discount = 0.025;
}
}
else {
discount = 0.0;
}
Этот фрагмент имеет много недостатков, один из которых в том, что проверяе#
мые условия избыточны. Когда вы удостоверились, что значение quantity больше
1000, вам не нужно дополнительно проверять, что оно больше 100 и больше 10.
А значит, вы можете преобразовать этот код таким образом:

Пример вложенных if, сконвертированных в набор if-then-else (Java)
if ( 1000 < quantity ) {
discount = 0.10;
}
else if ( 100 < quantity ) {
discount = 0.05;
}
else if ( 10 < quantity ) {
discount = 0.025;
}
else {
discount = 0;
}
Это решение проще, чем могло бы быть, потому что закономерность увеличения
чисел проста. Вот как изменить вложенные if, если бы числа не были так упоря#
дочены:

Пример вложенных if, преобразованных в набор if-then-else, для случая,
когда числа не упорядочены (Java)
if ( 1000 < quantity ) {
discount = 0.10;
}

442

ЧАСТЬ IV

Операторы

else if ( ( 100 < quantity ) && ( quantity 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();
Похожий признак плохого кода — наличие специального конструктора, прини#
мающего подмножество нормальных данных инициализации и нужного для на#
писания чего#нибудь вроде:

ГЛАВА 24

Рефакторинг

557

Пример кода подготовки к вызову метода и кода уборки — плохой код (C++)
withdrawal = new WithdrawalTransaction( customerId, balance,
withdrawalAmount, withdrawalDate );
withdrawal.ProcessWithdrawal();
delete withdrawal;
Натолкнувшись на код, подготавливающий программу к вызову метода или вос#
станавливающий ее боеспособность после вызова, спросите себя, формирует ли
интерфейс метода адекватную абстракцию. В последнем примере список парамет#
ров метода ProcessWithdrawal, возможно, следовало бы изменить так:

Пример метода, не требующего ни подготовки, ни уборки, — хороший код (C++)
ProcessWithdrawal( customerId, balance, withdrawalAmount, withdrawalDate );
Заметьте, что похожая проблема возникает и в обратном случае. Так, если у вас
обычно есть объект WithdrawalTransaction, но в метод ProcessWithdrawal переда#
ются только несколько значений объекта, вам следует подумать о рефакторинге
интерфейса метода, чтобы он принимал объект WithdrawalTransaction, а не его
отдельные поля:

Пример кода, требующего нескольких вызовов методов (C++)
ProcessWithdrawal( withdrawal.GetCustomerId(), withdrawal.GetBalance(),
withdrawal.GetWithdrawalAmount(), withdrawal.GetWithdrawalDate() );
Каждый из этих подходов может быть как верным, так и неверным: все зависит
от того, какую абстракцию формирует интерфейс метода ProcessWithdrawal(). Если
абстракция подразумевает, что метод ожидает четыре отдельных элемента дан#
ных, используйте один подход; если метод ожидает сразу весь объект Withdrawa%
lTransaction, выберите другой.
Программа содержит код, который может когда'нибудь понадобиться
Программисты очень плохо угадывают, какая функциональность может потребо#
ваться позднее. «Проектирование впрок» связано со многими предсказуемыми
проблемами, описанными ниже.
 Требования к коду, «проектируемому впрок», не разрабатываются в полном

объеме, поэтому программист скорее всего не угадает эти будущие требова#
ния. Код, «написанный в расчете на будущее», в итоге придется выбросить.
 Если догадка программиста по поводу будущих требований близка к истине,

он все же не сможет предвосхитить все их тонкости. Эти тонкости опроверг#
нут исходные предпосылки проектирования, и «проект, разработанный в рас#
чете на будущее», также придется выбросить.
 Программисты, использующие код, который в свое время был «спроектирован

впрок», не знают об этом или предполагают, что код лучше, чем есть на самом
деле. Они думают, что этот код был написан, протестирован и подвергнут об#
зору с той же тщательностью, что и остальной. Они могут потратить много
времени на создание кода, использующего «спроектированное впрок», только
для того, чтобы обнаружить в итоге, что «спроектированное впрок» на самом
деле не работает.

558

ЧАСТЬ V

Усовершенствование кода

 Дополнительный код, «проектируемый впрок», создает дополнительную слож#

ность, что приводит к дополнительному тестированию, исправлению дефек#
тов и т. д. Результат — замедление работы над проектом.
Мнение экспертов по этому поводу однозначно: если вы хотите наилучшим об#
разом подготовиться к будущим требованиям, не пишите гипотетически нужный
код, а уделите повышенное внимание ясности и понятности кода, который нужен
прямо сейчас, чтобы будущие программисты знали, что он делает, чего не делает,
и могли быстро и правильно изменить его (Fowler, 1999; Beck, 2000).

http://cc2e.com/2443
























Контрольный список: разумные причины
выполнения рефакторинга

 Код повторяется.
 Метод слишком велик.
Цикл слишком велик или слишком глубоко вложен в другие циклы.
Класс имеет плохую связность.
Интерфейс класса не формирует согласованную абстракцию.
Метод принимает слишком много параметров.
Отдельные части класса изменяются независимо от других частей.
При изменении программы требуется параллельно изменять несколько классов.
Вам приходится параллельно изменять несколько иерархий наследования.
Вам приходится параллельно изменять несколько блоков case.
Родственные элементы данных, используемые вместе, не организованы в
классы.
Метод использует больше элементов другого класса, чем своего собственного.
Элементарный тип данных перегружен.
Класс имеет слишком ограниченную функциональность.
По цепи методов передаются бродячие данные.
Объект-посредник ничего не делает.
Один класс слишком много знает о другом классе.
Метод имеет неудачное имя.
Данные-члены сделаны открытыми.
Подкласс использует только малую долю методов своих предков.
Сложный код объясняется при помощи комментариев.
Код содержитглобальные переменные.
Перед вызовом метода выполняется подготовительный код (после вызова
метода выполняется код «уборки»).
Программа содержит код, который может когда-нибудь понадобиться.

Когда не следует выполнять рефакторинг?
Значение слова «рефакторинг» довольно размыто: так называют исправление де#
фектов, реализацию новой функциональности, модификацию проекта — по сути
любое изменение кода. Это неуместно. Целенаправленный процесс изменений может
быть эффективной стратегией, обеспечивающей постепенное повышение качества
программы при ее сопровождении и предотвращающей всем известную смертель#
ную спираль энтропии ПО, но само по себе изменение достоинств не имеет.

ГЛАВА 24

Рефакторинг

559

24.3. Отдельные виды рефакторинга
Ниже вы найдете список видов рефакторинга, многие из которых я сформулиро#
вал на основе более подробных описаний, приведенных в книге «Refactoring»
(Fowler, 1999). Однако я не пытался сделать этот список исчерпывающим. В не#
котором смысле каждый пример плохого и аналогичного хорошего кода в этой
книге претендует на то, чтобы быть видом рефакторинга. Ради экономии места я
привел только те виды рефакторинга, которые счел наиболее полезными.

Рефакторинг на уровне данных
Следующие виды рефакторинга связаны с использованием переменных и других
видов данных.
Замена магического числа на именованную константу Если вы использу#
ете численный или строковый литерал, скажем, 3.14, замените его именованной
константой, такой как PI.
Присвоение переменной более ясного или информативного имени Если имя
переменной неясно, присвойте ей лучшее имя. Конечно, этот же совет относится
и к переименованию констант, классов и методов.
Встраивание выражения в код Замените промежуточную переменную, кото#
рой присваивается результат вычисления выражения, на само выражение.
Замена выражения на вызов метода Этот вид рефакторинга обычно служит
для устранения из кода повторяющихся выражений.
Введение промежуточной переменной Присвойте результат вычисления вы#
ражения промежуточной переменной, имя которой резюмирует суть выражения.
Преобразование многоцелевой переменной в несколько одноцелевых пе'
ременных Если переменная используется более чем с одной целью (к частым
подозреваемым относятся такие переменные, как i, j, temp и x), создайте для каж#
дой из целей отдельную переменную, выбрав для нее более определенное имя.
Использование локальной переменной вместо параметра Если исключи#
тельно входной параметр метода служит в качестве локальной переменной, по#
думайте, не лучше ли создать для этого настоящую локальную переменную.
Преобразование элементарного типа данных в класс Если элементарный
тип данных нужно расширить дополнительными формами поведения (например,
более строгим контролем типа) или дополнительными данными, преобразуйте его
в класс и реализуйте нужное поведение. Это относится и к простым численным
типам вроде Money и Temperature, и к перечислениям, таким как Color, Shape, Country
или OutputType.
Преобразование набора кодов в класс или перечисление
граммах часто встречаются фрагменты вида:

В более старых про#

const int SCREEN = 0;
const int PRINTER = 1;
const int FILE = 2;
Вместо определения таких отдельных констант лучше было бы создать класс Output%
Type: это обеспечило бы вам достоинства более строгого контроля типов и по#

560

ЧАСТЬ V

Усовершенствование кода

зволило бы расширить семантику класса. Создание перечисления — иногда хо#
рошая альтернатива созданию класса.
Преобразование набора кодов в класс, имеющий производные классы Если
разные элементы, ассоциированные с разными типами, могут иметь разное по#
ведение, подумайте о создании базового класса для типа и производных классов
для каждого кода типа. Так, для базового класса OutputType можно было бы создать
подклассы Screen, Printer и File.
Преобразование массива в класс Если элементами массива являются разные
типы, создайте класс, включающий поля для каждого элемента массива.
Инкапсуляция набора Если класс возвращает набор, наличие нескольких эк#
земпляров набора может привести к проблемам с синхронизацией. Сделайте так,
чтобы класс возвращал набор, допускающий только чтение, и создайте методы для
добавления элементов в набор и их удаления.
Замена традиционной записи на класс данных Создайте класс, содержащий
члены записи (record). Это позволит централизовать проверку ошибок, слежение
за персистентностью и выполнение других операций, касающихся записи.

Рефакторинг на уровне отдельных операторов
Декомпозиция логического выражения Упростите логическое выражение, вве#
дя грамотно названные промежуточные переменные, документирующие суть вы#
ражения.
Вынесение сложного логического выражения в грамотно названную булеву
функцию Если выражение достаточно сложное, этот вид рефакторинга повысит удо#
бочитаемость кода. Если выражение используется более одного раза, он исключит
необходимость внесения параллельных изменений и снизит вероятность ошибок.
Консолидация фрагментов, повторяющихся в разных частях условного
оператора Если в конце блока else содержится такой же фрагмент кода, что и
в конце блока if, вынесите этот фрагмент за пределы оператора if%then%else.
Использование оператора break или return вместо управляющей перемен'
ной цикла Если для управления циклом используется такая переменная, как done,
удалите ее и выполняйте выход из цикла при помощи оператора break или return.
Возврат из метода сразу после получения ответа вместо установки воз'
вращаемого значения внутри вложенных операторов ifthenelse Выход
из метода сразу же после нахождения возвращаемого значения часто помогает об#
легчить чтение кода и снизить вероятность ошибок. Альтернативный вариант —
установка возвращаемого значения и следование к выходу из метода через зато#
ры операторов — может оказаться более сложным.
Замена условных операторов (особенно многочисленных блоков case) на
вызов полиморфного метода Значительную часть логики, обычно включае#
мой при структурном программировании в блоки case, можно реализовать с по#
мощью наследования в виде полиморфных методов.
Создание и использование «пустых» объектов вместо того, чтобы про'
верять, равно ли значение null Иногда «пустой» объект должен иметь обоб#

ГЛАВА 24

Рефакторинг

561

щенное поведение или обобщенные данные, например, определять неизвестно#
го человека как «гражданина». В этом случае подумайте о перемещении ответствен#
ности за обработку значений null из клиентского кода в класс, т. е. вместо того,
чтобы проверять в клиентском коде класса Customer (заказчик), известно ли имя
заказчика, и подставлять значение «гражданин», если оно неизвестно, определи#
те неизвестного заказчика как «гражданина» прямо в классе Customer.

Рефакторинг на уровне отдельных методов
Извлечение метода из другого метода
ный метод.

Превратите фрагмент метода в отдель#

Встраивание кода метода Если метод прост и понятен, можете не вызывать
его, а встроить его код в программу.
Преобразование объемного метода в класс Если метод слишком велик, по#
пробуйте превратить его в класс и разбить на несколько методов. Иногда это по#
вышает удобочитаемость кода.
Замена сложного алгоритма на простой
не требует.

Этот вид рефакторинга пояснений

Добавление параметра Если метод должен получать из вызывающего кода боль#
ше информации, передавайте ее в форме дополнительного параметра.
Удаление параметра

Если метод не нуждается в каком#то параметре, удалите его.

Отделение операций запроса данных от операций изменения данных
Как правило, операции запроса данных не должны изменять состояние объекта.
Если операция вроде GetTotals() (получение итоговой суммы) изменяет состояние
объекта, разделите функциональность запроса и функциональность изменения со#
стояния объекта, создав два отдельных метода.
Объединение похожих методов путем их параметризации Два похожих
метода могут различаться только используемой в них константой. Объедините их
в один метод и передавайте в него нужное значение как параметр.
Разделение метода, поведение которого зависит от полученных пара'
метров Если метод выполняет разный код в зависимости от значения входно#
го параметра, подумайте о разбиении метода на отдельные методы, которые можно
было бы вызывать, не передавая этот входной параметр.
Передача в метод целого объекта вместо отдельных полей Если вы пе#
редаете в метод несколько значений одного объекта, подумайте о таком измене#
нии интерфейса метода, чтобы он принимал сразу весь объект.
Передача в метод отдельных полей вместо целого объекта Если вы со#
здаете объект только затем, чтобы передать его в метод, подумайте о таком изме#
нении метода, чтобы он принимал отдельные поля, а не весь объект.
Инкапсуляция нисходящего приведения типов При возвращении объекта из
метода обычно следует возвращать максимально определенный тип объекта. Это
особенно справедливо для методов, возвращающих объекты#итераторы, наборы,
элементы наборов и т. д.

562

ЧАСТЬ V

Усовершенствование кода

Рефакторинг реализации классов
Замена объектов'значений на объекты'ссылки Если вы создаете и поддер#
живаете много копий крупных или сложных объектов, измените подход так, что#
бы существовал только один оригинал объекта (объект#значение), а в остальном
коде использовались ссылки на этот объект (объекты#ссылки).
Замена объектов'ссылок на объекты'значения Если вам приходится при#
лагать усилия для обработки ссылок на небольшие или простые объекты, сделай#
те все объекты#ссылки объектами#значениями.
Замена виртуальных методов на инициализацию данных Если у вас есть
набор подклассов, различающихся только возвращаемыми константами, не пере#
определяйте методы#члены в подклассах, а инициализируйте базовый класс в
подклассах соответствующими константами и включите в него обобщенный код,
использующий эти константы.
Изменение положения методов'членов или данных'членов в иерархии насле'
дования Подумайте о внесении в иерархию наследования нескольких общих
изменений. Следующие изменения обычно выполняются для устранения повто#
рений кода в производных классах:
 перемещение метода в суперкласс;
 перемещение поля в суперкласс;
 перемещение тела конструктора в суперкласс.

Другие изменения обычно вносятся с целью поддержки специализации в произ#
водных классах:
 перемещение метода в производные классы;
 перемещение поля в производные классы;
 перемещение тела конструктора в производные классы.

Перемещение специализированного кода в подкласс Если какой#то код класса
используется только в подмножестве его экземпляров, переместите этот специа#
лизированный код в отдельный подкласс.
Объединение похожего кода и его перемещение в суперкласс Если несколько
подклассов включают похожий код, объедините этот код и переместите его в су#
перкласс.

Рефакторинг интерфейсов классов
Перемещение метода в другой класс Создайте в целевом классе новый ме#
тод и переместите тело метода из исходного класса в целевой класс. После этого
вы можете вызывать новый метод из старого.
Разделение одного класса на несколько Если класс имеет более одной обла#
сти ответственности, разбейте его на несколько классов, имеющих ясно опреде#
ленные области ответственности.
Удаление класса Если класс почти ничего не делает, переместите его код в другие,
более связные классы и удалите его.
Сокрытие делегата Иногда Класс A вызывает и Класс B, и Класс C, тогда как
на самом деле A должен вызывать только B, а B — C. Спросите себя, какова пра#

ГЛАВА 24

Рефакторинг

563

вильная абстракция взаимодействия Класса A с Классом B. Если за вызов C дол#
жен отвечать B, внесите нужные изменения.
Удаление посредника Если Класс A вызывает Класс B, а Класс B вызывает Класс
C, подумайте, не лучше ли вызывать C непосредственно из A. Целесообразность
делегирования полномочий Классу B зависит от того, улучшит ли это целостность
его интерфейса или ухудшит.
Замена наследования на делегирование Если из одного класса нужно исполь#
зовать другой класс, но вы хотите получить больший контроль над интерфейсом
второго класса, сделайте суперкласс полем бывшего подкласса и создайте для
доступа к нему набор открытых методов, формирующих связную абстракцию.
Замена делегирования на наследование Если класс предоставляет доступ ко
всем открытым методам класса#делегата (класса#члена), выполните наследование
от класса#делегата, а не просто используйте его.
Создание внешнего метода Если в класс нужно включить дополнительный ме#
тод, но изменять класс нельзя, вы можете создать нужный метод в клиентском
классе.
Создание класса'расширения Если в класс нужно включить несколько допол#
нительных методов, но изменять класс нельзя, вы можете создать новый класс,
объединяющий функциональность неизменяемого класса с дополнительной функ#
циональностью. Для этого вы можете или выполнить наследование от исходного
класса и добавить новые методы в подклассы, или заключить класс в оболочку,
предоставив доступ к нужным методам.
Инкапсуляция открытой переменной'члена Если данные#члены открыты,
сделайте их закрытыми и реализуйте доступ к ним при помощи методов.
Удаление методов установки значений неизменяемых полей Если поле
предполагается устанавливать во время создания объекта и не изменять впослед#
ствии, инициализируйте поле в конструкторе объекта и не создавайте вводящий
в заблуждение метод Set().
Сокрытие методов, которые не следует вызывать извне класса
метода интерфейс класса будет более согласованным, скройте метод.

Если без

Инкапсуляция неиспользуемых методов Если обычно вы используете толь#
ко часть интерфейса класса, создайте новый интерфейс, предоставляющий до#
ступ только к необходимым методам. Убедитесь в том, что новый интерфейс фор#
мирует согласованную абстракцию.
Объединение суперкласса и подкласса, имеющих очень похожую реали'
зацию Если степень специализации подкласса невысока, объедините его с су#
перклассом.

Рефакторинг на уровне системы
Создание эталонного источника данных, которые вы не можете конт'
ролировать Иногда какие#то данные трудно согласованно использовать из дру#
гих объектов, которым нужны эти данные. В качестве примера можно привести
данные элемента управления с GUI#интерфейсом. В этом случае вы можете создать

564

ЧАСТЬ V

Усовершенствование кода

класс, воспроизводящий данные элемента управления, и рассматривать этот класс
как эталонный источник данных и для элемента управления, и для другого кода.
Изменение однонаправленной связи между классами на двунаправленную
Если два класса должны использовать возможности друг друга, но только один класс
знает о другом классе, измените классы так, чтобы они оба знали друг о друге.
Изменение двунаправленной связи между классами на однонаправленную
Если два класса известны друг другу, но на самом деле только один класс должен
знать о другом, измените характер связи между классами.
Предоставление фабричного метода вместо простого конструктора Ис#
пользуйте фабричный метод, если вам нужно создавать объекты на основе кода типа
или если вы хотите работать с объектами#ссылками, а не объектами#значениями.
Замена кодов ошибок на исключения или наоборот Убедитесь, что вы ис#
пользуете стандартный подход к обработке ошибок, основанный на той или иной
стратегии.

http://cc2e.com/2450














Контрольный список: виды рефакторинга

Рефакторинг на уровне данных
 Замена магического числа на именованную константу.
Присвоение переменной более ясного или информативного имени.
Встраивание выражения в код.
Замена выражения на вызов метода.
Введение промежуточной переменной.
Преобразование многоцелевой переменной в несколько одноцелевых переменных.
Использование локальной переменной вместо параметра.
Преобразование элементарного типа данных в класс.
Преобразование набора кодов в класс или перечисление.
Преобразование набора кодов в класс, имеющий производные классы.
Преобразование массива в класс.
Инкапсуляция набора.
Замена традиционной записи на класс данных.

Рефакторинг на уровне отдельных операторов
 Декомпозиция логического выражения.
 Вынесение сложного логического выражения в грамотно названную булеву
функцию.
 Консолидация фрагментов, повторяющихся в разных частях условного оператора.
 Использование оператора break / return вместо управляющей переменной
цикла.
 Возврат из метода сразу после получения ответа вместо установки возвращаемого значения внутри вложенных операторов if-then-else.
 Замена условных операторов (особенно многочисленных блоков case) на вызов
полиморфного метода.
 Создание и использование «пустых» объектов вместо проверки того, равно
ли значение null.

ГЛАВА 24

Рефакторинг

565

Рефакторинг на уровне отдельных методов
 Извлечение метода из другого метода.
 Встраивание кода метода.
 Преобразование объемного метода в класс.
 Замена сложного алгоритма на простой.
 Добавление параметра.
 Удаление параметра.
 Отделение операций запроса данных от операций изменения данных.
 Объединение похожих методов при помощи их параметризации.
 Разделение метода, поведение которого зависит от полученных параметров.
 Передача в метод целого объекта вместо отдельных полей.
 Передача в метод отдельных полей вместо целого объекта.
 Инкапсуляция нисходящего приведения типов.
Рефакторинг реализации классов
 Замена объектов-значений на объекты-ссылки.
 Замена объектов-ссылок на объекты-значения.
 Замена виртуальных методов на инициализацию данных.
 Изменение положения методов-членов или данных-членов в иерархии наследования.
 Перемещение специализированного кода в подкласс.
 Объединение похожего кода и его перемещение в суперкласс.
Рефакторинг интерфейсов классов
 Перемещение метода в другой класс.
 Разделение одного класса на несколько.
 Удаление класса.
 Сокрытие делегата.
 Удаление посредника.
 Замена наследования на делегирование.
 Замена делегирования на наследование.
 Создание внешнего метода.
 Создание класса-расширения.
 Инкапсуляция открытой переменной-члена.
 Удаление методов установки значений неизменяемых полей.
 Сокрытие методов, которые не следует вызывать извне класса.
 Инкапсуляция неиспользуемых методов.
 Объединение суперкласса и подкласса, имеющих очень похожую реализацию.
Рефакторинг на уровне системы
 Создание эталонного источника данных, которые вы не можете контролировать.
 Изменение однонаправленной связи между классами на двунаправленную.
 Изменение двунаправленной связи между классами на однонаправленную.
 Предоставление фабричного метода вместо простого конструктора.
 Замена кодов ошибок на исключения или наоборот.

566

ЧАСТЬ V

Усовершенствование кода

24.4. Безопасный рефакторинг
Вмешательство в рабочую систему больше похоже на вскрытие головного мозга и замену
нерва, чем на замену прокладки в кране. Облегчилось ли бы
сопровождение программ, если
б оно называлось «нейрохирургией ПО»?
Джеральд Вайнберг
(Gerald Weinberg)

Рефакторинг — эффективный способ повышения качества
кода. Но, как и все эффективные инструменты, при невер#
ном использовании он может причинить вред. Несколько
простых советов помогут предотвратить неверное приме#
нение рефакторинга.
Сохраняйте первоначальный код Перед началом рефак#
торинга убедитесь, что вы сможете вернуться к коду, с ко#
торого начинаете. Сохраните код в системе управления вер#
сиями или скопируйте корректные файлы в резервный ка#
талог.

Стремитесь ограничить объем отдельных видов рефакторинга Некото#
рые виды рефакторинга масштабнее других, к тому же не всегда можно точно ска#
зать, что именно составляет «один вид рефакторинга». Чтобы четко представлять
все следствия вносимых изменений, не раздувайте виды рефакторинга. Примеры
соблюдения этого принципа см. в книге «Refactoring» (Fowler, 1999).
Выполняйте отдельные виды рефакторинга по одному за раз Некоторые
виды рефакторинга сложнее других. За исключением самых простых случаев
выполняйте все виды рефакторинга по одному за раз, компилируя и тестируя
программу перед следующим видом рефакторинга.
Составьте список действий, которые вы собираетесь предпринять Ес#
тественным расширением Процесса Программирования с Псевдокодом является
составление списка видов рефакторинга, которые приведут вас из точки А в точ#
ку Б. Составление такого списка поможет поддерживать каждое изменение в со#
ответствующем контексте.
Составьте и поддерживайте список видов рефакторинга, которые сле'
дует выполнить позже Выполняя один вид рефакторинга, вы можете счесть
необходимым еще один вид. Взявшись за него, вы можете обнаружить в коде не#
достатки, призывающие к третьему виду. Если изменение не требуется сию мину#
ту, включите его в список изменений, которые следовало бы внести в программу
когда#то, но не обязательно вносить прямо сейчас.
Часто создавайте контрольные точки Иногда рефакторинг внезапно ухо#
дит в сторону, поэтому вам следует не только сохранять первоначальный код, но
и создавать контрольные точки на разных этапах рефакторинга, чтобы можно было
вернуться к работоспособной программе, если рефакторинг заведет вас в тупик.
Используйте предупреждения компилятора Компилятор часто не замеча#
ет небольших ошибок. Задав компилятору самый строгий уровень диагностики,
вы сможете исправлять многие ошибки почти сразу после их внесения.
Выполняйте регрессивное тестирование Дополните обзоры измененного
кода регрессивным тестированием. Конечно, это зависит от наличия хорошего
набора тестов (см. главу 22).

ГЛАВА 24

Рефакторинг

567

Создавайте дополнительные тесты Не ограничивайтесь регрессивным тес#
тированием программы с использованием старых тестов — создавайте новые
блочные тесты для проверки нового кода. Тесты, устаревшие в результате рефак#
торинга, удаляйте.
Выполняйте обзоры изменений Если обзоры важны при
Перекрестная ссылка Об обзопервоначальном написании кода, то при последующих изме#
рах см. главу 21.
нениях их важность лишь повышается. Эд Йордон сообщает,
что первая попытка внесения изменения в код более чем в половине случаев оказы#
вается ошибочной (Yourdon, 1986b). Интересно, что, если программисты имеют дело
не с несколькими строками кода, а с более объемным фрагментом, вероятность вне#
сения корректного изменения более высока (рис. 24#1). Точнее говоря, по мере уве#
личения числа изменяемых строк с одной до пяти вероятность внесения непра#
вильного изменения повышается, а после этого снижается.

Рис. 24'1. Небольшие изменения чаще оказываются ошибочными,
чем более крупные (Weinberg, 1983)

Программисты относятся к небольшим изменениям легкомысленно. Они не ана#
лизируют их, не просят коллег выполнить их обзор, а иногда даже не запускают
программу, чтобы проверить, что исправление корректно.
Мораль проста: рассматривайте простые изменения так, как если бы они
были сложными. В одной организации, в которой были введены обзоры
изменений одной строки кода, было обнаружено, что доля ошибочных
изменений снизилась с 55% до 2% (Freedman and Weinberg, 1982). В другой орга#
низации, работающей в сфере телекоммуникаций, введение обзоров изменений
позволило повысить их корректность с 86% до 99,6% (Perrott, 2004).
Изменяйте подход в зависимости от рискованности рефакторинга Не#
которые виды рефакторинга сопряжены с более высоким риском, чем другие. Так,
«замена магического числа на именованную константу» относительно безопасна.
Виды рефакторинга, предполагающие изменение интерфейса класса или метода,
схемы БД или булевых тестов, обычно более рискованны. Если рефакторинг не#
сложен, вы можете оптимизировать процесс рефакторинга, выполняя более од#
ного вида за раз и ограничиваясь регрессивным тестированием кода без его офи#
циального обзора.

568

ЧАСТЬ V

Усовершенствование кода

В случае более рискованных видов рефакторинга будьте более осторожны. Вы#
полняйте их по одному за раз. Попросите кого#то провести обзор изменения или
выполните рефакторинг в паре, дополнив этим обычную проверку кода средствами
компилятора и блочное тестирование.

Плохие причины выполнения рефакторинга
Несмотря на всю свою эффективность, рефакторинг — не панацея и допускает
несколько специфических видов злоупотребления.
Не рассматривайте рефакторинг как оправдание на'
писания плохого кода с намерением исправить его
позднее Самой большой проблемой с рефакторингом яв#
ляется его неверное применение. Иногда программисты
говорят, что они выполняют рефакторинг, хотя на самом
Джон Манзо (John Manzo)
деле они просто пробуют что попало в надежде хоть как#
то привести код в работоспособное состояние. Рефакторинг — это изменение
работоспособного кода, не влияющее на поведение программы. Программисты,
которые возятся с плохим кодом, не выполняют рефакторинг — они занимаются
хакерством.

Не реализуйте какую-то возможность частично, намереваясь позднее завершить ее в
процессе рефакторинга.

Не рассматривайте рефакторинг как способ, позво'
ляющий избежать переписывания кода Иногда код
невозможно улучшить небольшими изменениями — его нуж#
Кент Бек (Kent Beck)
но выбросить и переписать с нуля. Если вы выполняете круп#
номасштабный рефакторинг, спросите себя, не следует ли вместо этого перепро#
ектировать и переписать фрагмент кода.

Крупномасштабный рефакторинг — путь к катастрофе.

24.5. Стратегии рефакторинга
Число видов рефакторинга, выгодных для любой конкретной программы, прак#
тически бесконечно. Рефакторинг подчиняется тому же закону снижения выго#
ды, что и другие процессы программирования, и к нему также относится прави#
ло 80/20. Тратьте время на 20% видов рефакторинга, обеспечивающих 80% выго#
ды. При определении наиболее важных видов рефакторинга учитывайте следую#
щие советы.
Выполняйте рефакторинг при создании новых методов Создавая метод,
проверьте, хорошо ли организованы связанные с ним методы. При необходимо#
сти выполните их рефакторинг.
Выполняйте рефакторинг при создании новых классов Создание нового
класса часто подчеркивает недостатки имеющегося кода. Используйте эту возмож#
ность для рефакторинга других классов, тесно взаимодействующих с новым классом.
Выполняйте рефакторинг при исправлении дефектов Используйте знания,
полученные при исправлении ошибки, для улучшения других фрагментов кода,
которые могут быть подвержены похожим ошибкам.
Выполняйте рефакторинг модулей, подверженных ошибкам Некоторые
модули более подвержены ошибкам, чем другие. Есть ли в коде фрагмент, внуша#
ющий страх вам и всем остальным членам вашей группы? Скорее всего он под#

ГЛАВА 24

вержен ошибкам. Большинство людей естественным образом
стараются избегать таких проблемных фрагментов кода,
однако вы добьетесь большего успеха, если будете рассмат#
ривать их как мишени рефакторинга (Jones, 2000).

Рефакторинг

569

Перекрестная ссылка О коде,
подверженном ошибкам, см.
подраздел «Какие классы содержат наибольшее число ошибок?» раздела 22.4.

Выполняйте рефакторинг сложных модулей Другим
подходом к рефакторингу является концентрация на моду#
лях, имеющих максимальные оценки сложности (см. подраздел «Как измерить слож#
ность» раздела 19.6). Одно классическое исследование показало, что использова#
ние этой методики при сопровождении программ приводило к существенному
повышению их качества (Henry and Kafura, 1984).

При сопровождении программы улучшайте фрагменты, к которым при'
касаетесь Код, который никогда не изменяется, не требует рефакторинга. Но
если вы все же изменяете фрагмент кода, убедитесь, что вы оставляете его в луч#
шем состоянии, чем обнаружили.
Определите интерфейс между аккуратным и безобразным кодом и пере'
местите безобразный код на другую сторону этого интерфейса «Реаль#
ность» часто оказывается грязнее, чем нам хотелось бы. Эта грязь может быть
обусловлена сложными бизнес#правилами, интерфейсами с оборудованием, про#
граммными интерфейсами и т. д. При работе со старыми системами часто при#
ходится иметь дело с плохим кодом, который тем не менее должен постоянно
оставаться работоспособным.
Эффективной стратегией омоложения внедренных старых систем является опре#
деление фрагментов, относящихся к грязному реальному миру, фрагментов, фор#
мирующих идеализированный новый мир, и фрагментов, определяющих интер#
фейс между двумя мирами (рис. 24#2).

Рис. 24'2. Ваш код не обязан быть грязным только потому, что таков реальный
мир. Рассматривайте систему как комбинацию идеального кода, грязного кода
и интерфейса между идеальным и грязным кодом

570

ЧАСТЬ V

Усовершенствование кода

Работая с системой, вы можете постепенно перемещать грязный код через «интер#
фейс между двумя мирами» в более организованный идеальный мир. Если вы толь#
ко начинаете работать с унаследованной системой, она может почти полностью
относиться к грязному миру. Одна эффективная политика подразумевает, что каж#
дый раз, когда вы прикасаетесь к какому#то фрагменту грязного кода, вы должны
привести его в соответствие с текущими стандартами кодирования, присвоить пе#
ременным ясные имена и т. д. — иначе говоря, переместить его в идеальный мир.
Со временем это может обеспечить быстрое улучшение базы кода (рис. 24#3).

Рис. 24'3. Одной из стратегий улучшения готовой системы является постепенный
рефакторинг плохого унаследованного кода — перемещение его на другую сторону
«интерфейса между двумя мирами»

http://cc2e.com/2457












Контрольный список: безопасный рефакторинг

 Является ли каждое изменение частью систематичной
стратегии изменений?
Сохранили ли вы первоначальный код перед началом рефакторинга?
Стараетесь ли вы ограничить объем каждого вида рефакторинга?
Выполняете ли вы отдельные виды рефакторинга по одному за раз?
Составили ли вы список действий, которые собираетесь предпринять во время
рефакторинга?
Ведете ли вы список видов рефакторинга, которые следовало бы выполнить позднее?
Выполняете ли вы регрессивное тестирование после каждого вида рефакторинга?
Выполняете ли вы обзор сложных изменений и изменений, влияющих на
критически важный код?
Рассматриваете ли вы рискованность отдельных видов рефакторинга и адаптируете ли вы свой подход соответствующим образом?
Убеждаетесь ли вы, что изменения улучшают внутреннее качество программы,
а не ухудшают его?
Не рассматриваете ли вы рефакторинг как оправдание написания плохого
кода или как способ избежать переписывания плохого кода?

ГЛАВА 24

Рефакторинг

571

Дополнительные ресурсы
Процесс рефакторинга имеет много общего с процессом
http://cc2e.com/2464
устранения дефектов (см. раздел 23.3). Факторы риска, свя#
занные с рефакторингом, похожи на факторы риска, каса#
ющиеся оптимизации кода. Об управлении факторами риска при оптимизации
кода см. раздел 25.6.
Fowler, Martin. Refactoring: Improving the Design of Existing Code. Reading, MA: Add#
ison Wesley, 1999. Это самое полное и подробное руководство по рефакторингу
содержит детальное обсуждение многих конкретных видов рефакторинга, упомя#
нутых в этой главе, а также других видов, которых я не касался. Фаулер привел
много примеров пошагового выполнения каждого вида рефакторинга.

Ключевые моменты
 Изменения программы неизбежны как во время первоначальной разработки,

так и после выпуска первой версии.
 Изменения могут приводить как к улучшению, так и к ухудшению ПО. Главное

Правило Эволюции ПО заключается в том, что при эволюции кода внутрен#
нее качество программы должно повышаться.
 Одним из условий успешности рефакторинга является пристальное внимание

к многочисленным предупреждающим знакам — «запахам», указывающим на
необходимость рефакторинга.
 Другое условие — изучение многих конкретных видов рефакторинга.
 Заключительным условием успешности рефакторинга является следование

стратегии безопасного рефакторинга. Одни подходы к рефакторингу лучше,
а другие хуже.
 Рефакторинг во время разработки — самая благоприятная возможность улуч#

шения программы и внесения в нее всех изменений, которые вам так или иначе
захочется внести позднее. Используйте эту возможность!

572

ЧАСТЬ V

Г Л А В А

Усовершенствование кода

2 5

Стратегии
оптимизации кода

http://cc2e.com/2578

Содержание
 25.1. Общее обсуждение производительности ПО
 25.2. Введение в оптимизацию кода
 25.3. Где искать жир и патоку?
 25.4. Оценка производительности
 25.5. Итерация
 25.6. Подход к оптимизации кода: резюме

Связанные темы
 Методики оптимизации кода: глава 26
 Архитектура ПО: раздел 3.5

В этой главе обсуждается исторически противоречивая проблема — повышение
производительности кода. В 1960#х годах ресурсы компьютеров были крайне
ограниченны, поэтому эффективность их использования была вопросом перво#
степенной важности. По мере роста производительности компьютеров в 70#х
программисты начали понимать, насколько упор на производительность вредит
удобочитаемости и легкости сопровождения кода, и оптимизация кода отошла на
задний план. Вместе с микрокомпьютерной революцией, свидетелями которой мы
стали в 80#х, проблема эффективного использования ресурсов вернулась, но в 90#х
ее важность постепенно уменьшилась. В 2000#х мы опять столкнулись с этой про#
блемой, только теперь она связана с ограниченной памятью мобильных телефо#
нов, карманных ПК и подобных устройств, а также со временем выполнения ин#
терпретируемого кода.
Проблемы с производительностью можно решать на двух уровнях: стратегическом
и тактическом. В этой главе рассматриваются стратегические вопросы произво#
дительности: мы выясним, что такое производительность, насколько она важна,
и обсудим общий подход к ее повышению. Если стратегии достижения нужной
производительности вам уже хорошо известны и вы хотите узнать конкретные

ГЛАВА 25 Стратегии оптимизации кода

573

методики ее повышения на уровне кода, можете перейти к главе 26. Однако прежде
чем приступать к серьезной работе над повышением производительности, хотя
бы просмотрите эту главу, чтобы не потратить время на оптимизацию кода тог#
да, когда следовало бы заняться чем#то другим.

25.1. Общее обсуждение производительности ПО
Оптимизация кода — лишь один из способов повышения производительности
программы. Часто можно найти другие способы, обеспечивающие большее повы#
шение производительности за меньшее время и с меньшим вредом для кода.

Характеристики качества
и производительность
Некоторые люди смотрят на мир через розовые очки. Про#
граммисты склонны воспринимать мир через кодовые очки.
Мы полагаем, что чем лучше будет наш код, тем сильнее наше
ПО понравится клиентам.

Во имя эффективности — причем достигается она далеко не
всегда — совершается больше
компьютерных грехов, чем по
любой другой причине, включая
банальную глупость.

Эта точка зрения верна лишь отчасти. Пользователей боль#
ше интересуют явные характеристики программы, а не ка#
У. А. Вульф (W. A. Wulf)
чество кода. Производительность обычно привлекает их
внимание, только когда она сказывается на их работе. Боль#
шее значение для пользователей имеет не грубая производительность приложе#
ния, а объем информации, который оно позволяет обработать за конкретный срок,
а также такие факторы, как своевременное получение программы, ясный пользо#
вательский интерфейс и предотвращение простоев.
Приведу пример. Я делаю цифровой камерой минимум 50 снимков в неделю. Чтобы
скопировать снимки на компьютер при помощи ПО, поставляемого с камерой, я
должен выбрать каждый снимок по очереди, причем в окне программы отобра#
жаются только 6 снимков сразу. В результате копирование 50 изображений пре#
вращается в долгий и нудный процесс, требующий десятков щелчков и массы
переключений между окнами. Устав от всего этого, я купил карту памяти, подклю#
чаемую прямо к компьютеру и воспринимаемую им как диск. Теперь для копиро#
вания изображений на диск компьютера я могу использовать Проводник Windows.
Я делаю пару щелчков, нажимаю Ctrl+A и перетаскиваю все файлы в нужное мес#
то. Меня не волнует, передает ли карта памяти каждый файл вдвое медленнее или
быстрее, чем другое ПО, потому что сейчас я могу обработать больший объем
информации за меньшее время. Каким бы быстрым или медленным ни был код
драйвера карты памяти, его производительность выше.
Производительность только косвенно связана с быстродействием кода.
Чем больше вы работаете над скоростью кода, тем меньше внимания
уделяете другим характеристикам его качества. Не приносите их в жер#
тву быстродействию. Стремление к повышению быстродействия может снизить
общую производительность программы, а не повысить ее.

574

ЧАСТЬ V

Усовершенствование кода

Производительность и оптимизация кода
Решив, что эффективность кода — будь то его быстродействие или объем — дол#
жна быть приоритетом, не торопитесь улучшать быстродействие или объем на
уровне кода, а рассмотрите несколько вариантов. Подумайте об эффективности
в контексте:
 требований к программе;
 проекта программы;
 проектов классов и методов;
 взаимодействия программы с ОС;
 компиляции кода;
 оборудования;
 оптимизации кода.

Требования к программе
Высокая производительность считается требованием гораздо чаще, чем на самом
деле им является. Барри Бом рассказывает, что при разработке одной системы в
компании TRW сначала решили, что время реакции системы не должно превы#
шать 1 секунды. Это требование привело к разработке очень сложного проекта,
на реализацию которого пришлось бы потратить примерно 100 млн долларов. Даль#
нейший анализ показал, что в 90% случаев пользователей устроит время реакции,
равное 4 секундам. Изменение требования позволило снизить общую стоимость
системы примерно на 70 млн долларов (Boehm, 2000b).
Прежде чем тратить время на решение проблемы с производительностью, убеди#
тесь, что она действительно требует решения.

Проект программы
Перекрестная ссылка Проектирование высокопроизводительных программ рассматривается
в работах, указанных в разделе «Дополнительные ресурсы»
в конце главы.

Проект программы определяет ее основные черты — глав#
ным образом, способ ее разделения на классы. Проект мо#
жет как облегчить, так и затруднить создание высокопро#
изводительной системы.

Например, при высокоуровневом проектировании одной
реальной программы сбора и обработки данных в качестве
ключевого атрибута была определена пропускная способность обработки резуль#
татов измерений. Каждое измерение включало определение значения электриче#
ской величины, калибровку значения, масштабирование значения и преобразо#
вание исходных единиц измерения (таких как милливольты) в единицы прикладной
области (такие как градусы Цельсия).
Если бы при высокоуровневом проектировании программисты не оценили все
факторы риска, им в итоге пришлось бы оптимизировать алгоритмы вычисления
многочленов 13 степени, т. е. многочленов, содержащих 14 переменных с макси#
мальной степенью 13. Вместо этого они решили проблему, выбрав другое обору#
дование и разработав высокоуровневый проект, предусматривающий использо#
вание десятков многочленов 3 степени. Оптимизировав код, они вряд ли получи#
ли бы нужные результаты. Это пример проблемы, которую нужно было решать на
уровне проектирования.

ГЛАВА 25 Стратегии оптимизации кода

Если объем и быстродействие программы действительно
важны, разработайте архитектуру так, чтобы она позволя#
ла добиться намеченных показателей, а затем задайте для
отдельных подсистем, функций и классов целевые показа#
тели использования ресурсов. Преимущества такого подхода
таковы.

575

Перекрестная ссылка О том, как
программисты стремятся к достижению поставленных целей,
см. подраздел «Задание целей»
раздела 20.2.

 Задание отдельных целевых показателей использования ресурсов делает про#

изводительность системы предсказуемой. Если каждая функция соответствует
целевым показателям, вся система будет обладать нужной производительнос#
тью. Вы можете уже на ранних этапах определить проблемные подсистемы и
перепроектировать их или оптимизировать их код.
 Простое задание явных целей повышает вероятность их достижения. Програм#

мисты стремятся к достижению целей, если знают, каковы они; чем определен#
нее цели, тем легче к ним стремиться.
 Вы можете поставить цели, которые непосредственно не направлены

на повышение эффективности, но способствуют этому в долгосрочной
перспективе. Эффективность часто лучше всего рассматривать в контексте
других аспектов. Так, простота внесения изменений может создать лучшие
условия для достижения высокой эффективности, чем явное определение эф#
фективности как одной из целей. Если проект отличается высокой степенью
модульности и простотой изменения, менее эффективные компоненты мож#
но с легкостью заменить на более эффективные.

Проекты классов и методов
Проектирование классов и методов предоставляет еще одну
Перекрестная ссылка О типах
возможность повышения производительности ПО. Одним из
данных и алгоритмах см. книспособов повышения производительности на этом уровне
ги, указанные в разделе «Дополнительные ресурсы» в конце
является выбор типа данных или алгоритма, от чего обычно
главы.
зависит и объем используемой памяти, и быстрота выполне#
ния кода. Именно на этом уровне вы можете выбрать быст#
рую сортировку вместо пузырьковой или двоичный поиск вместолинейного.

Взаимодействие программы с ОС
Если ваша программа работает с внешними файлами, дина#
Перекрестная ссылка О стратемической памятью или устройствами вывода, она скорее
гиях борьбы со слишком медвсего взаимодействует с ОС. Низкая производительность в
ленными или объемными метоэтом случае может объясняться большим объемом или мед#
дами ОС на уровне кода см.
главу 26.
ленным выполнением методов ОС. Вы можете даже не знать,
что программа взаимодействует с ОС: иногда вызовы сис#
темных методов генерируются компилятором или содержатся в коде библиотек.

Компиляция кода
Хорошие компиляторы преобразуют ясный высокоуровневый код в оптимизиро#
ванный машинный код. Иногда правильный выбор компилятора позволяет вооб#
ще забыть о дальнейшей оптимизации кода.

576

ЧАСТЬ V

Усовершенствование кода

В главе 26 вы найдете многочисленные примеры ситуаций, когда код, сгенерирован#
ный компилятором, оказывается эффективнее кода, оптимизированного вручную.

Оборудование
Иногда самым выгодным и эффективным способом повышения производитель#
ности программы является покупка нового оборудования. Конечно, если программа
предназначен для сотнен или тысяч компьютеров по всей стране, этот вариант
нереалистичен. Но если вы разрабатываете специализированное ПО для нескольких
пользователей, обновление оборудования на самом деле может оказаться самым
дешевым вариантом. Это позволит сократить расходы, связанные с улучшением
производительности ПО и проблемами, которые могут из#за этого улучшения воз#
никнуть при сопровождении. Кроме того, это повысит производительность всех
программ, выполняемых на новых системах.

Оптимизация кода
Оптимизацией кода (code tuning), которой посвящена оставшаяся часть этой главы,
называют изменение корректного кода, направленное на повышение его эффек#
тивности. «Оптимизация» подразумевает внесение небольших изменений, затра#
гивающих один класс, один метод, а чаще всего — несколько строк кода. Крупно#
масштабные изменения проекта или другие высокоуровневые способы повыше#
ния производительности оптимизацией не считаются.
Каждый из уровней от проектирования системы до оптимизации кода допускает
существенное повышение производительности ПО. Джон Бентли утверждает, что в
некоторых случаях формула общего повышения производительности системы имеет
мультипликативный характер (Bentley, 1982). Так как каждый из шести уровней
допускает десятикратный рост производительности, значит, что теоретически про#
изводительность программы может быть повышена в миллион раз. Хотя для этого
необходимо, чтобы результаты, получаемые на каждом из уровней, были незави#
симы от других уровней (что наблюдается редко), этот потенциал вдохновляет.

25.2. Введение в оптимизацию кода
Чем соблазняет оптимизация кода? Это не самый эффективный способ повыше#
ния производительности: улучшение архитектуры программы, перепроектирование
классов и выбор более эффективного алгоритма обычно приводят к более впе#
чатляющим результатам. Кроме того, это не самый легкий способ повысить про#
изводительность: легче купить новое оборудование или компилятор с улучшен#
ным модулем оптимизации. Наконец, это не самый дешевый способ повысить
производительность: на оптимизацию кода вручную изначально уходит много вре#
мени, а потом оптимизированный код труднее сопровождать.
Оптимизация кода привлекательна по ряду причин. Прежде всего она бросает вызов
законам природы. Нет ничего радостнее, чем ускорить выполнение метода в 10
раз путем изменения всего лишь нескольких его строк.
Кроме того, овладение мастерством написания эффективного кода — признак
превращения в серьезного программиста. В теннисе вы не получаете очков за то,
как берете мяч, но все же должны освоить правильный способ делать это. Вы не

ГЛАВА 25 Стратегии оптимизации кода

577

можете просто наклониться и взять его рукой. Если вы не хотите показаться но#
вичком, вы ударяете по нему ракеткой, пока он не подпрыгнет до пояса, и тогда
вы его ловите. Более трех ударов мяча о землю — серьезная оплошность. Несмот#
ря на кажущуюся незначительность, способ подбора мяча считается в культуре
теннисистов отличительным признаком. Компактность вашего кода также обыч#
но не волнует никого, кроме вас и других программистов. Тем не менее в про#
граммисткой культуре способность создавать компактный и эффективный код слу#
жит подтверждением вашего класса.
Увы, эффективный код не всегда является «лучшим». Этот вопрос мы и обсудим
ниже.

Принцип Парето
Принцип Парето, известный также как «правило 80/20», гласит, что 80% резуль#
тата можно получить, приложив 20% усилий. Относящийся не только к програм#
мированию, этот принцип очень точно характеризует оптимизацию программ.
Барри Бом сообщает, что на 20% методов программы приходятся 80%
времени ее выполнения (Boehm, 1987b). В классической работе «An Empi#
rical Study of Fortran Programs» Дональд Кнут указал, что менее 4% кода обыч#
но соответствуют более чем 50% времени выполнения программы (Knuth, 1971).
Кнут обнаружил это неожиданное отношение при помощи инструмента профи#
лирования, поддерживающего подсчет строк. Следствие очевидно: вам нужно найти
в коде «горячие точки» и сосредоточиться на оптимизации процентов, использу#
емых более всего. Профилируя свою программу подсчета строк, Кнут обнаружил,
что половину времени она проводила в двух циклах. Он изменил несколько строк
кода и удвоил скорость профайлера менее чем за час.
Джон Бентли описывает случай, когда программа из 1000 строк проводила 80%
времени в 5#строчном методе вычисления квадратного корня. Утроив быстродей#
ствие этого метода, он удвоил быстродействие программы (Bentley, 1988). Опи#
раясь на принцип Парето, можно дать еще один совет: напишите большую часть
кода на интерпретируемом языке (скажем, на Python), а потом перепишите про#
блемные фрагменты на более быстром компилируемом языке, таком как C.
Бентли также сообщает о случае, когда группа обнаружила, что ОС половину вре#
мени проводит в одном небольшом цикле. Переписав цикл на микрокоде, разра#
ботчики ускорили его выполнение в 10 раз, но производительность системы ос#
талась прежней — они переписали цикл бездействия системы!
Разработчики языка ALGOL — прародителя большинства современных языков,
сыгравшего одну из самых главных ролей в истории программирования, — руко#
водствовались принципом «Лучшее — враг хорошего». Стремление к совершен#
ству может мешать завершению работы. Доведите работу до конца и только по#
том совершенствуйтесь. Часть, которую нужно довести до совершенства, обычно
невелика.

578

ЧАСТЬ V

Усовершенствование кода

Бабушкины сказки
С оптимизацией кода связано множество заблуждений.
Сокращение числа строк высокоуровневого кода повышает быстродей'
ствие или уменьшает объем итогового машинного кода — НЕВЕРНО!
Многие убеждены в том, что, если сократить какой#то фрагмент до одной или двух
строк, он будет максимально эффективным. Рассмотрим код инициализации мас#
сива из 10 элементов:

for i = 1 to 10
a[ i ] = i
end for
Как вы думаете, он выполнится быстрее или медленнее, чем эти 10 строк, решаю#
щих ту же задачу?

a[
a[
a[
a[
a[
a[
a[
a[
a[
a[

1 ] = 1
2 ] = 2
3 ] = 3
4 ] = 4
5 ] = 5
6 ] = 6
7 ] = 7
8 ] = 8
9 ] = 9
10 ] = 10

Если вы придерживаетесь старой догмы «меньшее число строк выполняется быс#
трее», вы скажете, что первый фрагмент быстрее. Однако тесты на Microsoft Visual
Basic и Java показали, что второй фрагмент минимум на 60% быстрее первого.

Язык

Время
выполнения
цикла for

Время выполнения
последовательного
кода

Экономия
времени

Соотношение
быстродействия

Visual Basic

8,47

3,16

63%

2,5:1

Java

12,6

3,23

74%

4:1

Примечания: (1) Временные показатели в этой и следующих таблицах данной главы ука#
зываются в секундах, а их сравнение имеет смысл только в пределах конкретных строк
каждой из таблиц. Действительные показатели будут зависеть от компилятора, парамет#
ров компилятора и среды, в которой выполняется тестирование. (2) Большинство резуль#
татов сравнительного тестирования основано на выполнении фрагментов кода от несколь#
ких тысяч до многих миллионов раз, что призвано устранить колебания результатов.
(3) Конкретные марки и версии компиляторов не указываются. Показатели производитель#
ности во многом зависят от марки и версии компилятора. (4) Сравнение результатов те#
стирования фрагментов, написанных на разных языках, имеет смысл не всегда, так как
компиляторы разных языков не всегда позволяют задать одинаковые параметры генери#
рования кода. (5) Фрагменты, написанные на интерпретируемых языках (PHP и Python),
в большинстве случаев тестировались с использованием более чем в 100 раз меньшего числа
тестов, чем фрагменты, написанные на других языках. (6) Некоторые из показателей «эко#
номии времени» не совсем точны из#за округления «времени выполнения кода до опти#
мизации» и «времени выполнения оптимизированного кода».

ГЛАВА 25 Стратегии оптимизации кода

579

Разумеется, это не значит, что увеличение числа строк высокоуровневого кода
всегда приводит к повышению быстродействия или сокращению объема програм#
мы. Это означает, что независимо от эстетической привлекательности компакт#
ного кода ничего определенного о связи между числом строк кода на высокоуров#
невом языке и объемом и быстродействием итоговой программы сказать нельзя.
Одни операции, вероятно, выполняются быстрее или компактнее других
— НЕВЕРНО! Если речь идет о производительности, не может быть никаких «ве#
роятно». Без измерения производительности вы никак не сможете точно узнать,
помогли ваши изменения программе или навредили. Правила игры изменяются
при каждом изменении языка, компилятора, версии компилятора, библиотек, вер#
сий библиотек, процессора, объема памяти, цвета рубашки, которую вы надели
(ладно, это шутка), и т. д. Результаты, полученные на одном компьютере с одним
набором инструментов, вполне могут оказаться противоположными на другом ком#
пьютере с другим набором инструментов.
Исходя из этого, можно назвать несколько причин, по которым производитель#
ность не следует повышать путем оптимизации кода. Если программа должна быть
портируемой, помните: методики, повышающие производительность в одной среде,
могут снижать ее в других. Если вы решите изменить или модернизировать ком#
пилятор, возможно, новый компилятор будет автоматически выполнять те виды
оптимизации, что вы выполнили вручную, и все ваши усилия окажутся бесполез#
ными. Хуже того: оптимизировав код, вы можете помешать компилятору выпол#
нить более эффективные виды оптимизации, ориентированные на простой код.
Оптимизируя код, вы обрекаете себя на перепрофилирование каждого оптими#
зированного фрагмента при каждом изменении марки компилятора, его версии,
версий библиотек и т. д. Если вы не будете перепрофилировать код, оптимизация,
бывшая выгодной, после изменения среды сборки программы вполне может стать
невыгодной.
Оптимизацию следует выполнять по мере написания
Возможности небольшого повыкода — НЕВЕРНО! Кое#кто утверждает, что если вы буде#
шения эффективности следует
те стремиться написать самый быстрый и компактный код
игнорировать, скажем, в 97%
случаев: необдуманная оптимипри работе над каждым методом, то итоговая программа бу#
зация — корень всего зла.
дет быстрой и компактной. Однако на самом деле это ме#
Дональд Кнут (Donald Knuth)
шает увидеть за деревьями лес, и программисты, чрезмер#
но поглощенные микрооптимизацией, начинают упускать
из виду по#настоящему важные глобальные виды оптимизации. Основные недо#
статки этого подхода рассмотрены ниже.
 До создания полностью работоспособной программы найти узкие места в коде

почти невозможно. Программисты очень плохо угадывают, на какие 4% кода
приходятся 50% времени выполнения, поэтому, оптимизируя код по мере его
написания, они будут тратить примерно 96% времени на оптимизацию кода,
который не нуждается в оптимизации. На оптимизацию по#настоящему важ#
ных 4% кода времени у них уже не останется.
 В тех редких случаях, когда узкие места определяются правильно, разработ#

чики уделяют им слишком большое внимание, и критически важными стано#
вятся уже другие узкие места. Результат очевиден: все то же снижение произ#

580

ЧАСТЬ V

Усовершенствование кода

водительности. Если оптимизация выполняется после создания полной систе#
мы, разработчики могут определить все проблемные области и их относитель#
ную важность, что способствует эффективному распределению времени.
 Концентрация на оптимизации во время первоначальной разработки отвле#

кает от достижения других целей. Разработчики погружаются в анализ алго#
ритмов и сокровенные дискуссии, от которых пользователям ни тепло, ни хо#
лодно. Корректность, сокрытие информации, удобочитаемость и т. д. стано#
вятся вторичными целями, хотя потом улучшить их сложнее, чем производи#
тельность. Работа над повышением производительности после создания пол#
ной программы обычно затрагивает менее 5% кода. Что легче: повысить про#
изводительность 5% кода или улучшить удобочитаемость всего кода?
Короче говоря, главный недостаток преждевременной оптимизации — отсутствие
перспективы. Это сказывается на быстродействии итогового кода, других, еще более
важных атрибутах производительности и качестве программы, ну а расплачиваться
за это в итоге приходится пользователям. Если время, сэкономленное благодаря
реализации наиболее простой программы, посвятить ее последующей оптимиза#
ции, итоговая программа непременно будет работать быстрее, чем программа,
разработанная с использованием неорганизованного подхода к оптимизации
(Stevens, 1981).
Иногда оптимизация программы после ее написания не позволяет достичь нуж#
ных показателей производительности, из#за чего приходится вносить крупные
изменения в завершенный код. Можете утешить себя тем, что в этих случаях оп#
тимизация небольших фрагментов все равно не привела бы к нужным результа#
там. Проблема в таких ситуациях объясняется не низким качеством кода, а неадек#
ватной архитектурой программы.
Если оптимизацию нужно выполнять до создания полной программы, сведите риск
к минимуму, интегрировав в процесс оптимизации перспективу. Один из спосо#
бов сделать это — задать целевые показатели объема и быстродействия отдель#
ных функций и провести оптимизацию кода по мере его написания, направлен#
ную на достижение этих показателей. Определив такие цели в спецификации, вы
сможете следить сразу и за лесом, и за конкретными деревьями.
Быстродействие программы не менее важно, чем ее
корректность — НЕВЕРНО! Едва ли можно представить
ситуацию, когда программу прежде всего нужно сделать
быстрой или компактной и только потом корректной. Дже#
ральд Вайнберг рассказывает историю о программисте, ко#
торого вызвали в Детройт, чтобы он помог отладить нера#
ботоспособную программу. Через несколько дней разработчики пришли к выво#
ду, что ситуация безнадежна.

Дополнительные сведения Описания других занимательных и
поучительных случаев можно
найти в книге Джеральда Вайнберга «Psychology of Computer
Programming» (Weinberg, 1998).

На пути домой он обдумывал проблему и внезапно понял ее суть. К концу полета
у него уже был набросок нового кода. В течение нескольких дней программист
тестировал код и уже собирался вернуться в Детройт, но тут получил телеграмму,
в которой утверждалось, что работа над проектом прекращена из#за невозмож#
ности написания программы. И все же он снова прилетел в Детройт и убедил
руководителей в том, что проект можно было довести до конца.

ГЛАВА 25 Стратегии оптимизации кода

581

Далее он должен был убедить в этом участников проекта. Они выслушали его, и
когда он закончил, создатель старой системы спросил:
— И как быстро выполняется ваша программа?
— Ну, в среднем она обрабатывает каждый набор введенных данных примерно за
10 секунд.
— Ага! Но моей программе для этого требуется только 1 секунда.
Ветеран откинулся назад, удовлетворенный тем, что он приструнил выскочку.
Другие программисты, похоже, согласились с ним, но новичок не смутился.
— Да, но ваша программа не работает. Если бы моя не обязана была работать, я
мог бы сделать так, чтобы она обрабатывала ввод почти мгновенно.
В некоторых проектах быстродействие или компактность кода действительно имеет
большое значение. Однако таких проектов немного — гораздо меньше, чем ка#
жется большинству людей, — и их число постоянно сокращается. В этих проек#
тах проблемы с производительностью нужно решать путем предварительного
проектирования. В остальных случаях ранняя оптимизация представляет серьез#
ную угрозу для общего качества ПО, включая производительность.

Когда выполнять оптимизацию?
Создайте высококачественный проект. Следите за правиль#
ностью программы. Сделайте ее модульной и изменяемой,
чтобы позднее над ней было легко работать. Написав кор#
ректную программу, оцените ее производительность. Если
программа громоздка, сделайте ее быстрой и компактной.
Не оптимизируйте ее, пока не убедитесь, что это на самом
деле нужно.

Правила оптимизации Джексона: Правило 1. Не делайте этого. Правило 2 (только для экспертов). Не делайте этого пока
— до тех пор, пока вы не получите совершенно ясное неоптимизированное решение.
М. А. Джексон

Несколько лет назад я работал над программой на C++, ко#
(M. A. Jackson)
торая должна была генерировать графики, помогающие ана#
лизировать данные об инвестициях. Написав код расчета первого графика, мы
провели тестирование, показавшее, что программа отображает график пример#
но за 45 минут, что, конечно, было неприемлемо. Чтобы решить, что с этим де#
лать, мы провели собрание группы. На собрании один из разработчиков выкрик#
нул в сердцах: «Если мы хотим иметь хоть какой#то шанс выпустить приемлемый
продукт, мы должны начать переписывать весь код на ассемблере прямо сейчас».
Я ответил, что мне так не кажется — что около 50% времени выполнения скорее
всего приходятся на 4% кода. Было бы лучше исправить эти 4% в конце работы
над проектом. После еще некоторых споров наш руководитель поручил мне по#
работать над производительностью программы (что мне и было нужно: «О, нет!
Только не бросай меня в тот терновый куст!»1 ).
Как часто бывает, я очень быстро нашел в коде пару ослепительных узких мест.
Внеся несколько изменений, я снизил время рисования с 45 минут до менее чем
30 секунд. Гораздо меньше 1% кода соответствовало 90% времени выполнения.

1

Часто цитируемая фраза из негритянской сказки про Братца Кролика и Братца Волка в изло#
жении У. Фолкнера. — Прим. перев.

582

ЧАСТЬ V

Усовершенствование кода

Ну, а к моменту выпуска ПО нам удалось сократить время рисования почти до 1
секунды.

Оптимизация кода компилятором
Современные компиляторы могут оптимизировать код куда эффективнее, чем вам
кажется. В случае, который я описал выше, мой компилятор выполнил оптимиза#
цию вложенного цикла так эффективно, что я едва ли получил бы лучшие резуль#
таты, переписав код. Покупая компилятор, сравните производительность каждо#
го компилятора с использованием своей программы. Каждый компилятор имеет
свои плюсы и минусы, и одни компиляторы лучше подходят для вашей програм#
мы, чем другие.
Оптимизирующие компиляторы лучше оптимизируют простой код. Если вы жон#
глируете индексами циклов и делаете другие «хитрые» вещи, компилятору будет
труднее выполнить свою работу, от чего пострадает ваша программа. В подразде#
ле «Размещение одного оператора на строке» раздела 31.5 вы найдете пример про#
стого кода, который после оптимизации компилятором оказался на 11% быстрее,
чем аналогичный «хитрый» код.
Хороший оптимизирующий компилятор может повысить быстродействие кода на
40 и более процентов, тогда как многие из методик, описанных в следующей гла#
ве, — только на 15–30%. Так почему ж просто не написать ясный код и не позво#
лить компилятору выполнить свою работу? Вот результаты нескольких тестов,
показывающие, насколько успешно компиляторы оптимизировали метод встав#
ки#сортировки:
Время выполнения кода, оптимизированного
Экономия
компилятором
времени

Язык

Время выполнения кода без
оптимизации

Компилятор C++ 1

2,21

1,05

52%

2:1

Компилятор C++ 2

2,78

1,15

59%

2,5:1

Компилятор C++ 3

2,43

1,25

49%

2:1

Компилятор C#

1,55

1,55

0%

1:1

Visual Basic

1,78

1,78

0%

1:1

Java VM 1

2,77

2,77

0%

1:1

Java VM 2

1,39

1,38

100:1

Python

Интерпретируемый

>100:1

Как видите, в плане быстродействия языки C++, Visual Basic и C# примерно одина#
ковы. Код на Java выполняется несколько медленнее. PHP и Python — интерпрети#
руемые языки, и код, написанный на них, обычно выполняется в 100 и более раз
медленнее, чем написанный на C++, Visual Basic, C# или Java. Однако к общим ре#
зультатам, указанным в этой таблице, следует относиться с осторожностью. Отно#
сительная эффективность C++, Visual Basic, C#, Java и других языков во многом за#
висит от конкретного кода (читая главу 26, вы сами в этом убедитесь).

586

ЧАСТЬ V

Усовершенствование кода

Ошибки Наконец, еще одним источником проблем с производительностью яв#
ляются некоторые виды ошибок. Какие? Вы можете оставить в итоговой версии про#
граммы отладочный код (например, записывающий трассировочную информацию
в файл), забыть про освобождение памяти, неграмотно спроектировать таблицы БД,
опрашивать несуществующие устройства до истечения лимита времени и т. д.
При работе над первой версией одного приложения мы столкнулись с операци#
ей, выполнявшейся гораздо медленнее других похожих операций. Сделав массу
попыток объяснить этот факт, мы выпустили версию 1.0, так и не поняв полнос#
тью, в чем дело. Однако, работая над версией 1.1, я обнаружил, что таблица БД,
используемая в этой операции, не была проиндексирована! Простая индексация
таблицы повысила скорость некоторых операций в 30 раз. Определение индекса
для часто используемой таблицы нельзя считать оптимизацией — это просто
хорошая практика программирования.

Относительное быстродействие
распространенных операций
Хотя нельзя с полной уверенностью утверждать, что одни операции медленнее
других, не оценив их, определенные операции все же обычно дороже. Отыскивая
патоку в своей программе, используйте табл. 25#2, которая поможет вам выдвинуть
первоначальные предположения о том, какие фрагменты кода неэффективны.

Табл. 25-2. Быстрота выполнения часто используемых операций
Относительное время выполнения
Операция

Пример

C++

Java

Исходный показатель
(целочисленное присваивание)

i=j

1

1

Вызовы методов
Вызов метода без параметров

foo()

1



Вызов закрытого метода
без параметров

this.foo()

1

0,5

Вызов закрытого метода
с одним параметром

this.foo( i )

1,5

0,5

Вызов закрытого метода
с двумя параметрами

this.foo( i, j )

2

0,5

Вызов метода объекта

bar.foo()

2

1

Вызов метода производ#
ного объекта

derivedBar.foo()

2

1

Вызов полиморфного метода

abstractBar.foo()

2,5

2

Обращение к объекту
1#го уровня

i = obj.num

1

1

Обращение к объекту
2#го уровня

i = obj1.obj2. num

1

1

Стоимость каждого
дополнительного уровня

i = obj1.obj2.obj3...

неизмеряема

неизмеряема

Обращения к объектам

ГЛАВА 25 Стратегии оптимизации кода

587

Табл. 25-2. (продолжение)
Относительное время выполнения
Операция

Пример

C++

Java

Целочисленное присваивание
(локальная операция)

i=j

1

1

Целочисленное присваивание
(унаследованная операция)

i=j

1

1

Сложение

i=j+k

1

1

Вычитание

i=j%k

1

1

Умножение

i=j*k

1

1

Деление

i=j\k

5

1,5

Присваивание

x=y

1

1

Сложение

x=y+z

1

1

Вычитание

x=y%z

1

1

Умножение

x=y*z

1

1

Деление

x=y/z

4

1

Извлечение квадратного корня
из числа с плавающей запятой

x = sqrt( y )

15

4

Вычисление синуса числа
с плавающей запятой

x = sin( y )

25

20

Вычисление логарифма числа
с плавающей запятой

x = log( y )

25

20

Вычисление экспоненты числа
с плавающей запятой

x = exp( y )

50

20

i = a[ 5 ]

1

1

1

1

Операции над целочислен
ными переменными

Операции над переменными
с плавающей запятой

Трансцендентные функции

Операции над массивами
Обращение к массиву целых чи#
сел с использованием константы

Обращение к массиву целых чисел i = a[ j ]
с использованием переменной
Обращение к двумерному
массиву целых чисел с исполь#
зованием констант

i = a[ 3, 5 ]

1

1

Обращение к двумерному
массиву целых чисел с исполь#
зованием переменных

i = a[ j, k ]

1

1

Обращение к массиву чисел
с плавающей запятой с исполь#
зованием константы

x = z[ 5 ]

1

1

Обращение к массиву чисел
с плавающей запятой с исполь#
зованием целочисленной
переменной

x = z[ j ]

1

1

(см. след. стр.)

588

ЧАСТЬ V

Усовершенствование кода

Табл. 25-2. (окончание)
Относительное время выполнения
Операция

Пример

C++

Java

Обращение к двумерному
массиву чисел с плавающей
запятой с использованием
констант

x = z[ 3, 5 ]

1

1

Обращение к двумерному
массиву чисел с плавающей
запятой с использованием
целочисленных переменных

x = z[ j, k ]

1

1

Примечание: показатели, приведенные здесь, сильно зависят от локальной среды,
компилятора и выполняемых компилятором видов оптимизации. Результаты, указанные
для языков C++ и Java, нельзя сравнивать непосредственно.

С момента выхода первого издания этой книги относительное быстродействие
отмеченных операций значительно изменилось, так что, если вы все еще подхо#
дите к оптимизации кода, опираясь на идеи 10#летней давности, пересмотрите свои
взгляды.
Большинство частых операций — в том числе вызовы методов, присваивание, ариф#
метические операции над целыми числами и числами с плавающей запятой — имеет
примерно одинаковую цену. Трансцендентные математические функции очень
дороги. Вызовы полиморфных методов чуть дороже вызовов других методов.
Табл. 25#2 или похожая таблица, которую вы можете создать сами, — ключ, от#
крывающий все двери в мир быстрого кода, описанные в главе 26. В каждом слу#
чае повышение быстродействия исходит из замены дорогой операции на более
дешевую (см. главу 26).

25.4. Оценка производительности
На небольшие фрагменты программы обычно приходится непропорционально
большая доля времени ее выполнения, поэтому перед оптимизацией кода вам
следует оценить его и найти в нем горячие точки. Обнаружив горячие точки и
оптимизировав их, снова оцените код, чтобы узнать, насколько вы его улучшили.
Многие аспекты производительности противоречат интуиции. Выше я уже при#
вел один пример этого, когда 10 строк кода оказались в итоге значительно быст#
рее и компактнее, чем одна строка.
Опыт также не особо полезен при оптимизации. Опыт может быть ос#
нован на использовании старого компьютера, языка или компилятора,
но когда что#либо из этого изменяется, все начинается сначала. Невоз#
можно точно сказать, каковы результаты оптимизации, не оценив их.
Много лет назад я написал программу, суммирующую элементы матрицы. Перво#
начальный код выглядел примерно так:

ГЛАВА 25 Стратегии оптимизации кода

Пример простого кода, суммирующего элементы матрицы (C++)
sum = 0;
for ( row = 0; row < rowCount; row++ ) {
for ( column = 0; column < columnCount; column++ ) {
sum = sum + matrix[ row ][ column ];
}
}

589

Дополнительные сведения Джон
Бентли описывает похожий случай, когда переписывание кода
с использованием указателей
снизило производительность
примерно на 10%. В другой ситуации этот же подход повысил
производительность более чем
на 50%. См. «Software Exploratorium: Writing Efficient C Programs» (Bentley, 1991).

Как видите, код был прост, но суммирование элементов
матрицы должно было выполняться как можно быстрее, а
я знал, что все обращения к массиву и проверки условий
цикла довольно дороги. Я знал, что при каждом обращении к двумерному масси#
ву выполняются дорогие операции умножения и сложения. Так, обработка мат#
рицы размером 100 на 100 требовала 10 000 умножений и сложений, что допол#
нялось еще и затратами, связанными с управлением циклами. Использовав указа#
тели, рассудил я, я смогу просто увеличивать указатель, заменив 10 000 дорогих
умножений на 10 000 относительно дешевых операций инкремента. Я тщательно
преобразовал код и получил:

Пример попытки оптимизации кода, суммирующего элементы матрицы (C++)
sum = 0;
elementPointer = matrix;
lastElementPointer = matrix[ rowCount  1 ][ columnCount  1 ] + 1;
while ( elementPointer < lastElementPointer ) {
sum = sum + *elementPointer++;
}
Хотя код стал менее удобочитаемым, особенно для программистов, не являющихся
экспертами в C++, я был очень доволен собой. Оно и понятно: все#таки я изба#
вился от 10 000 умножений и многих операций, связанных с управлением цикла#
ми! Я был так доволен, что решил подкрепить свои чувства конкретными цифра#
ми и оценить повышение скорости, хотя в то время я выполнял это не всегда.
Знаете, что я обнаружил? Никакого улучшения. Ни для мат#
Ни один программист никогда
риц размером 100 на 100. Ни для матриц размером 10 на
не мог предсказать или обнару10. Ни для каких#либо других матриц. Я был так разочаро#
жить узкие места, не обладая
данными. Что бы вы ни думаван, что погрузился в ассемблерный код, сгенерированный
ли, реальность окажется соверкомпилятором, чтобы понять, почему моя оптимизация не
шенно другой.
сработала. К моему удивлению, оказалось, что я был не пер#
Джозеф М. Ньюкамер
вым, кому понадобилось перебирать элементы массива: ком#
(Joseph M. Newcomer)
пилятор сам преобразовывал обращения к массиву в опе#
рации над указателями. Я понял, что единственным резуль#
татом оптимизации, в котором можно быть полностью уверенным без измерения
производительности, является затруднение чтения кода. Если оценка эффектив#
ности не оправдывает себя, не стоит приносить понятность кода в жертву сомни#
тельному повышению производительности.

590

ЧАСТЬ V

Усовершенствование кода

Оценка должна быть точной
Оценка производительности должна быть точной. Измере#
ние времени выполнения программы с помощью секундо#
мера или путем подсчета «один слон, два слона, три слона»
точным не является. Используйте инструменты профилиро#
вания или системный таймер и методы, регистрирующие
истекшее время выполнения операций.

Перекрестная ссылка Об инструментах профилирования см.
подраздел «Оптимизация кода»
раздела 30.3.

Используете ли вы инструмент, написанный другим программистом, или пишете
для оценки производительности программы собственный код, убедитесь, что из#
меряете время выполнения только того оптимизируемого кода. Опирайтесь на
число тактов процессора, выделенных вашей программе, а не на время суток. Иначе
при переключении системы с вашей программы на другую программу один из
ваших методов будет оштрафован на время, выделенное другой программе. Кро#
ме того, попытайтесь исключить влияние процесса оценки кода и запуска про#
граммы на первоначальный и оптимизированный код.

25.5. Итерация
Обнаружив в коде узкое место и попробовав его устранить, вы удивитесь, насколько
можно повысить производительность кода путем его оптимизации. Единственная
методика редко приводит к десятикратному улучшению, но методики можно эф#
фективно комбинировать, поэтому даже после обнаружения одного удачного вида
оптимизации продолжайте пробовать другие виды.
Однажды я написал программную реализацию алгоритма Data Encryption Standard
(DES). Ну, на самом деле я писал ее не один раз, а около тридцати. При шифрова#
нии по алгоритму DES цифровые данные кодируются так, что их нельзя расшиф#
ровать без правильного пароля. Этот алгоритм шифрования так хитер, что иног#
да кажется, что он сам зашифрован. Моя цель состояла в том, чтобы файл объе#
мом 18 кб шифровался на IBM PC за 37 секунд. Первая реализация алгоритма вы#
полнялась 21 минуту 40 секунд, так что мне предстояла долгая работа.
Хотя большинство отдельных видов оптимизации было незначительно, в сумме
они привели к впечатляющим результатам. Никакие три или даже четыре вида
оптимизации не позволили бы мне достичь цели, однако итоговая их комбина#
ция оказалась эффективной. Мораль: если копать достаточно глубоко, можно до#
биться подчас неожиданных результатов.
Оптимизация алгоритма DES — самая агрессивная оптими#
зация, которую я когда#либо проделывал. В то же время я
никогда не создавал более непонятного и трудного в сопро#
вождении кода. Первоначальный алгоритм был сложен. Код,
получившийся в результате трансформаций высокоуровневого кода, оказался
практически нечитаемым. После преобразования кода на ассемблер я получил один
метод из 500 строк, на который боюсь даже смотреть. Это отношение между оп#
тимизацией кода и его качеством справедливо почти всегда. Вот таблица, отра#
жающая историю оптимизации:

Перекрестная ссылка Методики, указанные в этой таблице,
обсуждаются в главе 26.

ГЛАВА 25 Стратегии оптимизации кода

Вид оптимизации

Время выполнения

591

Улучшение

Первоначальная реализация

21:40

Преобразование битовых полей в массивы

7:30

65%

Развертывание самого внутреннего цикла for

6:00

20%

Удаление перестановок

5:24

10%

Объединение двух переменных

5:06

5%

Использование логического тождества
для объединения первых двух этапов
алгоритма DES

4:30

12%

Объединение областей памяти, используемых
двумя переменными, для сокращения числа
операций над данными во внутреннем цикле

3:36

20%

Объединение областей памяти, используемых
двумя переменными, для сокращения числа
операций над данными во внешнем цикле

3:09

13%

Развертывание всех циклов и использование
литералов для индексации массива

1:36

49%

Удаление вызовов методов и встраивание
всего кода

0:45

53%

Переписывание всего метода на ассемблере

0:22

51%

Итог

0:22

98%

Примечание: постепенный процесс оптимизации, описанный в этой таблице, не под#
разумевает, что все виды оптимизации эффективны. Я мог бы указать массу других ви#
дов, приводивших к удвоению времени выполнения. Минимум две трети видов оптими#
зации, которые я попробовал, оказались неэффективными.

25.6. Подход к оптимизации кода: резюме
Рассматривая целесообразность оптимизации кода, придерживайтесь следующе#
го алгоритма:
1. Напишите хороший и понятный код, поддающийся легкому изменению.
2. Если производительность вас не устраивает:
a. сохраните работоспособную версию кода, чтобы позднее вы могли вернуться
к «последнему нормальному состоянию»;
b. оцените производительность системы с целью нахождения горячих точек;
c. узнайте, обусловлено ли плохое быстродействие неадекватным проектом, не#
верными типами данных или неудачными алгоритмами и определите, умест#
на ли оптимизация кода; если оптимизация кода неуместна, вернитесь к п. 1;
d. оптимизируйте узкое место, определенное на этапе (c);
e. оцените каждое улучшение по одному за раз;
f.

если оптимизация не привела к улучшению кода, вернитесь к коду, сохра#
ненному на этапе (a) (как правило, более чем в половине случаев попытки
оптимизации будут приводить лишь к незначительному повышению про#
изводительности или к ее снижению).

3. Повторите процесс, начиная с п. 2.

592

ЧАСТЬ V

Усовершенствование кода

Дополнительные ресурсы
В этом разделе я указал работы, посвященные повышению
производительности в общем. Книги, в которых обсуждаются
специфические методики оптимизации кода, указаны в раз#
деле «Дополнительные ресурсы» в конце главы 26.
http://cc2e.com/2585

Производительность
Smith, Connie U. and Lloyd G. Williams. Performance Solutions:
A Practical Guide to Creating Responsive, Scalable Software. Boston,
MA: Addison#Wesley, 2002. В этой книге обсуждается создание
высокопроизводительного ПО, предусматривающее обеспечение нужной произво#
дительности на всех этапах разработки. В ней вы найдете много примеров и кон#
кретных случаев, относящихся к программам нескольких типов, а также конкрет#
ные рекомендации по повышению производительности Web#приложений. Особое
внимание в книге уделяется масштабируемости программ.
http://cc2e.com/2592

Newcomer, Joseph M. Optimization: Your Worst Enemy. May 2000,
www.flounder.com/optimization.htm. В этой статье, принадле#
жащей перу опытного системного программиста, описыва#
ются разные ловушки, в которые вы можете попасть, используя неэффективные
стратегии оптимизации.
http://cc2e.com/2599

Алгоритмы и типы данных
Knuth, Donald. The Art of Computer Programming, vol. 1, Fundamental Algorithms, 3d
ed. Reading, MA: Addison#Wesley, 1997.
Knuth, Donald. The Art of Computer Programming, vol. 2, Seminumerical Algorithms, 3d
ed. Reading, MA: Addison#Wesley, 1997.
Knuth, Donald. The Art of Computer Programming, vol. 3, Sorting and Searching, 2d ed.
Reading, MA: Addison#Wesley, 1998.
Это три первых тома серии, которая по первоначальному замыслу автора должна
включить семь томов. В этих несколько пугающих книгах алгоритмы описываются
не только на обычном языке, но и с использованием математической нотации,
или MIX — языка ассемблера для воображаемого компьютера MIX. Кнут подроб#
нейшим образом описывает огромное число вопросов, и, если вы испытываете
сильный интерес к конкретному алгоритму, лучшего ресурса вам не найти.
Sedgewick, Robert. Algorithms in Java, Parts 1%4, 3d ed. Boston, MA: Addison#Wesley,
2002. В четырех частях этой книги исследуются лучшие методы решения широ#
кого диапазона проблем. В число тем книги входят фундаментальные сведения,
сортировка, поиск, реализация абстрактных типов данных и более сложные во#
просы. В книге Седжвика Algorithms in Java, Part 5, 3d ed. (Sedgewick, 2003) обсуж#
даются алгоритмы, основанные на графах. Книги Algorithms in C++, Parts 1%4, 3d
ed. (Sedgewick, 1998), Algorithms in C++, Part 5, 3d ed. (Sedgewick, 2002), Algorithms
in C, Parts 1%4, 3d ed. (Sedgewick, 1997) и Algorithms in C, Part 5, 3d ed. (Sedgewick,
2001) организованы похожим образом. Седжвик имеет степень доктора филосо#
фии и в свое время был учеником Кнута.

ГЛАВА 25 Стратегии оптимизации кода

Контрольный список: стратегии оптимизации кода

593

http://cc2e.com/2506

Производительность программы в общем
 Рассмотрели ли вы возможность повышения производительности посредством изменения требований к программе?
 Рассмотрели ли вы возможность повышения производительности путем
изменения проекта программы?
 Рассмотрели ли вы возможность повышения производительности путем
изменения проектов классов?
 Рассмотрели ли вы возможность повышения производительности путем
сокращения объема взаимодействия с ОС?
 Рассмотрели ли вы возможность повышения производительности путем
устранения операций ввода/вывода?
 Рассмотрели ли вы возможность повышения производительности путем
использования компилируемого языка вместо интерпретируемого?
 Рассмотрели ли вы возможность повышения производительности путем видов
оптимизации, поддерживаемых компилятором?
 Рассмотрели ли вы возможность повышения производительности путем
перехода на другое оборудование?
 Рассматриваете ли вы оптимизацию кода только как последнее средство?

Подход к оптимизации кода
 Убедились ли вы в полной корректности программы перед началом оптимизации кода?
 Нашли ли вы узкие места в коде перед началом его оптимизации?
 Оцениваете ли вы результаты выполнения каждого вида оптимизации кода?
 Отменяете ли вы изменения, которые не привели к ожидаемому улучшению?
 Пробуете ли вы несколько способов оптимизации каждого узкого места, т. е.
используете ли вы итерацию?

Ключевые моменты
 Производительность — всего лишь один из аспектов общего качества ПО, и,

как правило, не самый важный. Оптимизация кода — лишь один из способов
повышения производительности ПО, и тоже обычно не самый важный. Быст#
родействие программы и ее объем обычно в большей степени зависят не от
эффективности кода, а от архитектуры программы, детального проектирова#
ния выбора структур данных и алгоритмов.
 Важнейшее условие максимизации быстродействия кода — его количествен#

ная оценка. Она необходима для обнаружения областей, производительность
которых действительно нуждается в повышении, а также для проверки того,
что в результате оптимизации производительность повысилась, а не пони#
зилась.

594

ЧАСТЬ V

Усовершенствование кода

 Как правило, основная часть времени выполнения программы приходится на

небольшую часть кода. Не выполнив оценку, вы не найдете этот код.
 Достижение желаемого повышения производительности кода при помощи его

оптимизации обычно требует нескольких итераций.
 Во время первоначального кодирования нет лучше способа подготовки к по#

вышению производительности программы, чем написание ясного и понятно#
го кода, поддающегося легкому изменению.

ГЛАВА 25 Стратегии оптимизации кода

Г Л А В А

595

2 6

Методики
оптимизации кода

Содержание
 26.1. Логика

http://cc2e.com/2665

 26.2. Циклы
 26.3. Изменения типов данных
 26.4. Выражения
 26.5. Методы
 26.6. Переписывание кода на низкоуровневом языке
 26.7. Если что#то одно изменяется, что#то другое всегда остается постоянным

Связанные темы
 Стратегии оптимизации кода: глава 25
 Рефакторинг: глава 24

Оптимизация кода уже давно привлекает пристальное внимание программистов.
Если вы решили повысить производительность и хотите сделать это на уровне кода
(с учетом предупреждений, описанных в главе 25), то можете использовать це#
лый ряд методик.
Эта глава посвящена в первую очередь повышению быстродействия, но включает
и несколько советов по сокращению объема кода. Производительность обычно
охватывает и быстродействие, и объем кода, но при необходимости сокращения
объема кода обычнолучше прибегнуть к перепроектированию классов и данных,
а не к оптимизации кода. Последняя подразумевает небольшие изменения, а не
изменения более крупномасштабных аспектов проектирования.
В этой главе вы почти не найдете методик, настолько общих, чтобы код приме#
ров можно было копировать прямо в другие программы. Я просто хочу проил#
люстрировать ряд видов оптимизации кода, которые вы сможете приспособить
к своей ситуации.
Виды оптимизации, описываемые в этой главе, могут показаться похожими на виды
рефакторинга из главы 24, однако помните, что рефакторинг направлен на улуч#

596

ЧАСТЬ V

Усовершенствование кода

шение внутренней структуры программы (Fowler, 1999). То, о чем мы будем гово#
рить в этой главе, возможно, лучше называть «антирефакторингом». Эти измене#
ния ухудшают внутреннуюю структуру программы ради повышения ее произво#
дительности. Это верно по определению. Если бы изменения не ухудшали внут#
реннюю структуру, мы не считали бы их видами оптимизации — мы использова#
ли бы их по умолчанию и считали стандартными методиками кодирования.
Некоторые авторы характеризуют методики оптимизации
кода как «практические правила» или приводят данные, го#
ворящие о том, что определенный вид оптимизации непре#
менно обеспечит желательный результат. Однако, как вы ско#
ро увидите, концепция «практических правил» плохо описывает оптимизацию кода.
Единственным надежным практическим правилом является оценка результатов
каждого вида оптимизации в конкретной среде. Так что в этой главе представлен
каталог «вещей, которые стоит попробовать»: многие из них в вашей среде пользы
не принесут, но некоторые на самом деле окажутся очень эффективными.

Перекрестная ссылка Оптимизация кода основана на эвристике (см. раздел 5.3).

26.1. Логика
Перекрестная ссылка О других
аспектах использования операторов, определяющих логику
программы, см. главы 14–19.

Многие задачи программирования связаны с манипулиро#
ванием логикой программы. В этом разделе мы рассмотрим
эффективное использование логических выражений.

Прекращение проверки сразу же после получения ответа
Допустим, у вас есть выражение:

if ( 5 < x ) and ( x < 10 ) then ...
Как только вы определили, что x больше 5, вторую часть проверки выполнять не
нужно.
Перекрестная ссылка О сокращенной оценке логических выражений см. также подраздел «Понимание правил вычисления логических выражений» раздела 19.1.

Некоторые языки поддерживают так называемую «сокращен#
ную оценку выражений», при которой компилятор генери#
рует код, автоматически прекращающий проверку после
получения ответа. Сокращенная оценка выполняется, напри#
мер, для стандартных операторов C++ и «условных» опера#
торов Java.

Если ваш язык не поддерживает сокращенную оценку, избегайте операторов and
и or, используя вместо них дополнительную логику. Для сокращенной оценки наш
код следовало бы изменить так:

if ( 5 < x ) then
if ( x < 10 ) then ...
Принцип прекращения проверки сразу по получении ответа уместен и в других
случаях. В качестве примера можно привести цикл поиска. Если вы сканируете
массив введенных чисел и вам нужно только узнать, присутствует ли в массиве
отрицательное значение, вы могли бы проверять значения по очереди, устанав#

ГЛАВА 26 Методики оптимизации кода

597

ливая при обнаружении отрицательного числа флаг negativeFound. Вот как вы#
глядел бы такой цикл поиска:

Пример, в котором цикл продолжает выполняться даже после получения ответа (C++)
negativeInputFound = false;
for ( i = 0; i < count; i++ ) {
if ( input[ i ] < 0 ) {
negativeInputFound = true;
}
}
Лучше было бы прекращать просмотр массива сразу по обнаружении отрицатель#
ного значения. Любой из следующих советов привел бы к решению проблемы.
 Включите в код оператор break после строки negativeInputFound = true.
 Если язык не поддерживает оператор break, имитируйте его при помощи опе#

ратора goto, передающего управление первой команде, расположенной после
цикла.
 Измените цикл for на цикл while и проверяйте значение negativeInputFound

вместе с проверкой того, не превысил ли счетчик цикла значение count.
 Измените цикл for на цикл while, поместите сигнальное значение в первый

элемент массива, расположенный после последнего исходного значения, а в
условии цикла while просто проверяйте, не отрицательно ли значение. По за#
вершении цикла узнайте, относится ли индекс обнаруженного отрицательно#
го значения к исходному массиву или превышает на 1 индекс верхней грани#
цы массива. Подробнее о сигнальных значениях см. ниже.
Вот результаты использования ключевого слова break в коде C++ и Java:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C++

4,27

3,68

14%

Java

4,85

3,46

29%

Примечания: (1) Временные показатели в этой и следующих таблицах данной главы ука#
зываются в секундах, а их сравнение имеет смысл только в пределах конкретных строк каждой
из таблиц. Действительные показатели будут зависеть от компилятора, параметров компи#
лятора и среды, в которой выполняется тестирование. (2) Большинство результатов срав#
нительного тестирования основано на выполнении фрагментов кода от нескольких тысяч
до многих миллионов раз, что призвано устранить колебания результатов. (3) Конкретные
марки и версии компиляторов не указываются. Показатели производительности во многом
зависят от марки и версии компилятора. (4) Сравнение результатов тестирования фрагментов,
написанных на разных языках, имеет смысл не всегда, поскольку компиляторы разных языков
не всегда позволяют задать одинаковые параметры генерирования кода. (5) Фрагменты,
написанные на интерпретируемых языках (PHP и Python), в большинстве случаев тестиро#
вались с использованием более чем в 100 раз меньшего числа тестов, чем фрагменты, на#
писанные на других языках. (6) Некоторые из показателей «экономии времени» не совсем
точны из#за округления «времени выполнения кода до оптимизации» и «времени выполне#
ния оптимизированного кода».

598

ЧАСТЬ V

Усовершенствование кода

Результаты этого вида оптимизации во многом зависят от числа проверяемых
значений и вероятности обнаружения отрицательного значения. В данном тесте
число значений в среднем было равным 100, а отрицательные значения состав#
ляли половину всех значений.

Упорядочение тестов по частоте
Упорядочивайте тесты так, чтобы самый быстрый и чаще всего оказывающийся
истинным тест выполнялся первым. Нормальные случаи следует обрабатывать
первыми, а вероятность выполнения неэффективного кода должна быть низкой.
Этот принцип относится к блокам case и цепочкам операторов if%then%else.
Рассмотрим, например, оператор Select%Case, обрабатывающий символы, вводимые
с клавиатуры:

Пример плохо упорядоченного логического теста (Visual Basic)
Select inputCharacter
Case “+”, “=”
ProcessMathSymbol( inputCharacter )
Case “0” To “9”
ProcessDigit( inputCharacter )
Case “,”, “.”, “:”, “;”, “!”, “?”
ProcessPunctuation( inputCharacter )
Case “ “
ProcessSpace( inputCharacter )
Case “A” To “Z”, “a” To “z”
ProcessAlpha( inputCharacter )
Case Else
ProcessError( inputCharacter )
End Select
Порядок обработки символов в этом фрагменте близок к порядку сортировки ASCII.
Однако блоки case во многом похожи на большой набор операторов if%then%else,
так что если первым введенным символом будет «a», данный фрагмент проверит,
является ли символ математическим символом, числом, знаком пунктуации или
пробелом, и только потом определит, что это алфавитно#цифровой символ. Зная
примерную вероятность ввода тех или иных символов, вы можете разместить самые
вероятные случаи первыми. Вот переупорядоченные блоки case:

Пример хорошо упорядоченного логического теста (Visual Basic)
Select inputCharacter
Case “A” To “Z”, “a” To “z”
ProcessAlpha( inputCharacter )
Case “ “
ProcessSpace( inputCharacter )
Case “,”, “.”, “:”, “;”, “!”, “?”
ProcessPunctuation( inputCharacter )
Case “0” To “9”
ProcessDigit( inputCharacter )
Case “+”, “=”

ГЛАВА 26 Методики оптимизации кода

599

ProcessMathSymbol( inputCharacter )
Case Else
ProcessError( inputCharacter )
End Select
Теперь наиболее вероятные символы обрабатываются первыми, что снижает об#
щее число выполняемых тестов. При типичной смеси вводимых символов резуль#
таты этого вида оптимизации таковы:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C#

0,220

0,260

#18%

Java

2,56

2,56

0%

Visual Basic

0,280

0,260

7%

Примечание: тестирование выполнено для ввода, включавшего 78% алфавитых симво#
лов, 17% пробелов и 5% знаков пунктуации.

С Visual Basic все ясно, а вот результаты тестирования кода Java и C# довольно
неожиданны. Очевидно, это объясняется способом структурирования операторов
switch%case в языках C# и Java: из#за необходимости перечисления всех значений
по отдельности, а не в форме диапазонов, код C# и Java не выигрывает от этого
вида оптимизации в отличие от кода Visual Basic. Это доказывает, что никакой из
видов оптимизации не следует применять слепо: результаты будут во многом за#
висеть от реализации конкретных компиляторов.
Вы могли бы предположить, что для аналогичного набора операторов if%then%else
компилятор Visual Basic сгенерирует похожий код. Взгляните на результаты:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C#

0,630

0,330

48%

Java

0,922

0,460

50%

Visual Basic

1,36

1,00

26%

Совершенно иная картина. Те же тесты на Visual Basic теперь выполняются мед#
леннее в пять раз без оптимизации и в четыре — в случае оптимизированного кода.
Это говорит о том, что для блоков case и операторов if%then%else компилятор ге#
нерирует разный код.
Результаты оптимизации операторов if%then%else более согласованны, но общая
ситуация от этого не проясняется. Обе версии кода C# и Visual Basic, основанно#
го на блоках case, выполняются быстрее, чем обе версии кода, написанного на
основе if%then%else, тогда как в случае Java все наоборот. Это различие результатов
наводит на мысль о третьем виде оптимизации, описанном чуть ниже.

Сравнение быстродействия похожих структур логики
Описанное выше тестирование можно выполнить и для блоков case, и для опера#
торов if%then%else. В зависимости от среды любой из подходов может оказаться более

600

ЧАСТЬ V

Усовершенствование кода

выгодным. Ниже данные из двух предыдущих таблиц представлены в форме, об#
легчающей сравнение быстродействия оптимизированного кода, написанного с
применением обоих подходов:
case

C#

0,260

0,330

–27%

1:1

Java

2,56

0,460

82%

6:1

Visual Basic

0,260

1,00

258%

1:4

if-then-else

Экономия времени

Соотношение
быстродействия

Язык

Эти результаты не имеют логического объяснения. В одном из языков case гораз#
до лучше, чем if%then%else, а в другом наоборот. В третьем языке различие относи#
тельно невелико. Можно было бы предположить, что из#за похожего синтаксиса
case в C# и Java результаты тестирования этих языков также будут похожими, но
на самом деле имеет место обратное.
Этот пример ясно показывает, что оптимизация кода не подчиняется ни «прак#
тическим правилам», ни «логике». Так что без оценки результатов вам не обойтись.

Замена сложных выражений на обращение к таблице
Иногда просмотр таблицы может оказаться более быстрым,
чем выполнение сложной логической цепи. Суть сложной
цепи обычно сводится к категоризации чего#то и выполне#
нии того или иного действия, основанного на конкретной
категории. Допустим, вы хотите присвоить чему#то номер категории на основе
принадлежности этого чего#то к группам A, B и C:

Перекрестная ссылка Об использовании таблиц вместо
сложной логики см. главу 18.

Вот как эта задача решается при помощи сложной логической цепи:

Пример сложной логической цепи (C++)
if ( ( a &&
category
}
else if ( (
category
}
else if ( c
category
}
else {

!c ) || ( a && b && c ) ) {
= 1;
b && !a ) || ( a && c && !b ) ) {
= 2;
&& !a && !b ) {
= 3;

ГЛАВА 26 Методики оптимизации кода

601

category = 0;
}
Вместо этого теста вы можете использовать более модифицируемый и быстро#
действующий подход, основанный на просмотре табличных данных:

Пример использования таблицы вместо сложной логики (C++)
// Определение таблицы categoryTable.
Определение таблицы понять нелегко, поэтому используйте любые комментарии, способные
помочь.

> static int categoryTable[ 2 ][ 2 ][ 2 ] = {
// !b!c !bc b!c bc
0, 3, 2, 2,
1, 2, 1, 1

//
//

!a
a

};
...
category = categoryTable[ a ][ b ][ c ];
Определение этой таблицы кажется запутанным, но, если она хорошо докумен#
тирована, читать ее будет не труднее, чем код сложной логической цепи. Кроме
того, изменить таблицу будет гораздо легче, чем более раннюю логику. Вот ре#
зультаты сравнения быстродействия обоих подходов:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

C++

5,04

3,39

33%

1,5:1

Visual Basic 5,21

2,60

50%

2:1

Отложенные вычисления
Один из моих бывших соседей любил все откладывать на потом. В оправдание
своей лени он говорил, что многое из того, что люди порываются сделать, делать
просто не нужно. Если подождать достаточно долго, утверждал он, неважные дела
канут в Лету, и он не будет тратить на них свое время.
Методика отложенных вычислений основана на принципе моего соседа: программа
делает что#то, только когда это действительно нужно. Отложенное вычисление
похоже на стратегию решения задач «по требованию», при которой работа вы#
полняется максимально близко к тому месту, где нужны ее результаты.
Допустим, ваша программа работает с таблицей из 5000 значений, полностью
генерируя ее при запуске и затем обращайтесь к ней по мере выполнения. Если
программа использует только небольшую часть элементов таблицы, возможно, есть
смысл вычислять их по мере надобности, а не все сразу. После вычисления эле#
мента его можно сохранить на будущее (это называется «кэшированием»).

602

ЧАСТЬ V

Усовершенствование кода

26.2. Циклы
Перекрестная ссылка О циклах
см. также главу 16.

Так как циклы выполняются многократно, горячие точки
часто следует искать именно внутри циклов. Методики,
описываемые в этом разделе, помогают ускорить выполне#
ние циклов.

Размыкание цикла
Замыканием (switching) цикла называют принятие решения внутри цикла при
каждой его итерации. Если во время выполнения цикла решение не изменяется,
вы можете разомкнуть (unswitch) цикл, приняв решение вне цикла. Обычно для
этого нужно вывернуть цикл наизнанку, т. е. поместить циклы в условный опера#
тор, а не условный оператор внутрь цикла. Вот пример цикла до размыкания:

Пример замкнутого цикла (C++)
for ( i = 0; i < count; i++ ) {
if ( sumType == SUMTYPE_NET ) {
netSum = netSum + amount[ i ];
}
else {
grossSum = grossSum + amount[ i ];
}
}
В этом фрагменте проверка if ( sumType == SUMTYPE_NET ) выполняется при каж#
дой итерации, хотя ее результат остается постоянным. Вы можете ускорить вы#
полнение этого кода, переписав его так:

Пример разомкнутого цикла (C++)
if ( sumType == SUMTYPE_NET ) {
for ( i = 0; i < count; i++ ) {
netSum = netSum + amount[ i ];
}
}
else {
for ( i = 0; i < count; i++ ) {
grossSum = grossSum + amount[ i ];
}
}
Примечание: Этот фрагмент нарушает несколько правил хорошего программирования.
Удобочитаемость и удобство сопровождения кода обычно важнее его быстродействия
или размера, но темой этой главы является производительность, а для ее повышения час#
то нужно поступиться другими целями. Как и в предыдущей главе, здесь вы найдете при#
меры методик кодирования, которые в других частях этой книги не рекомендуются.

ГЛАВА 26 Методики оптимизации кода

603

Размыкание этого цикла позволяет ускорить его выполнение примерно на 20%:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C++

2,81

2,27

19%

Java

3,97

3,12

21%

Visual Basic

2,78

2,77

i = 0;
while ( i < count ) {
a[ i ] = i;
i = i + 1;
}
После частичного развертывания цикла при каждой его итерации обрабатывает#
ся не один случай, а два или более. Это ухудшает удобочитаемость, но не наруша#
ет общность цикла. Вот цикл, развернутый один раз:

Пример однократного
развертывания цикла (Java)
i = 0;
while ( i < count  1 ) {
a[ i ] = i;
a[ i + 1 ] = i + 1;
i = i + 2;
}

ГЛАВА 26 Методики оптимизации кода

605

Эти строки обрабатывают случай, который может быть упущен из-за увеличении счетчика цикла
на 2, а не на 1.

> if ( i == count ) {
a[ count  1 ] = count  1;
}
Как видите, мы заменили первоначальную строку a[ i ] = i на две строки и увели#
чиваем счетчик цикла на 2, а не на 1. Дополнительный код после цикла while ну#
жен на случай нечетных значений переменной count, при которых цикл завер#
шается, так и не обработав один элемент массива.
Конечно, девять строк хитрого кода труднее читать и сопровождать, чем пять строк
простого. Что греха таить: после развертывания цикла качество кода ухудшилось.
Однако любой подход к проектированию предполагает поиск компромиссных
решений, и, даже если конкретная методика обычно плоха, в определенных об#
стоятельствах она может стать оптимальной.
Вот результаты развертывания цикла:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C++

1,75

1,15

34%

Java

1,01

0,581

43%

PHP

5,33

4,49

16%

Python

2,51

3,21

#27%

Примечание: тестирование выполнено для случая count = 100.

Возможность ускорения кода на 16–43% заслуживает внимания, хотя, как пока#
зывает тест кода, написанного на Python, тут тоже не все однозначно. Главная
опасность при развертывании цикла — ошибка завышения или занижения на
единицу в коде, обрабатывающем последнюю итерацию.
Что, если мы продолжим развертывание цикла? Принесет ли дополнительную
выгоду двойное развертывание?

Пример двукратного
развертывания цикла (Java)
i = 0;
while ( i < count  2 ) {
a[ i ] = i;
a[ i + 1 ] = i+1;
a[ i + 2 ] = i+2;
i = i + 3;
}
if ( i discounts>factors>net;
}
Присвоив результат выражения удачно названной переменной, вы улучшите удо#
бочитаемость кода, а может, и ускорите его выполнение:

Пример упрощения сложного выражения с указателями (C++)
quantityDiscount = rates>discounts>factors>net;
for ( i = 0; i < rateCount; i++ ) {
netRate[ i ] = baseRate[ i ] * quantityDiscount;
}
Дополнительная переменная quantityDiscount (оптовая скидка) ясно показывает,
что элементы массива baseRate умножаются на показатель скидки. В первом фраг#
менте это совсем не было очевидно. Кроме того, вынесение сложного выражения
за пределы цикла устраняет три разыменования указателей при каждой итерации,
что приводит к таким результатам:

ГЛАВА 26 Методики оптимизации кода

Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

607

Экономия
времени

C++

3,69

2,97

19%

C#

2,27

1,97

13%

Java

4,13

2,35

43%

Примечание: тестирование выполнено для случая rateCount = 100.

За исключением компилятора Java экономия времени не так уж и велика, поэто#
му при первоначальном кодировании вы можете применить любую методику,
улучшающую удобочитаемость кода, и отложить работу над быстродействием на
потом.

Сигнальные значения
Если цикл включает проверку сложного условия, время его выполнения часто
можно сократить, упростив проверку. В случае циклов поиска это можно сделать,
использовав сигнальное значение (sentinel value) — значение, которое распола#
гается сразу после окончания диапазона поиска и непременно завершает поиск.
Классический пример сложной проверки, которую можно упростить с помощью
сигнального значения, — условие цикла поиска, включающее проверки обнару#
жения нужной переменной и выхода за пределы диапазона поиска. Вот код тако#
го цикла:

Пример проверки сложного условия цикла (C#)
found = FALSE;
i = 0;
Проверка сложного условия.

>while ( ( !found ) && ( i < count ) ) {
if ( item[ i ] == testValue ) {
found = TRUE;
}
else {
i++;
}
}
if ( found ) {
...
При каждой итерации этого цикла проверяются два условия: !found и i < count.
Проверка !found служит для определения того, найден ли нужный элемент. Про#
верка i < count нужна для предотвращения выхода за пределы массива. Кроме того,
внутри цикла проверяются отдельные значения массива item[], так что на самом
деле при каждой итерации цикла выполняются три проверки.

608

ЧАСТЬ V

Усовершенствование кода

Этот вид циклов поиска позволяет объединить три проверки и выполнять при
каждой итерации только одну проверку: для этого нужно поместить в конце диа#
пазона поиска «сигнальное значение», завершающее цикл. В нашем случае мож#
но просто присвоить искомое значение элементу, располагающемуся сразу пос#
ле окончания диапазона поиска (объявляя массив, не забудьте выделить место для
этого элемента). Далее вы проверяете по очереди каждый элемент: если вы дос#
тигаете сигнального значения, значит, нужного вам значения в массиве нет. Вот
соответствующий код:

Пример использования сигнального значения для ускорения цикла (C#)
// Установка сигнального значения с сохранением начальных значений.
initialValue = item[ count ];
Не забудьте зарезервировать в конце массива место для сигнального значения.

> item[ count ] = testValue;
i = 0;
while ( item[ i ] != testValue ) {
i++;
}
// Обнаружено ли значение?
if ( i < count ) {
...
Если item содержит целые числа, выгода может быть весьма существенной:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

C#

0,771

0,590

23%

Java

1,63

0,912

44%

2:1

Visual Basic 1,34

0,470

65%

3:1

1,3:1

Примечание: поиск выполнялся в массиве из 100 целых чисел.

Результаты, полученные для Visual Basic, особенно впечатляют, но и остальные тоже
очень неплохи. Однако при изменении типа массива результаты также изменя#
ются. Если item включает числа с плавающей запятой, результаты таковы:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C#

1,351

1,021

24%

Java

1,923

1,282

33%

Visual Basic

1,752

1,011

42%

Примечание: поиск выполнялся в массиве из 100 четырехбайтовых чисел с плаваю#
щей запятой.

ГЛАВА 26 Методики оптимизации кода

609

Как обычно, многое зависит от языка.
Сигнальное значение можно использовать почти в любой ситуации, требующей
выполнения линейного поиска, причем не только в массивах, но и в связных спис#
ках. Вы только должны тщательно выбирать сигнальные значения и с осторож#
ностью включать их в структуры данных.

Вложение более ресурсоемкого цикла
в менее ресурсоемкий
Если вы имеете дело с вложенными циклами, подумайте, какой из них должен быть
внешним, а какой внутренним. Вот пример вложенного цикла, который можно
улучшить:

Пример вложенного цикла, который можно улучшить (Java)
for ( column = 0; column < 100; column++ ) {
for ( row = 0; row < 5; row++ ) {
sum = sum + table[ row ][ column ];
}
}
Ключ к улучшению цикла в том, что внешний цикл состоит из гораздо большего
числа итераций, чем внутренний. С выполнением любого цикла связаны наклад#
ные расходы: в начале цикла индекс должен быть инициализирован, а при каж#
дой итерации — увеличен и проверен. Общее число итераций равно 100 для внеш#
него цикла и 100 * 5 = 500 для внутреннего цикла, что дает в сумме 600 итераций.
Просто поменяв местами внешний и внутренний циклы, вы можете снизить чис#
ло итераций внешнего цикла до 5, тогда как число итераций внутреннего цикла
останется тем же. В итоге вместо 600 итераций будут выполнены только 505. Можно
ожидать, что перемена циклов местами приведет примерно к 16%#ому улучшению:
(600 – 505) / 600 = 16%. На самом деле результаты таковы:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C++

4,75

3,19

33%

Java

5,39

3,56

34%

PHP

4,16

3,65

12%

Python

3,48

3,33

4%

Значительные различия в очередной раз доказывают, что по поводу следствий
оптимизации нельзя сказать ничего определенного, не оценив их в конкретной
среде.

Снижение стоимости операций
Под снижением стоимости (strength reduction) понимают замену дорогой опера#
ции на более дешевую, например, умножения на сложение. Иногда внутри цикла
выполняется умножение индекса на какие#то другие значения. Сложение обычно
выполняется быстрее, чем умножение, и, если вы можете вычислить то же число,

610

ЧАСТЬ V

Усовершенствование кода

заменив умножение на прибавление значения при каждой итерации цикла, это
скорее всего приведет к ускорению выполнения кода. Вот пример кода, основан#
ного на умножении:

Пример умножения с использованием индекса цикла (Visual Basic)
For i = 0 to saleCount  1
commission( i ) = (i + 1) * revenue * baseCommission * discount
Next
Этот код прост, но дорог. В то же время цикл можно переписать так, чтобы при
каждой итерации выполнялось более дешевое сложение:

Пример замены умножения на сложение (Visual Basic)
incrementalCommission = revenue * baseCommission * discount
cumulativeCommission = incrementalCommission
For i = 0 to saleCount  1
commission( i ) = cumulativeCommission
cumulativeCommission = cumulativeCommission + incrementalCommission
Next
Этот вид изменения похож на купон, предоставляющий скидку со стоимости цикла.
В первоначальном коде при каждой итерации выполнялось умножение выраже#
ния revenue * baseCommission * discount на счетчик цикла, увеличенный на едини#
цу: сначала на 1, затем на 2, затем на 3 и т. д. В оптимизированном коде значение
выражения revenue * baseCommission * discount присваивается переменной incre%
mentalCommission. После этого при каждой итерации цикла значение incremental%
Commission прибавляется к cumulativeCommission. При первой итерации оно при#
бавляется один раз, при второй — два, при третьей — три и т. д. Эффект тот
же, что и при умножении incrementalCommission на 1, на 2, на 3 и т. д., но оптими#
зированный вариант дешевле.
Чтобы этот вид оптимизации оказался возможным, первоначальное умножение
должно зависеть от индекса цикла. В данном примере индекс цикла был единствен#
ной изменяющейся частью выражения, поэтому мы и смогли сделать выражение
более эффективным. Вот к чему это привело:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени

C++

4,33

3,80

12%

Visual Basic

3,54

1,80

49%

Примечание: тестирование выполнено для saleCount = 20. Все используемые в вычис#
лении переменные были переменными с плавающей запятой.

ГЛАВА 26 Методики оптимизации кода

611

26.3. Изменения типов данных
Изменение типов данных может быть эффективным способом сокращения кода
и повышения его быстродействия. Проектирование структур данных в этой кни#
ге не рассматривается, но умеренные изменения реализации отдельных типов
данных также могут повышать производительность. Ниже описано несколько
способов оптимизации типов данных.

Использование целых чисел вместо чисел
с плавающей запятой
Сложение и умножение целых чисел, как правило, выпол#
няются быстрее, чем аналогичные операции над числами с
плавающей запятой. Например, циклы выполняются быст#
рее, если индекс имеет целочисленный тип.

Перекрестная ссылка Об использовании целых чисел и чисел с плавающей запятой см.
главу 12.

Пример неэффективного цикла с индексом
с плавающей запятой (Visual Basic)
Dim x As Single
For x = 0 to 99
a( x ) = 0
Next
Сравните этот код с аналогичным циклом, в котором явно используется целочис#
ленный индекс:

Пример эффективного цикла с целочисленным индексом (Visual Basic)
Dim i As Integer
For i = 0 to 99
a( i ) = 0
Next
Насколько выгоден этот вид оптимизации? Вот результаты выполнения указанных
фрагментов кода Visual Basic и аналогичных циклов, написанных на C++ и PHP:

Язык

Соотношение
Время выполнения
Время выполнения Экономия быстродействия
кода до оптимизации оптимизированного времени
кода

C++

2,80

0,801

71%

PHP

5,01

4,65

7%

1:1

0,280

96%

25:1

Visual Basic 6,84

3,5:1

Использование массивов с минимальным
числом измерений
Использовать массивы, имеющие несколько измерений,
накладно. Если вы сможете структурировать данные так,
чтобы их можно было хранить в одномерном, а не двумер#

Перекрестная ссылка О массивах см. раздел 12.8.

612

ЧАСТЬ V

Усовершенствование кода

ном или трехмерном массиве, вы скорее всего ускорите выполнение программы.
Допустим, у вас есть подобный код инициализации массива:

Пример стандартной инициализации двумерного массива (Java)
for ( row = 0; row < numRows; row++ ) {
for ( column = 0; column < numColumns; column++ ) {
matrix[ row ][ column ] = 0;
}
}
При инициализации массива из 50 строк и 20 столбцов этот код выполняется вдвое
дольше, чем код инициализации аналогичного одномерного массива, сгенериро#
ванный тем же компилятором Java. Вот как выглядел бы исправленный код:

Пример одномерного представления массива (Java)
for ( entry = 0; entry < numRows * numColumns; entry++ ) {
matrix[ entry ] = 0;
}
А вот результаты тестирования этого кода и похожего кода, написанного на не#
скольких других языках:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

C++

8,75

7,82

11%

1:1

C#

3,28

2,99

9%

1:1

Java

7,78

4,14

47%

2:1

PHP

6,24

4,10

34%

1,5:1

Python

3,31

2,23

32%

1,5:1

Visual Basic 9,43

3,22

66%

3:1

Примечание: временные показатели, указанные для Python и PHP, получены в резуль#
тате более чем в 100 раз меньшего числа итераций, чем показатели, приведенные для
других языков, поэтому их непосредственное сравнение недопустимо.

Результаты этого вида оптимизации прекрасны для Visual Basic и Java, хороши для
PHP и Python, но довольно заурядны для C++ и C#. Правда, время выполнения не#
оптимизированного кода C# было лучшим, так что на это едва ли можно жаловаться.
Широкий разброс результатов лишь подтверждает недальновидность слепого сле#
дования любым советам по оптимизации. Не испытав методику в конкретных об#
стоятельствах, ни в чем нельзя быть уверенным.

Минимизация числа обращений к массивам
Кроме минимизации числа обращений к двумерным или трехмерным массивам
часто выгодно минимизировать число обращений к массивам вообще. Подходя#
щий кандидат для применения этой методики — цикл, в котором повторно ис#

ГЛАВА 26 Методики оптимизации кода

613

пользуется один и тот же элемент массива. Вот пример необязательного обраще#
ния к массиву:

Пример необязательного обращения к массиву внутри цикла (C++)
for ( discountType = 0; discountType < typeCount; discountType++ ) {
for ( discountLevel = 0; discountLevel < levelCount; discountLevel++ ) {
rate[ discountLevel ] = rate[ discountLevel ] * discount[ discountType ];
}
}
При изменении индекса discountLevel по мере выполнения внутреннего цикла
обращение к массиву discount[ discountType ] остается все тем же. Вы можете вы#
нести его за пределы внутреннего цикла, и тогда у вас будет одно обращение к
массиву на одну итерацию внешнего, а не внутреннего цикла. Вот оптимизиро#
ванный код:

Пример вынесения обращения к массиву за пределы цикла (C++)
for ( discountType = 0; discountType < typeCount; discountType++ ) {
thisDiscount = discount[ discountType ];
for ( discountLevel = 0; discountLevel < levelCount; discountLevel++ ) {
rate[ discountLevel ] = rate[ discountLevel ] * thisDiscount;
}
}
Результаты:
Язык

Время выполнения
кода до оптимизации

Время выполнения
оптимизированного кода

Экономия
времени
#7%

C++

32,1

34,5

C#

18,3

17,0

7%

Visual Basic

23,2

18,4

20%

Примечание: тестирование выполнено для typeCount = 10 и levelCount = 100.

Как обычно, результаты зависят от конкретного компилятора.

Использование дополнительных индексов
Использование дополнительного индекса предполагает добавление данных, свя#
занных с основным типом данных и повышающих эффективность обращений к
нему. Связанные данные можно добавить к основному типу или хранить в парал#
лельной структуре.

Индекс длины строки
Примером использования дополнительного индекса может служить одна из форм
представления строк. В языке C строки заканчиваются нулевым байтом. Что каса#
ется строк Visual Basic, то их длина хранится в начальном байте. Чтобы опреде#
лить длину строки C, нужно начать с начала строки и продвигаться по ней, под#
считывая байты, до достижения нулевого байта. Для определения длины строки
Visual Basic, нужно просто прочитать байт длины. Байт длины строки Visual Basic

614

ЧАСТЬ V

Усовершенствование кода

— наглядный пример дополнения типа данных индексом, ускоряющим выполне#
ние определенных операций, таких как вычисление длины строки.
Идею индексации длины можно приспособить к любому типу данных перемен#
ной длины. Слежение за длиной структуры часто — более эффективный подход,
чем вычисление длины каждый раз, когда она требуется.

Независимая параллельная структура индексации
Иногда выгоднее работать с индексом типа данных, а не с самим типом данных.
Если элементы типа данных велики или их накладно перемещать (скажем, на диск),
сортировка и поиск по индексам будут выполняться быстрее, чем непосредственные
операции над данными. Если каждый элемент данных велик, вы можете создать
вспомогательную структуру, состоящую из ключевых значений и указателей на
подробную информацию. Если различие размеров элемента структуры данных и
элемента вспомогательной структуры достаточно велико, элемент#ключ можно
хранить в памяти, а сами данные — на внешнем носителе. Все операции поиска и
сортировки будут выполняться в памяти, а к диску можно будет обращаться толь#
ко после определения точного расположения нужного вам элемента.

Кэширование
Кэширование — это такой способ хранения нескольких значений, при котором
значения, используемые чаще всего, получить легче, чем значения, используемые
реже. Так, если программа случайным образом читает записи с диска, метод мо#
жет хранить в кэше записи, считываемые наиболее часто. Получив запрос запи#
си, метод проверяет, имеется ли запись в кэше. Если да, запись возвращается не#
посредственно из памяти, а не считывается с диска.
Кэширование можно применять и в других областях. В программе обработки
шрифтов Microsoft Windows узким местом было получение ширины символа при
его отображении на экране. Кэширование ширины символа, использованного
последним, позволило примерно вдвое ускорить отображение.
Вы можете кэшировать и результаты ресурсоемких вычислений, особенно если
их параметры просты. Пусть, например, вам нужно найти длину гипотенузы пря#
моугольного треугольника по длинам двух катетов. Простая реализация этого
метода была бы примерно такой:

Пример метода, напрашивающегося на кэширование (Java)
double Hypotenuse(
double sideA,
double sideB
) {
return Math.sqrt( ( sideA * sideA ) + ( sideB * sideB ) );
}
Если вы знаете, что те же значения скорее всего будут переданы в метод повтор#
но, их можно кэшировать:

ГЛАВА 26 Методики оптимизации кода

615

Пример кэширования для предотвращения дорогих вычислений (Java)
private double cachedHypotenuse = 0;
private double cachedSideA = 0;
private double cachedSideB = 0;
public double Hypotenuse(
double sideA,
double sideB
) {
// Присутствуют ли параметры треугольника в кэше?
if ( ( sideA == cachedSideA ) && ( sideB == cachedSideB ) ) {
return cachedHypotenuse;
}
// Вычисление новой гипотенузы и ее кэширование.
cachedHypotenuse = Math.sqrt( ( sideA * sideA ) + ( sideB * sideB ) );
cachedSideA = sideA;
cachedSideB = sideB;
return cachedHypotenuse;
}
Вторая версия метода сложнее и объемнее первой, поэтому она должна обосно#
вать свое право на жизнь быстродействием. Многие схемы кэширования предпо#
лагают кэширование более одного элемента, и с ними связаны еще большие за#
траты. Вот быстродействие двух версий метода:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

C++

4,06

1,05

74%

4:1

Java

2,54

1,40

45%

2:1

Python

8,16

4,17

49%

2:1

Visual Basic 24,0

12,9

47%

2:1

Примечание: эти результаты предполагают, что на каждый промах кэша приходятся
два попадания.

Польза кэширования зависит от относительной стоимости обращения к кэширо#
ванному элементу, создания некэшированного элемента и сохранения нового
элемента в кэше. Она также зависит от числа запросов кэшированной информа#
ции. Иногда она может зависеть и от аппаратного кэширования. В целом чем до#
роже генерирование нового элемента и чем чаще запрашивается та же самая ин#
формация, тем выгоднее кэширование. Чем дешевле обращение к кэшированно#
му элементу и сохранение новых элементов в кэше, тем выгоднее кэширование.
Как и другие методики оптимизации, кэширование усложняет код и часто оказы#
вается источником ошибок.

616

ЧАСТЬ V

Усовершенствование кода

26.4. Выражения
Многие задачи программирования требуют применения
математических и логических выражений. Сложные выра#
жения обычно дороги, и в этом разделе мы рассмотрим
способы их удешевления.

Перекрестная ссылка О выражениях см. раздел 19.1.

Алгебраические тождества
Алгебраические тождества иногда позволяют заменить дорогие операции на бо#
лее дешевые. Так, следующие выражения логически эквивалентны:

not a and not b
not (a or b)
Выбрав второе выражение вместо первого, вы сэкономите одну операцию not.
Устранение одной операции not, вероятно, не приведет к заметным результатам,
однако в целом этот принцип очень полезен. Так, Джон Бентли пишет, что в од#
ной программе проверялось условие sqrt(x) < sqrt(y) (Bentley, 1982). Так как sqrt(x)
меньше sqrt(y), только когда x меньше, чем y, исходную проверку можно заменить
на x < y. Если учесть дороговизну метода sqrt(), можно ожидать, что это приведет
к огромной экономии. Так и есть:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

C++

7,43

0,010

99,9%

750:1

Visual Basic 4,59

0,220

95%

20:1

Python

0,401

90%

10:1

4,21

Снижение стоимости операций
Как уже было сказано, снижение стоимости операций подразумевает замену до#
рогой операции более дешевой. Вот некоторые возможные варианты:
 замена умножения сложением;
 замена возведения в степень умножением;
 замена тригонометрических функций их эквивалентами;
 замена типа longlong на long или int (следите при этом за аспектами произво#

дительности, связанными с применением целых чисел естественной и неес#
тественной длины);
 замена чисел с плавающей запятой числами с фиксированной точкой или целые

числа;
 замена чисел с плавающей запятой с удвоенной точностью числами с одинар#

ной точностью;
 замена умножения и деления целых чисел на два операциями сдвига.

Допустим, вам нужно вычислить многочлен. Если вы забыли, что такое многочле#
ны, напомню, что это выражения вида Ax2 + Bx + C. Буквы A, B и C — это коэффи#

ГЛАВА 26 Методики оптимизации кода

617

циенты, а x — переменная. Обычный код вычисления значения многочлена n#ной
степени выглядит так:

Пример вычисления многочлена (Visual Basic)
value = coefficient( 0 )
For power = 1 To order
value = value + coefficient( power ) * xˆpower
Next
Если вы подумаете о снижении стоимости операций, то поймете, что оператор
возведения в степень — не самое эффективное решение в этом случае. Возведе#
ние в степень можно заменить на умножение, выполняемое при каждой итера#
ции цикла, что во многом похоже на снижение стоимости, выполненное нами
ранее, когда умножение было заменено на сложение. Вот как выглядел бы код,
снижающий стоимость вычисления многочлена:

Пример снижения стоимости вычисления многочлена (Visual Basic)
value = coefficient( 0 )
powerOfX = x
For power = 1 to order
value = value + coefficient( power ) * powerOfX
powerOfX = powerOfX * x
Next
Если вы имеете дело с многочленами второй или более высокой степени, выгода
может быть очень приличной:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

Python

3,24

Visual Basic 6,26

2,60

20%

1:1

0,160

97%

40:1

Если вы действительно серьезно отнесетесь к снижению стоимости операций, то
позаботитесь и о двух умножениях чисел с плавающей запятой. Стоимость опе#
раций, выполняемых в цикле, можно сделать еще меньшей, если постепенно воз#
водить в нужную степень сразу несколько компонентов выражения, а не находить
нужную степень при каждой итерации путем умножения:

Пример дальнейшего снижения стоимости вычисления многочлена (Visual Basic)
value = 0
For power = order to 1 Step 1
value = ( value + coefficient( power ) ) * x
Next
value = value + coefficient( 0 )
В этой версии метода отсутствует переменная powerOfX, а вместо двух умноже#
ний при каждой итерации выполняется одно. Результаты таковы:

618

ЧАСТЬ V

Язык

Усовершенствование кода

Экономия
Время
Время выполнения Время выполнения времени за
кода выполнения после первой
после второй
счет второй
до оптимизации оптимизации
оптимизации
оптимизации

Python

3,24

2,60

2,53

3%

Visual Basic

6,26

0,16

0,31

#94%

Это хороший пример расхождения теории и практики. Код, имеющий снижен#
ную стоимость, казалось бы, должен работать быстрее, но на деле это не так. Воз#
можно, в Visual Basic снижение производительности объясняется декрементом
счетчика цикла на 1 вместо инкремента, но чтобы говорить об этом с уверенно#
стью, эту гипотезу нужно оценить.

Инициализация во время компиляции
Если вы вызываете метод, передавая ему в качестве единственного аргумента
именованную константу или магическое число, попробуйте предварительно вы#
числить нужное значение, присвоить его константе и избежать вызова метода. Это
же справедливо для умножения, деления, сложения и других операций.
Однажды мне понадобилось вычислять значение двоичного логарифма целого
числа, округленное до ближайшего целого числа. Система не предоставляла ме#
тод вычисления двоичного логарифма, поэтому я написал собственный. Быстрый
и легкий подход был основан на формуле:

log(x)base = log(x) / log(base)
Перекрестная ссылка О связывании переменных со значениями см. раздел 10.6.

Опираясь на это тождество, я написал такой метод:

Пример метода, вычисляющего двоичный логарифм
с использованием системных методов (C++)

unsigned int Log2( unsigned int x ) {
return (unsigned int) ( log( x ) / log( 2 ) );
}
Этот метод был очень медленным, а так какзначение log(2) измениться не может,
я заменил вызов метода log(2) на действительное значение, равное 0.69314718:

Пример метода, вычисляющего двоичный логарифм
с использованием системного метода и константы (C++)
const double LOG2 = 0.69314718;
...
unsigned int Log2( unsigned int x ) {
return (unsigned int) ( log( x ) / LOG2 );
}
Вызов метода log() довольно дорог — гораздо дороже преобразования типа или
деления, и поэтому резонно предположить, что уменьшение числа вызовов мето#
да log() вдвое должно примерно в два раза ускорить выполнение метода. Вот ре#
зультаты измерений:

ГЛАВА 26 Методики оптимизации кода

Время выполнения
кода до оптимизации

Язык

Время выполнения
оптимизированного кода

619

Экономия
времени

C++

9,66

5,97

38%

Java

17,0

12,3

28%

PHP

2,45

1,50

39%

Обоснованное предположение оказалось довольно близким к действительности.
Если учесть невысокую предсказуемость результатов, приводимых в этой главе,
точность моего прогноза в этом случае доказывает только то, что даже слепые белки
иногда наталкиваются на орехи.

Недостатки системных методов
Системные методы дороги и часто обеспечивают избыточную точность. Напри#
мер, большинство системных математических методов написаны с тем расчетом,
чтобы космонавт, отправившийся на Луну, прилунился с точностью ±2 фута. Если
вам не нужна такая точность, нет смысла тратить на нее время.
В предыдущем примере метод Log2() возвращал целое число, но использовал для его
вычисления метод log(), возвращающий число с плавающей запятой. Я не нуждался в
такой точности, так что после своей первой попытки я написал ряд целочисленных
проверок, которые прекрасно вычисляли целое значение двоичного логарифма:

Пример метода, возвращающего примерное значение двоичного логарифма (C++)
unsigned int Log2( unsigned int x ) {
if ( x < 2 ) return 0 ;
if ( x < 4 ) return 1 ;
if ( x < 8 ) return 2 ;
if ( x < 16 ) return 3 ;
if ( x < 32 ) return 4 ;
if ( x < 64 ) return 5 ;
if ( x < 128 ) return 6 ;
if ( x < 256 ) return 7 ;
if ( x < 512 ) return 8 ;
if ( x < 1024 ) return 9 ;
...
if ( x < 2147483648 ) return 30;
return 31 ;
}
Этот метод использует целочисленные операции, никогда не преобразовывает
целые числа в числа с плавающей запятой и значительно превосходит по быст#
родействию оба метода, работающих с числами с плавающей запятой:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

C++

9,66

0,662

Java

17,0

0,882

95%

20:1

PHP

2,45

3,45

#41%

2:3

93%

15:1

620

ЧАСТЬ V

Усовершенствование кода

Большинство так называемых «трансцендентных» функций разработано для наи#
худшего случая, т. е. внутри себя они даже целочисленный аргумент преобразуют
в число с плавающей запятой с удвоенной точностью. Обнаружив вызов такой
функции в проблемном фрагменте кода, уделите ей самое пристальное внимание,
если, конечно, вам не нужна подобная точность.
Но вернемся к нашему методу. Еще один вариант его оптимизации основан на том
факте, что деление на 2 аналогично операции сдвига вправо. Двоичный логарифм
числа равен количеству операций деления на 2, которое можно выполнить над этим
числом до получения нулевого значения. Вот как выглядит соответствующий код:

Пример альтернативного метода, определяющего примерное значение
двоичного логарифма с использованием оператора сдвига вправо (C++)
unsigned int Log2( unsigned int x ) {
unsigned int i = 0;
while ( ( x = ( x >> 1 ) ) != 0 ) {
i++;
}
return i ;
}
Читать этот код трудно, особенно программистам, не работавшим с C++. Слож#
ное выражение в условии цикла while — прекрасный пример того, что следует
использовать только в крайних случаях.
Этот метод выполняется примерно на 350% дольше, чем более длинная предыду#
щая версия (2,4 и 0,66 секунды соответственно), но он быстрее, чем первый опти#
мизированный метод, и легко адаптируется к 32#, 64#разрядным и другим средам.
Этот пример ясно показывает, насколько полезно продолжать поиск после
нахождения первого успешного вида оптимизации. Первый вид оптими#
зации привел к приличному повышению быстродействия на 30–40%, но
это не идет ни в какое сравнение с результатами второго и третьего видов опти#
мизации.

Использование констант корректного типа
Используйте именованные константы и литералы, имеющие тот же тип, что и
переменные, которым они присваиваются. Если константа и соответствующая ей
переменная имеют разные типы, перед присвоением константы переменной ком#
пилятор должен будет выполнить преобразование типа. Хорошие компиляторы
преобразуют типы во время компиляции, чтобы не снижалась производительность
в период выполнения программы.
Однако менее эффективные компиляторы или интерпретаторы генерируют код,
преобразующий типы в период выполнения. Чуть ниже указаны различия во вре#
мени инициализации переменной с плавающей точкой x и целочисленной пере#
менной i в двух случаях. В первом случае инициализация выглядит так:

x = 5
i = 3.14

ГЛАВА 26 Методики оптимизации кода

621

и требует преобразований типов. Во втором случае преобразования типов не
нужны:

x = 3.14
i = 5
Результаты в очередной раз указывают на большие различия между компиляторами:

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

C++

1,11

C#

1,49

1,48



int interestIndex =
Math.round( ( interestRate  LOWEST_RATE ) * GRANULARITY * 100.00 );
return loanAmount / loanDivisor[ interestIndex ][ months ];
}
Итак, сложное вычисление мы заменили вычислением индекса массива и одним
обращением к массиву. К чему же это привело?

Язык

Время выполнения
Время выполнения
оптимизированного Экономия Соотношение
кода до оптимизации кода
времени
быстродействия

Java

2,97

0,251

92%

10:1

Python

3,86

4,63

–20%

1:1

В зависимости от обстоятельств вы должны были бы предварительно вычислять
массив loanDivisor при инициализации программы или читать его из файла. Вы
также могли бы инициализировать массив нулями, вычислять каждый элемент при
его первом запросе, сохранять его, а впоследствии просто извлекать из массива.
Это было бы одной из форм кэширования, которое обсуждалось выше.

ГЛАВА 26 Методики оптимизации кода

623

Предварительное вычисление выражения не обязывает вас создавать таблицу —
возможен и иной вариант. Допустим, у вас есть код, вычисляющий взносы, кото#
рые нужно уплатить при погашении ссуд на разную сумму:

Пример второго сложного выражения,
которое можно вычислить предварительно (Java)
double ComputePayments(
int months,
double interestRate
) {
for ( long loanAmount = MIN_LOAN_AMOUNT; loanAmount < MAX_LOAN_AMOUNT;
loanAmount++ ) {
payment = loanAmount / (
( 1.0 – Math.pow( 1.0+(interestRate/12.0),  months ) ) /
( interestRate/12.0 )
);
Следующий код делал бы что-то с переменной payment; что именно — в данном примере не важно.

>

...
}
}
Даже без таблицы, вы можете предварительно вычислить сложную часть выраже#
ния вне цикла и использовать найденное значение внутри цикла:

Пример предварительного вычисления второго сложного выражения (Java)
double ComputePayments(
int months,
double interestRate
) {
long loanAmount;
Предварительное вычисление части выражения.

>

double divisor = ( 1.0 – Math.pow( 1.0+(interestRate/12.0).  months ) ) /
( interestRate/12.0 );
for ( long loanAmount = MIN_LOAN_AMOUNT; loanAmount 10 ) Then
If ( soldCount > 100 And prevMonthSales > 10 ) Then
If ( soldCount > 1000 ) Then
markdown = 0.1
profit = 0.05
Else
markdown = 0.05
End If
Else
markdown = 0.025
End If
Else
markdown = 0.0
End If

728

ЧАСТЬ VII

Мастерство программирования

В чем причина причудливого форматирования выражений Else в конце примера?
Они последовательно выровнены под соответствующими ключевыми словами, но
вряд ли можно утверждать, что такие отступы проясняют логическую структуру.
И если при модификации кода изменится длина первой строки, такой стиль фор#
матирования потребует изменения отступов во всех соответствующих выражениях.
Так что возникает проблема сопровождения, которой не существует для стилей
явных блоков, их эмуляции и использования пар begin%end для обозначения гра#
ниц блоков.
Вы можете решить, что эти примеры придуманы лишь в демонстрационных це#
лях, но такой стиль применяется очень упорно, несмотря на все его недостатки.
Масса учебников и справочников по программированию рекомендует этот стиль.
Самая первая увиденная мной книга, содержащая эту рекомендацию, была опуб#
ликована в середине 1970#х, последняя — в 2003 году.
Вообще форматирование в конце строки неаккуратно, его сложно применять еди#
нообразно и тяжело сопровождать. Далее я приведу другие проблемы такого стиля.

Какой стиль наилучший?
При работе в Visual Basic используйте отступы для явных блоков (тем более что в
среде разработки Visual Basic затруднительно не придерживаться этого стиля).
В Java стандартной практикой является применение формата явных блоков.
В C++ можно просто выбрать тот стиль, который вам больше нравится или кото#
рому отдают предпочтение большинство разработчиков вашей команды. Как эмуля#
ция явных блоков, так и обозначение границ с помощью begin%end работает оди#
наково хорошо. Единственное исследование, сравнивавшее эти два стиля, не об#
наружило статистически значимых различий между ними с точки зрения понят#
ности кода (Hansen and Yim, 1987).
Ни один из этих стилей не обеспечивает защиты от дурака, и оба время от време#
ни требуют «разумного и очевидного» компромисса. Вы можете предпочесть тот
или иной стиль по эстетическим причинам. В этой книге в примерах кода при#
меняется стиль явных блоков, так что вы можете увидеть массу иллюстраций это#
го стиля, просто просмотрев листинги. Выбрав однажды стиль, вы получите наи#
большую выгоду от хорошего форматирования, применяя его единообразно.

31.4. Форматирование управляющих структур
Перекрестная ссылка О документировании управляющих
структур см. подраздел «Комментирование управляющих
структур» раздела 32.5. О других аспектах управляющих
структур см. главы 14–19.

Форматирование некоторых программных элементов час#
то является только эстетическим вопросом. Однако форма#
тирование управляющих структур влияет на удобство чте#
ния и понимания и поэтому имеет практическое значение.

Тонкие моменты форматирования
блоков управляющих структур

Работа с блоками управляющих структур требует внимания к деталям. Вот неко#
торые советы.

ГЛАВА 31 Форматирование и стиль

729

Избегайте отсутствия отступов в парах begin'end В стиле форматиро#
вания, проиллюстрированном в листинге 31#24, пара begin%end выровнена по гра#
нице управляющей структуры, а в выражениях, охватываемых операторами begin
и end, сделаны отступы относительно begin.

Листинг 31-24. Пример пары begin-end, не выделенной отступами (Java)
Ключевое слово begin выровнено по границе for.

> for ( int i = 0; i < MAX_LINES; i++ )
{
В выражениях сделан отступ относительно begin.

>

ReadLine( i );
ProcessLine( i );
Слово end также выровнено по границе структуры for.

>}
Хотя такой подход выглядит хорошо, он нарушает Основную теорему формати#
рования, так как не показывает логическую структуру кода. При таком располо#
жении begin и end не являются частью управляющей структуры, но в то же время,
они не являются и частью блока выражений, расположенного далее.
Листинг 31#25 демонстрирует абстрактное представление этого подхода:

Листинг 31-25. Абстрактный пример вводящего в заблуждение выравнивания
A
B
C
D
E

XXXXXXXXXXXXXXXXXXXX
XXXXXXX
XXXXXXXX
XXXXXXXXXXXXXX
XXXX

Можно ли сказать, что оператор B подчиняется оператору A? Он не выглядит ча#
стью оператора A, и нет оснований считать, что он ему подчиняется. Если вы
используете такой подход, смените его на один из двух вариантов стиля, описан#
ных ранее, и ваше форматирование будет более целостным.
Избегайте двойных отступов при использовании begin и end Следствием
правила относительно использования пары begin%end без отступов является слу#
чай, касающийся дополнительных отступов после begin%end. Этот стиль, продемон#
стрированный в листинге 31#26, содержит отступы как перед begin и end, так и
перед выражениями, которые они охватывают:

Листинг 31-26. Пример неуместного двойного отступа
в блоке begin-end (Java)
for ( int i = 0; i < MAX_LINES; i++ )
{

730

ЧАСТЬ VII

Мастерство программирования

В выражениях после begin сделан лишний отступ.

>

ReadLine( i );
ProcessLine( i );
}
Еще один пример стиля, внешне привлекательного, но нарушающего Основную
теорему форматирования. Исследования показали, что с точки зрения понимания
программы, использующие одинарные и двойные отступы, не отличаются друг от
друга (Miaria et al., 1983), но приведенный стиль неточно отображает логическую
структуру программы. ReadLine() и ProcessLine() показаны так, будто они подчи#
нены паре begin%end, что не соответствует действительности.
Этот подход также преувеличивает сложность логической структуры программы.
Какая из структур, приведенных в листингах 31#27 и 31#28, выглядит сложнее?

Листинг 31-27. Абстрактная структура 1
XXXXXXXXXXXXXXXXXXXX
XXXXX
XXXXXXXXX
XXXXXXXXXXXX
XXXXX

Листинг 31-28. Абстрактная структура 2
XXXXXXXXXXXXXXXXXXXX
XXXXX
XXXXXXXXXX
XXXXXXXXXXXXX
XXXXX
Обе являются абстрактными представлениями структуры цикла for. Структура 1
выглядит сложней, хотя и представляет тот же код, что и Структура 2. Если бы вам
понадобилось создать 2 или 3 уровня вложенности операторов, то из#за двойных
отступов вы получили бы 4 или 6 уровней отступов. Такое форматирование вы#
глядело бы сложнее, чем на самом деле. Избавьтесь от этой проблемы, эмулиро#
вав явные блоки или применив begin и end в качестве границ блока, выравнивая
begin и end так же, как и выражения, которые они охватывают.

Другие соглашения
Хотя отступы в блоках являются главным вопросом форматирования управляю#
щих структур, вы также можете столкнуться с другими проблемами, поэтому да#
лее я привожу еще несколько советов.
Используйте пустые строки между абзацами Некоторые блоки не разгра#
ничиваются с помощью пар begin%end. Логический блок — группа подходящих друг
к другу операторов — должны рассматриваться так же, как абзацы в обычной книге.
Отделяйте их друг от друга с помощью пустых строк. Листинг 31#29 представля#
ет пример абзацев, которые следует разделить:

ГЛАВА 31 Форматирование и стиль

731

Листинг 31-29. Пример кода, который следует сгруппировать
и разбить на абзацы (C++)
cursor.start = startingScanLine;
cursor.end
= endingScanLine;
window.title = editWindow.title;
window.dimensions
= editWindow.dimensions;
window.foregroundColor = userPreferences.foregroundColor;
cursor.blinkRate
= editMode.blinkRate;
window.backgroundColor = userPreferences.backgroundColor;
SaveCursor( cursor );
SetCursor( cursor );
Этот код выглядит неплохо, но пустые строки позволят улуч#
Перекрестная ссылка Если вы
шить его с двух точек зрения. Во#первых, если у вас есть груп#
придерживаетесь Процесса программирования с псевдокодом,
па операторов, не требующих выполнения в определенном
ваш код автоматически разбипорядке, заманчиво объединить их именно так. Вам не надо
вается на абзацы (см. главу 9).
усовершенствовать порядок выражений для помощи компь#
ютеру, но читатели#люди оценят дополнительные подсказ#
ки о том, какие операторы выполнять в определенном порядке, а какие — просто
расположены рядом друг с другом. Дисциплина добавления пустых строк в коде
программы заставляет вас тщательней обдумывать вопрос, какие операторы на самом
деле подходят друг другу. Исправленный фрагмент кода, представленный в листинге
31#30, показывает, как организовать данный набор операторов.

Листинг 31-30. Пример кода, который правильно сгруппирован
и разбит на абзацы (C++)
Эти строки настраивают текстовое окно.

> window.dimensions = editWindow.dimensions;
window.title = editWindow.title;
window.backgroundColor = userPreferences.backgroundColor;
window.foregroundColor = userPreferences.foregroundColor;
Эти строки устанавливают параметры курсора и должны быть отделены от предыдущих строк.

> cursor.start = startingScanLine;
cursor.end = endingScanLine;
cursor.blinkRate = editMode.blinkRate;
SaveCursor( cursor );
SetCursor( cursor );
Реорганизованный код подчеркивает выполнение двух разных действий. В пер#
вом примере недостатки организации операторов, отсутствие пустых строк, а также
старый трюк с выравниванием знаков равенства приводили к тому, что выраже#
ния выглядели связанными сильнее, чем на самом деле.
Второе преимущество использования пустых строк, приводящее к улучшению кода,
заключается в появлении естественного промежутка для добавления комментариев.
В листинге 31#30 комментарии над каждым блоком станут отличным дополнени#
ем к улучшенному формату.

732

ЧАСТЬ VII

Мастерство программирования

Форматируйте блоки из одного оператора единообразно Блоком из од#
ного оператора называется единственный оператор, следующий за управляющей
структурой, например, оператор, который следует за проверкой условия if. В этом
случае для корректной компиляции пара begin и end необязательна, и вы можете
выбрать один из трех вариантов стиля, показанных в листинге 31#31:

Листинг 31-31. Пример вариантов стиля для блоков из одного оператора (Java)
Стиль 1

>if ( expression )
одиноператор;
Стиль 2а

>if ( expression ) {
одиноператор;
}
Стиль 2б

>if ( expression )
{
одиноператор;
}
Стиль 3

>if ( expression ) одиноператор;
Существуют аргументы в защиту каждого из этих трех подходов. Стиль 1 следует
схеме отступов, используемой для блоков, поэтому он согласуется с остальными
подходами. Стиль 2 (как 2а, так и 2б) также не противоречит общим правилам, а
пары begin%end уменьшают вероятность добавления операторов после условия if,
не указав begin и end. Это станет особенно трудноуловимой ошибкой, потому что
отступы будут подсказывать вам, что все в порядке, однако компилятор не интер#
претирует отступы. Основное преимущества Стиля 3 над Стилем 2 в том, что его
легче набирать. Его преимущество над стилем 1 в том, что при копировании в
другую часть программы он с большей вероятностью будет скопирован правиль#
но. А недостаток в том, что при использовании построчного отладчика такая строка
будет рассматриваться как одно целое и вы не узнаете, выполняется ли выраже#
ние после проверки условия if.
Я использовал Стиль 1 и много раз становился жертвой неправильной модифи#
кации. Мне не нравится нарушение стратегии отступов в Стиле 3, поэтому его я
тоже избегаю. В групповых проектах я предпочитаю вариации Стиля 2 из#за по#
лучаемого единообразия и возможности безопасной модификации. Независимо
от избранного стиля применяйте его последовательно и используйте один и тот
же стиль в условиях if и во всех циклах.
В сложных выражениях размещайте каждое условие на отдельной строке
Размещайте каждую часть сложного выражения на отдельной строке. Листинг 31#32
содержит пример выражения, форматированного без учета удобочитаемости:

ГЛАВА 31 Форматирование и стиль

733

Листинг 31-32. Пример совершенно неформатированного
(и нечитаемого) сложного выражения (Java)
if (((‘0’ > 1;
}
Операция сдвига вправо в этом примере выбрана намеренно. Почти всем програм#
мистам известно, что в случае целых чисел сдвиг вправо функционально эквива#
лентен делению на 2.
Если это известно почти всем, зачем это документировать? Потому что целью
данного кода является не сдвиг вправо, а деление на 2. Важно то, что програм#
мист использовал не ту методику, которая лучше всего соответствует цели. Кроме
того, большинство компиляторов в любом случае заменяют целочисленное деле#
ние на 2 операцией сдвига вправо, из чего следует, что ухудшение ясности кода
обычно не требуется. В данной ситуации комментарий говорит, что компилятор

ГЛАВА 32 Самодокументирующийся код

781

не оптимизирует деление на 2, поэтому замена деления на сдвиг вправо существен#
но ускоряет выполнение кода. Благодаря комментарию программист, читающий
код, тут же поймет мотивацию использования неочевидного подхода. Если бы
комментария не было, тот же программист мог бы ворчать, что код усложнен без
причины. Обычно такое ворчание оправданно, поэтому документировать исклю#
чения из правил важно.
Избегайте сокращений Комментарии должны быть однозначны и не застав#
лять программистов расшифровывать сокращения. Никаких сокращений, кроме
самых распространенных, в комментариях быть не должно. Обычно такой соблазн
возникает только при использовании комментариев в концах строк. Это еще один
аргумент против данного вида комментариев.
Проведите различие между общими и детальными комментариями
Иногда желательно провести различие между комментариями разных уровней, ука#
зав, что детальный комментарий является частью предыдущего, более общего ком#
ментария. Эту задачу можно решить несколькими способами. Вы можете подчер#
кивать общие комментарии и не подчеркивать детальные:

Пример проведения различия между общим и детальными комментариями
при помощи подчеркивания — не рекомендуется (C++)
Общий комментарий подчеркивается.

> // копирование всех строк таблицы,
// кроме строк, подлежащих удалению
//—————————————————————————————————————
Детальный комментарий, являющийся частью действия, описываемого общим комментарием, не
подчеркивается ни здесь…

>// определение числа строк в таблице
...
…ни здесь.

> // маркирование строк, подлежащих удалению
...
Слабость этого подхода в том, что он заставляет подчеркивать больше коммента#
риев, чем хотелось бы. Подчеркивание какого#то комментария предполагает, что
все следующие неподчеркнутые комментарии являются подчиненными по отно#
шению к нему. Соответственно первый комментарий, не относящийся к подчер#
кнутому комментарию, также должен быть подчеркнут, и все начинается сызно#
ва. Результат? Избыток подчеркивания или его несогласованность.
Эта тема имеет несколько вариаций, с которыми связана та же проблема. Если вы
используете в общем комментарии только заглавные буквы, а в детальных — только
строчные, вы просто заменяете избыток подчеркивания на избыток заглавных букв.
Некоторые программисты проводят различие между общим и детальными ком#
ментариями при помощи регистра только первой буквы, но это различие слиш#
ком мало, и его легко не заметить.

782

ЧАСТЬ VII

Мастерство программирования

Лучше предварять детальные комментарии многоточием:

Пример проведения различия между общим и детальными комментариями
при помощи многоточий (C++)
Общий комментарий форматируется как обычно.

> // копирование всех строк таблицы,
// кроме строк, подлежащих удалению
Детальный комментарий, являющийся частью действия, описываемого общим комментарием, предваряется многоточием здесь…

>// ... определение числа строк в таблице
...
…и здесь.

> // ... маркирование строк, подлежащих удалению
...
Другой подход, который часто оказывается самым лучшим, — вынесение кода,
соответствующего общему комментарию, в отдельный метод. В идеале методы
должны быть логически «плоскими»: все выполняемые в них действия должны
относиться примерно к одному логическому уровню. Если в методе выполняют#
ся и общие, и детальные действия, метод не является плоским. В результате выне#
сения сложной группы действий в отдельный метод вы получите два логически
ясных метода вместо одного логически скомканного.
Данное обсуждение общих и детальных комментариев не относится к коду с от#
ступами, содержащемуся внутри циклов и условных операторов. В этих случаях
часто имеется общий комментарий перед циклом и более детальные коммента#
рии в коде с отступами. Логическая организация комментариев характеризуется
отступами. Таким образом, вышесказанное относилось только к последователь#
ным абзацам кода, когда полная операция охватывает несколько абзацев, а неко#
торые абзацы подчинены другим.
Комментируйте все, что имеет отношение к ошибкам или недокументи'
рованным возможностям языка или среды Если в языке или среде есть ошиб#
ка, она, вероятно, недокументирована. Даже если она где#то описана, не помеша#
ет сделать это еще раз в коде. Недокументированная возможность по определе#
нию не описана нигде, и ее следует задокументировать в коде.
Допустим, вы обнаружили, что библиотечный метод WriteData( data, numItems,
blockSize ) работает неверно, если blockSize имеет значение 500. Метод прекрасно
обрабатывает значения 499, 501 и все остальные, которые вы когда#либо пробо#
вали, но имеет дефект, проявляющийся, только когда параметр blockSize равен 500.
Напишите перед вызовом WriteData(), почему вы создали специальный случай для
этого значения параметра. Вот как это могло бы выглядеть:

Пример документирования кода, предотвращающего ошибку (Java)
blockSize = optimalBlockSize( numItems, sizePerItem );

ГЛАВА 32 Самодокументирующийся код

783

/* Следующий код нужен потому, что метод WriteData() содержит ошибку,
проявляющуюся, только когда третий параметр равен 500.
Значение ‘500’ ради ясности заменено на именованную константу.
*/
if ( blockSize == WRITEDATA_BROKEN_SIZE ) {
blockSize = WRITEDATA_WORKAROUND_SIZE;
}
WriteData ( file, data, blockSize );
Обосновывайте нарушения хорошего стиля программирования Если вы
вынуждены нарушить хороший стиль программирования, объясните причину.
Благодаря этому программисты, исполненные благих намерений, узнают, что
попытка улучшения вашего кода может привести к нарушению его работы, и не
станут изменять его. Объяснение ясно скажет, что вы знали, что делаете, а не до#
пустили небрежность — улучшите свою репутацию, если есть такая возможность!
Не комментируйте хитрый код — перепишите его
из проекта, в котором я принимал участие:

Вот один комментарий

Пример комментирования
хитрого кода (C++)
//
//
//
//

ОЧЕНЬ ВАЖНОЕ ЗАМЕЧАНИЕ:
Конструктор этого класса принимает ссылку на объект UiPublication.
Объект UiPublication НЕЛЬЗЯ УНИЧТОЖАТЬ раньше объекта DatabasePublication,
иначе программу ожидает мученическая смерть.

Это хороший пример одного из самых распространенных и опасных заблужде#
ний, согласно которому комментарии следует использовать для документирова#
ния особенно «хитрых» или «нестабильных» фрагментов кода. Данная идея обо#
сновывается тем, что люди должны знать, когда им следует быть осторожными.
Это плохая идея.
Комментирование хитрого кода — как раз то, чего делать не следует. Коммента#
рии не могут спасти сложный код. Как призывают Керниган и Плоджер, «не доку#
ментируйте плохой код — перепишите его» (Kernighan and Plauger, 1978).
Исследования показали, что фрагменты исходного кода с большим чис#
лом комментариев обычно включали максимальное число дефектов и
отнимали б óльшую долю ресурсов, уходивших на разработку ПО (Lind
and Vairavan, 1989). Ученые предположили, что программисты склонны щедро ком#
ментировать сложный код.
Когда кто#то говорит: «Это по#настоящему хитрый код,» — я слышу: «Это
по#настоящему плохой код». Если вам что#то кажется хитрым, для кого#
то другого это окажется непонятным. Даже если что#то не кажется вам
очень уж хитрым, другой человек, который не сталкивался с этим трюком, сочтет
его очень замысловатым. Если вы спрашиваете себя: «Хитро ли это?» — это хит#
ро. Всегда можно найти несложный вариант решения проблемы, поэтому пере#
пишите код. Сделайте его несколько хорошим, чтобы нужда в комментариях во#
обще отпала, а затем прокомментируйте код, чтобы сделать его еще лучше.

784

ЧАСТЬ VII

Мастерство программирования

Этот совет относится преимущественно к коду, который вы пишете впервые. Если
вы сопровождаете программу и не имеете возможности переписывать плохой код,
комментирование хитрых фрагментов — хороший подход.

Комментирование объявлений данных
Перекрестная ссылка О форматировании данных см. подраздел «Размещение объявлений
данных» раздела 31.5. Об эффективном использовании данных см. главы 10–13.

Комментарий объявления переменной описывает аспекты
переменной, которые невозможно выразить в ее имени. Тща#
тельно документировать данные важно: по крайней мере одна
компания, изучавшая собственные методики, пришла к вы#
воду, что комментировать данные даже важнее, чем процес#
сы, в которых эти данные используются (SDC в Glass, 1982).

Указывайте в комментариях единицы измерения численных величин Ес#
ли число представляет длину, укажите единицы представления: дюймы, футы, метры
или километры. Если речь идет о времени, поясните, в чем оно выражено: в се#
кундах, прошедших с 1 января 1980 года, миллисекундах, прошедших с момента
запуска программы, или как#то иначе. Если это координаты, напишите, что они
представляют (ширину, долготу и высоту) и в чем они выражены (в радианах или
градусах), укажите систему координат и т. д. Не предполагайте, что единицы из#
мерения очевидны — для нового программиста они такими не будут. Для кого#то,
кто работает над другой частью системы, они такими не будут. После значитель#
ного изменения программы они тоже очевидными не будут.
Единицы измерения часто следует указывать в именах переменных, а не в ком#
ментариях. Например, выражение вида distanceToSurface = marsLanderAltitude вы#
глядит корректным, тогда как distanceToSurfaceInMeters = marsLanderAltitudeInFeet
ясно указывает на ошибку.
Указывайте в комментариях диапазоны допустимых
значений численных величин Если предполагается, что
значение переменной должно попадать в некоторый диа#
пазон, задокументируйте это. Одной из мощных возможно#
стей языка Ada была возможность указания диапазона до#
пустимых значений численной переменной. Если ваш язык
не поддерживает такую возможность (а большинство язы#
ков ее не поддерживает), используйте для документирования диапазона ожидае#
мых значений комментарии. Например, если переменная представляет денежную
сумму в долларах, укажите, что в вашем случае она должна находиться в пределах
от 1 до 100 долларов. Если переменная представляет напряжение, напишите, что
оно должно находиться в пределах от 105 В до 125 В.

Перекрестная ссылка Более
эффективный способ документирования диапазонов допустимых значений переменных —
использование утверждений в
начале и в конце метода (см.
раздел 8.2).

Комментируйте смысл закодированных значений Если ваш язык поддер#
живает перечисления (как C++ и Visual Basic), используйте их для выражения смысла
закодированных значений. Если нет, указывайте смысл каждого значения в ком#
ментариях и представляйте каждое значение в форме именованной константы, а
не литерала. Так, если переменная представляет виды электрического тока, заком#
ментируйте тот факт, что 1 представляет переменный ток, 2 — постоянный, а 3
— неопределенный вид.

ГЛАВА 32 Самодокументирующийся код

785

Вот пример, иллюстрирующий три предыдущих рекомендации: вся информация
о диапазонах значений указана в комментариях:

Пример грамотного документирования объявлений
переменных (Visual Basic)
Dim cursorX As Integer
Dim cursorY As Integer

‘ горизонтальная позиция курсора; диапазон: 1..MaxCols
‘ вертикальная позиция курсора; диапазон: 1..MaxRows

Dim antennaLength As Long
Dim signalStrength As Integer

‘ длина антенны в метрах; диапазон: >= 2
‘ мощность сигнала в кВт; диапазон: >= 1

Dim characterCode As Integer
‘ код символа ASCII; диапазон: 0..255
Dim characterAttribute As Integer ‘ 0=Обычный; 1=Курсив; 2=Жирный; 3=Жирный курсив
Dim characterSize As Integer
’ размер символа в точках; диапазон: 4..127
Комментируйте ограничения входных данных Входные данные могут быть
получены в виде входного параметра, прочитаны из файла или введены пользо#
вателем. Предыдущие советы относятся к входным параметров методов в той же
степени, что и к другим видам данных. Убедитесь, что вы документируете ожида#
емые и неожиданные значения. Комментарии — это один из способов докумен#
тирования того, что метод никогда не должен принимать некоторые данные. За#
документировать диапазоны допустимых значений можно также, использовав
утверждения, и тогда эффективность обнаружения ввода неверных данных заметно
повысится.
Документируйте флаги до уровня отдельных би'
тов Если переменная используется как битовое поле,
укажите смысл каждого бита:

Перекрестная ссылка Об именовании переменных-флагов см.
подраздел «Именование переменных статуса» раздела 11.2.

Пример документирования флагов
до уровня битов (Visual Basic)
‘ Значения битов переменной statusFlags в порядке от самого старшего
’ бита (MSB) до самого младшего бита (LSB):
’ MSB
0
обнаружена ли ошибка?: 1=да, 0=нет

12
тип ошибки: 0=синтаксич., 1=предупреждение, 2=тяжелая, 3=фатальная

3
зарезервировано (следует обнулить)

4
статус принтера: 1=готов, 0=не готов

...

14
не используется (следует обнулить)
’ LSB
1532 не используются (следует обнулить)
Dim statusFlags As Integer
Если бы мы писали этот пример на C++, следовало бы использовать синтаксис
битовых полей — тогда значения полей были бы самодокументирующимися.
Включайте в комментарии, относящиеся к переменной, имя переменной
Если какие#то комментарии относятся к конкретной переменной, убедитесь, что
вы обновляете их вместе с переменной. Помочь в этом может использование в
комментариях имени переменной. Благодаря этому при поиске переменной в коде
вы найдете не только ее, но и связанные с ней комментарии.

786

ЧАСТЬ VII

Мастерство программирования

Документируйте глобальные данные Если вы исполь#
зуете глобальные данные, комментируйте их в местах объяв#
ления. В этом комментарии следует указать роль данных и
причину, по которой они должны быть глобальными. Используя их, каждый раз
поясняйте, что данные глобальны. Первое средство подчеркивания глобального
статуса переменной — конвенция именования. Если такую конвенцию именова#
ния вы не приняли, комментарии могут восполнить этот пробел.

Перекрестная ссылка О глобальных данных см. раздел 13.3.

Комментирование управляющих структур
Перекрестная ссылка Об управляющих структурах см. также
разделы 31.3 и 31.4 и главы с
14 по 19.

Обычно самое подходящее место для комментирования
управляющей структуры — предшествующие ей строки. Если
это оператор if или блок case, вы можете пояснить в ком#
ментарии условие и результаты. Если это цикл, можно ука#
зать его цель.

Пример комментирования цели управляющей структуры (C++)
Цель цикла.

> // копирование символов входного поля до запятой
while ( ( *inputString != ‘,’ ) && ( *inputString != END_OF_STRING ) ) {
*field = *inputString;
field++;
inputString++;
Комментарий конца цикла (полезен в случае длинных вложенных циклов, хотя необходимость такого комментария указывает на чрезмерную сложность кода).

>} // while — копирование символов входного поля
*field = END_OF_STRING;
if ( *inputString != END_OF_STRING ) {
Цель цикла. Положение комментария ясно говорит, что переменная inputString устанавливается с
целью использования в цикле.

>

// пропуск запятой и пробелов для нахождения следующего входного поля
inputString++;
while ( ( *inputString == ‘ ‘ ) && ( *inputString != END_OF_STRING ) ) {
inputString++;
}
} // if — конец строки
Опираясь на этот пример, можно дать несколько советов по комментированию
управляющих структур.
Пишите комментарий перед каждым оператором if, блоком case, циклом
или группой операторов Эти конструкции часто требуют объяснения, а мес#
то перед ними лучше всего подходит для этого. Используйте комментарии для по#
яснения цели управляющих структур.

ГЛАВА 32 Самодокументирующийся код

787

Комментируйте завершение каждой управляющей структуры Исполь#
зуйте комментарий для объяснения того, что именно завершилось, например:

} // for clientIndex — обработка записей всех клиентов
Комментарии особенно полезно применять для обозначения концов длинных
циклов и для пояснения их вложенности. Вот пример комментариев, поясняющих
концы циклов:

Пример использования комментариев, иллюстрирующих вложенность (Java)
for ( tableIndex = 0; tableIndex < tableCount; tableIndex++ ) {
while ( recordIndex < recordCount ) {
if ( !IllegalRecordNumber( recordIndex ) ) {
...
Эти комментарии сообщают, какая управляющая структура завершается.

} // if
} // while
} // for
Эта методика комментирования дополняет визуальную информацию о логиче#
ской структуре кода, предоставляемую отступами кода. Если циклы коротки, а вло#
женности нет, эта методика не нужна, однако в случае глубокой вложенности или
длинных циклов она окупается.
Рассматривайте комментарии в концах циклов как предупреждения о
сложности кода Если цикл настолько сложен, что в его конце нужен коммен#
тарий, подумайте, не упростить ли цикл. Это же правило относится к сложным
операторам if и блокам case.
Комментарии в концах циклов сообщают полезную информацию о логической
структуре кода, но писать и поддерживать их иногда утомительно. Зачастую луч#
ший способ предотвратить эту нудную работу — переписать код, который в силу
своей сложности требует подобной документации.

Комментирование методов
С комментариями методов связан одни из самых худших
советов, который дается в типичных учебниках по програм#
мированию. Многие авторы советуют независимо от размера
или сложности метода нагромождать перед его началом це#
лые информационные баррикады:

Перекрестная ссылка О форматировании методов см. раздел
31.7. О создании высококачественных методов см. главу 7.

Пример монолитного натуралистичного
пролога метода (Visual Basic)
‘**********************************************************************
’ Имя: CopyString

’ Цель:
Этот метод копирует строкуисточник (источник)

в строкуприемник (приемник).


788

ЧАСТЬ VII

Мастерство программирования

’ Алгоритм:
Метод получает длину “источника”, после чего поочередно

копирует каждый символ в “приемник”. В качестве индекса

массивов “источника” и “приемника” используется индекс

цикла. Индекс цикла/массивов увеличивается после

копирования каждого символа.

’ Входные данные: input
Копируемая строка

’ Выходные данные: output
Строка, содержащая копию строки “input”

’ Предположения об интерфейсе: нет

’ История изменений: нет

’ Автор:
Дуайт К. Кодер
’ Дата создания:
01.10.04
’ Телефон:
(555) 2222255
’ SSN:
111223333
’ Цвет глаз:
Зеленый
’ Девичья фамилия: —
’ Группа крови:
AB
’ Девичья фамилия матери: 
’ Любимый автомобиль: “Понтиак Ацтек”
’ Персонализированный номер автомобиля: “Tekie”
’**********************************************************************
Это глупо. Метод CopyString тривиален и скорее всего включает не более пяти строк
кода. Комментарий совершенно не соответствует объему метода. Цель и алгоритм
метода высосаны из пальца, потому что трудно описать что#то настолько простое,
как CopyString, на уровне детальности между «копированием строки» и самим ко#
дом. Предположения об интерфейсе и история изменений также бесполезны —
эти комментарии только занимают место в листинге. Фамилия автора дополнена
избыточными данными, которые можно легко найти в системе управления реви#
зиями. Заставлять указывать всю эту информацию перед каждым методом — зна#
чит подталкивать программистов к написанию неточных комментариев и затруд#
нять сопровождение программы. Эти лишние усилия не окупятся никогда.
Другая проблема с тяжеловесными заголовками методов состоит в том, что они
мешают факторизовать код: затраты, связанные с созданием нового метода, так
велики, что программисты будут стремиться создавать меньше методов. Конвен#
ции кодирования должны поощрять применение хороших методик — тяжеловес#
ные заголовки методов поощряют их игнорировать.
А теперь нескоько советов по комментированию методов.
Располагайте комментарии близко к описываемому ими коду Одна из при#
чин того, что пролог метода не должен содержать объемной документации, в том,
что при этом комментарии далеки от описываемых ими частей метода. Если ком#
ментарии далеки от кода, вероятность того, что их не будут изменять вместе с кодом
при сопровождении, повышается. Смысл комментариев и кода начинает расхо#
диться, и внезапно комментарии становятся никчемными. Поэтому соблюдайте

ГЛАВА 32 Самодокументирующийся код

789

Принцип Близости и располагайте комментарии как можно ближе к описывае#
мому ими коду. Тогда их будут поддерживать, а они сохранят свою полезность.
Несколько компонентов, которые по мере необходимости следует включать в
прологи методов, описаны ниже. Ради удобства создавайте стандартизованные
прологи. Не думайте, что перед каждым методом нужно указывать всю информа#
цию. Включайте действительно важные элементы и опускайте остальные.
Описывайте каждый метод одним'двумя предложе'
Перекрестная ссылка Удачный
ниями перед началом метода Если вы не можете опи#
выбор имени метода — важнейсать метод одним или двумя краткими предложениями, вам,
ший аспект документирования
методов (см. раздел 7.3).
вероятно, следует лучше обдумать роль метода. Если крат#
кое описание придумать трудно, значит, проект метода не
так хорош. Попробуйте перепроектировать метод. Краткое резюмирующее пред#
ложение должно присутствовать почти во всех методах, кроме простых методов
доступа Get и Set.
Документируйте параметры в местах их объявления Самый простой спо#
соб документирования входных и выходных переменных — написать коммента#
рии после их объявления:

Пример документирования входных и выходных данных
в местах их объявления — хороший подход (Java)
public void InsertionSort(
int[] dataToSort, // массив элементов, подлежащих сортировке
int firstElement, // индекс первого сортируемого элемента (>=0)
int lastElement
// индекс последнего сортируемого элемента (=0)
* @param lastElement индекс последнего сортируемого элемента (