Принципы стандартизации оформления программного кода
, В.
Работа посвящена вопросам разработки стандартов оформления программных продуктов на уровне модуля. Дан обзор мировой практики в этом направлении. Описана методология создания стандартов оформления кода, разработанная в компании “Cognitive Technologies”.
В области методологии разработки стандартов программирования работа является пионерской.
Введение
Практически каждый профессиональный разработчик сталкивался с трудностями при чтении чужого, а то и своего собственного старого кода, пытаясь понять, что за алгоритм здесь реализован. Или с ужасом смотрел на непонятно как выровненный код из-за проблем использования табуляции и пробелов в начале строк. Или сутками искал «плавающую» ошибку, являвшуюся результатом того, что просто не была инициализирована переменная. Поэтому целесообразность введения правил написания текста программы, создания стандарта кодирования, обычно не вызывает сомнений.
Но как только дело доходит до обсуждения того, какие правила должны входить в стандарт, а какие нет, то возникает море разных соображений, приводящих к неразрешимым спорам. Можно ли использовать goto? Насколько подробно надо комментировать код? Что использовать для отступа: пробел или табуляцию? А на сколько символов? А споры о пробелах перед скобками уже вошли в легенды.
В данной статье описаны принципы, опираясь на которые был разработан стандарт кодирования в компании «Cognitive Technologies».
1. Требования к программному продукту
Коммерческое использование программного продукта предъявляет к нему ряд качественных технологических требований. Часть этих требований универсальна, другая часть специфична для конкретного продукта. Универсальными требованиями в порядке убывания значимости можно считать следующие:
a. Полезная функциональность.
b. Стабильная работа.
c. Простота использования.
d. Поддерживаемость.
e. Развиваемость.
Наиболее важным требованием является первое. Нарушение иных требований, безусловно, снижает интерес к продукту. Однако, если функциональность продукта малоценна, сам интерес отсутствует изначально.
Требования стабильности и простоты эксплуатации равно важны, порядок здесь условный. Неустойчивая работа программы, сложность её использования, способны оттолкнуть потребителя от самого многообещающего продукта. Поэтому требования стабильности и простоты использования входят в тройку наиважнейших требований. Эти три требования представляют собой общий интерес у всех типов потребителей программного продукта.
Следующим универсальным требованием следует считать поддерживаемость. Это требование существенно отличается от предыдущих двумя особенностями.
Во-первых, наличие поддержки является существенным, но, всё же, дополнительным достоинством продукта в глазах потребителя. У пользователя высококачественного продукта может и не возникнуть потребности прибегнуть к поддержке за всё время эксплуатации продукта. Однако вероятность того, что поддержка может понадобиться, всегда изначально значительная. И отсутствие поддержки в тот момент, когда она реально понадобилась, воспринимается пользователем крайне негативно. Поэтому опытный потребитель обычно заранее включает требование поддерживаемости в число своих наиболее значимых требований при определении своего отношения к конкретному программному продукту.
Во-вторых, поддерживаемость не является только внутренним свойством продукта. Она объединяет некоторое качество продукта и специально организованный на всё время эксплуатации продукта производственный процесс.
Последнее универсальное требование – развиваемость. По своей природе оно также существенно отличается от первых трёх.
Во-первых, это требование, строго говоря, уже не универсально. Существуют, и оправданно, многие не развиваемые продукты. Однако самые перспективные продукты обязательно должны развиваться, и, как правило, успешно развиваются. Появление требования развиваемости к конкретному продукту не связано с его частными особенностями. Поэтому методически правильно включить это требование в число основных, не забывая при этом, что иногда оно может исчезать.
Во-вторых, это требование можно считать строго внутренним свойством продукта лишь с натяжкой. Теоретически можно так отладить и продуктизовать программу, что её легко смогут развивать прилетевшие много лет спустя инопланетяне. Однако практически успех развития сильно связан с преемственностью разработчиков и отсутствием продолжительного застоя. То есть помимо образцового состояния продукта почти обязательны доступность нематериализованного знания и не угасший полностью эволюционный процесс.
Существует ещё немало важных требований к программным продуктам. Производительность, полнота функциональности, изолированность, проверяемость корректности использования в целом ряде случаев выходят на первый план. Классы продуктов, к которым предъявляются такие требования, часто весьма представительны. Однако идентификация этих классов всё же идёт по конкретным особенностям. Поэтому авторы эти требования в число универсальных не включили.
2. Организация и стандарты программного продукта
Недостаточное соответствие предъявляемым к программному продукту технологическим требованиям ведёт к финансовым потерям. Для обеспечения выполнения этих требований используются стандарты разработки. Усиление конкуренции приводит к ужесточению стандартов.
Внутренняя организация программного продукта содержит несколько логических слоёв. Пакет эксплуатационных требований, пронизывая продукт, расщепляется. Разные требования оседают на соответствующих горизонтах как локальные требования к конкретному срезу. Стандарт, описывающий идеальную организацию уровня, представляет собой набор нормативов. Соответствие этим нормативам должно, по замыслу, гарантировать соблюдение исходных требований к данному уровню.
Нормировать логические срезы проблематично, для этого лучше подходят физические уровни. Сам программный продукт, как правило, имеет три физических слоя – уровень модуля, уровень библиотеки и уровень собственно продукта. Физические горизонты не совпадают с логическими, но достаточно хорошо им соответствуют.
Все описанные выше универсальные требования к продукту в такой же мере относятся и к технологиям (программным компонентам, не являющимися продуктами).
Предмет нашего исследования – обеспечение соблюдения технологических требований к продукту, существенно проявляющихся на уровне оформления модуля.
Было решено включать в стандарт оформления модуля только те правила, которые безразличны к роли этого модуля на вышестоящем уровне. Предполагалось, что нормирование оформления специфичных модулей – ввода/вывода, интерфейса и т. п. – должно осуществляться пакетом нормативов оформления библиотеки.
3. Известные подходы к разработке стандартов
Большинство источников, с которыми нам пришлось столкнуться [1-5], предоставляют просто набор правил, которых следует придерживаться при написании программ. Подробно разбирается, какие проблемы разрешает это правило, когда его можно использовать, а когда нет. Такие книги и статьи являются прекрасным источником для правил, которые можно включать в стандарт кодирования и для этих целей они нами и использовались. К сожалению, в этих работах довольно схематично освещены требования к самим стандартам кодирования.
Например, Майерс [1] пишет, что предлагаемые им правила позволят вам использовать язык программирования (имеется в виду C++) эффективно. Это означает, что полученное программное обеспечение будет ясным (comprehensible), удобным в поддержке (maintainable), расширяемым (extensible), эффективным (efficient), и поведёт себя так, как ожидалось. Здесь мы видим требования к его правилам, но развёрнутой структуризации этих требований в книге нет.
В статье Хоффа [3] есть некоторые интересные соображения по внедрению и, опять же, сами правила.
Книга Макконела [5] посвящена вопросам создания кода. В ней подробно рассматриваются различные аспекты создания модулей, функций, структур данных и других артефактов программы. Обсуждая, что означает хорошо организованный модуль, функция и т. д., автор касается и вопросов организации и оформления кода. В этой работе приводится много соображений и данных исследований, которые могут быть использованы при создании стандартов кодирования. Но и этот автор не предлагает методики создания таких стандартов.
4. Использованная методология разработки стандартов
Разработка авторами стандартов программирования на уровне модуля содержала четыре стадии.
4.1. Постановка задачи
1) Целью работы была поставлена разработка:
a. Технических требований к стандартам кодирования.
b. Стандарта оформления программного модуля (файла) на актуальных для компании языках программирования, удовлетворяющего техническим требованиям.
c. Способа оценки качества результата работы.
2) Затем были разработаны технические требования к правилам оформления модуля:
- Убедительность каждого правила. Любое правило должно выглядеть бесспорным само по себе или в локальном контексте нормативного документа. Это важно для внедрения. Программистам легче соблюдать правила, если они верят в их осмысленность.
- Целесообразность каждого правила. Любое правило должно логически вытекать из какого-либо универсального технологического требования короткой цепочкой промежуточных выводов. Правдоподобные, но реально ненужные правила вредны. Они затрудняют внедрение и снижают экономический эффект от соблюдения стандартов.
- Проверяемость полноты соблюдения универсальных требований. Должно быть ясно, насколько разработанный стандарт, при условии его соблюдения, гарантирует удовлетворение исходных требований. Это важно для оценки качества решения исходной задачи. В ещё большей степени важно, чтобы получившийся продукт – пакет нормативов – сам был развиваемым.
Авторы сознательно не включили в число требований ни дешевизну проверки соблюдения нормативов, ни полноту соблюдения универсальных требований. Мы полагаем, что проверка экспертом возможна всегда, а пожелание автоматизации процедуры проверки никоим образом не должно влиять на отбор конкретных правил. А полное автоматическое соблюдение универсальных требований в результате соответствия стандарту – недостижимый идеал. Реальное же значение имеет возможность проверки степени полноты, обеспечивающее дешевизну эволюции нормативов при появлении в этом необходимости.
3) Были проанализированы исполнявшиеся в нашей компании в 2003 году проекты на предмет использовавшихся языков программирования. По количеству проектов и условным долям использования каждому языку программирования был выставлен условный рейтинг. Получилось: C++ (35); Basic (5); PHP (4); SmallTalk(2); другие по 1 и меньше. Поскольку использование C++ составляет около 75%, была поставлена задача построения нормативов именно для этого языка программирования.
(Ввиду заметной роли языков Basic, PHP и корректности для этих языков значительной части разработанных в процессе работы норм, впоследствии оказалось возможным адаптировать разработанные стандарты и к этим языкам.)
4.2. Проектирование
Авторы разработали конструктивную схему решения, обеспечивающую возможность создания нормативов, удовлетворяющих сформулированным выше техническим требованиям.
Архитектурно конструкция решения представляет собой иерархический граф. Условная вершина – полный набор универсальных требований к продукту. Из неё выходят направленные звенья к каждому универсальному требованию.
В случае, когда какое либо требование представляется в виде прямой суммы независимых целостных требований, от узла графа, соответствующего разложенному на составные части требованию, спускаются звенья к каждому дочернему требованию. В случае, когда требование естественным образом не разлагается на подчинённые требования, это требование считается конечным.
Конечные требования являются корневыми вершинами для набора тезисов. Тезис – это выглядящий бесспорным простой и понятный лозунг. Например, «сокрытие реализации», «запрет дублирования», «полнота класса». Тезисы обязательно должны соответствовать духу вышестоящего конечного требования.
Тезисы, в свою очередь, являются корневыми вершинами для правил. Правило является нормой оформления кода. Любое правило должно соответствовать духу вышестоящего тезиса. Спроектированная нами архитектура решения представлена на рисунке 1.

