Как стать автором
Обновить

Дао Кодинга (о стиле написания кода)

Уровень сложностиСредний
Время на прочтение14 мин
Количество просмотров8.8K
Автор оригинала: Koen Witters

Я натолкнулся на этот мануал в интернете, когда гуглил описание "Верблюжьей вёрстки". Меня, технически, очень продвинул данный текст, поэтому я взялся за его перевод, для себя. Переводчик я — так себе, моя основная цель — передать смысл текста в максимально полном объёме. Навык программиста и энтузиазм мне в помощь.

Текст написан от первого лица, и я решил сохранить этот момент. Поэтому "я" — это не я, а оригинальный автор статьи "Tao of Coding" — Коэн Уиттерс.

Читать далее

Данное руководство описывает стиль написания программного кода, который я разрабатывал несколько лет. Но стиль мой настолько неизвестен, что я не знаю никого, кто пользовался бы таким странным способом программировать, каким пользуюсь я. Тем не менее, он мне нравится и я бы хотел поделиться этим знанием (эй ты, везунчик, слышишь меня?). Этот стиль я использовал в разных языках: С, С++, Java, C#, Python,... .

Если вы хотите бегло ознакомиться со стилистикой написания, просто пролистайте страницу вниз и посмотрите на участки кода, написанные моим, "deWiTTERS" способом, и вы увидите, как удачно и красиво он выглядит.

Почему "стиль кодирования"?

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

  • разработка настолько читаемого и чистого кода, что любой, кто начнет разбираться в его дебрях, сделает это достаточно быстро. А ещё — это важно для тебя самого: тот случай, когда ты ковыряешь свой старый код, написанный год-два назад, и думаешь: "Блин, какого я тут понаписал, не могу вспомнить, что это всё значит..."

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

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

Основные правила

Я постарался объединить большинство аспектов своего стиля в следующих правилах.

  1. Написанное должно быть абсолютно понятным настолько, насколько это вообще возможно.

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

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

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

Всё нужно упрощать до тех пор, пока это возможно, но не более того.

Альберт Энштейн

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

Написание понятного и читаемого кода стало возможным с рождения современных языков программирования. Дни, когда программирование было связано лишь с ассемблером и двоичным кодом, давно прошли. Следовательно, мой стиль старается быть ближе к натуральному языку, насколько это возможно. Практически, вы можете проговаривать мой код, читая его как в книге. И кстати, скорее всего поэтому мой код плохо задокументирован. Я почти не оставляю комментариев! Я считаю комментирование кода плохим поведением (не в самом хорошем смысле этих слов), в принципе. Только когда я делаю что-то странное и непонятное, я добавляю комментарии, чтобы объяснить — зачем это надо. И я думаю, что комментированием не нужно объяснять, что делает ваш код. Код должен сам объяснять, что он делает.

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

Мартин Фаулер.

Идентификаторы

Начнем со стиля самого востребованного раздела кодирования: идентификаторов. Любые идентификаторы, прочий код, комментарии — должны быть написаны на английском языке. Нормально, что разработанный проект мигрирует от одного программиста к другому, из компании в компанию, оказывается на другой стороне планеты. Ты не знаешь, где твой код окажется завтра — пиши всё на английском.

Переменные

Имена переменных необходимо писать в нижнем регистре, разделяя каждое слово в имени подчёркиванием. Подчёркивание очень похоже на натуральную запись языка и поэтому добавляет коду читаемости. Оно просто заменяет пробелы, как в обычных предложениях. Переменную "red_push_button", прочитать легче чем "RedPushButton", поверьте мне.

