Философия программирования Win95. Глава 3

Разное



Отсканированная мной книга Лу Гринзоу "Философия программирования Windows 95/NT", глава 3. Обсуждение, исправление опечаток и вопросы - в комментариях.

Философия разработки программного обеспечения для Windows: микропроблемы

'Yes, I have a pair of eyes,' replied Sam, and that's just it. If
they wos a pair o'patent double million magnifyin' gas mi-
croscopes of hextra power, p'raps I might be able to see
through a flight o' stairs and a deal door; but bein' only eyes,
you see my wision's limited.'

Чарльз Диккенс. «Посмертные записки Пиквикского клуба»

У всех программистов дела со зрением обстоят точно так же, как у Сэма - оно
ограничено. Но иногда и этого бывает более чем достаточно. Если,
перефразируя другое известное высказывание, мы делаем решительное усилие
«мыслить глобально, а действовать локально», то наши старания окупаются
сторицей.

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

Шесть микроуровневых рекомендаций

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

Некоторые микрорекомендации, представленные в этой главе, могут пока-
заться неплохими кандидатами на включение в макросписок. В конце концов,
разве не относятся к области проектирования такие вопросы, как способ хране-
ния постоянных данных или четкое отделение друг от друга фрагментов кода,
занимающихся пользовательским интерфейсом и собственно реализацией
алгоритмов задачи? Да, относятся. Или, но крайней мере, должны относиться.
Но все дело в том, что те методы, которыми сегодня часто осуществляется Win-
dows-программирование, просто сталкивают их с макро- на микроуровень.
Проблема - в постоянной нехватке времени, ведущей к значительному
сокращению (а иногда - даже к полной ликвидации) этапа проектирования в
цикле разработки проекта. Эта тенденция серьезно повышает отношение «кла-
виатурного времени» к «бумажному времени», то есть важные дизайнерские
решения все чаще и чаще принимаются не хладнокровно, не на ясную голову,
не в специально предназначенное для этого время, а в пылу сумасшедшей
борьбы с проблемами и ошибками (своими и чужими) при непосредственном
написании кода.

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

Быстрая разработка приложений (БРП, от RAD, rapid application development) -
сегодня на нее делается все больший и больший акцент в нашей с вами профессии.
Но, несмотря на то, что эта тенденция кажется мне полезной и захватывающей,
я вынужден добавить, что она лишь усугубляет проблему попадания проектов под
власть рефлекторных и поспешных решений. Программисты (а во многих
случаях и непрограммисты) куда-то тыкают курсором, щелкают мышью,
что-то перетаскивают и бросают, как сумасшедшие, и, имея зачастую
лишь самые туманные представления об архитектуре Windows, создают
бросовые программы, иногда выходящие в свет даже без минимального
тестирования. Что особенно примечательно, эти программы действи-
тельно оказываются вполне работоспособными (если выразиться точнее,
продолжительность их нормальной работы оказывается вполне приемле-
мой), в то время как их создатели практически ничего не смыслят в том,
как и почему функционируют специфичные для Windows детали их
творений.

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

Иными словами, инструментарий для БРП одновременно делает две со-
вершенно противоположные вещи:

1.        Экспертам и специалистам он дает возможность создавать программы
более продуктивно, затрачивая намного меньше сил на
«проникновение в тайны» Windows-программирования. Как человек,
«слонявшийся по закоулкам Windows» достаточно много, я без
лишнего стеснения могу заявить: это очень хорошее дело.

2.        Неспециалистам он позволяет создавать программы и «заставлять их
работать». Но в зависимости от того, какова природа этих программ, куда они
 потом попадают, как и кем они используются, и кем они
сопровождаются, это может быть очень и очень плохим делом.
Например, я слышал уже много историй о том, как сотрудники
обращались в отдел информатики и компьютерной автоматизации
своей компании с просьбой о каком-нибудь новом программном
обеспечении, а в ответ получали лишь подробнейшее объяснение,
почему этот отдел имеет такое огромное количество недоделанных
программных заказов, а впридачу - копию Visual Basic или Delphi.
Да, именно так в некоторых случаях компании прямо-таки
заставляют своих бухгалтеров, инженеров и прочих далеких от
программирования сотрудников помаленьку заниматься этим делом
самостоятельно.

10. Абстрагируйтесь, абстрагируйтесь и снова абстрагируйтесь (и не беспокойтесь
о цене вызова функции, пока программа сама не заявит об этом)


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

Одной из ваших основных целей в программировании должна быть работа
на как можно более высоком логическом уровне. Сегодня очень немногие
программисты пишут на ассемблере. И в основном потому, что это уже не так
часто необходимо. Такие языки высокого уровня, как C/C++, Pascal и BASIC,
являются гораздо более продуктивными инструментами для большинства
программистских задач. Работая на более высоком логическом уровне любого
из этих компилируемых языков, программист фактически имеет дело с не-
которой виртуальной машиной, установленной поверх других виртуальных ма-
шин (включая стандартную библиотеку языка, третьесторонние библиотеки,
Windows, DOS и т. д.).

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

(Подробнее об абстрагировании и, в частности, об оберточных библиотеках для
Win32 я буду говорить в главе 10.)

  11. Применяйте оборонительное программирование

Things without all remedy
Should be without regard; what's done is done.

Уильям Шекспир

Never check for an error condition you don't know how to handle.

Программистский афоризм

This error should never happen.

Огромное число диалоговых окон

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

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

Что такое «оборонительное программирование»?

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

Например, моя позиция такова, что серьезным браком является даже та
ошибка, после которой программа продолжает нормально работать (то есть
работать без потерн функциональности или данных), но нарушается лишь
внешний вид пользовательского интерфейса. Многие программисты, с
которыми я разговаривал, не соглашались со мной в этом примере. И я
встречал даже работников из отделов поддержки пользователей, которые рас-
сматривали такой пример как чисто косметическую проблему, ясно давая по-
нять, что до тех пор, пока программа не потеряла мои данные, мне не о чем
беспокоиться. (Кстати, было так же ясно и то, что такие проблемы их тоже не
беспокоят.) Когда я сажусь в один из двух своих автомобилей (оба -· марки
«Хонда»), я не перестаю дивиться тому, насколько надежно и удобно
спроектированы эти машины, и тут же меня охватывает страх: что произошло
бы, если бы японцы когда-нибудь вдруг решили сделать индустрию разработки
программного обеспечения областью своих стратегических интересов.

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

Природа предположений

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

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

1. Предположения о данных. Данные - это кровь вашей программы.
Если не будет данных, то всей логике в мире будет не с чем работать.
Возможно, это тривиальная мысль, но она прямиком ведет к довольно
"тревожному факту: точно так же, как и настоящая кровь, кровь вашей
программы может оказаться зараженной. Я имею в виду не ком-
пьютерные вирусы, а именно плохие данные - данные, которые неправильны,
недопустимы или не соответствуют ожидаемому форма-
ту. Плохие данные могут появиться откуда угодно - от опечатки поль-
зователя при вводе, от другого кода вашей программы, от используе-
мой вами третьесторонней библиотеки и даже от самой Windows.

Помните, что при программировании для Windows не все источники
плохих данных очевидны. Так, например, Windows/DOS Developer's
Journal в своей серии аннотаций к Windows SDK указал несколько
случаев, в которых вызов функции может приводить к изменению
переданных ей данных, несмотря на то, что документация этой функ-
ции явно утверждает обратное. Такие скрытые ловушки могут приво-
дить к скверным сюрпризам и длительным отладочным сеансам.

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

Почему вам следует заботиться о чем-то еще, кроме документации? Да
потому, что, к сожалению, в самой документации очень часто
встречаются откровенные ошибки или недоговоренности, и у вас не ос-
тается другого выбора. Самый классический пример такого брака -
документация по Win32 API: существуют многочисленные функции,
реагирующие на одни и те же входные данные по-разному на разных
платформах (Win32s, Windows 95 и Windows NT), а документация по
таким вещам, как расширенные коды ошибок, возвращаемые функци-
ей GetLastError(), поистине ужасна. (Если вы желаете увидеть яркий
пример вариаций поведения одного и того же API на разных плат-
формах Win32, обратите внимание на обсуждение функции GetShort-
PathName() в главе 14 на стр. 486.) А это означает, что во многих
случаях вы вынуждены гадать и проводить эксперименты для того,
чтобы понять, как на самом деле работают процедуры. И поскольку ни
у кого из пас нет времени на тестирование буквально каждого API, нам
приходится делать еще больше опасных предположений.

