Как заставить неправильный код выглядеть неправильно

From The Joel on Software Translation Project

Jump to: navigation, search

Автор: Джоэл Сполски
Переводчик: Илья Болодурин
В английском оригинале статья называется Making Wrong Code Look Wrong и была написана в среду, 11 мая 2005 г.

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

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

Я спросил: «Здесь всегда так грязно?»

«Что? О чем вы говорите?» — сказал менеджер. «Мы только что закончили уборку. Сейчас здесь самая лучшая чистота за много недель».

Да, парни.

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

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

Примерно так выглядит формовщик

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

После двух месяцев в пекарне вы узнавали, как «видеть» чистоту.

То же самое в программировании.

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

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

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

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

Например, в C:

char* dest, src;

Это абсолютно верный код; он может соответствовать вашему соглашению о кодировании, и это даже может правильно делать то, для чего оно предназначено, но если вы имеете достаточно опыта работы на C, вы заметите, что здесь dest объявляется как указатель на char, в то время как src объявляется просто как char, и хотя возможно, что вы именно этого и хотели, но скорее всего это не так. Этот код выглядит немного грязновато.

Еще более тонкий случай:

if (i != 0)
    foo(i);

В этом случае код правилен на 100%; это соответствует большинству соглашений о кодировании и в этом нет ничего неправильного, но тот факт, что тело оператора if, содержащего одну строку, не помещено в фигурные скобки, может заставить вас подумать где-то в подсознании, что, черт возьми, кто-нибудь может добавить туда еще одну строку кода

if (i != 0)
    bar(i);
    foo(i);

... и забудет добавить фигурные скобки, и таким образом случайно сделает foo(i) выполняющимся безусловно! Итак, когда вы видите блоки кода, не заключенные в фигурные скобки, вы можете ощутить только крошечное, микроскопическое подозрение в нечистоплотности, которое может вас беспокоить.

Хорошо, пока я упомянул три уровня квалификации программиста:

  1. Вы не можете отличить чистого от грязного.
  2. У вас есть поверхностное суждение относительно чистоты, главным образом на уровне соответствия соглашениям о кодировании.
  3. Вы начинаете улавливать тонкие намеки на нечистоплотность, лежащих где-то в глубине, и они раздражают вас достаточно сильно для того, чтобы вы исправили код.

Есть еще один, более высокий уровень, который, тем не менее, и является тем, о чем я действительно хочу поговорить:

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

Это настоящее искусство: создавать здравый код, буквально на ходу изобретая соглашения, которые заставляют ошибки выделиться на экране.

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

Но если вы так сильно убеждены, что Венгерская Нотация это Плохая Штука и что обработка исключений является лучшим изобретением человечества после шоколадного молочного коктейля, и вы даже не хотите слышать любых других мнений, ну, в общем, можете вместо этого посмотреть классные комиксы; скорее всего, вы всё равно ничего не потеряете; буквально через минуту я собираюсь разбирать реальные образцы кода, которые, вероятно, вгонят вас в сон даже раньше, чем вы начнете чувствовать негодование. Да. Я думаю, что мой план будет состоять в том, чтобы убаюкать вас так, чтобы вы почти полностью заснули, и затем внушить вам такие вещи, как Венгерская Нотация=хорошо, Исключения=плохо, воспользовавшись тем, что вы сонны и не можете реально сопротивляться.

Contents

Пример

Где-то в Умбрии

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

Теперь, есть уязвимость в безопасности, называемая Cross Site Scripting Vulnerability (Уязвимость Межстраничных Скриптов), или XSS. Я не буду здесь вдаваться в детали: все, что вы должны знать — это что, когда вы строите сетевое приложение, вы должны проявлять осторожность и никогда не передавать обратно никаких строк, введенных пользователем в формы.

Так, например, если у вас есть веб-страница, которая спрашивает «Как вас зовут?» в полем ввода, и затем эта страница пересылает вас на другую страницу, которая говорит «Привет, Элмер!» (если введенное имя пользователя Элмер), ну, в общем, это опасная уязвимость, потому что пользователь мог ввести везде вместо «Элмер» любой HTML или JavaScript-код, и этот невероятный код JavaScript может сделать какую-нибудь гадость, и теперь эти гадости, похоже, будут исходить от вас, так, например, они могут прочитать ваши куки (cookie) и информацию из них послать на злой сайт доктора Зло.