Если вы хотите, чтобы ваши переменные были максимально понятными, вы должны давать им очевидные, осмысленные имена. Естественно, что переменные представляют собой какой-то объект ("object") или значение ("value"), поэтому называйте их соответственно. Не тратьте время с переменными asflkwPrefixing slkfjjjfVariables fskjfeWith lskdTheir или kslfjType, потому что они непонятны — это бред какой-то. Если у вас есть переменная "age" (возраст), естественно и понятно, что это тип integer (целое) или unsigned short (короткое целое без знака). Если это "filename" (имя файла), что ж, скорее всего это string (строка). Думать не надо — всё просто! Иногда смысл переменной становится понятнее, если в её имя поместить тип, например для кнопки графического интерфейса: "play_button" или "cancel_button".

Существуют пре- и постфиксы, которые могут добавить читаемости вашим переменным. Вот список наиболее распространённых:

  • is_, has_. Используйте их в именах всех логических (boolean) переменных, и у вас никогда не будет проблем с пониманием типа этих переменных. И они будут отлично смотреться в выражениях условных переходов "if".

  • the_. Начинайте все глобальные переменные префиксом "the_", и всегда будет понятно, что она только одна такая.

  • _count. Используйте постфикс _count во всех переменных, которые представляют собой счетчики (количество элементов чего-либо). Используйте единственную форму "bullet_count" вместо множественной ˜– "bullets" , так как множественной формой мы будем представлять массивы.

Массивы или другие переменные, представляющие списки должны указываться в множественной форме, как "enemies", "walls" или "weapons". Но не нужно делать так для всех типов массивов, так как некоторые из них реально не являются списками элементов. Например "char filename[64]" или "byte buffer[128]"

Const (или Final)

Константы или файналы должны быть написаны исключительно в ВЕРХНЕМ_РЕГИСТРЕ, словами с подчёркиванием в качестве разделителя, для их лучшей читаемости, например MAX_CHILDREN, X_PADDING или PI. Этот метод написания должен быть повсеместным для того, чтобы не путать константы с обычными переменными.

Можно использовать слова MAX и MIN в именах констант, чтобы представлять лимиты значений.

Типы

Типы — это целая классификация переменных. И это довольно абстрактный термин для того, чтобы описывать их используя классический английский язык. Но мы должны явно отделять типы друг от друга и от других идентификаторов. Именно для этого я использую ВерблюжийТипРаспиновкиСлов. Для каждого класса, структуры или перечисления (enum), и им подобные типы объявлений — используйте ВерблюжийРегистр.

Таким же образом вы можете использовать такой тип описания для стандартных, встроенных переменных. Ну например:

HelpWindow help_window;
FileHeader file_header;
Weapon weapons[ MAX_WEAPONS ];

Текст программы

if, else if, else

Есть несколько способов описания условия if. Начнем со скобок. Есть три вида помещения скобок за объявлением if

    if(condition)

    if (condition)

    if( condition )

Я никогда не видел в тексте на английском языке, чтобы скобки размещались так, как в первом примере. Так почему мы должны использовать этот вариант в написании кода? Слова просто неправильно разделены между собой. Во втором примере, скобки с условием ставятся вместо оператора if (или отчуждаются от оператора), в то время как выражение в скобках является частью оператора if, а не самим оператором, поэтому последний пример является лучшим в описании. А также, последний пример является преимущественным, лучше предоставляя для обозрения содержимое в скобках.

    if (!((age > 12) && (age < 18)))

    if( !((age > 12) && (age < 18)) )

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

А что насчет фигурных скобок? Может быть не использовать их?! Но к сожалению C, C++, Java или C# не позволят нам этого сделать. Только Python. Поэтому мы не сможем их проигнорировать, но мы можем поставить их так, чтобы они выглядели как в программах на Python — просто и чисто:

    if( condition ) {
        statements;
    }
    else if( condition ) {
        statements;
    }
    else {
        statements;
    }

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

    if( (current_mayor_version < MIN_MAYOR_VERSION)
        || (current_mayor_version == MIN_MAYOR_VERSION
            && current_minor_version < MIN_MINOR_VERSION) )
    {
        update();
    }