«Доверяй, но проверяй»

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

Оборонительное программирование осуществляется по трем основным
направлениям, на следующих трех фронтах:

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

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

// Проверка параметров: отсутствует

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

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

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

void DoStuff ( int х, char* у ) {

  if((x > max_x) || (x < min_x) || (y == NULL) || (strlen(y) == 0) )
    return BAD_PARAMETER;
/* А теперь выполняем необходимую работу */
}
 
оператор if, выполняющий проверку параметров, переводит программу
из состояния, в котором определенность в отношении данных была
практически нулевой, в такое состояние, когда на правильность дан-
ных можно полностью положиться и использовать их дальше без ка-
ких-либо проверок. Такой «интервал неопределенности» данных суще-
ствует в начале каждой функции, а также в любой точке вашей
программы, где данные появляются из сторонних источников. До тех
пор, пока ваш код или какая-либо вызываемая им функция не ликви-
дирует этот «интервал неопределенности», он сможет распространять-
ся по всему коду, оставаясь лазейкой для «заражения крови» вашей
программы. Последствия этого могут быть не просто опасными, но и
трудными для обнаружения - передаваемые туда-сюда данные в кон-
це концов могут быть сохранены в постоянном хранилище и привести
к явным сбоям лишь через несколько дней. (Если вы когда-либо стал-
кивались с проблемами, порожденными плохими сохраненными дан-
ными, и если в конце концов вам удавалось отыскать те места в коде,
в которых на самом деле происходила «порча» этих данных, то вы на-
верняка знаете, какими невероятно тяжелыми, мучительными и долги-
ми бывают порой подобные «расследования».)

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

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

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

2. Проверка результатов работы кода, который вы вызываете. Это мо-
жет быть довольно рискованным делом, в особенности при использо-
вании Win32 API настоящего фонтана сюрпризов. Основная труд-
ность заключается в том, что вы не можете до конца протестировать
каждый отдельный API. Практически все, что вы можете делать - это
изучать документацию (включая все сообщения Microsoft об об-
наруженных ошибках, публикуемые на их CD Developer Network) и
тщательно тестировать поведение тех функций,  которые наиболее
критичны в вашем конкретном проекте. В их число, как правило, по-
падают те функции, которым вы часто передаете непроверенные дан-
ные, или результат работы которых вы проверяете при помощи Get-
LastError() - самого «многоликого» представителя Win32 API.

Каждый вам скажет, что не следует рассчитывать на недокумен-
тированные побочные эффекты различных API, а тем более не следует
использовать их. Это замечательный совет, хотя иногда ему очень
трудно следовать - так часто эти досадные побочные эффекты как раз
и делают то, что вам нужно (а в некоторых случаях вообще являются
единственным способом добиться желаемого). Всеми силами сопротив-
ляйтесь соблазну использовать что-либо недокументированное, если
только у вас нет действительно непреодолимой причины делать это. А
если такая причина возникла, задокументируйте то, что вы сделали, и
объясните, почему вы это сделали. Объяснение должно быть настоль-
ко ясным, чтобы всякий мог безошибочно понять, что произошло.

Придерживайтесь такой философии, что всякий код за пределами той
подпрограммы, которую вы в данный момент пишете, вероятно изоби-
лует коварными ошибками и, наверняка, был написан каким-нибудь
лунатиком. Под таким углом вы должны смотреть и на свой собствен-
ный код, и на Windows API, и на все остальное окружение. Увы, эта
философия поразительно контрастирует с тем предположением, ко-
торое обычно делают (как правило, неявно) все программисты - с
уверенностью в том, что их программы находятся в центре доброй и
любящей вселенной, которая всегда оградит их от сумасшедших поль-
зователей, гамма-лучей и несвежего пива. Но так никогда не было и
никогда не будет. Глядя на каждую подпрограмму, которую пишете,
вы должны пытаться увидеть все возможные изобретательные (читай-
те: извращенные) пути ее неправильного использования, а затем по-
стараться перекрыть эти лазейки (если не все, то хотя бы столько,
сколько получится при разумных затратах). Ключевым моментом
здесь опять является термин «разумно». Вы не должны пытаться дос-
тигнуть какого-то фантастического совершенства; вы всего лишь долж-
ны пытаться предугадать (то есть предвидеть и принять соответст-
вующие меры предосторожности), каким образом могут возникнуть
неприятности.

Я припоминаю один коллективный проект, в котором я принимал
участие, и в котором мы однажды столкнулись с довольно простой на
первый взгляд проблемой: программа упорно и регулярно «ложилась»
при попытке совершить некоторые действия над несуществующим
файлом. Нас тогда очень заинтриговала причина такого поведения в,
казалось бы, элементарной ситуации. Вместе с коллегой я исследовал код,
и мы обнаружили фрагмент, написанный другим программистом
и выглядевший приблизительно так (в псевдокоде):

file_handle = OpenFile(file_name);
rc = ReadFile(file_handle,buffer);
while(rc == OK) {
  ProcessBuffer(buffer);
  rc = ReadFile(file_handle,buffer);
}
CloseFile(file_handle);

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

3. Проектирование вашего собственного кода и вытекающие из этого
возможные способы его использования другими.
Многие программи-
сты возразят мне, что этот вопрос, мол, вовсе не имеет отношения к
оборонительному программированию. Правильный дизайн кода, пред-
назначенного для вызова его из других частей программы, очень ва-
жен, и вы должны стремиться предугадывать возможные ошибки вы-
зывающей стороны точно так же, как предвидеть типичные ошибки в
данных, которые ваш код получает. (В главе 10, посвященной
оберточным функциям для Win32 API, будет приведено множество
моих рассуждений на тему проектирования публичного кода, но имен-
но сейчас я хотел бы сконцентрироваться на тех аспектах проектирова-
ния, которые связаны с принципом оборонительного программирова-
ния.)

Первой линией обороны против неправильного использования вашего
кода является документация. Сделайте ее полной и точной, и другим
программистам (возможно, включая вас самих) останется очень мало
места для ее «творческой интерпретации». Я уже не раз высказывал
мое негативное отношение к пресловутым «спорам программистов в ка-
фетерии», тем не менее я не удержусь и замечу, что чем лучше задо-
кументирован ваш код, тем больше у вас шансов выиграть в таком
споре, когда другой программист Попытается упрекнуть ваш код в не-
корректности. Мне хотелось бы надеяться, что большинство програм-
мистов стоят выше подобных разборок, но, к сожалению, офисная по-
литика всегда находит способ проникнуть и в технические области. Вы
должны всегда задумываться над тем, что может произойти, когда
вызывающая сторона игнорирует или неправильно интерпретирует тот
индикатор успеха/провала, который возвращает ваша функция. Такое
иногда случается, поэтому во многих случаях есть смысл раскошелить-
ся на несколько дополнительных строк кода, которые обеспечат ос-
мысленные (или по крайней мере максимально безопасные) значения
для всех выходных данных вашей функции даже тогда, когда она не
сможет успешно выполнить свою работу до конца.

Небрежное отношение к выходным данным функций может
привести к серьезным неприятностям, которые вы можете и не
заметить. Никогда не делайте бездумно «то, что делают все», и
не полагайтесь наивно на «то, что всякий знает». Например, я
несметное количество раз наблюдал такое нарушение этого
правила: функция, которая, согласно ее описанию, должна
возвращать индикатор успеха/провала типа BOOL, на поверку
возвращает результат вызова другой аналогичной «булевской»
функции. Такой прием в особенности чреват при работе с Win32
API, в котором некоторые функции вместо TRUE (определен-
ного как 1) могут возвращать совершенно непредсказуемые не-
нулевые значения. На первый взгляд, это незначительная
проблема, но я так не считаю: если описание вашей функции
гласит, что она должна возвращать BOOL, но она возвращает
что-либо отличное от TRUE или FALSE, это ошибка. И все тут.
(Тот факт, что вы имеете полное право обвинить Win32 API но
этой статье, значения не имеет - грехи Microsoft не могут быть
оправданием подобного поведения с вашей стороны.)
{mospagebreak}

