Шесть советов по написанию более понятного программного кода

Джефф Вогел, Президент, Spiderweb Software

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

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

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

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

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

Пример

В качестве иллюстрации в этой статье я буду рассматривать программу, представляющую собой гипотетическую компьютерную игру под названием Kill Bad Aliens (Убей плохого инопланетянина). Участник этой игры должен управлять космическим кораблем. Этот корабль перемещается по горизонтали в нижней части экрана и стреляет снарядами по вертикали. Управление этим космическим кораблем будет осуществляться, скажем, с помощью клавиатуры.

Рис. 1. Наша гипотетическая игра
Наша гипотетическая игра

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

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

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

Итак, вы садитесь за рабочий стол и начинаете писать программный код игры Kill Bad Aliens на языке C++. Сначала вы определяете объекты, которые будут представлять космический корабль, снаряды игрока, инопланетян и их бомбы. Затем вы пишете код для графического представления на экране всех указанных объектов. После этого вы пишете код для перемещения объектов по экрану в зависимости от времени. И, наконец, вы пишете игровую логику, искусственный интеллект инопланетян, программный код для ввода с клавиатуры команд игрока и т.д.

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

Совет 1. Будьте благоразумны - пишите комментарии.

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

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

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

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

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

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

И уж если мы подошли к этому, еще одна рекомендация - никогда не делайте такого:

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

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

  1. Несколько предложений в начале процедуры/функции, объясняющих, что она делает.
  2. Описание значений, передаваемых в эту процедуру/функцию.
  3. В случае функции - описание смысла возвращаемых параметров.
  4. Внутри процедуры/функции - комментарии, разбивающие программный код на короткие подзадачи.
  5. Для особо сложных фрагментов кода - краткое пояснение того, что происходит.

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

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

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

Совет 2. Используйте оператор #define чаще. КАК МОЖНО ЧАЩЕ.

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

Хороший способ: В некотором глобальном файле напишите следующую строку:

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

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

Если впоследствии вы решите изменить размеры игрового окна (а это весьма вероятно), то возможность централизованного изменения этих значений сэкономит вам время дважды. Во-первых, вам не придется просматривать весь свой код в поисках всех мест, где вы указали ширину экрана, равную 800 пикселам. («800 пикселов! И о чем я только думал?») Во-вторых, вам не придется исправлять неизбежные ошибки, связанные со ссылками, которые вы неминуемо пропустите.

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

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

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

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

Рис. 2. Игра Kill Bad Aliens до изменения констант
Игра Kill Bad Aliens до изменения констант

Рис. 3. Игра Kill Bad Aliens после увеличения всех констант (играть, может быть, трудновато, но посмотреть интересно)
Игра Kill Bad Aliens после увеличения всех констант (играть, может быть, трудновато, но посмотреть интересно)

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

Совет 3. Не давайте переменным имена, способные ввести в заблуждение.

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

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

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

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

я, скорее всего, написал бы следующее:

Любое недоразумение, обусловленное более коротким именем, будет устранено очень быстро, а «читаемость» кода улучшится.

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

Обратите внимание, что массив для всех инопланетян так и называется - aliens. И это очень хорошо. Данное имя передает именно то, что я хотел сказать, однако при этом оно достаточно короткое, чтобы я мог ввести его с клавиатуры тысячи раз и не сойти при этом с ума. По всей вероятности, вы будете использовать этот массив ОЧЕНЬ ЧАСТО. Если вы назовете этот массив all_aliens_currently_on_screen, ваш программный код станет на десять миль длиннее и настолько же непонятнее.

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

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

Совет 4. Проверяйте свою программу на наличие ошибок. Вы ведь делаете ошибки. Да-да, именно вы.

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

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

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

Пока все идет нормально. Теперь задайте себе вопрос: «Что в этом коде может быть не так?»

Во-первых, один очевидный момент. Что произойдет, если переменная num_points будет иметь отрицательное значение? Можем ли мы допустить, чтобы счет игрока снижался? Возможно. Однако в описании игры я до этого нигде не упоминал о возможности потери игроком баллов. Кроме того, игры должны приносить удовольствие, а потеря баллов этому противоречит. Таким образом, мы приходим к выводу, что отрицательное число очков - это ошибка, которую необходимо поймать.

Этот пример был достаточно простым. Существует и менее очевидная проблема (с которой я постоянно сталкиваюсь в своих играх). Что произойдет, если переменная num_points будет равна нулю?

Это весьма правдоподобная ситуация. Не забудьте, что по окончании каждой волны мы даем игроку бонусные баллы в зависимости от скорости ее прохождения. Что произойдет, если игрок будет действовать слишком медленно и мы решим дать ему 0 баллов? Вполне вероятно, что, работая над своим кодом в 3 часа ночи, вы решите вызвать процедуру change_score и передать ей значение 0.

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

Ну вот. Так гораздо лучше.

Обратите внимание, что это была очень простая функция. В ней совершенно отсутствуют всякие новомодные указатели, которые так любят использовать молодые лихие программисты. Если вы передаете массивы или указатели, вам ОБЯЗАТЕЛЬНО нужно предусмотреть выявление ошибок или плохих данных.

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

Этот подход экономит массу времени и заслуживает регулярного применения. Время - наш самый ценный ресурс.

Совет 5. «Преждевременная оптимизация - корень всех зол», - Дональд Кнут (Donald Knuth).

Эту фразу придумал не я. Однако она есть в Википедии, и поэтому, по всей видимости, не лишена смысла.

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

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

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

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

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

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

И, наконец, раз уж мы заговорили о болезненном, вот мой завершающий совет:

Совет 6. Не умничайте.

Возможно, вы слышали о существовании мероприятия под названием International Obfuscated C Code Contest - международного конкурса по самому запутанному программному коду на языке C. Все дело в том, что языки C и C++, при всех своих преимуществах, позволяют создавать кошмарно запутанный код. Этот конкурс демонстрирует преимущества понятного кода «от противного» - посредством награждения самых безумных программистов. Отличная идея.

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

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

Каждый программист имеет свой допустимый уровень сложности создаваемого кода. Лично я пишу свои программы так, как водит машину типичная бабушка. На мой взгляд, если ваш код на C требует понимания тонких различий между выражениями i++ и ++i, то он слишком сложен.

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

Заключение

Возможно, дочитав до этого места, вы думаете: «И это все? Только зря потратил время. Это же очевидно и всем известно. Зачем автор все это писал?» Очень надеюсь, что вы думаете именно так. Значит, вы уже сами все знаете. Очень рад за вас.

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

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=6446