Там, где есть только одно выражение, в теле условия, после объявления if, вы можете опустить фигурные скобки, и убедитесь, что поместили это выражение на следующей строке, но только если это не return или break, которые лучше написать в одну строку.

    if( bullet_count == 0 )
        reload();

    if( a < 0 ) return;

while

Цикл while пишется точно так же, как и условие if. Для отступов я использую 4 пробела.

    while( condition ) {
        statements;
    }

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

    do {
        statements;
    } while( condition )

for

Цикл for — единственная цель в жизни оператора — итерация цикла. Это именно то, что и делает этот оператор. Да, цикл for может быть безболезненно заменён циклом while, но лучше так не делать. Когда вам нужно повторить некоторое количество операторов — используйте for, а только если это реально невозможно, пользуйтесь while. Структура for проста и прекрасна:

    for( int i = 0; i < MAX_ELEMENTS; i++ ) {
        statements;
    }

Bспользуйте литеры i, j, k, l, m для перебора чисел, а для перебора объектов используйте 'it'.

switch (case)

Оператор switch имеет схожую с if и while структуру. Единственная вещь, на которую стоит обратить внимание — это дополнительные отступы. Также, добавьте пустую строку сразу после break.

    switch( variable ) {
        case 0:
            statements;
            break;

        case 1:
            statements;
            break;

        default:
            break;
    }

Функции

Функции производят разные действия, и их наименование должно раскрывать эти действия. Поэтому, в наименование функции обязательно, включайте глагол действия этой функции. Без исключений! Используйте тот же метод написания, что и с переменными: маленький регистр, разделитель — подчёркивание. Такой метод позволит вам производить милые предложения в коде, которые будут понятны всем.

Также, удостоверьтесь, что функция делает именно то, что говорится в её имени — ни больше, ни меньше. Если у вас есть функция 'load_resources', убедитесь, что функция только ресурсы и загружает, ничего более, никаких других инициализаций. Зачастую существует соблазн, поместить по-быстрому в load_resources инициализацию разных переменных, потому что такая функция вызывается с большим приоритетом, но это принесёт больше проблем в будущем. Стиль deWiTTERS использует крайне мало комментариев, а функция должна производить только то, что написано в её имени. А когда функция возвращает что-то, убедитесь, что из имени функции будет понятно, что она возвращает.

Некоторые функции, как пара "инь и ян" — выполняют противоположные действия, и вы должны соответствовать этому в своих названиях для таких функций. Например: get/set, add/remove, insert/delete, create/destroy, start/stop, increment/decrement, new/old, begin/end, first/last, up/down, next/prev, open/close, load/save, show/hide, enable/disable, resume/suspend и т.д.

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

    do_something( with, these, parameters );

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

    HWND hwnd = CreateWindow( "MyWin32App", "Cool application",
                              WS_OVERLAPPEDWINDOW,
                              my_x_pos, my_y_pos,
                              my_width, my_height,
                              NULL, NULL, hInstance, NULL );

Определение функции (definition)

Вот типичное описание функции:

    bool do_something_today( with, these, parameters ) {
        get_up();
        go_to( work, car );
        work();
        go_to( home, car );
        sleep();

        return true;
    }

Убедитесь, что ваша функция не слишком огромна. Или, цитируя Линуса:

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

Классы

Для наименования классов я использую всё ту же ВерблюжьюРаскладку, как и для определения типов. И не стоит особо заморачиваться насчёт добавления префикса 'C' для каждого класса — это бессмысленная трата байт кода и времени на его набор.

Как и для всего остального: давайте классам обдуманные, чистые имена. Если класс является потомком класса "Window", назовите его "MainWindow".

Когда вы создаёте новый класс, запомните: всё начинается со структуры данных.

Данные доминируют. Если вы выбрали правильную структуру данных, всё правильно организовали, то алгоритмы будут почти всегда само-очевидными. Структура данных, не алгоритмы — главное в программировании.

Фред Брукс.

Наследование