Оборонительное программирование и повторное использование кода

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

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

Как я уже упоминал в самом начале, большая часть представленного в этой
книге кода не содержит ни примеров крутого стиля написания, ни сложных за-
умных алгоритмов. Но и такой код может успешно предохранить вас и пользо-
вателей вашей программы от будущих неприятностей. Перефразируя известное
высказывание из совершенно другой области, важно не то, что вы имеете, а то,
как вы этим пользуетесь. Самыми хорошими кандидатами для набора
проверочных функций могут быть маленькие и простые подпрограммы, ко-
торые исследуют переданные им данные на предмет соответствия каким-либо
форматам или состояниям, а затем возвращают значение типа BOOL, показы-
вающее результат. Неплохие примеры таких функций есть и в самом Win32
API IsIconic(), IsChild(), IsWindows() и др. Обратите внимание на приня-
тый в данном случае способ наименования - он не только четко описывает
предназначение функции, но и приближает внешний вид кода к естественному
языку:

if(IsIconic(fred_hWnd))
::SendMessage(fred_hWnd,WM_SYSCOMMAND,SC_RESTORE,0);

He увлекайтесь составлением вашего набора проверочных функций слиш-
ком страстно. Если вы начнете искать все возможные и невозможные подходя-
щие случаи для создания очередной маленькой защитной подпрограммы, вы
вскоре обнаружите, что занимаетесь уже написанием программистского ин-
струментария, а не самих программ. (Это, кстати, тесно перекликается с одной
из хронических проблем С++: программисты порой могут тратить уйму време-
ни на проектирование, реализацию и совершенствование классов, а не на их ис-
пользование.) Не волнуйтесь, если ваш набор будет более угловатым и менее
элегантным, чем хотелось бы. Вы должны позволить ему расти и развиваться
органично: как только у вас появится твердая уверенность, что вы в самом деле
будете часто использовать ту пли иную новую функцию, тогда уделите немного
времени на ее написание, тестирование и документацию, и затем включите ее в
ваш набор. Зацикленность на поиске новых добавлений не сулит ничего
хорошего - вы просто угробите слишком много сил на возню с мелкими дета-
лями и нюансами, которые потом могут никогда вам не понадобиться.

Также не следует беспокоиться, если у вас получаются подпрограммы, не
дающие 100-процентной гарантии правильности проверенных данных. Если до-
кументировать и применять такие процедуры должным образом, они могут
быть весьма и весьма полезны. Хорошим примером такого инструмента может
послужить функция LooksLikeCISID(), проверяющая строку на соответствие
формату идентификатора сети CompuServe (ее код полностью приведен и
подробно прокомментирован в этой книге, во врезке «LooksLikeCISID: путеше-
ствие по уровням неопределенности» на стр. 134). Дело в том, что никакой
изолированный код не в состоянии абсолютно достоверно определить, является
ли строка правильным идентификатором CompuServe (единственный способ
сделать это - позвонить в CompuServe и послать электронную почту по соот-
ветствующему адресу). Максимум того, что программа реально может
сделать, - это посмотреть, кажется ли строка соответствующей требуемому
формату. Такая неточность проверки вполне осмысленна: когда ваша
программа запрашивает у пользователя идентификатор CompuServe, вы просто
вызываете эту функцию и в том случае, когда она возвращает FALSE, выдаете
пользователю диалог с сообщением примерно следующего вида: «Введенный
вами идентификатор CompuServe, возможно, не соответствует правильному
формату. Хотите ли вы использовать его, несмотря на это?» Тем самым вы, с
одной стороны, предохраняете пользователя от большинства случайных оши-
бок, а с другой - оставляете возможность преодолеть параноидальное не-
доверие вашей программы к вводимым данным. (Последнее всегда является
отличной идеей в условиях сегодняшнего постоянно меняющегося мира, то и
дело порождающего на свет «ложные отрицания».) Кроме того, в диалог сле-
дует включить кнопку Help, которая выдаст справочную информацию, пока-
зывающую, как, по мнению вашей программы, должен выглядеть «нормаль-
ный» идентификатор CompuServe, и подробно объясняющую, почему
введенная пользователем строка вызывает подозрения.

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

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

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

Функция
AppendSlash(). При работе с именами каталогов и файлов
очень часто встречаются ситуации, когда у вас нет стопроцентной
уверенности в наличии завершающего символа обратной косой черты
в строке. Это как раз те случаи, когда вам может пригодиться функция
AppendSlash(). Алгоритм ее работы прост: она проверяет последний
символ в заданной строке и, если он не является обратной косой
чертой, дописывает ее в конец строки (предварительно проверяя, дос-
таточно ли в строке места для этого).

