RDM/2 The Russian Electronic Developer Magazine  
RDM/2 Русский электронный журнал разработчика  
ДомойОт редактораПишите намОбратная связьRU/2

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

От редактора
Хотя эта статья написано программистом под Windows, я думаю, что она будет интересна и программистам, работающим под OS/2.

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

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

В 1990 году немногочисленная компьютерная пресса России оценивала количество программистов в СССР как пятьсот тысяч человек. За прошедшие пять лет популярность программирования заметно увеличилась, компьютеры стали более доступными, а средства разработки более совершенными. Казалось бы, сейчас специалистов по разработке программ должно быть как минимум вдвое больше, чем раньше, но по количеству выпускаемых в России коммерческих программных продуктов мы отстаем даже от восточно-европейских стран. Наверное можно было бы объяснить эту несуразность тем, что пик популярности профессии программиста закончился пару лет назад и многие светлые умы ушли в бизнес. Но ореол романтичности и элитарности привлекает все больше и больше молодых людей в ряды разработчиков software. Я думаю, что приводимые цифры были завышены, как минимум, десятикратно, но даже число 50.000 существенно превышает количество программистов в Microsoft, Novell, Borland и Lotus вместе взятых. Мне кажется, что столь низкая активность в секторе коммерческих разработок ПО в России объясняется тем, что в индустрии ПО произошла технологическая революция которая полностью изменила правила игры.

Раньше главными проблемами людей решивших создать компьютерную программу на продажу были производство таких атрибутов коммерческого продукта как коробки, документации и носители информации ( дискеты и стримерные кассеты ). Формирование рынков сбыта также было сложным делом, но стояла она обычно особняком. Сейчас к этим трудно разрешимым проблемам добавились сложности, как ни парадоксально, с написанием самой программы. Конкуренция на рынке ПО привела к усложнению коммерческих продуктов, а популярность новейших информационных технологий ( например OLE2 ) заставляет тратить огромные ресурсы на обеспечении их поддержки. Сложность программ уже переросла способности отдельных программистов. Современные требование таковы, что для успешной реализации проекта требуется сплоченный коллектив разработчиков в котором существует разделение труда, специализация и жесткая иерархическая система взаимоотношений членов группы. К сожалению, в России еще мало людей способных организовывать работу таким образом. Мы пока не можем составить серьезную конкуренцию зарубежным коммерческим продуктам - это дело будущего. Но вместе с тем, Россия может претендовать на роль лидера по производству программ называемых здесь shareware, но являющихся по сути настоящими коммерческими программами реализованными приблизительно на 70-90%.

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

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

Пример 1.

Cprotector* current = new Cprotector( 377 );
 ......
 if( status == CORRECT && isReady() )
         delete current;


Пример 2.

Cprotector* current = new Cprotector( 377 );
 ......
 // Вызываем метод уничтожающий все лишнее
 destroyAll( current, &floor, &k );
2) Пишите код так, чтобы программа состояла из небольших ( десять - пятнадцать строк ) методов. Если написанная Вами функция не вмещается на экране, разбейте ее на несколько меньших. Этот совет прост, но, тем не менее, им часто пренебрегают. Многие программисты утверждают, что с ростом количества функций в программе уменьшается ее быстродействие, но применение inline-функций позволит Вам избежать этого единственного недостатка. Основной довод сторонников "коротких функций" звучит так: "Мы можем прочитав код короткой функции убедится в ее правильности, но читая большую функцию, мы, часто, не можем даже понять, что она делает". Этот довод кажется мне весьма убедительным. Представьте, что Вам поручили поддерживать средний проект объемом чуть больше 100000 строк, а отдел тестирования передал Вам десяток рекламаций которые нужно исправить в течении недели, и Вы станете активным сторонником "коротких функций".

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

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

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

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

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

Несмотря на то, что макроопределение ASSERT известно еще с середины 70-ых, активное распространение техники его использования в России началось только с переходом некоторых групп программистов на Microsoft Foundation Classes. Идея проста - при компиляции препроцессор заменяет каждый ASSERT на блок реакции приблизительно так:

Было:

  ASSERT( pFile != NULL );