Отношение “Is a” должно быть смоделировано как наследование (inheritance), а “has a” — как сдерживание (containment). Удостоверьтесь, что не переборщили с наследованием. Это отличная технология, но только при правильном применении.

Члены класса

Определённо, необходимо делать различие между членами класса и обычными переменными. Если вы не будете этого делать — в будущем вы об этом горько пожалеете. Возможными именами будут m_Member или fMember. Для динамических членов класса я бы использовал my_member, а для статических our_member. Такой метод позволяет получить неплохие предложения в теле ваших методов, например:

    if( my_help_button.is_pressed() ) {
        our_screen_manager.go_to( HELP_SCREEN );
    }

Для всего остального, всё, касающееся наименований и применяемое к переменным, так же применимо и к членам класса. Единственная здесь проблема: я так и не смог для себя определить корректное наименование булевских членов класса. Вы помните, что логические значения должны иметь префикс "is" или "has". Но в комбинации с префиксом "my_" мы получаем безумие, типа "my_is_old" или "my_has_children". Я так и не смог найти хорошего решения для этой проблемы, если у вас есть предложения, пожалуйста, оставьте комментарий к этому посту.

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

Методы

Всё, что применимо к функциям, применимо и к методам, в полном объёме, вплоть до добавления глагола в имя метода. Так же, удостоверьтесь, что в имени метода нет наименования класса.

Структура кода

Выравнивайте похожие строки, чтобы дать вашему коду хороший внешний вид. Например:

    int object_verts[6][3] = {
        {-100,  0, 100}, {100, 0,  100}, { 100, 0, -100},
        {-100, 11, 100}, (100, 0, -100}, {-100, 0, -100}
    };

    RECT rect;
    rect.left   = x;
    rect.top    = y;
    rect.right  = rect.left  + width;
    rect.bottom = rect.right + height;

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

    if( x & 0xff00 ) { exp -= 16; x >>= 16; }
    if( x & 0x00f0 ) { exp -=  4; x >>=  4; }
    if( x & 0x000c ) { exp -=  2; x >>=  2; }

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

    int x, y;
    int length;

Пространства имен и наборы библиотек

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

Дизайн

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

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

  1. Когда функции становятся слишком большими, разбивай проблему на несколько вспомогательных функций.

  2. Если класс содержит очень много членов и методов, разбей его на несколько вспомогательных классов и используй их в своём главном классе (но не используй для этого наследование!). Убедись, что вспомогательный класс не использует для работы основной, где бы то ни было.

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

  4. Когда закончилось добавление нового функционала или завершилось исправление ошибки, перечитай все измененные файлы, чтобы убедиться, что всё находится в идеальном состоянии.

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

Файлы

Файлы нужно называть именем класса, который он содержит. Не держите в одном файле больше одного класса, и вы всегда будете знать где нужно искать тот или иной класс. Структура каталогов проекта представляет структуру пространств имён.

Структура файла .h

Заголовочные файлы языка C или C++ содержат интерфейс реализации. Очень важно знать это при создании .h файла. В классе, сначала, определяются public интерфейсы, которые можно будет использовать в других классах, затем определяются все protected методы и члены класса. Такой способ дизайна позволяет держать нужную для людей, пользующихся вашим классом, информацию в начале. Я не использую private методы или члены класса, поэтому все члены класса сгруппированы внизу объявления класса. Это позволяет быстрее осмотреть содержимое класса, снизу. Группируйте методы вместе, по их смыслу.

    /*
     * license header
     */

    #ifndef NAMESPACE_FILENAME_H
    #define NAMESPACE_FILENAME_H

    #include <std>

    #include "others.h"

    namespace dewitters {

        class SomeClass : public Parent {
            public:
                Constructor();
                ~Destructor();

                void public_methods();

            protected:
                void protected_methods();

                int     my_fist_member;
                double  my_second_member;

                const static int MAX_WIDTH;
        };

        extern SomeClass the_some_class;
    }

    #endif

Структура файлов .java и .cs