////////////////////////////////////////////////////////
// AppendSlash: Добавляет обратную косую черту ('\\') в конец заданной
// строки только в том случае, если для этого есть свободное место, и
// если эта строка уже не заканчивается таким символом.
//
// dest: строка, которую нужно проверить и изменить
//
// max_len: максимально допустимая длина строки dest, включая завершающий NULL
// Проверка параметров:
//
//    NULL или пустая строка в качестве dest отвергаются
//    значения mах_len, меньшие 3, отвергаются
////////////////////////////////////////////////////////
void AppendSlash(char *dest, size_t max_len)
{
  #ifdef _DEBUG
if((dest == NULL) || (*dest == '\0') || (max_len < 3))
OutputDebugString("AppendSlash: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (*dest == '\0') || (max_len < 3))
return;

if((strlen(dest) + 1 < max_len) &&
(dest[strlen(dest) - 1] != '\\'))
strlcat(dest,"\\", max_len);
}

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

В Функции stripLeading(), stripTrailing(), strip LT(). Как я уже упоми-
нал ранее, пробелы в началах и концах строк могут стать источником
ужасных головных болей. К счастью, это один из тех случаев, когда
вы. почти наверняка можете исправить данные самостоятельно -
привести их к нужному виду без риска потерять существенную ин-
формацию или вызвать какие-либо другие проблемы. Я использую эти
три функции в своей повседневной работе на каждом шагу, порой даже
тогда, когда они почти наверняка не нужны.

///////////////////////////////////////////////////////////////////////////
// stripLeading: удаляет лидирующие пробелы и символы табуляции из строки
//
// Проверка параметров:
//    NULL или пустая строка отвергаются
///////////////////////////////////////////////////////////////////////////

void stripLeading(char *s)
{

#ifdef _DEBUG
if((s == NULL) || (*s == '\0'))
  OutputDebugString("stripLeading: обнаружен недопустимый параметр!\n");
#endif

if((s == NULL) || (*s == '\0' )) return;

char *z = s;
while(*z && ((*z == ' ') || (*z == '\t')))
  z++;

if(s != z)
  memmove(s,z,strlen(z) + 1); // Безопасно - переполнения не будет
}

///////////////////////////////////////////////////////////////////////////
// stripLT: удаляет лидирующие и завершающие пробелы и символы табуляции из строки.
//
// Проверка параметров:
//    NULL или пустая строка отвергаются
///////////////////////////////////////////////////////////////////////////

void stripLT(char *s)
{

#ifdef _DEBUG
  if((s == NULL) | | (*s == '\0'))
    OutputDebugString("stripLT: обнаружен недопустимый параметр!\n");
#endif

if((s == NULL) || (*s == '\0' ))
  return;
 
  stripLeading(s);
  stripTrailing(s);
}

///////////////////////////////////////////////////////////////////////////
// stripTrailing: удаляет завершающие пробелы и символы табуляции из строки.
//
// Проверка параметров:
//    NULL или пустая строка отвергаются
///////////////////////////////////////////////////////////////////////////

void stripTrailing(char *s)
{

#ifdef _DEBUG
if((s == NULL) || (*s == '\0'))
  OutputDebugString("stripTrailing: обнаружен недопустимый параметр!\n");
#endif

if((s == NULL) || (*s == '\0' ))
return;

int z = strlen(s);

while((z >= 0) && ((s[z] == ' ') || (s[z] == '\t') || (s[z] == '\0')))
z--;

s[z + 1] = 0;
}

В Функции-зажимы. Случаются ситуации, в которых вы можете безо-
пасно скорректировать параметр, насильно приведя его значение к
требуемому интервалу (очевидно, подобные трюки необходимо тща-
тельно обдумывать и совершать осторожно). В особенности такой
прием годится для тех случаев, когда «X абсолютно никогда не может
быть меньше Y или больше Z» - ведь рано или поздно Z обязательно
найдет способ нарушить эту иллюзию и привести вашу программу в
хаотическое состояние.

///////////////////////////////////////////////////////////////////////////
// ClipInt: принудительно переводит целое число внутрь заданного интервала.
//
// х: значение, которое необходимо зажать в интервал
// limit1, limit2: границы интервала для х
//
// ЗАМЕЧАНИЕ: параметры limit1 и limit2 не обязательно должны иметь значения
// в возрастающем порядке
//
// Проверка параметров: НЕТ (допустимы любые значения)
///////////////////////////////////////////////////////////////////////////

long int ClipInt(int x, int limit1, int limit2)
{

long int min_x, max_x;

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

if(limit1 < limit2)
{
  min_x = limit1;
  max_x = limit2;
}
else
{
  min_x =   limit2;
  max_x =   limit1;
}

if(x <= min_x)
  return min_x;
else
 if(x >= max_x)
   return max_x;
   else
    return x;
}

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

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

Я выкручиваюсь из этого при помощи своего набора функций, которые
знают о длине той строки, в которую производится копирование, до-
бавление и тому подобные действия. Эти функции никогда не перепол-
нят выходной буфер и никогда не оставят его в незавершенном состоя-
нии (как это делает в некоторых случаях strncpy()). Конечно же, для
использования этих функций требуется определенная сила воли. Я не-
однократно выслушивал от других людей возражения против этих
функций, которые сводились к следующему тезису: их нельзя исполь-
зовать все время, потому что иногда максимальная допустимая длина
строки неизвестна. И в ответ на подобные заявления я каждый раз за-
давал людям простой вопрос: если вы не знаете, насколько длинной
может быть выходная строка без риска перейти границу отведенного
для нее участка памяти и испортить другие данные, то какого дьявола
вы копируете в нее данные или добавляете к ней другую строку, и
почему вы считаете, что делать это безопасно? (Обычно за этим
вопросом следует классическая немая сцена.) Кроме шуток, я повсюду
нахожу подобные приемчики в рабочем коде - подпрограмма по-
лучает указатель на строку (но не получает ее максимально допусти-
мую длину) и начинает производить над этой строкой те или иные дей-
ствия, которые запросто могут привести к ее удлинению. Такая прак-
тика, очевидно, является отвратительной, и ее следует объявлять вне
закона.

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

Когда речь идет о подобных подпрограммах «безопасной работы со
строками», возникает один типичный и немаловажный вопрос: а что
следует делать, когда выходной буфер и вправду окажется недоста-
точно большим? Должна ли функция заполнить буфер до конца, или
ей лучше вообще отказаться от работы? Ответ и прост, и сложен од-
новременно: все зависит от конкретной ситуации в вызывающем коде.
Легко представить себе реальные примеры, в которых и то, и другое
поведение будет правильным. Я решаю эту проблему путем предостав-
ления отдельных функций, работающих по каждому из возможных
сценариев. Те функции, имена которых имеют суффикс «Atomic», бу-
дут работать только в 'том случае, если им удастся полностью за-
кончить необходимые операции. (Выбор такого суффикса призван
подчеркнуть, что эти функции работают в манере «все или ничего», а
не то, насколько мощно они взрываются при получении неправильных
параметров.)

///////////////////////////////////////////////////////////////////////////
// strlcat: Прицепляет строку src к концу строки dest с учетом максимально
// допустимой для dest длины (max_len) и всегда обеспечивает должное завершение.
// Эта функция прицепит к dest столько символов, сколько в нее поместится,
//
// max_len включает завершающий NULL
//
// Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае.
//
// Проверка параметров:
//    NULL или пустая строка в качестве src или dest отвергаются
//     Значения max_len, равные 0 или меньшие начальной длины dest, отвергаются
/////////////////////////////////////////////////

BOOL strlcat(char *dest, const char *src, size_t max_len)
{
#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len ==0) || (strlen(dest) >=max_len - 1))
  OutputDebugString("strlcat: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (src == NULL) || (max_len == 0))
return FALSE;

UINT d_len = strlen(dest);
if(d_len >= max_len - 1) return FALSE;
strncat(dest, src, max_len - d_len - 1);
return TRUE;
}

///////////////////////////////////////////////////////////////////////////
// strlcatAtomic: Прицепляет строку src к концу строки dest с учетом максимально
// допустимой для dest длины (max_len) и всегда обеспечивает должное завершение.
// Эта функция сработает только тогда, когда все символы из src поместятся в dest
//
//
max_len включает завершающий NULL
//
// Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае
//
//Проверка параметров:
// NULL или пустая строка в качестве src или dest отвергаются
// Значения max_len, равные 0 или меньшие начальной длины dest, отвергаются
///////////////////////////////////////////////////////////////////////////
BOOL strlcatAtomic(char *dest, const char *src, size_t max_len)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len == 0) || (strlen(dest) >=max_len - 1))
OutputDebugString("strlcatAtomic: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (src == NULL) || (max_len == 0))
  return FALSE;
UINT d_len = strlen(dest);

if(d_len + strlen(src) >= max_len - 1) // Проверяем, все ли символы влезут
  return FALSE;
strncat(dest,src,max_len - d_len - 1);
return TRUE;
}

///////////////////////////////////////////////////////////////////////////
// strlcpy: Копирует символы из строки src в строку dest с учетам максимально
// допустимой для dest длины (max_len) и всегда обеспечивает должное завершение.
// Эта функция поместит в dest столько символов, сколько в нее поместится.
//
// max_len включает завершающий NULL
//
// Возвращает TRUE, если копирование произошло, и FALSE - в противном случае.
//
// Проверка параметров:
//    NULL или пустая строка в качестве src или dest отвергаются
//    Значение max_len, равное 0, отвергается
/////////////////////////////////////////////////////////////////////////
BOOL strlcpy(char *dest, const char *src, size_t max_len)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len == 0))
OutputDebugString("strlcpy: обнаружен недопустимый параметр! \n");
#endif

if((dest == NULL) || (src == NULL) || (max_len == 0))
return FALSE;

*dest = '\0';
return strlcat(dest,src,max_len);
}

///////////////////////////////////////////////////////////////////////////
// strlcpyAtomic: Копирует символы из строки src в строку dest с учетом максимально
// допустимой для dest длины (max_len) и всегда обеспечивает должное завершение.
// Эта функция сработает только тогда, когда все символы из src поместятся в dest
//
// max_len включает завершающий NULL
//
// Возвращает TRUE, если копирование произошло, и FALSE - в противном случае.
//
// Проверка параметров:
//    NULL или пустая строка в качестве src или dest отвергаются
//    Значение max_len, равное 0, отвергается
///////////////////////////////////////////////////////////////////////////
BOOL strlcpyAtomic(char *dest, const char *src, size_t max_len)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len == 0))
OutputDebugString("strlcpyAtomic: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (src == NULL) || (max_len == 0))
return FALSE;

*dest = '\0';
return strlcatAtomic(dest,src, max_len);
}

///////////////////////////////////////////////////////////////////////////
// strlncat: Прицепляет не более n первых символов из строки src к концу
// строки dest с учетом максимально допустимой для dest длины (max_len)
// и всегда обеспечивает должное завершение. Эта функция прицепит к dest
// столько символов, сколько в нее поместится (но не больше n).
//
// max_len включает завершающий NULL
//
// Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае.
//
// Проверка параметров:
//    NULL или пустая строка в качестве src или dest отвергаются
//    Значения max_len, равные 0 или меньшие начальной длины dest, отвергаются
//////////////////////////////////////////////////////////////

BOOL strlncat(char *dest, const char *src, size_t max_len, size_t n)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len ==0) || (strlen(dest) >=max_len - 1))
OutputDebugString("strlncat: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (src == NULL) || (max_len == 0))
return FALSE;

UINT d_len = strlen(dest);
if(d_len >= max_len - 1)
return FALSE;

// Достаточно ли в dest места для n символов?
if(max_len - d_len - 1 >= n)

strncat(dest,src.max.len - d_len - 1); // Нет: копируем только то, что влезет
return TRUE;
}


///////////////////////////////////////////////////////////////////////////
// strlncatAtomic: Прицепляет не более n первых символов из строки src к концу
// строки dest с учетом максимально допустимой для dest длины (max_len)
// и всегда обеспечивает должное завершение. Эта функция сработает только тогда,
// когда все символы из src (или хотя бы первые n из них) поместятся в dest.
//
// max_len включает завершающий NULL
// Возвращает TRUE, если прицепление произошло, и FALSE - в противном случае
//
// Проверка параметров:
//    NULL или пустая строка в качестве src или dest отвергаются
//    Значения mах_1еп, равные 0 или меньшие начальной длины dest, отвергаются
//////////////////////////////////////////////////////////////////////////
BOOL strlncatAtomic(char *dest, const char *src, size_t max_len, size_t n)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len = 0) || (strlen(dest) >=max_len - 1))
OutputDebugString("strlncatAtomic: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) ||(src == NULL) ||(max_len ==0))
return FALSE;

if(max_len - strlen(dest) -1 < n)   // Проверяем, все ли поместится
return FALSE;

strncat(dest,src,n); // Да: копируем все n символов
return TRUE;
}

//////////////////////////////////////////////////////////////////////////
// strlncpy: Копирует не более n первых символов из строки src в
// строку dest с учетом максимально допустимой для dest длины (mах_lеп)
// и всегда обеспечивает должное завершение. Эта функция скопирует в dest
// столько символов, сколько в нее поместится (но не больше n).
//
// max_len включает завершающий NULL
//
// Возвращает TRUE, если копирование произошло, и FALSE - в противном случае.
//
// Проверка параметров:
//    NULL или пустая строка в качестве src или dest отвергаются
//    Значение max_len, равное 0, отвергается
///////////////////////////////////////////////////////////////////////////
BOOL strlncpy(char *dest, const char *src, size_t max_len, size_t n)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len == 0))
OutputDebugString("strlncpy: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (src == NULL) || (max_len == 0))
return FALSE;

*dest = '\0';
return strlncat(dest,src,max_len, n);
}

///////////////////////////////////////////////////////////////////////////
// strlncpyAtomic: Копирует не более n первых символов из строки src в
// строку dest с учетом максимально допустимой для dest длины (max_len)
// и всегда обеспечивает должное завершение. Эта функция сработает только
// тогда, когда все символы из src (или хотя бы первые n из них) поместятся
// в dest.
//
// max_len включает завершающий NULL
//
// Возвращает TRUE, если копирование произошло, и FALSE - в противном случае
//
// Проверка параметров:
//   NULL или пустая строка в качестве src или dest отвергаются
//    Значение max_len, равное 0, отвергается
//////////////////////////////////////////////////////////////////////////
BOOL strlncpyAtomic(char *dest, const char *src, size_t max_len, size_t n)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (max_len == 0))
OutputDebugString("strlncpyAtomic: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (src == NULL) || (max_len == 0))
return FALSE;

*dest ='\0';
return strlncatAtomic(dest,src,max_len,n);
}

В Функция CopyFileForceRW(). Несмотря на предельную простоту
этой функции, пользователи наверняка скажут вам спасибо за ее
применение. По сути, она является слегка усовершенствованным
вариантом функции CopyFile() из Win32 API она просто вызывает
CopyFile(), передавая ей свои параметры, а затем делает выходной
файл доступным для чтения/записи. Без этого дополнительного дейст-
вия файлы, скопированные с CD,  сохраняли бы свои атрибуты, включая
бит read-only. И если вы забудете про этот факт, недоступ-
ность выходного файла для модификации может раздражать пользова-
телей (да и ваши собственные программы тоже).

///////////////////////////////////////////////////////////////////////////
// CopyFileForceRW: Использует CopyFile API для копирования файла, а затем
// делает выходной файл доступным для чтения/записи.
//
// Возвращает TRUE, если копирование произошло и атрибут результирующего
// был успешно установлен в R/W. В противном случае, возвращает FALSE.
//
// Проверка параметров:
//    NULL или пустая строка в качестве src или dest отвергаются
///////////////////////////////////////////////////////////////////////////

BOOL CopyFileForceRW(char *src, char *dest, BOOL copy_flag)
{

#ifdef _DEBUG
if((dest == NULL) || (src == NULL) || (*dest == '\0') || (*src == '\0'))
OutputDebugString("CopyFileForceRW: обнаружен недопустимый параметр!\n");
#endif

if((dest == NULL) || (src == NULL) || (*dest == '\0') || (*srс == '\0'))
{
SetLastError(ERROR_INVALID_PARAMETER);
return FALSE;
}

// Все остальные вызываемые функции используют SetLastError(),
// поэтому мы отдаем код ошибки им на откуп,
if (!CopyFile(src,dest,copy_flag))
return FALSE;

DWORD fa = GetFileAttributes(dest);
if(fa != 0xffffffff)
{
  fa &= ~FILE_ATTRIBUTE_READONLY;
  return (SetFileAttributes(dest,fa) != 0);
 }
return FALSE;
}

В Функции FileExists() и DirExists(). Эти две еще более простые
подпрограммы могут спасти шею вашей программы (кстати, на тот
случай, если вы вдруг этого не знаете: шея программы - это незадо-
кументированная часть заголовка исполняемого файла в формате РЕ).
Все, что они делают - это возвращают значение типа BOOL, которое
указывает, существует ли на. самом деле файл или каталог, имя ко-
торого было передано этим функциям.

Несмотря на простоту этих функций, между ними есть небольшие, но
существенные различия. Функция FileExists() вернет FALSE, если
объект существует, но является каталогом. Аналогично, DirExists()
вернет FALSE при «встрече» с существующим файлом. Это различие
отражает предполагаемые условия использования этих подпрограмм:
вам следует использовать FileExists() только для проверки строк, ко-
торые далее будут использоваться для передачи имен файлов, a DirEx-
istsO -- соответственно, для работы с именами каталогов.

Обратите внимание на финальный оператор return в DirExists() - на
то, как эта функция прилагает специальные усилия, чтобы в случае ус-
пеха не просто вернуть ненулевой результат логической операции
AND, а выполнить строгое правило: «булевская функция» обязана
возвращать лишь одно из двух возможных значений - либо TRUE,
либо FALSE.

 

//////////////////////////////////////////////////////////////
// FileExists: Проверяет предположение о том, что заданный файл
// существует и НЕ является каталогом,
//
// Возвращает TRUE, если файл существует, или FALSE - если файл
// не существует или является каталогом.
//
// Проверка параметров:
//    NULL или пустая строка отвергается
//////////////////////////////////////////////////////////

BOOL FileExists(const char *fn)
{

#ifdef _DEBUG
if(fn == NULL || (strlen(fn)) == 0)
OutputDebugString("FileExists: обнаружен недопустимый параметр!\n");
#endif

if(fn == NULL || (strlen(fn)) == 0)
return FALSE;

DWORD dwFA = GetFileAttributes(fn);
if(dwFA == 0xFFFFFFFF)
  return FALSE;
else
  return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) != FILE_ATTRIBUTE_DIRECTORY);
}
 
////////////////////////////////////////////////////////////////
// DirExists: Проверяет предположение о том, что заданный файл
// существует и является каталогом.
//
// Возвращает TRUE, если файл существует и является каталогом,
// в противном случае возвращает FALSE,
//
// Проверка параметров:
//    NULL или пустая строка отвергается
//////////////////////////////////////////////////////////////