Cтало:

  ( ( pFile != NULL ) ? (void)0 : \
     _assert( "pFile != NULL", __FILE__,
      __LINE__ ) );
Функция _assert реализовывается по разному в разных операционных системах. Многие разработчики предпочитают собственную реализацию этого макроопределения. Смысл этой функции сводится к выдаче строки, файла и сообщения об условии приведшем к сбою. Используются различные реализации макроопределения, в том числе и по реакции, например, ASSERT выдает сообщение о ошибке и аварийно завершает работу, а макроопределение CHECKS выдает сообщение, после которого можно продолжить работу. С появлением разделения проекта на отладочную ( Debug ) и завершающую ( Release ) версии появилось правило - в Release версии assert заменяется на пустой код, что позволяет получать такую же производительную программу, как и программу в коде которой ASSERT не используется.

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

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

Хорошей манерой является комментирование ASSERT с указанием возможных причин некорректной работы. Комментирование ASSERT жизненно необходимо в случае его нахождения в ключевом методе, когда небрежные правки могут полностью нарушить работоспособность программы. Если Вы используете MFC 2.X имеет смысл переопределить ASSERT в Release-версии, выдавая сообщение о ошибке, что позволит конечным пользователям передавать разработчикам более точную диагностику ошибки. Обычно ASSERT ставится на все функции возвращающие флаг ошибки ( TRUE/FALSE ), что позволяет сразу же определить такие неприятности, как, например, невозможность создания окна, или неверный идентификатор ресурса при попытке его загрузки.

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

Что же должно быть включено в такой документ? Абсолютно все, включая количество добавляемых пробелов при табуляции, добавлять или нет пробелы перед и после условия в круглых скобках и описание трюков, использование которых запрещено. Важным в таком документе является и описание традиций программирования в фирме, например, если оператор switch обрабатывает ограниченное количество значений, то в default-значении должен стоять ASSERT.

switch( accountCode ) {
  case 0:
           .....
           break;
  case 1:
           .....
  default:
           ASSERT( FALSE );
}
Стандартный подход позволяет увидеть не характерное для программиста оформление кода программы и выявить ошибку даже при беглом просмотре исходных текстов.

5) Никому не доверяйте. Каждый метод должен работать таким образом, чтобы любое неразумное поведение внешнего кода не приводило к неразумному поведению Вашей части программы. Максимально используйте чужие наработки, но относитесь к ним со всей возможной осторожностью. Этот совет состоит из двух частей. Во-первых, пишите каждый метод так, как будто кроме Вашего кода весь остальной ( внешний ) код функционирует некорректно. Это спасет разработчика использующего Ваши исходные тексты от ошибок связанных с недостаточным пониманием их работы и незнанием особенностей реализации. А во-вторых, очень осторожно относитесь к использованию чужих исходных текстов. Сейчас уже невозможно организовывать работу основываясь только на собственном опыте. Для того, чтобы программисты могли максимально быстро освоить новый механизм средств разработки, вместе с компиляторами поставляются наборы специализированных примеров ( samples ). Например, если Вы хотите использовать в своем приложении под Windows нестандартные элементы управления, Вам достаточно использовать часть кода проекта  ctrltest  поставляемого вместе с Visual C++. Разработчики Microsoft называют это технологией  Developers Cut&Paste . Так вот, Вы можете практически без опасений переносить в свои программы код примеров поставляемых с компиляторами. Очень осторожно используйте примеры приводимые в масс-медиа ( журналы и CD-ROM энциклопедии для разработчиков ). Вытаскивать и использовать код своих знакомых не рекомендуется вообще. В последнем случае лучше всего детально рассмотреть идею по реализации, а аналогичный код написать самостоятельно для того, чтобы модифицировать его в дальнейшем было значительно проще. Опасность  самопальной  реализации состоит в том, что переноситься обычно очень нетривиальный код, который в большинстве случаев основывается на априорных предположениях и функционирует в сложной системе с большим количеством параметров, учесть воздействие которых полностью невероятно сложно.

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

Максим Рожков

---
Интересные ссылки:

---

---
Комментариев к странице: 0 | Добавить комментарий
---
Редактор: Дмитрий Бан
Оформление: Евгений Кулешов
(C) Russian Underground/2