Давайте представим это в псевдокоде. Представьте что

s = Request("name")

читает вводимое значение (аргумент POST) из формы HTML. Если вы когда-нибудь напишите этот код:

Write "Hello, " & Request("name")

то ваш сайт будет уязвим для атак XSS. Это все, что для них нужно.

Вместо этого вы должны перекодировать строку прежде, чем вы пошлете ее обратно в HTML. Перекодирование обозначает замену " на ", замену > на >, и т.д. Итак, код

Write "Hello, " & Encode(Request("name"))

является совершенно безопасным.

Все строки, которые приходят от пользователя, опасны. Любая опасная строка не должна быть выведена без перекодирования.

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

Возможное Решение #1

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

s = Encode(Request("name"))

Таким образом наше соглашение говорит следующее: если вы когда-либо увидите Request, который не окружен Encode, то код неправильный.

Вы начинаете учить свои глаза искать голые Request-ы, потому что они нарушают соглашение.

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

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

Хорошо. Попробую еще раз.

Возможное Решение #2

Что, если мы сделаем соглашение о кодировании, которое гласит, что когда вы выводите какую-то строку, вы должны ее перекодировать?

s = Request("name")
// намного позже:
Write Encode(s)

Теперь всякий раз, когда вы видите голый Write без Encode, вы знаете, что что-то неправильно.

Хорошо, но это тоже работает довольно плохо: иногда вам нужно поместить небольшие фрагменты кода HTML вокруг вашей строки, и вы не можете их перекодировать:

If mode = "linebreak" Then prefix = "<br>"
// намного позже:
Write prefix

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

Write Encode(prefix)

Но теперь "<br>", которое должно начать новую строку, будет перекодировано в &lt;br&gt; и будет выведено пользователю как символы < b r >. Но это неправильно.

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

s = Request("name")
...на несколько страниц ниже...
name = s
...на несколько страниц ниже...
recordset("name") = name // сохранить имя в db в поле "name"
...через несколько дней...
theName = recordset("name")
...на несколько страниц или даже на месяц позже...
Write theName

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

Правильное Решение

Теперь позвольте мне предложить соглашение о кодировании, которое работает. У нас будет только одно правило:

Все строки, которые приходят от пользователя, должны быть сохранены в переменных (или полях базы данных) с названием, начинающимся с приставки "us" (для Опасных (UnSafe) Строк). Все строки, которые были перекодированы для HTML или которые прибыли из заведомо-безопасного места, должны быть сохранены в переменных с названием, начинающимся с приставки "s" (для Безопасных (Safe) строк).

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

us = Request("name")
...на несколько страниц ниже...
usName = us
...на несколько страниц ниже...
recordset("name") = usName // сохранить имя в db в поле "name"
...через несколько дней...
sName = Encode(recordset("name"))
...на несколько страниц или даже на месяц позже...
Write sName

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

s = Request("name")

является заведомо неправильным, потому что вы видите результат Request, который присваивается переменной, название которой начинается с s, и это противоречит правилам. Результат Request всегда опасен, поэтому он должен всегда присваиваться переменной, название которой начинается "us".

us = Request("name")

всегда правильно.

usName = us

всегда правильно.

sName = us

абсолютно неверно.

sName = Encode(us)

конечно правильно.

Write usName

абсолютно неверно.

Write sName

конечно правильно, так же как

Write Encode(usName)

Каждая строка кода может быть просмотрена отдельно, и если каждая строка кода правильна, весь объем кода верен.

В конечном счете, с этим соглашением о кодировании, ваши глаза учатся замечать Write usXXX и видеть, что это неправильно, и вы также сразу же понимаете, как это исправить. Я знаю, что в начале немного трудно замечать неправильный код, но если делать это в течение трех недель, то ваши глаза научатся это видеть, точно так же как рабочие пекарни научились смотреть на гигантскую фабрику хлеба и немедленно говорить, "боже, никто не вычистил изнутри формовщик! Что за ленивые коровы этим занимались, а?"

Фактически мы можем немного расширить правило и переименовать (или обернуть) функции Request и Encode, чтобы они стали UsRequest и SEncode... другими словами, имена функций, которые возвращают опасные или безопасные строки, будут начинаться с Us и S, точно так же как и переменные. Теперь взгляните на код:

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SEncode(recordset("usName"))
Write sName

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

us = UsRequest("name") // порядок, с обоих сторон начинается с US
s = UsRequest("name") // ошибка
usName = us // порядок
sName = us // конечно неправильно
sName = SEncode(us) // конечно правильно

Хей, я могу пойти еще на один шаг дальше, переименовав Write в WriteS и SEncode в SFromUs:

us = UsRequest("name")
usName = us
recordset("usName") = usName
sName = SFromUs(recordset("usName"))
WriteS sName

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

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

Примечание переводчика (программиста): Более правильно будет автоматизировать процесс выявления подобных ошибок, либо вообще исключить возможность их появления. То есть создать типы safe_string и unsafe_string с операцией преобразования одного типа в другой. Функция Request должна возвращать тип unsafe_string, а функция Write должна получать тип safe_string. Таким образом компилятор (интерпертатор) гарантирует отсутствие ошибок использования непроверенных данных, и никакой "венгерской нотации" не понадобиться. Она нужна там, где средства языка не позволяют использовать достаточно чёткую типизацию - например, в VB, Си; в языке С++ возможно написание кода без использования венгерской нотации и без головной боли с проверкой кода вручную - проверка производится автоматически.

Более того, возможно и автоматическое преобразование типа unsafe_string в тип safe_string, что вообще снимает рассматриваемую проблему. Пример С++ кода, описывающего высказанную здесь идею:

class unsafe_string : public string
{
public:
unsafe_string(string & s) : string(s) {};
};

class safe_string : public string
{
public:
safe_string(string & s) : string(s) {};
safe_string & operator=(unsafe_string & src)
{
// Здесь декодируем входящую строку
...
return *this;
}
};
safe_string a;
unsafe_string b = "UNSAFE";
a = b; // Всё нормально
Конечно, Венгерская нотация имеет смысл для обозначения "вида" переменной, но для проверки использования лучше применять автоматический контроль, каки показано выше.

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

Общее Правило

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

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

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

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

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

i = j * 5;

... в C, вы, по крайней мере, знаете, что j умножается на пять и результаты сохраняются в i.

Но если вы видите тот же самый отрывок кода в C++, вы не можете сказать о нем ничего. Абсолютно ничего. Единственный способ узнать в C++, что же здесь действительно происходит, состоит в том, чтобы узнать, какие типы у i и j, что-то, что могло быть объявлено где-нибудь в другом месте. Это нужно потому что, в зависимости от типа j, перегруженный оператор * может сделать что-нибудь ужасно остроумное, когда вы пытаетесь выполнить простое умножение. Да и оператор = возле i тоже может быть перегруженным, типы могут быть несовместимыми, и в результате может быть автоматически вызвана функция приведения типов. И единственным способом разобраться во всем этом является не просто проверка типов, но и поиск кода, реализующего именно эти типы, и Бог вам в помощь, если вам попадется наследование, потому что теперь вы должны будете тащиться по всей иерархии класса, пытаясь найти, где все-таки на самом деле находится нужный вам код, а если где-нибудь есть полиморфизм, то вы действительно попали в беду, потому что тогда недостаточно знать, с какими типами объявлены i и j, надо точно знать, какие типы у них именно сейчас, и это может повлечь просмотр неизвестного количества кода, и вы никогда полностью не убедитесь в том, что полностью разобрались в какой-то проблеме (фу!).

Когда вы видите в C++ код i=j*5, вы можете рассчитывать только на себя, парни, и это, по-моему, уменьшает возможность обнаружить возможные проблемы, просто глядя на код.

Но, конечно, ничто из этого не должно иметь никакого значения. Когда вы делаете всякие хитроумные прикольные штуки, такие как перегрузка оператора *, это просто необходимо для того, чтобы помочь вам обеспечить хорошую надёжную абстракцию. Черт возьми, j имеет тип Unicode String, и умножение строки Unicode на целое число -- несомненно прекрасная абстракция для преобразования Традиционного Китайского в Стандартный Китайский, не так ли?

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

Скотт Мейерс (Scott Meyers) сделал целую карьеру, показывая вам все те способы, которые вас подводят и кусают, по крайней мере в C++. (Между прочим, только что вышло третье издание книги Эффективный C++ (Effective C++) Скотта; она полностью переписана; закажите себе экземпляр прямо сегодня!) Хорошо. Я ухожу в сторону. Лучше я подытожу Всё, Сказанное Выше:

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

Я Венгр

Лугнано, Умбрия, Италия

Итак, теперь мы возвращаемся к пресловутой Венгерской Нотации.

Венгерская Нотация была изобретена программистом Microsoft Чарльзом Симони (Charles Simonyi). Одним из главных проектов, над которым работал Симони в Microsoft, был Word; фактически он вел проект по созданию первого в мире текстового редактора, работающего в режиме WYSIWYG, того, что в Xerox Parc называли Bravo.

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

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

Пример

Я использую здесь слово вид (kind) специально, потому что Симони по ошибке использовал в своей статье слово тип (type), и целые поколения программистов неправильно поняли то, что он подразумевал.

Если вы читали статью Симони внимательно, то вы обратили бы внимание, что он использовал соглашение о именах переменных, подобное тому, которое я использовал в своем примере выше, где мы решили, что us обозначает "опасную строку", а s обозначает "безопасную строку." Обе эти переменные типа string. Компилятор не поможет вам, если вы замените один на другой, и Intellisense не выдаст никаких тревожных сообщений. Но семантически они отличны; они должны интерпретироваться и рассматриваться по-разному, и если вы присваиваете значение переменной одного типа переменной другого типа, то обязательно должна вызываться некая функция для конвертации, или у вас возникнет ошибка во время выполнения. Если вам повезет.

Оригинальная концепция Венгерской Нотации Симони была названа в Microsoft Венгерской для Приложений (Apps Hungarian), потому что использовалась она в Подразделении Приложений (Applications Division), которое, в частности, разрабатывало Word и Excel. В исходном тексте Excel вы увидите много rw и col, и когда вы их увидите, вы сразу поймете, что они относятся к строкам и колонкам. Да, они оба целые, но никогда не имеет смысла присваивать значение одного другому. В Word, как мне говорили, вы увидите много xl и xw, где xl означает «горизонтальная координата относительно листа», и xw означает «горизонтальная координата относительно окна». Оба целые. Не взаимозаменяемые. В обоих приложениях вы увидите много cb, означающего "счетчик байтов." Да, это тоже целое, но вы можете узнать об этом много больше, только посмотрев на имя переменной. Это счетчик байтов: размер буфера. И если вы видите xl = cb, ну, в общем, слышите Свисток Плохого Кода, который говорит, что здесь код несомненно является неправильным, потому что даже при том, что и xl, и cb являются целыми, полный идиотизм присваивать размер буфера горизонтальной координате.

В Венгерской для Приложений префиксы используются для функций так же, как и для переменных. Так, говоря по правде, я никогда не видел исходного текста Word, но я готов держать пари с вами на доллар против цента, что там есть функция по имени YlFromYw, которая делает преобразование из вертикальных координат относительно окна в вертикальные координаты относительно листа. Венгерская для Приложений требует вместо более привычного TypeToType использовать TypeFromType так, чтобы название каждой функции могло начаться с того типа вещей, которую она возвращает, точно так же, как я сделал это ранее в примере, когда я переименовал функцию Encode в SFromUs. Фактически по правилам Венгерского для Приложений функция Encode должна была быть названа именно SFromUs. Венгерская для Приложений действительно не давала бы вам никакого выбора в том, как называть эту функцию. И это хорошо, потому что вам приходится помнить на одну вещь меньше, и вам не придется задаться вопросом, что обозначает слово Encode: у вас есть нечто намного более определённое.

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