Рисунок 1.
Такая конструктивная организация нормативов, с нашей точки зрения, даёт возможность обеспечить соблюдение технических требований при разработке конкретных норм наиболее лёгким и естественным способом. В самом деле, убедительность правила следует из убедительности лозунга и соответствия правила лозунгу. И то, и другое, легко оценивается интуитивно. Целесообразность правила верифицируется проверкой подъёмов по графу вверх от правила к универсальному требованию. При этом во многих случаях ярких тезисов проверка связей выше тезиса кажется излишней. И, наконец, степень полноты набора правил проверяется спуском по графу сверху вниз.
Разработанное архитектурное решение имеет ещё одно полезное свойство, которое можно использовать для дополнительной, усиленной проверки полноты набора правил. А именно, это возможность анализа независимости требований. Любое отличие графа-модели от структуры упорядоченного дерева, проявляет частичную взаимозависимость требований – тех вершин, из которых произошёл спуск в одинаковый узел. А это, в свою очередь, вызывается либо частичной корреляцией исходных универсальных требований, либо ошибкой составления модели требований. Оглядка на исходные требования позволяет в этом случае выловить скрытую ошибку модели, если таковая имеет место.
4.3. Сбор и подготовка данных
Следующая стадия разработки состояла в создании обширного набора правдоподобных правил и черновом моделировании графа требований от универсальных к конечным. Разрезав каркас решения по уровню тезисов, мы стремились для каждой половинки этого каркаса пройти максимальный независимый путь.
Для старта мы, во-первых, взяли несколько общеизвестных правил и придумали несколько своих. Во-вторых, не глядя в набранные правила, мы попытались сделать набросок иерархического графа требований. Сравнив затем эти два типа данных, мы получили информацию к размышлению.
Далее наша работа шла по трём направлениям, частично или полностью независимым.
Первое – это набор максимального количества правил. Осуществлялся как методическим поиском среди доступных нам источников, так и придумыванием.
Второе – это оценка каждого из набранных правил. Она складывалась из результатов нашего анализа, консультаций с экспертами и опросов общественного мнения. В этот момент мы ещё и подбирали черновой тезис для каждого правила, поскольку это тоже часть верификации.
Третье – системный анализ требований, предъявляемых к оформлению программного модуля и построение максимально развёрнутой иерархической модели требований.
4.4. Интеграция
Последняя стадия заключалась в согласовании отобранных верифицированных правил и логической модели требований, построенной почти независимо от этих правил.
Сначала каждый блок, состоящий из тезиса и подчинённых правил, подвешивался к наиболее близкому по смыслу конечному требованию. В случае, когда эта близость оказывалась явно слабой, логическая модель подвергалась ревизии вдоль направления от данного конечного требования к вершине модели.
Затем сравнивались степень наполненности тезисами и правилами различных конечных требований. В случае сильных различий данные участки модели тщательно анализировались.
Аналогичные процедуры применялись после этого при движении от конечных требований вверх по графу модели.
Следующая часть согласования модели и правил состояла в последовательном спуске от вершины модели с проверкой на каждом уровне как полноты развёртки вышестоящего требования, так и существенной наполненности текущей вершины низлежащими правилами.
При обнаружении неполноты модель дорабатывалась. При низкой наполненности вершины правилами в первую очередь мы пытались ликвидировать этот недостаток. Для этого мы анализировали созвучные участки модели на предмет неточно соотнесённых к ним правил, а также пытались сами придумать новые правила. Если этого сделать не удавалась, слабо функциональный узел требований ликвидировался. Это осуществлялось слиянием с близкородственным узлом и частичным изменением акцентов в отдельных требованиях. В случаях же, когда наполненность узла правилами была совсем низкой, а пристроить их в другое место не удавалось, мы отказывались от включения данных отдельных правил в финальный набор.
Мы провели серию таких итеративных уточнений с разными комбинациями верифицирующих движений вверх и вниз по графу. Главным критерием прекращения итерационного процесса было появившееся чувство внутреннего удовлетворения, убедительности получившейся модели в наших собственных глазах. Вторым убеждающим нас признаком успешного завершения разработки комплекса нормативов стала проявившаяся синхронно с чувством удовлетворения почти полная ортогональность, взаимная независимость требований внутри модели. Итоговый граф оказалось возможным представить как чисто деревянную структуру, так как тяготение различных вершин графа к альтернативам на вышестоящем уровне оказалось слабым.
Можно добавить ещё, что практически стадия сбора данных и стадия их интеграции были отделены друг от друга всё же не стопроцентно. Какой-то промежуточный обмен между ними был. Но, в грубом приближении, их можно было считать независимыми, что оказалось удобно при изложении методологии.
5. Логическая модель оснований при разработке стандартов
Были выделены основные требования, проявляющиеся на уровне модуля. В нашей модели это:
- Ясность
- Развиваемость
- Поддерживаемость
- Стабильность
Рассмотрим модель решения по каждому из этих основных требований.
5.1. Ясность.
Это требование довольно сложного происхождения. Его природа двойственна в силу частичной корреляции исходных универсальных требований к продукту.
Первое побуждение – рассматривать данное требование как простую проекцию универсального требования простоты использования на уровень модуля. И, в принципе, это абсолютно правильно. Однако в таком проецировании есть важный момент, который легко проглядеть.
Универсальное требование – это не только свойство, которое должно выполняться, но и понимание типа потребителя данного свойства и класса нужд этого потребителя. При проецировании исходного требования к продукту на отдельный уровень продукта сужение исходного требования может происходить во всех его разных качествах. Может сужаться само свойство, а может категория его пользователей. Или тип нужд какой-либо категории пользователей.
В данном случае происходит сужение категории пользователей. Простота использования на уровне модуля важна уже не всем типам пользователей, а только разработчикам продукта. А нужды этого типа пользователей не изменились. Разработчикам нужно обеспечивать развиваемость и поддержку, и ничего больше. Однако эти два требование – исходные универсальные!
Замечательным образом здесь вскрывается частичная корреляция универсальных требований. Их общая часть – ровно та, которая сохранилась от требования простоты использования при проецировании на уровень модуля. А именно, ясность как необходимое условие поддерживаемости и развиваемости.
Отметим ещё, что это единственная обнаруженная нами взаимозависимость исходных требований.
В нашей модели ясность обеспечивается соблюдением трёх локальных требований к оформлению модуля:
a. Структурированность (упорядоченность) модуля. Всё лежит на своих полочках. Модуль представляет собой хорошо убранную квартиру, а не бессистемно набитую кладовку, не свалку разных вещей.
b. Выразительность (наглядность) элементов. Каждый локальный объект сам о себе сообщает явно дополнительную информацию.
c. Документированность. Все мало-мальски интересные явления должны быть описаны.
5.2. Развиваемость.
Программа уже используется, но при этом продолжает активно развиваться. Многие её части переделываются, заменяются, переносятся. Людские ресурсы значительны, можно и горы свернуть, и иногда приходится. Но эти горы не должны быть искусственными. Сократить, насколько возможно, объёмы работ, которые могут потребоваться, ибо эти объёмы значительны – вот смысл этого требования.
В нашей модели развиваемость, не связанная с ясностью, проецирована трёмя локальными требованиями к оформлению модуля:
a. Изолированность. Все логически самостоятельные части программы отделены друг от друга. Соединения между логическими частями минимизированы.
b. Цельность (полнота и монолитность). Любая идейно целостная часть программы является вполне законченным объектом, не рыхлым внутренне. Сделано всё, что положено, и ничего лишнего.
c. Ограниченность. Любая представляющая потенциальный интерес часть не должна быть слишком большой и сложной.
5.3. Поддерживаемость.
Программа давно используется, но практически уже не развивается. Инерционная стадия. Давно забылось, как всё это делалось. А временами надо что-то точечно подправить. Безопасность – прежде всего! Ведь никакой долгой отладки не будет. Программа должны быть устроена так, чтобы любое небольшое разумное действие разумного, но не слишком опытного человека не приводило к катастрофическим последствиям для программы. Поддерживающий – слаб, о нём надо позаботиться заранее, когда сил много.
Нам представляется, что развиваемость, не связанная с ясностью, выражается двумя локальными требованиями к оформлению модуля:
a. Автоконтроль. Сделал человек нечаянно что-то неправильно – немедленно увидел возмущённую реакцию программы.
b. Точная трактовка (естественность, отсутствие ловушек). Человек склонен домысливать больше того, что он реально видит. Ровный ковёр зелёной травы воспринимается как твёрдая поверхность. А под ним, хотя и очень редко, может скрываться трясина. Или специально вырытая и замаскированная яма. Установка растяжек и ловушек оправдана на войне или охоте. А закладывать мины для своих, наказывать наследников за естественные рефлексы – злейшее вредительство. Виноват не тот, кто попал в ловушку, а тот, кто её расставил.
5.4. Стабильность и другие требования.
Три вышеописанных требования в результате отбора подчинённых им наборов тезисов с правилами, оказались наполненными примерно в одинаковой степени. Требование же стабильности оказалось примерно в три раза менее представительным. Поэтому мы решили отказаться от развёртки этого требования в локальные, и привязали к нему тезисы напрямую.
Под стабильностью мы подразумевали, что: поведение программы устойчиво; плавающих ошибок нет; работает одинаково на любой машине, под любой операционной системой и т. п. Разработанная нами логическая модель требований к оформлению модуля представлена на рисунке 2.

