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

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

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

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

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

Ошибка #1. Плохая практика комментирования

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

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

В конце 90-х годов я писал версию 1.0 ZENPRESS, одной из первых систем управления контентом. Я рассчитывал, что она будет помогать писать статьи в течение нескольких лет. Через четырнадцать лет она все еще служила поставщиком материалов, и с ее помощью было подготовлено около 75 тысяч статей и агрегировано 2,6 миллиарда страниц.

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

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

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

Ошибка #2. Плохие имена переменных

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

Пример простой, но подходящий для наших целей. Допустим, вы встречаете в коде строку a = b/c. Что это значит? Что такое b и c? Как они связаны с остальным кодом? Написав программу, через десять минут вы это забудете. Тем более это не поймет кто-то другой, кому придется вносить в ваш код исправления или писать обновление программы.

Теперь взгляните на следующее выражение: gallons = miles/mpg. Тут сразу ясно, что означает каждая из переменных. Одна представляет галлоны, другая представляет мили, а третья — мили на галлон. Все понятно.

Подумайте о родственной связи понятных имен переменных с использованием английского языка (или вашего родного разговорного языка) и комментариев. Допустим вам от кого-то досталась порция кода, и вы видите a = b/c. Что здесь имеется в виду? Вам что-то приходит в голову?

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

Ошибка #3. Недооценка важности лабораторных записок

Я начал писать ZENPRESS в середине 1997 г. и выпустил в свет в январе 1998 г. К сожалению, я спешил закончить проект, и не хотел терять время на лабораторные записки к этому первому релизу. Потом я об этом не раз жалел. С июня 1999 г., принявшись за версию 2, я регулярно вел лабораторные записки.

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

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

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

Ошибка #4. Нежелание учиться писать на человеческом языке

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

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

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

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

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

Ошибка #5. Плохое форматирование кода

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

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

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

Под структурированностью я понимаю характер оформления кода. Для своих студентов я сделал на этот счет видео, которое можно посмотреть на YouTube.

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

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

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

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

Ошибка #6. Плохая проверка ошибок

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

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

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

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

Вы убедитесь, что этот подход исключительно информативен.

Ошибка #7. Использование операторов print вместо настоящего отладчика

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

Одним из примеров этого является различие между моими студентами, программирующими на C#, и разработчиками, использующими созданный на базе Open Source язык PHP, с которыми я работал над некоторыми из моих проектов. Практически никто из C#-программистов не представляет отладку своего кода без использования символьного отладчика. Дело в том, что код C# естественным образом программируется с использованием в качестве среды кодирования Visual Studio, где имеется встроенный отладчик.

Напротив, я видел бесконечный поток PHP-разработчиков, по мнению которых для отладки кода им вполне достаточно задействовать операторы echo или var_dump. В какой-то мере это обусловлено тем, что большинство PHP-программистов, как правило, программирует в редакторе, а не в среде разработки. Главным отличием первых от вторых является отладчик.

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

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

Здесь моя часть наблюдений и советов заканчивается, и я передаю слово профессору Джеймсу Коннору.

Ошибка #8. Использование магических чисел

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

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

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

В этом случае код мог бы иметь вид: commission = .03 * sale. В этом контексте 0.03 является магическим числом. Поскольку программист думает, что магическим образом так будет вечно, он жестко вносит в свой код число 0.03.

Но величина комиссионного сбора обычно от года к году меняется. Если в следующем году комиссия вырастет на полпроцента (до 0.035), будет очень сложно отыскать нужные места в тысячах строк кода.

Вместо использования магических чисел, задавайте в одном месте переменные или константы, и пусть ваш код использует эти переменные. Если вы ввели переменную commission_rate, то код типа commission = commission_rate * sale уже не потребует изменений.

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

Ошибка #9. Небрежное обращение с временами и датами

Трудный вопрос: сколько дней в году? Обычный ответ — 365, но в нынешнем году, конечно, 366. Бывает ли в году 365,25 дней? Нет, не бывает.

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

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

Обратимся к аналогичному вопросу со временем. Каждые несколько лет (так как вращение Земли замедляется) к одному из дней добавляется лишняя секунда, обычно 30 июня или 31 декабря. Ее называют секундой координации, и из-за этого возможна ситуация, когда часы переходят от 11:59:59 к 11:59:60, а потом к 12:00:00.

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

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

Ошибка #10. Выбор неадекватных структур данных

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

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

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

Если вам нужно удалить тарелку, вы берете ее с верха стопки. Это называется last-in, first-out (последним пришел, первым вышел). Проблема в том, что если вам надо что-то удалить из начала стека, это будет сложно. Пусть в вашем стеке десять тарелок. Чтобы добраться до первой, вам придется сначала убрать все остальные.

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

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

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

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

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

Резюме

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