Но затем случилось нечто ужасное.

Наступили черные дни для Венгерской Нотации.

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

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

Венгерская для приложений была очень полезна, были определены конкретные приставки, такие как "ix" для обозначения индекса в массиве, "c" для счетчиков, "d" для разницы между двумя числами (например, "dx" означал "ширину"), и т.д.

Системная Венгерская имела намного менее полезные приставки, такие как "l" для длинного целого (long), "ul" для беззнакового длинного целого (unsigned long), и "dw" для двойного слова (double word), которое, фактически, мм, является беззнаковым длинным целым. В Системной Венгерской единственной вещью, о которой говорил вам префикс, фактически был только тип данной переменной.

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

Системную Венгерскую широко распространили; она стала повсеместным стандартом для документации Windows для программистов; она была широко разрекламирован в книгах, таких как [http://www.charlespetzold.com/pw5/index.html Программирование в Windows Чарльза Петзолда (Charles Petzold's Programming Windows)], настоящей библии для изучающих программирование в Windows, и она быстро стало доминирующей формой Венгерской Нотации, даже в Microsoft, где только очень немногие программисты, не входящие в команды Word и Excel поняли, какая ошибка была сделана.

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

Пик Большого Восстания совпал с первым выпуском .NET. Microsoft наконец начала говорить людям, что «Венгерская Нотация Не Рекомендуется». Была большая радость. Я не думаю, что они даже потрудились объяснить почему. Они просто открыли раздел руководства с принципами именований и написали «Не Используйте Венгерскую Нотацию» во всех статьях. К этому моменту Венгерская Нотация была так чёртовски непопулярна, что никто не стал жаловался, и все в мире вне комманд Excel и Word только облегчённо вздохнули, что больше нет необходимости использовать неуклюжее соглашение об именах, которое, как все думали, было не нужно в эпоху жесткой проверки типа и Intellisense.

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

Прежде, чем мы пойдем дальше, я должен сделать одну вещь, которую я обещал сделать: нанести удар по обработке исключений (exceptions). В последний раз, когда я это сделал, я попал в большую неприятность. В неподготовленном замечании на домашней странице Joel on Software я написал, что не люблю исключения, потому что они, фактически, являются невидимым goto, а это, как я рассуждал, еще хуже, чем явный goto. Конечно, миллионы людей заткнули мне глотку. Единственным человеком в мире, который ко мне присоединился, был, конечно, Раймонд Чен (Raymond Chen), который, между прочим, лучший программист в мире, и это о чём-то говорит, не правда ли?

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

dosomething();
cleanup();

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

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

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

Дополнительная Литература

Если вы все еще остаетесь фанатичным поклонником исключений, прочитайте эссе Раймонда Чена Чище, элегантней и сложней для понимания (Cleaner, more elegant, and harder to recognize) и Чище, элегантней, но неправильно (Cleaner, more elegant, and wrong). «Необычно трудно увидеть различие между плохим кодом, основанным на исключениях, и не-плохим кодом, основанным на исключениях... исключения слишком сложны, и я не достаточно умен, чтобы с ними работать».

Разглагольствования Раймонда о Смерти от Макросов, Речь против макросов управления исполнением программы, описывает другой случай, когда невозможность получить всю информацию в одном и том же месте делает код невозможным для поддержки. «Когда вы видите код, который использует [макрос], вы должны просмотреть заголовочные файлы для того, чтобы понять, как это работает».

Для понимания истории Венгерской Нотации начните с оригинальной статьи Симони Венгерская Нотация. Дуг Кландер (Doug Klunder) [1] объяснил ее команде Excel в несколько более ясной статье. Для того, чтобы узнать больше историй о Венгерском и о том, как он был разрушен авторами документации, прочитайте письмо Ларри Остермана (Larry Osterman), а особенно комментарий Скотта Людвига (Scott Ludwig), или сообщение Рика Шота (Rick Schaut).

---

Personal tools