Рисунок 2.
Кроме правил, подчинённых четырём выше отмеченным требованиям, среди прошедших наш отбор были единичные правила, отражавшие не вошедшие в число универсальных требования. Это правила, связанные с производительностью программы и с единообразием устройства разных программ, производимых одной корпорацией.
Заключение
В данной статье мы описали методологию, которая позволяет успешно разрабатывать стандарты программирования в различных условиях.
Конкретные особенности фирмы, разрабатывающей программное обеспечение, и специфика производимых продуктов могут изменять акценты в универсальных требованиях и добавлять новые требования. Вариативность исходных требований, специфика используемого языка программирования, вынуждают разрабатывать каждый раз уникальные стандарты, нужные именно для данной фирмы.
Анализ литературы по стандартам программирования привёл нас к выводу, что в теории этого вопроса существует сейчас большой пробел. Мы надеемся, что наше исследование исправляет это положение дел.
Литература
[1] Scott Meyers, 1997. Effective C++, 2nd Edition, Addison-Wesley,
[2] Scott Meyers, 1995, More Effective C++, Addison-Wesley.
[3] Todd Hoff, C++ Coding Standard, http://www. /Cpp/CppCodingStandard. htm
[4] Geotechnical Software Services, C++ Programming Style Guidelines, http://geosoft. no/style. html
[5] Keith Gabryelski, Wildfire C++ Programming Style,
http://www. chris-lott. org/resources/cstyle/Wildfire-C++Style. html
[6] McConnell, Steve. 1993. Code Complete. Redmond, Wash.: Microsoft Press.