BOOL DirExists(const char *dn)
{

#ifdef _DEBUG
if((dn == NULL) || (*dn == '\0'))
OutputDebugString("DirExists: обнаружен недопустимый параметр!\n");
#endif

if((dn == NULL) || (*dn == '\0' ))
return FALSE:

DWORD dwFA = GetFileAttributes(dn);
if(dwFA == 0xFFFFFFFF)
  return FALSE;
else
  return ((dwFA & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY);
}
{mospagebreak}
В Функции AliasToLFN() и LFNToAlias(). Признаться, эти две функ-
ции не имеют отношения к оборонительному программированию. Од-
нако они, как минимум, являются ближайшей родней всем остальным
подпрограммам, описанным в этом разделе, поскольку помогают сде-
лать вашу программу немного более интеллигентной. Эти функции за-
нимаются всего лишь трансляцией файловых псевдонимов в длинные
имена файлов и обратно. Обе они возвращают только локальную часть
имени файла, отбрасывая имя логического диска и путь.

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

Если файл с указанным именем не существует, функции возвращают
FALSE. Это отражает тот факт, что они, по определению, служат не
для абстрактной алгоритмической трансформации предложенной им
строки, а для поиска другого, уже существующего имени файла. А эта
операция не имеет смысла, когда сам файл не существует.

//////////////////////////////У////////////////////////////////////////////
// AliasToLFN: Конвертирует имя файла из формата 8.3 в его длинное имя.
// Возвращаемое имя содержит только локальную часть полного имени файла,
// т.е. НЕ включает путь.
// Эта функция сработает ТОЛЬКО в том случае, если файл существует.
//
// lfn_length включает завершающий NULL
//
// В случае успеха возвращает длину возвращаемого длинного имени файла,
// в противном случае возвращает -1.
//
// Проверка параметров:
//    значения NULL в качестве указателей на строки отвергаются
//    lfn_length, меньшая МАХ_РАТН, отвергается
///////////////////////////////////////////////////////////////////////////
int AliasToLFN(const char *alias, char *lfn, UINT lfn_length)
{

#ifdef _DEBUG
if((lfn == NULL) || (alias == NULL) || (lfn_length < MAX_PATH))
  OutputDebugString("AliasToLFN: обнаружен недопустимый параметр!\n");
#endif

if((lfn == NULL) || (alias == NULL) || (lfn_length < MAX_PATH))
  return -1;

WIN32_FIND_DATA temp;
HANDLE search_handle = FindFirstFile(alias,&temp);
if(search_handle == INVALID_HANDLE_VALUE){
  lfn[0] = 0x0;
  return -1;
}
else {
strlcpy(lfn,temp,cFileName, lfn_length - 1);
FindClose(search_handle);
return strlen(lfn);
}
}

///////////////////////////////////////////////////////////////////////////
// LFNToAlias: Конвертирует длинное имя файла в формат 8.3.
// Возвращаемое имя содержит только локальную часть полного имени файла,
// т.е. НЕ включает путь.
// Эта функция сработает ТОЛЬКО в том случае, если файл существует.
//
// alias_length включает завершающий NULL
//
// В случае успеха возвращает длину возвращаемого псевдонима файла,
// в противном случае возвращает -1.
//
// Проверка параметров:
//    значения NULL в качестве указателей на строки отвергаются
//    alias_length, меньшая МАХ_РАТН, отвергается
int LFNToAlias(const char *lfn, char *alias, UINT alias_length)
{

#ifdef _DEBUG
if((lfn == NULL) || (alias == NULL) || (alias_length < 13))
  OutputDebugString("LFNToAlias: обнаружен недопустимый параметр!\n");
#endif

if((lfn == NULL) || (alias - NULL) || (alias_length < 13))
  return -1;

win32_FIND_DATA temp;
HANDLE search_handle = FindFirstFile(lfn, &temp);
if(search_handle == INVALID_HANDLE_VALUE)
{
  alias[0] = 0x0:
  return -1;
}
else
{
  strlcpy(alias,temp.cAlternateFileName,aliao_length - 1);
  FindClose(search_handle);
  return strlen(alias);
 }
}

Возможно, это только моя личная проблема, но я нахожу любопытным и
одновременно досадным тот факт, что в таком богатом и сложном API,
как Win32, не нашлось места для функций, выполняющих перечисленные
выше простые операции. Конечно, там имеются некоторые экземпляры,
которые кажутся близкими к тому, что хочется. Например, функция по
имени GetShortFileName(), которая, к сожалению, сама путается в собст-
венных причудах. Или другая вице-мисс - GetFullPathName(), которая
всего лишь берет заданное имя файла и использует его для построения
полного пути, используя при этом имена текущего диска и текущего ката-
лога (я вообще назвал бы эту функцию иначе - например,
PrependCurrentDir() - чтобы более точно описать ее деятельность).

LooksLikeCISID: путешествие по уровням неопределенности

Ситуации, в которых вы можете и должны проверять данные
(и даже корректировать их), встречаются очень часто. А еще бы-
вают случаи, когда, для удобства ваших пользователей, вы долж-
ны делать лишь «трезвую оценку» правильности данных и, если
они оказываются подозрительными, задавать пользователю вопрос
типа «Вы действительно хотите использовать их?». Хорошим
примером такой ситуации является исследование строки на пред-
мет соответствия ее формату идентификатора CompuServe. Этот
пример интересен еще по двум причинам: во-первых, он наглядно
демонстрирует, как программа может заработать себе дополни-
тельные очки на знании специфики области ее применения; во-
вторых, конкретная реализация этой проверки, представленная
ниже, служит неплохой иллюстрацией к методу «постепенного су-
жения интервала неопределенности» данных, о котором я недавно
упоминал.

Что мы знаем об идентификаторах CompuServe? Все они
имеют следующий вид:

<набор цифр><точка><набор цифр>

В них не встречаются ни буквы, ни пробелы, ни другие знаки
пунктуации, кроме точки (которая, кстати, может находиться
только где-то посредине строки). Какой длины бывают эти иден-
тификаторы? Самый длинный идентификатор CompuServe, ко-
торый я видел до сих пор, состоял из одиннадцати символов
(шесть цифр, точка и еще четыре цифры), а самый короткий - из
семи (пять цифр, точка и еще одна цифра). Если мы будем считать
подозрительными все строки, длина которых больше двенадцати
или меньше пяти, это будет безопасно. (Помните, постановка за-
дачи не требует 100-процентной уверенности, поскольку мы всего
лишь делаем синтаксический анализ строки, а не проверяем ее на
точное совпадение с действительным идентификатором Com-
puServe.)

Обратите ваше внимание на то, что код функции LooksLike-
CISIDO построен по очень простой схеме, удобной для многих
других подобных проверок: отталкиваясь от предположения, что
строка правильна, подпрограмма последовательно проверяет не-
сколько условий и немедленно отвергает строку (возвращает
FALSE), как только любое из этих условий не выполняется. И
если управление доходит до конца процедуры, значит строка ус-
пешно прошла все тесты, и функция может вернуть TRUE.

///////////////////////////////////////////////////////////////////////////
// LooksLikeCISID: Синтаксически проверяет заданную строку и выясняет,
// может ли она представлять собой правильный идентификатор CompuServe.
//
// Тесты:
//
// 1. Длина должна находиться в интервале от 5 до 12, включительно.
// 2. Должна присутствовать ровно одна точка, но не в начале и не в конце.
// 3. Все остальные символы могут быть только цифрами (0-9).
//
// Возвращает TRUE, если строка успешно проходит все вышеописанные тесты.
// В противном случае возвращает FALSE.
//
// Проверка параметров:
//    NULL или пустая строка отвергаются
///////////////////////////////////////////////////////////////////
BOOL LooksLikeCISID(const char *id)
{

#ifdef _DEBUG
if((id == NULL) || (*id == '\0' ))
OutputDebugString("LooksLikeCISID: обнаружен недопустимый параметр\n");
#endif

// Первая проверка;
if ((id == NULL) || (*id == '\0' ))
  return FALSE;

char local_id[MAX_PATH];

// Делаем локальную копию и "очищаем" ее:
strlcpy(local_id,id,sizeof(local_id));
stripLT(local_id);

// Грубая проверка длины:
if((strlen(local_id) < 5) || (strlen(local_id) > 12))
  return FALSE;

int commas = 0;

// Сканируем (в один проход) и ищем повторную точку или
// какой-нибудь нецифровой символ
for(UINT i = 0; i < strlen(local_id); i++)
{
  if(local_id[i] == '.' )
  {
// Точка не может находиться ни в начале, ни в конце
      if((i ==0) || (i == strlen(local_id) - 1))
        return FALSE;
      commas++;
     // He слишком ли много точек?
    if(commas !=1)
      return FALSE;
  }
 else
 {
  if(!isdigit(local_id[i]))
    return FALSE;
  }
// Все тесты пройдены, значит она похожа на CIS ID
return TRUE;
}

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

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

Я замечал, что на практике подобные провалы чаще всего происходят
именно в Windows-программировании и, что интересно, на противоположных
концах спектра технологий:

В В программах, написанных на С (не на С++), которые работают
старомодными способами, не используют каркасных библиотек и ведут
рукопашный бой с API за каждую его услугу. С точки зрения органи-
зации, эти программы часто представляют собой кошмарные зрелища,
а их исходные файлы - чудовищные нагромождения самых разно-
образных функций и данных, не имеющих никакой логической связи
друг с другом. Когда приходит время сопровождать такой код,
программист обычно вынужден тратить кучу времени на выяснение ис-
тинных взаимосвязей между всеми этими многочисленными элемента-
ми (функциями, константами и переменными). И даже простейшие за-
дачи по сопровождению - исправление небольшой ошибки порой или
добавление незначительной новой функциональности -- приходится
делать варварскими способами: переносом (или, еще хуже, копирова-
нием) кода из одной функции в другую, добавлением параметров к
старым: функциям и т. п. Любой из этих способов прямая дорога к
появлению одной или нескольких новых ошибок, опасность которых
может быть просто несоизмерима с тем, ради чего все затевалось.

В В программах, написанных на С++, вовсю использующих многочис-
ленные вспомогательные средства интегрированных сред разработки -
мастеров (wizards), экспертов (experts) и пр. - для выполнения той
работы, которую я называю ассенизацией Windows-программ. Даже
такая простая вещь, как установление связей между событиями Win-
dows и соответствующими фрагментами вашего кода, может отнимать
много сил и приводить к ошибкам (спасибо за это возвратным вызовам
(callbacks), разнообразию форматов сообщений, многочисленным со-
глашениям и исключениям из них и т. д.). Одно из сильнейших
разочарований в Windows-программировании мы испытываем, когда
часто добавляем в программу стопроцентно корректный код, а при по-
следующем тестировании обнаруживаем, что этот код втихомолку иг-
норируется только лишь потому, что мы неправильно сделали соответ-
ствующий участок канализации. Конечно, каркасные библиотеки и ин-
тегрированные среды разработки оказываются существенной подмогой
в этой области - вы просто бросаете на диалоговое окно кнопку, а за-
тем просите среду создать для вас функцию, которая будет получать
управление всякий раз, когда пользователь эту кнопку нажмет; более
того, эта функция будет автоматически показана в окне редактора, и
курсор установится прямо на ее тело - пиши не хочу. Полный рок-н-
ролл. И вот тут-то обрадованного программиста поджидает главная
опасность - соблазнившись простотой и автоматизмом предлагаемой
средой схемы, он начинает писать код прямо здесь, не отходя от кассы
(естественно, кому охота после такого праздничного фейерверка
мучиться с созданием новых файлов или функций в других файлах?).

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

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

Например, некоторые программисты встанут на дыбы при виде такого кода:

// Обработка нажатия кнопки Reset в нашем диалоге
void CLoadDlg::0nReset()
{
   ResetAllData();   // Выполняем все действия
   EndDlg(ID_OK);     // Выходим, закрывая диалог
}

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

13. Используйте умные файлы данных

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

Каждому программисту наиболее знаком такой пример умного файла дан-
ных, как исполняемый файл, хотя многие даже никогда не смотрели на него
под таким углом зрения. В частности, новый РЕ-формат исполняемых файлов
Win32-пporpaмм помимо кода и сегментов данных содержит массу дополни-
тельной информации. Чтобы не обвинять меня в чрезмерном сгущении красок
при таком отождествлении, вспомните, пожалуйста, что в самом начале своей
жизни в системе ваша программа действительно представляет собой не более
чем обычный файл данных. Пользователь совершает двойной щелчок мышью
по вашей программе (или по файлу, ассоциированному с ней), заставляя Win-
dows загрузить и запустить ваше приложение. Но для того, чтобы выполнить
ваш приказ, Windows должна сделать кучу дел: сначала убедиться, что указан-
ный файл в самом деле является исполняемым файлом; затем проверить, соот-
ветствует ли указанная в этом файле «ожидаемая версия Windows» данной сис-
теме (если, конечно, вы не имеете дела с Win32s, которой наплевать на такие
тонкости); после этого Windows должна разобраться со всеми неявными ссыл-
ками  на динамические библиотеки,  которые нужны  вашей  программе,  и
совершить еще уйму других мелких операций, прежде чем ваше творение типа
«Неllо, World» сможет открыть рот и вообще начать жить.

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

Важно отметить, что способ хранения данных программой оказывает нема-
ловажное влияние на ее качество, и в первую очередь - на надежность. Если
программа хранит свои настроечные параметры в INI-файле (то есть в чисто
текстовом файле), то ей очень .четко сделать подножку - достаточно какому-
нибудь авантюристу-пользователю начать развлекаться со своим любимым тек-
стовым редактором, открывая им все, что попало. (Кстати, хранение данных в
реестре Windows - в какой-то степени еще более рискованное дело. Ведь Win-
dows поставляется со специальной программой для редактирования реестра.
Как вы думаете, какую мысль первой спровоцирует этот факт в голове пользо-
вателя?) Все это является еще одной причиной, по которой я до сих пор
остаюсь фанатом бинарных конфигурационных файлов, несмотря на их ка-
жущуюся старомодность. При должном обращении c ними, они могут быть
эффективнее и немного безопаснее INI-файлов или реестра, хотя бы потому,
что у пользователей не будет достаточно удобного способа их покалечить. (Я,
правда, видал и таких людей, которые занимались редактированием DLL-фай-
лов при помощи текстовых редакторов, даже не обращая внимания на то, что
их экраны были заполнены россыпью бинарной абракадабры вокруг несколь-
ких байт членораздельного текста. К сожалению, в природе нет способов от-
говорить человека от таких проделок, однако большинство пользователей,
загрузив в Notepad бинарный файл, все-таки сумеет быстро понять, что они
вторглись в запретную зону, и что лучше будет ретироваться.)

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

14. Элегантно реагируйте на сюрпризы со стороны

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

На самом деле, эти маленькие программистские пустячки, которые все мы
пишем на автопилоте, находятся лишь на краю широчайшего спектра поведе-
ния программы в критических ситуациях. Этот спектр простирается от простей-
ших, очевидных проверок до более серьезных действий, за которыми может
стоять гораздо больше раздумий и решений (например, как обращаться с ди-
намическими библиотеками, или на каких платформах Win32 должна работать
программа). То, что стоит на более простом краю, это не более чем азы
оборонительного программирования. Но как только вы попытаетесь
распространить эту общую концепцию на более сложные проблемы, она выде-
ляется в отдельную тему (со своими правилами и рекомендациями) и немного
по-другому влияет на процесс разработки программы. В частности, в круг рас-
смотрения попадает вопрос о настороженном отношении программы к тем
проблемам, которые не созданы ей самой. А это, в свою очередь, приводит нас
прямиком к понятиям разумности и услужливости программ, о которых мы уже
говорили ранее.

Вот неполный перечень наиболее важных моментов из этой области, о ко-
торых вы должны знать, и которые вы должны делать:

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

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

Следует признать, что, на самом деле, мы сейчас заговорили о
макропроблеме внутри микропроблемы. Ведь при принятии решения о
поддержке (или неподдержке) той или иной платформы Win32 необ-
ходимо учитывать и перспективное планирование, включая такие
вопросы, как маркетинг и техническое обеспечение. Я не думаю, что
многие коллективы разработчиков могут принимать «на лету» такие
решения, как, например, отказ от поддержки Win32s (даже находясь
под сегодняшним аномальным воздействием временных ограничений).
По крайней мере, мне хотелось бы надеяться, что они так не делают.

В Разумно осуществляйте загрузку динамических библиотек. Пользова-
тели заслуживают хотя бы минимальной помощи с вашей стороны в
том случае, когда программа не может найти и загрузить одну из не-
обходимых ей DLL, - не молчаливого отказа от работы и даже не ску-
пого сообщения, которое иногда выдает в таких ситуациях Windows
95, а подробного объяснения и подсказки именно со стороны вашей
программы. Еще более важным является умение вашей программы,
если это возможно, элегантно сократить свою функциональность, ко-
гда соответствующая DLL не найдена или не может быть загружена.
(Классическим примером такого интеллигентного поведения может
быть отключение всех функций и свойств программы, относящихся к
электронной почте, если не удается подгрузить MAPI.DLL.)

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

15. Остерегайтесь противоестественных действий

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

Должно быть, теперь, при наличии этого здания и помощи Microsoft (и
других третьих фирм), вы наконец стали обладателем понятных, эффективных
и надежных средств достижения практически любых интересующих вас целей,
правильно? (Здесь я сделаю паузу на несколько секунд и подожду, пока более
опытные Windows-программисты закончат хохотать и поднимутся с пола.)
Конечно же, мир не так прост, и рано или поздно все мы приходим к тому, что
начинаем (или, как минимум, испытываем сильнейшее искушение) время от
времени использовать недокументированные возможности Windows.
Например, если ваша Win32-программа хочет вызвать функцию GetFreeSys-
temResourcesO, вы не можете сделать это напрямую (по крайней мере, со-
гласно правилам Microsoft). Вам придется обратиться к переходникам
(thunks) - очень уродливому методу. Но это лишь официальный способ реше-
ния. На самом же деле, вы можете вызывать 16-разрядную DLL из 32-разряд-
ной программы, минуя всякие переходники, но при условии, что вы готовы
пойти на использование недокументированного API. (Отвратительные детали
этого процесса вы найдете в главе 12.)

А теперь вы, наверное, ждете того момента, когда я вскочу на трибуну и
буду говорить вам (словами Джорджа Карлина [популярный (особенно в 70-х годах)
 американский комик, «специализирующийся» на пародиях и сатире в адрес  
администрации США]), что использование недокументированного API
«заразит вашу душу, согнет вашу спину и помешает стране
выиграть войну», правильно? Нет, пожалуй, я пощажу уши (и драгоценное
время) почтенной аудитории, поскольку подозреваю, что наверное для каждого
из всех собравшихся здесь очевидно, почему недокументированные возможности
 действительно опасны, особенно для публичных программ. В конце концов,
позволила же себе Microsoft проявить непростительную жестокость по отно-
шению к программистам даже в документированной части Win32 API! (См.
главы 12 и 14.) Почему же вы думаете, что она будет хоть сколько-нибудь бо-
лее милосердной к вам в случае с недокументированными возможностями? (Я
знаю известную шуточку о том, что, мол, недокументированные функции - это
именно те функции, которые действительно необходимы продуктам самой Mi-
crosoft, и поэтому как раз эти функции, вероятно, являются самыми надеж-
ными. Если кто и готов поставить на кон свою программу и держать пари за
такую трактовку ситуации, то это не я.)

Поистине отвратительной чертой недокументированных API является то,
что они заставляют вас принимать очень тяжелые решения. Как в вышеописан-
ном примере с функцией GetFreeSystemResources(): вам приходится выбирать
между простым, прямым путем (который, кстати, и был бы по-настоящему
прямым, если бы был задокументирован) сделать что-то и официально
одобренным маршрутом, при путешествии по которому сильно чешутся руки
(естественно, в фигуральном, а не в анатомическом смысле). К сожалению, по-
добный выбор может оказаться еще более экстремальным: либо использовать
недокументированную функцию или деталь, либо вообще отказаться от реали-
зации каких-то возможностей своей программы. Мне пришлось столкнуться с
таким выбором при написании Stickies!, когда я захотел показывать в About-
диалоге количество свободных ресурсов в модулях USER и GDI и вынужден
был обратиться к использованию недокументированного API. Мне не нрави-
лось делать это таким способом, однако в той конкретной ситуации у меня было
одно веское оправдание: если бы вдруг используемые мной API перестали
правильно работать, это не нанесло бы программе серьезного ущерба, а я смог
бы очень быстро изменить диалог About и ликвидировать проблему. (Кстати,
с выходом Windows 95 эта деталь Stickies! работать не перестала, и я все еще
держу скрещенные пальцы за спиной.)

Увы, использование недокументированных API и структур является
только лишь самым очевидным (и, кто-то правильно добавит, самым
экстремальным) примером противоестественных действий. Это лишь вершина
айсберга. К этому «жанру» можно причислить любое действие, при котором
программист сознательно неправильно использует возможности какого-либо
инструмента или интерфейса, рискуя создать себе и пользователям проблемы
(либо немедленные, либо перспективные). Особая щекотливость данной
проблемы заключается в том, что идущий на подобные действия программист
всегда имеет веские (по его мнению) причины на это. Поэтому, как всегда,
ключом при решении таких вопросов является поиск разумного баланса.
Программист может определить и объяснить вам сиюминутный выигрыш от
того или иного противоестественного действия, он может даже привести вам ми-
нимальные «доказательства» безопасности этого действия (см. обсуждение мифа
под названием «Видите? Работает!» в рекомендации 8 на стр. 99). Но для
решения спора этого, безусловно, мало. Вопрос должен ставиться только в та-
кой форме: так ли выгодно данное действие с точки зрения перспективы?

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

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

#define begin {
#define end }
#define integer int
#define then

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

integer fred = 0;
if(fred > 0) then
begin
  printf("fred was > 0\n\n")
end
else
begin
  printf("fred was <= 0\n\n")
end;         // Обратите внимание на беспричинную точку с запятой!

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

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

Другим, менее шокирующим примером, с которым иногда сталкиваются
программисты, является проблема модификации кода каркасной библиотеки.
Глядя на то, какими огромными и сложными стали сегодня такие библиотеки,
вы можете подумать, что вряд ли существуют какие-либо резонные причины
это делать. Однако такое действительно случается. Мне пришлось делать это с
OWL при работе над Stickies!, когда мне потребовалось изменить способ соз-
дания многострочного поля редактирования, изменить по всей классовой
иерархии вглубь до самого уровня CreateWindow. Поскольку я хотел
сохранить все остальные удобства, предоставляемые OWL для работы с по-
лями редактирования, я был вынужден модифицировать исходный код OWL
на нескольких уровнях, и в итоге я получил в точности тот эффект, которого
добивался. Вообще говоря, такой подход рискован, так как вы можете запросто
ввести в библиотеку неуловимую ошибку, которая потом отнимет у вас до не-
скольких рабочих дней. Еще более вероятна другая проблема: если вы сделаете
корректное исправление или изменение, то вам потом придется переносить его
в новые версии библиотеки по мере их появления. Например, однажды сломав-
шись и «подкрутив» что-нибудь в MFC, вы будете заниматься такими перено-
сами гораздо чаще, чем вам хотелось бы. Кроме того, всегда есть ненулевая
вероятность того, что авторы каркасной библиотеки в какой-то момент сами до-
бавят в свое детище ту же (или почти ту же) возможность, которая была вам
нужна и которую вы уже реализовали сами. И тогда вы окажетесь перед
непростым выбором - то ли адаптировать свою программу под использование
«официальной версии», то ли в очередной раз переносить свое «доморощен-
ное» решение в новую версию библиотеки.

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

Конечно, бывают случаи, когда у вас действительно нет другого выбора,
кроме как совершить грех против всего программистского сообщества. Дос-
таточно часто это диктуется причинами вовсе не технического происхождения,
и программисты просто идут в том направлении, которое было указано началь-
ством, и но которому сами они скорее всего не пошли бы. Тут может быть много
вариантов: использование «неправильного» языка или инструмента
программирования, третьесторонней библиотеки или каркасной библиотеки,
или даже такая экзотика, как использования одного внутреннего
стратегического продукта компании вместо другого продукта той же компании.
В подобных случаях программисты буквально принуждаются к противоестест-
венным действиям.

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

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