Нам часто приходится программировать, однако физики традиционно недооценивают значимость хорошего кода. Считается, что писать хороший код — это излишнее эстетство, непозволительная для серьёзных людей роскошь, баловство и посягательство на чужую компетенцию. При этом сами программисты рассматриваются нередко с некоторым пренебрежением, поскольку современный software enginier, или, вернее, его стереотипный образ в научном сообществе связывается с неумением понимать квантовую электродинамику и даже (о ужас!) незнанием линейной алгебры.
Ну что есть, то есть. Времена зачатия профессии программиста, когда хороший программист был непременно недурным математиком (цит. Дейкстра) ушли в прошлое. Нынешний кодер производящий даже толковые программы, обладающие всеми необходимыми качествами для последующего лёгкого использования, редко разбирается в той математике из которой родилось его ремесло. Тем не менее, никакое ремесло не должно быть недооценено.
Я могу много (и довольно веско) ассуждать, почему нужно уметь писать хороший код в прикладных считалках. Со множеством примеров из практики, — как нужно поступать, и как не следует, почему бывают важны паттерны проектирования, а когда можно всё бросить и повесить «на гвозди».
Впрочем, это будет нецелесообразно писать здесь — у каждого читателя свой порог и свой опыт. Целесообразно, тем не менее, для некоторых наших студентов готовить небольшую выжимку употребимых практик. Конечно, такое резюме не заменит тысяч часов практического обучения, — вы не запомните и половины, потому что многое покажется вам сомнительным или непонятным. На практику у нас, тем не менее, нет ни средств, ни времени, так что проверять справедливость моих следующих утверждений предстоит «в поле».
Любая хорошая программа — это хорошо схваченный компромисс между частным и общим, между детальностью и лаконичностью, между подробностями и концепцией. В высокоуровневых языках (всяких, что есть не бинарный код и не ассемблер) определяющими критериями для такой программы служит в большей степени человеческое восприятие:
if( !myContainer.empty() )
, и т.д.Это просто эмпирические сведения о людях приобретаемые на практике; с ними нужно считаться.
Код должен:
ind1
, func2
,
myFunction
, etc. Часто лучше написать длинное имя для функции или переменной,
которая используется в коде в двух‐трёх местах, чем экономить на спичках. Многое,
очень многое, упрощают конвенции — негласные, или гласные правила, которым следуют
опытные инженеры когда разбирают чужие проекты и пишут свои. Эти конвенции
подразумевают форматирование кода, структуру директорий проекта, правила написания
документации.i
, j
, k
— это целочисленные индексы, а метод класса increment()
увеличивает
какой‐нибудь единственный для этого класса счётчик на единицу. Длинные имена
затрудняют читаемость.M_PI
, определённое в заголовочном файле <math.h>
. Будет хорошо, если вы
будете пользоваться этим макросом, и не копировать его своего из справочника констант
1973 года. Ну, число Пи, может быть, не очень хороший пример, потому что вряд ли оно
когда‐либо изменится в нашей вселенной, однако всегда есть данные, которые
может_понадобиться изменить. И тогда большой проблемой будет отыскать в программе
все места где такие данные были определены.Я обычно настоятельно рекомендую всем начинающим писать свой код на чистом Си, когда позволяют задачи. Связано это с тем, что C и C++, вообще, предполагают различные стили организации программ, и частое смешивание двух стилей обычно не идёт на пользу читаемости кода. Си — язык парадигмально процедурный, он использует структуры для упорядочивания данных, но при этом не требует от пользователя понимания абстракций высоких уровней рефлексии — на нём можно обойтись без знания об итераторах, о балансировке красно‐чёрных деревьев и специализации шаблонов. Напротив, для эффективной разработки на C++ такие знания необходимы. Говоря языком более практическим — необходимость в C++ возникает ровно в тот момент, когда пользователя перестают удовлетворять структуры и процедуры, и сама собой возникает потребность в методах, наследовании с переопределением методов и областях видимости методов и полей.
До тех пор пока вы не можете внятно ответить себе на вопрос, почему функциональность этой части программы лучше инкапсулировать в рамках методов класса, — держитесь подальше от C++. Конечно, этот совет уже давно не очень актуален, для физиков, поскольку чаще всего им приходится взаимодействовать с такими фреймворками как Geant4 или ROOT, однако нам всё ещё иногда попадаются отдельные [под]задачи, которые не требуют такой интеграции (напомню, что в рамках одного проекта вы можете легко встраивать C в C++).
В любом фрагменте кода важна логика и структурность организации — хотя её и может быть слишком много, до того момента когда вам начнут вредить идеи организации нужно пройти довольно длинный путь, и не‐профессионалам этого опасаться стоит в последнюю очередь. Первой идеей должна стать декомпозиция задачи на отдельные подзадачи, выделение сущностей, их ролей и общих участков. Иначе говоря, перед тем как топтать клавиатуру, вам следует заранее подумать над тем, на какие структуры и функции (в случае Си) и на какие классы (в случае С++ — дополнительно к структурам и функциям) вам стоит разделить вашу программу. Подумать надо хорошо, записать это где‐то, ещё лучше — зарисовать структуру в любом понятном для вас виде (архитекторы‐профессионалы используют для этого специальные инструменты, но вам лучше воздержаться в начале пути от них). Основная сложность тут — побороть желание прекратить думать и начать писать. Толковый код будет написан только тогда, когда всё что вам остаётся — это записать его, глядя на свои заметки.
Поначалу может показаться, что вы тратите время впустую, и могли бы уже всё написать. Ну да, когда дело касается числодробилки, которая должна что‐то посчитать один раз и кануть в Лету, можно поступить как бродячий акын — спеть о том что видите и успокоиться. Однако практика показывает — временное слишком часто демонстрирует такую персистентность в нашем деле, что даже сейчас, сложив всё время которое мы тратим на разбор чужих поделок скроенных в таком ключе, можно, вероятно, превзойти время существования вида Homo Sapiens.
Думаю, уже на третий‐четвёртый раз вы заметите динамику, если будете следовать этому совету. Перейдём к некоторым практическим аспектам.
Первые учебники по ООП рекомендовали записать на бумагу все существительные, которые участвуют в вербализованном изложении сути вашей программы. Так простая операция «банк‐донор осуществляет транзакцию в банк‐рецепиент» позволяет выделить две сущности: банк и транзакцию. Этот совет не утратил актуальности, хотя и всякое подобное предложение нередко легко переформулировать, уменьшив существительные, но, в целом, это неплохая отправная точка. Конструктивной процедуры для выделения «необходимого и достаточного» в ООП не существует — очень многое начинает или перестаёт быть объектом просто по прихоти.
Ну и, кроме того, нередки случаи, когда объектом удобно представить какое‐то действие (в примере с банками транзакция, вообще говоря, действие, да).
Далее рекомендовалось детализировать объектную структуру, каждому объекту сопоставив набор его свойств. Собственно, тут и возникает идея структурного программирования. Банк‐донор и банк‐рецепиент могут иметь, например: - имя (напр. «Двалакомбанк» и «Обойнымклей»); - какой‐нибудь уникальный регистрационный идентификатор (напр. 42 и R0F7); - какой‐нибудь не‐уникальный идентификатор; - цвет, вес, имя директора, задекларированный доход жены директора, и пр. С точки зрения лексики программы весь этот калейдоскоп удобно обобщить в рамках объекта — часто удобно оперировать набором данных как единым целым. Вы всегда можете представлять такие вещи просто байтовым массивом, последовательно записав эти данные в кусок памяти, скажем, в килобайт.
Си со своими структурами, однако предоставляет определённую лексику для операций с объектами. Именно в этом смысле структура — механизм чтобы с одной стороны гарантировать наличие определённых свойств у объекта, с другой — иметь доступ к ним на лексическом уровне. Для своего времени такие лексические возможности были довольно новаторской штуковиной. Хотя физически каждый объект — это всё тот же байтовый массив, язык даёт вам возможность выразить операции с данными внутри него и таким образом избежать массы потенциальных ошибок при разработке связанных с изменением структуры данных на физическом уровне. Это сейчас стало довольно тривиальной вещью, и никто не станет вам говорить, что поля структуры — это «программистские штучки и красивости, а мы тут заняты серёзным делом», как любят они говорить, например, про щаблоны в C++. Я это написал, потому что суть одна и та же — шаблоны и структуры — это всего лишь лексические возможности языков, нужные, чтобы вы меньше писали и делали меньше ошибок, но различен слегка порог вхождения.
Ещё один пример структуры, который, как правило, очень нравится физикам — это вектор. Конечно, он легко заменяется массивом из n элементов, но уже удобнее, чем n различных чисел, которые нужно таскать с собой по процедурам. Если с вектором связаны какие‐то дополнительные свойства, собственные теперь уже сложному объекту, вроде точечной массы, заряда, типа и импульса частицы, «структурная» лексика начинает играть всеми красками.
С вектором или кватернионом, или ещё какими объектами сложной судьбы и нетривиальной алгебры штука в том, что с ними неизбежно оказывается связан десяток типовых операций, которые ожидаемо реализуются процедурами (в терминологии Си — «функциями», но сущностно это всё ещё старая‐добрая процедура).
Изначально, процедура была просто набором инструкций, на который программа могла многократно ссылаться, делегировать выполнение. Простейшие языки не накладывали каких‐то особенных ограничений на этот кусок кода в общем потоке. Вскоре, впрочем, понадобилось такие фрагменты обосабливать и возникло понятие «подпрограмм». Имея в виду, что