Стандарты кодирования

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

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

Поэтому стандарты кодирования отражают частично мотивацию картостроителей, частично мотивацию паковщиков, и в результате цели создателей стандартов перепутываются. Проявляется коммуникационный барьер между картостроителями и паковщиками, поэтому картостроители рвут на себе волосы, поскольку паковщики растирают сложность, пока она не станет невидимой на каждой отдельной странице, цитируя Пакет Знаний 47684 (Ясность Это Хорошо) и в то же самое время вытирая ясность из работы.

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

Сначала о ясности. Есть идея, что использование синтаксического богатства языка -- это Плохая Вещь, поскольку это "сложно". Ни когда составной синтаксис образует идиому. Ни даже когда эта идиома вводится и обсуждается в документации. И вообще не нужна ни при каких условиях. Когда Ньютон писал "Принципы" (Newton's Principia), то написал их словами, хотя мог использовать алгебраические символы, поскольку был сооткрывателем флюксий, или исчислений. В наше время мы решили, что в математике лучше иметь дело с алгебраической нотацией даже при описании. Сейчас то, что потребовало бы нескольких страниц текста, в алгебраической нотации займет всего страницу, и хотя при этом скорость чтения страницы снижается, общее время чтения стопки текста больше, поскольку в стопке текста гораздо труднее проследить мысль. Итак, следует ли нашим программам быть более похожими на прозу по концентрации сложности на страницу или на выражение, либо им следует быть ближе к математике? Мы предполагаем, что если человек -- новичок в языке, то лучше, если он сможет отчетливо увидеть всю структуру и тратить на каждую страницу несколько минут, чем читать каждую идиотскую строчку и не понимать, для чего это написано! Как часто нам приходится видеть напряженных людей, сидящих перед стопкой кода, не имеющих понятия, с чего начать, и думающих, что это их вина.

Второй момент -- соглашения. Прежде чем принять соглашение, убедитесь в том, что оно даст больше, чем будет стоить. Мы приводили ранее пример, в котором если кто-то не знает назначение переменной, то он ничего не получит от знания ее типа [тут идет речь о венгерской записи - С.К.]. Но это не просто вклад в перегрузку мозга соглашениями, которые, как предполагается, "хорошие" программисты должны хранить в виде пакетов знаний, что само по себе проблема. Дело еще и в том, что наличие слишком большого количества соглашений делает код некрасивым и даже уродливым. Если стремиться к красоте минимального кода, то этого сложнее достичь постоянно натыкаясь на обвешанных мусором уродцев типа gzw_upSaDaisies. Никогда не делайте того, что подавляет стремление команды создать великий продукт. Было место, где думали, что соглашения -- Хорошая Вещь. Несколько людей были назначены Создавать Правила, и они делали это должным образом. Один из них объявил, что длина имен переменных должна ограничиваться 31 символом. Очень разумно -- многие компиляторы могут различать имена не длиннее указанного. Другой объявил, что переменные, описанные в подсистеме, должны начинаться с трехбуквенного альфа-кода подсистемы. Любая переменная, даже если никто не использует ее глобально. (Еще один предложил помечать глобальные переменные.) Еще один произвел причудливую схему типов, параллельную собственным составным типам языка, и потребовал включения информации об этих типах в имена. Зачем -- мы не знаем. Еще один опубликовал список сокращений имен модулей кода, известных менеджеру конфигурации и сказал, что это тоже необходимо включить в каждое имя переменной. И т.д. Наступило настоящее веселье, когда оказалось, что все эти обязательные штучки плюс пометка типа "указатель на функцию, возвращающую указатель на запись" превысили 31 символ. Создатели Законов просто сказали, эта конструкция очень сложна и ее не следует использовать, хотя это было главным для архитектуры, и топтались вокруг описывания промежуточных переменных и приведения типов во время присваивания им значений, что совершенно не помогало ни ясности ни эффективности. Поэтому, наконец, пузырь лопнул и мы разработали некоторые прагматические стандарты, которые хорошо выглядели и говорили нам, как с помощью словаря проекта быстро образовать имя и некоторые разумные сокращения. Если посмотреть с точки зрения картостроитель/паковщик, мы видим, что ситуация развивалась подобным образом потому, что Создатели Законов были Объявителями Полезных Правил, что Хорошая Вещь, но цена, цена...

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

Четвертый момент -- об этих императивах. Есть некоторые средства, которые вообще не стоило изобретать, например scanf() и gets()в UNIX. Указания никогда не использовать их в разрабатываемом коде разумны. Но есть цели, которых просто невозможно безопасно достичь другим, лучшим путем. И всегда остается проблема баланса ясности. Мы рассмотрим два конкретных примера, из которых станет видно, что в Си все же существует хороший повод для использования goto -- хотя вам могли говорить, что таких поводов нет.

Вот первый, здесь нет другого способа. Представьте рекурсивный обход двоичного дерева:

void Walk(NODE *Node)
{
// Do whatever we came here to do...


// Shall we recurse left? if(Node->Left) Walk(Node->Left);

// Shall we recurse right? if(Node->Right) Walk(Node->Right); }

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

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

void Walk(NODE *Node)
{
Label:// Do whatever we came here to do...


// Shall we recurse left? if(Node->Left) Walk(Node->Left);

// Shall we recurse right? if(Node->Right) { // Tail recursion elimination used for efficiency Node = Node->Right; goto Label; } }

Мы используем стек для отслеживания того, куда мы попали слева, но после того, как мы обошли левую часть, переход на правую ветвь не требует хранения положения. Поэтому мы устраняем 50% вызовов и накладные расходы на возвраты. Это может изменить ситуацию при выборе между реализацией на Си или добавлением ассемблерного модуля. Чтобы увидеть другой пример, посмотрите на конструкцию Даффа (Duff's Device) в книге Страуструпа "Язык программирования C++" (Stroustrup's The C++ Programming Language).

Второй пример касается чистоты стиля -- вообще нет необходимости применять язык ассемблера. Помните, что когда Дейкстра посчитал goto вредным, он имел в виду привычку использовать goto для организации управления в неструктурированном коде 60-х годов. Идея заключалась в том, что меньше используя goto мы могли бы улучшить ясность. Идея не состояла в жертвовании ясностью избегая goto любой ценой. Представьте программу, которой нужно открыть порт, инициализировать его, инициализировать модем, установить соединение, зарегистрироваться (logon) и загрузить файл (download). Если что-то не так, в любом месте, нам нужно вернуться обратно в самое начало. Доморощенный структуралист мог бы написать нечто вроде:


BOOL Done = FALSE;


while(!Done) { if(OpenPort()) { if(InitPort()) { if(InitModem()) { if(SetupConnection()) { if(Logon()) { if(Fetch()) { Done = TRUE; // Ouch! Hit the right hand side! } } } } } } }

Что нам кажется просто глупым. Есть более понятная альтернатива, использующая то, что оператор && прекращается сразу, как только встречается выражение, принимающее значение FALSE -- "неправильное использование" языка, обычно запрещаемое в большинстве стандартов кодирования:


while(!(OpenPort()&&
InitPort()&&
InitModem()&&
SetupConnection()&&
Logon()&&
Fetch()));

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


Start: if(!OpenPort())goto Start;
if(!InitPort())goto Start;
if(!InitModem())goto Start;
if(!SetupConnection())goto Start;
if(!Logon())goto Start;
if(!Fetch())goto Start;

Это в точности то, что позволяют нам делать специализированные скриптовые языки, разработанные для такого вида работ!

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