.java или .cs файлы содержат не интерфейсы класса, а просто их реализацию. А так как структура данных важнее алгоритма, определяйте члены класса перед методами. Такой подход даёт вам возможность быстрее осознать содержимое класса, через его структуру данных (члены класса). Схожие методы стоит сгруппировать вместе.

Схематичный обзор файлов .java или .cs:

    /*
     * license header
     */
    package com.dewitters.example;

    import standard.modules.*;

    import custom.modules.*;

    class SomeClass extends Parent {
        public final int MAX_WIDTH = 100;

        protected int     my_first_member;
        protected double  my_second_member;

        Constructor() {
        }

        Methods() {
        }
    }

Шутки

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

Стиль deWiTTERS vs. другие стили

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

Если вы думаете, что видели код, который может дать фору моему стилю кодирования, оставьте комментарий ниже. Я использую его в своём, улучшенном стиле 'deWiTTERS' и опубликую его.

Indian Hill C Style

/*
 *	skyblue()
 *
 *	Determine if the sky is blue.
 */

int			/* TRUE or FALSE */
skyblue()

{
	extern int hour;

	if (hour < MORNING || hour > EVENING)
		return(FALSE);	/* black */
	else
		return(TRUE);	/* blue */
}

/*
 *	tail(nodep)
 *
 *	Find the last element in the linked list
 *	pointed to by nodep and return a pointer to it.
 */

NODE *			/* pointer to tail of list */
tail(nodep)

NODE *nodep;		/* pointer to head of list */

{
	register NODE *np;	/* current pointer advances to NULL */
	register NODE *lp;	/* last pointer follows np */

	np = lp = nodep;
	while ((np = np->next) != NULL)
		lp = np;
	return(lp);
}

Rewritten to deWiTTERS Style:

bool sky_is_blue() {
    return the_current_hour >= MORNING && the_current_hour <= EVENING;
}

Node* get_tail( Node* head ) {
    Node* tail;
    tail = NULL;

    Node* it;
    for( it = head; it != NULL; it = it->next ) {
        tail = it;
    }

    return tail;
}

“Commenting Code” from Ryan Campbell

/*
 * Summary:     Determine order of attacks, and process each battle
 * Parameters:  Creature object representing attacker
 *              | Creature object representing defender
 * Return:      Boolean indicating successful fight
 * Author:      Ryan Campbell
 */
function beginBattle(attacker, defender) {
    var isAlive;    // Boolean inidicating life or death after attack
    var teamCount;  // Loop counter

    // Check for pre-emptive strike
    if(defender.agility > attacker.agility) {
        isAlive = defender.attack(attacker);
    }

    // Continue original attack if still alive
    if(isAlive) {
        isAlive = attacker.attack(defender);
    }

    // See if any of the defenders teammates wish to counter attack
    for(teamCount = 0; teamCount < defender.team.length; i++) {
        var teammate = defender.team[teamCount];
        if(teammate.counterAttack = 1) {
            isAlive = teammate.attack(attacker);
        }
    }

    // TODO: Process the logic that handles attacker or defender deaths

    return true;
} // End beginBattle

Rewritten to deWiTTERS Style:

function handle_battle( attacker, defender ) {
    if( defender.agility > attacker.agility ) {
        defender.attack( attacker );
    }

    if( attacker.is_alive() ) {
        attacker.attack( defender );
    }

    var i;
    for( i = 0; i < defender.get_team().length; i++ ) {
        var teammate = defender.get_team()[ i ];
        if( teammate.has_counterattack() ) {
            teammate.attack( attacker );
        }
    }

    // TODO: Process the logic that handles attacker or defender deaths
}

Здесь статья Коэна заканчивается. От себя добавлю, что после её прочтения я перешёл в своих работах на js и abl на стиль с подчёркиванием и не жалею. Надеюсь, данная работа была для вас полезной.

Теги:
Хабы:
Всего голосов 14: ↑10 и ↓4+11
Комментарии30

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань