воскресенье, 18 декабря 2011 г.

[prog.memories] О том, как я от CamelCase к lower_case пришел

Когда я только начинал программировать – а это был 1990-й год – особых заморочек по поводу оформления кода не было. Да и смысла это не имело, поскольку сначала был какой-то Basic на БК1001, затем Turbo Pascal 3.0 на Robotron 1715. Насколько я помню, Basic сам все приводил к верхнему регистру. А на Robotron-е, кажется из-за шаманства с добавлением русских символов в кодовую таблицу, строчных латинских букв не было вообще. Поэтому первые программы писались только в UPPERCASE и других вариантов не было вообще.

Чуть позже с Robotron-ов пересел на IBM PC, на Turbo Pascal 5.0. И хоть Pascal не был чувствителен к регистру, программы хотелось оформлять красиво (т.е. так как в книжках и фирменных Borland-овских примерах), поэтому как-то само собой произошел переход к CamelCase. Деталей уже не помню, но вряд ли я писал If, Begin и End. Скорее if, begin и end. А вот функции и переменные точно писал в CamelCase. Даже одно время стандартные функции писал так WriteLn, ReadLn. Но не долго, намного проще было по старинке – writeln, readln.

Там же, на IBM PC к концу первого курса (а это была середина 1991) довелось познакомиться с языком C. После привычного, быстрого и безопасного Pascal язык C казался чем-то жутко сложным, да и изрядно тормознутым (в смысле скорости компиляции и линковки). Да и принятый в нем стиль lower_case вызывал двойственные чувства. Но C – это было намного круче Pascal (просто круче, без подробностей), поэтому изучал я его старательно.

В итоге, когда в самом начале второго курса я написал свою самую большую программу на Pascal (объемом в несколько тысяч строк), то я использовал уже lower_case. Т.е. в Pascal-евской программе все структуры, функции и переменные именовались строчными буквами, а разделителями в именах были подчеркивания. И таким стилем я пользовался затем около года, программируя уже на C, ассемблере x86 и потихоньку изучая C++.

Но где-то в 1992-м мне в руки попал двухтомник по программированию под Windows 3.0 за авторством А.В.Фролова и А.Г. Фролова. Оттуда я узнал по венгерскую нотацию :)

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

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

  • неудобство сопровождения кода. Со временем типы переменных менялись. Если раньше nPercentage была целочисленной, то со временем ее требовалось сделать вещественной и приходилось переименовывать ее в fPercentage. Соответственно, изменяя все места ее использования. Сейчас, при наличии IDE с мощными средствами рефакторинга в это сложно поверить, но тогда переименовать какую-то сущность в большом количестве исходников было не просто :)
  • маразм, до которого стали доходить в формировании префиксов. Когда венгерка ограничивалась префиксами i, n, f, l, p, pc, sz и еще парочкой таких же простых сочетаний, это было вполне удобно. Но когда приходилось писать что-то вроде ppcsz, pppi и пр., закрадывались сомнения в разумности происходящего :) Апогеем же стала как-то попавшая ко мне в руки книжка по Visual Basic-у, в которой перечислению “стандартных” префиксов была посвящена здоровенная таблица на десяток страниц.

Были и другие мелкие и не очень недостатки, но они свойственны не столько венгерской нотации, сколько стилю CamelCase вообще. А к стилю CamelCase я вернулся одновременно с переходом на венгерскую нотацию. И использовал его намного дольше, где-то до 2001 года.

Тут нужно чуть подробнее остановиться на особенностях CamelCase применительно к объектно-ориентированным языкам. В процедурных языках, вроде C или Pascal, все довольно просто. Но в том же C++ добавляются еще и классы, их атрибуты и методы. Как-то само собой получалось, что нужно уметь их выделять визуально среди остальных сущностей. Поэтому были разные варианты CamelCase, в которых существовали более-менее серьезные различия.

Нужно сказать, что упомянутая мной книга Фроловых о Windows 3.0 кроме приобщения к венгерской нотации, донесла до меня (и моих коллег) очень важную вещь – в отдельно взятом коллективе все должны использовать один и тот же стиль оформления кода. До этого я об этом даже не задумывался, а затем стал твердым приверженцем этой точки зрения, хотя и без фанатизма.

Так вот, для того, чтобы поддерживать единообразие кода в тех коллективах, где я работал в 1994-1996 годах, был выработан некий вариант стиля CamelCase, которым я пользовался до 2001 (и который, возможно, живет там до сих пор). Основными его чертами были:

  • префикс T для имен типов. Т.е. TFile, TByteStream и т.д.;
  • префикс m для имен атрибутов. Т.е. TFile::mHandle, TByteStream::mSize, TByteStream::mCapacity и т.д.;
  • именование методов со строчной буквы: TFile::open(), TByteStream::writeBlock() и т.д.;
  • именование свободных функций с прописной буквы: OpenFile, CreateByteStream и т.д.

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

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

file file(…);

Поскольку компилятор даст по рукам. А вот так запросто:

TFile file(…);

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

TPoint(int x, int y) : x(x), y(y) {}

нужно придумывать какие-то новые имена для параметров конструктора. Причем чем проще ситуация (как в примере с TPoint) тем сложнее выдумать что-то отличное от x и y. А вот с префиксами для атрибутов нет никаких заморочек:

TPoint(int x, int y) : m_x(x), m_y(y) {}

С атрибутами есть еще одна штука. В теле метода очень хочется видеть где ты изменяешь атрибут, а где локальную переменную. И префиксы для атрибутов в этом так же помогают. Я знаю, что многие для этих целей используют конструкции вида this->x или this.x, но я ленивый. Мне проще набрать m_x, чем this->x ;) Кроме того, если придется запрограммировать какое-нибудь математическое выражение, то с именами m_x, m_y оно еще будет читаться, а вот с this->x и this->y уже вряд ли.

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

Поскольку я стал пользоваться lower_case, то претерпели изменения четыре упомянутых выше принципа:

  • типы имеют суффикс _t вместо префикса T;
  • атрибуты имеют префикс m_ вместо m (поскольку если раньше mX читалось нормально, то сейчас mx уже совсем не то, в отличие от m_x);
  • имена методов и свободных функций записываются строчными буквами и не отличаются.

Нужно сказать, что поначалу я еще пытался делать разные правила для разных ситуаций. Например, пытался применять подчеркивание в конце имени свободной функции (т.е. create_file_ вместо create_file), но это оказалась нежизнеспособная идея. Так же использовал разные префиксы для разных сущностей: m_ для атрибутов, g_ для глобальных переменных, c_ для констант, e_ для перечислений. Но со временем выжили только m_ для атрибутов и g_ для глобальных переменных (да и то не всегда).

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

Главное при этом выдерживать единый стиль. Можно легко разобраться в коде, где человек именует классы, например, SMTP_session_t или SMPP_parser_t (вместо более привычного тебе smtp_session_t или smpp_parser_t). И придерживается этого стиля во всем своем коде. Но вот когда в начале заголовочного файла дается имя SMPP_paser_t, а следом за ним идет smpp_parser_state_t – вот это уже плохо. Т.к. заставляет искать разумную причину разных подходов к именам. Даже если ее нет :)

Напоследок еще несколько лирических отступлений. Самые удобные стандартные стили кодирования я видел в Eiffel и Ruby. Но, если говорить об Eiffel, то там язык не столь богат на возможности, как C++ – в нем есть только классы, методы и атрибуты. А вот в Ruby используется зашитая в синтаксис языка система префиксов для глобальных переменных, атрибутов классов и статических атрибутов классов (если пользоваться терминологией С++). Что мне представляется пока и разумным, и удобным. Понятное дело, что поскольку мне не нравится Java, то мне не нравится и ее стиль оформления кода. Ну а C# – это та же Java, но немного другая ;)

Если же говорить об отступах и фигурных скобках, то я перепробовал разные их сочетания:

if(...)
{
}

if(...) {

}

if(...)
  {
  }

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

if(...)
  {
  }
else
  {
  }

чем вот такой:

if(...)
{
}
else
{
}

Но, полагаю, это дело личных предпочтений.

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

if(0==Hf&&0==Mf)return 24;

если можно записать его вот так:

if( 0 == Hf && 0 == Mf )
   return 24;

В заключении пару ссылок на самого себя:

7 комментариев:

  1. Давно хотел спросить - в чем вы код для блога оформляете? В этом посте не так, но у вас обычно код на синем фоне

    ОтветитьУдалить
  2. Кстати, вот так вполне напишешь:

    struct TPoint
    {
    int x, y;
    TPoint(int x, int y) : x(x), y(y) {}
    }

    ОтветитьУдалить
  3. @Eugeniy

    да, напишешь

    я писал об этом чуть раньше, и теперь шаг 2 -- проблемы

    проблемы могут быть, но префикс m_ от них не спасет

    предположим, что this->y не может быть больше х, и тогда хочется написать

    TPoint(int x, int y) : x(x), y(std::min(x,y)) {}

    а затем после эволюции:

    TPoint(int x, int y) : x(x), y(std::min(x,y)) { call_global_f(x,y); } <----- какой y???

    решение -- не допускать таких совпадений имен, и писать либо

    TPoint(int x, int wishful_y) : x(x), y(std::min(x,wishful_y)) {}

    либо иногда еще лучше

    TPoint(int x, int wishful_y) : x(x), cropped_y(std::min(x,wishful_y)) {}

    то есть длинные, говорящие префиксы вместо m_

    ОтветитьУдалить
  4. @Евгений Охотников

    насчет CamelCase vs. camel_case_t -- по-моему, хорошего решения просто нет, все немножко плохи

    есть еще навроде ады Camel_Case -- вроде и хорошо, но выглядит непривычно, что тоже плохо

    нет в мире щастья, короче :-)

    ОтветитьУдалить
  5. добавлю насчет Camel_Case: моя нелюбовь к большим буквам доходит до того, что я и на русском пишу без них, как многие могли заметить

    а различать типы и значения как-то надо... вот и крутись :-)

    ОтветитьУдалить
  6. уточню свою мысль

    регистр символов и суффикс _t можно считать не очень важными вопросами эстетики и стиля

    но вот наличие двух разных переменных x и m_x с одним и тем же значением это уже существенно плохо

    более того, тревожно даже наличие двух переменных right и range.right -- с чего и начался этот разговор (однако, наличие двух переменных range и corrected_range одного типа и следовательно с одинаковыми полями, очевидно, нормально)

    в случае

    struct S {
    const double m_start;
    const double m_end;
    S(double middle, double length): m_start(middle-length/2), m_end(middle+length/2) {}
    }

    префикс m_ всего лишь это вопрос стиля, а вот в случае x и m_x это уже существенно хуже

    одна сущность должна называться одинаково, а разные сущности должны называться достаточно (существенно?) по-разному

    префикс m_ не дает однозначного ответа на вопрос -- имеют ли право переменные x и m_x принимать разные значения, или это всего лишь разные обозначения одного и того же?

    ОтветитьУдалить
  7. 2 имя
    >>> а затем после эволюции:
    >>> TPoint(int x, int y) : x(x), y(std::min(x,y)) { call_global_f(x,y); } <----- какой y???

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

    ОтветитьУдалить