9 июля 2012 г.

Оформление кода MQL

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

MQL не является таким языком из-за очень узкой специализации. Программистов мало, и их интерес часто поверхностный. То, что MQL корнями уходит к популярным C/C++, не делает его таковыми автоматически. Что касается оформления кода, то эти языки послужили ему скорее дурным примером. Когда C и даже C++ уже стали популярными, еще не были развиты некоторые современные методы и технологии программирования. В основном, это касается массового применения систем контроля версий и увеличения взаимодействия в целом.

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

Комментарии

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

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

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

Для контроля версий лишние украшения представляются проблемой по нескольким причинам:
  • если вместо пропуска строки стоит комментарий-разделитель (часто //-----), то при изменении строки в список изменений попадет сразу несколько соседних строк вместо одной, которую действительно изменили, так как программы поиска изменений используют в первую очередь пустые строки как разделители блоков кода;
  • при правке только разделителей-украшений вы получаете пустую ревизию, что усложняет работу с версиями;
  • украшения отвлекают внимание от действительных изменений.
В порядке демонстрации плохого оформления небольшой кусок типичного кода на MQL:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void deinit()
  {
   if(ExtHandle>=0) { FileClose(ExtHandle); ExtHandle=-1; }
  }
//+------------------------------------------------------------------+

Четыре строки бесполезных украшений. И четыре строки кода. Без комментариев.

Комментарии должны быть

Длинными или короткими, на русском или английском - зависит от конкретных требований, лично ваших или заказчика. Главное, чтобы они были. Переделаем кусок выше:

// Завершение программы
void deinit()
{
    // Закрыть файл, если он был ранее открыт
    if (ExtHandle >= 0)
    {
        FileClose(ExtHandle);
        //ExtHandle = -1; // непонятно, зачем это, результат копипасты?
    }
}

Про расположение фигурных скобок (структуру) будет отдельная статья.

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

Ваши контакты

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

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

Пунктуация и отступы

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

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

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

if(a>b){if(a>c){if(a>d)Print("a > b, a > c, a > d");else Print("a > b, a > c, a <= d");}}

Писать в таком стиле не нужно, это создает только проблемы.

Пунктуация

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

Пример кода без соблюдения этих элементарных правил пунктуации:

if(count>arraySize)
    arraySize=ArrayResize(strings,arraySize+100);

Тот же код с пунктуацией:

if (count > arraySize)
    arraySize = ArrayResize(strings, arraySize + 100);

Отступы

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

Дополнение. С моим любимым шрифтом оказалось, что лучше выглядит отступ в 5 или 6 пробелов, хотя раньше очень долго использовал 4. Символы в этом шрифте довольно узкие, и обычно достаточно широкий отступ в 4 пробела здесь оказался маловат. Одна быстрая настройка в редакторе кода, и весь мой код теперь с новым отступом.

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

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

int a = 100;<-пробелы->// инициализация a
int b = 0;<--пробелы-->// инициализация b
<-пустая строка для лучшей читаемости->
if (a > b)
{
<-табуляция->Print("a > b");<-пробелы->// вывод 1
<-табуляция->Print("b < a");<-пробелы->// вывод 2
<-табуляция-><--------пробелы--------->// продолжение предыдущего комментария
}

Длинные строки разбиваются на несколько так, чтобы каждая часть была не слишком длинной. Длина определяется современным состоянием рынка мониторов, удобством чтения длинных строк и прочими критериями, актуальными на данный момент. Распространенные стандарты - 80 и 120 символов. Массовое распространение мышек с колесом делает такое ограничение менее нужным, теперь для листания по горизонтали можно совершать меньше движений, чем, например, 10 лет назад (если бы MetaEditor поддерживал горизонтальную прокрутку). Однострочные выражения лучше подходят для систем контроля версий. Исключением может быть ситуация, когда выражение разбивается на несколько строк для лучшего отображения его структуры.

Другое ограничение связано с использованием систем контроля версий. Сейчас еще популярно начинать следующую часть строки с отступом до некоторой отметки, например, началу списка параметров, вот так:

bool OrdersSend(int cmd, double volume, double price, int slippage, 
                double stopLoss, double takeProfit, string comment = "", 
                datetime expiration = 0, color arrowColor = CLR_NONE)

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

bool OrdersSend(int cmd, double volume, double price, int slippage, 
    double stopLoss, double takeProfit, string comment = "", 
    datetime expiration = 0, color arrowColor = CLR_NONE)

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

bool OrdersSend(
    int cmd,        // команда
    double volume,  // объем
    double price,   // цена открытия
    int slippage,   // отклонение
...)

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

Дополнение. Чтобы комментарии справа от кода сдвигать как можно реже, используйте какой-нибудь удобный шаг (сетку), например, кратную 4, 5, 8 или 10, тогда в большинстве случаев новые изменения не будут смещать остальные комментарии.

Структура

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

Сегодня есть языки (из популярных - Python), где сама структура выступает в роли границ блоков кода, исключая некоторую избыточность. Возможно, вам нужно увлечься одним из них только для того, чтобы научиться хорошо оформлять код на других современных языках и MQL.

Чтобы сразу увидеть, где заканчивается блок кода, необходимо ставить закрывающую фигурную скобку ровно под открывающей. Египетские скобки (открывающая скобка в конце строки оператора) могут быть оправданы для веб-языков, но не в MQL.

В стиле MetaQuotes фигурные скобки имеют дополнительный отступ относительно оператора, для которого они применяются.

   if (a > b)
     {
       Print("a > b");
     }

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

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

   if (a > b)
     {
       if (a > c)
         {
           if (a > d)
             Print("a > b, a > c, a > d");
           else
             Print("a > b, a > c, a <= d");
         }
     }

Этот пример будет проще прочесть без дополнительных отступов у скобок:

    if (a > b)
    {
        if (a > c)
        {
            if (a > d)
                Print("a > b, a > c, a > d");
            else
                Print("a > b, a > c, a <= d");
        }
    }

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

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

    if (a > b)
        if (a > c)
           if (a > d)
                Print("a > b, a > c, a > d");
      else // угадайте, к какому if я отношусь
          Print("a > b, a > c, a <= d");

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

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

    switch (orderType)
    {
        case OP_BUY:
            // код с новой строки, чтобы не нарушать вертикаль
            return(ArrayCopy(tickets, orders__buys));

        // выделение блока варианта пустой строкой
        case OP_SELL:
            return(ArrayCopy(tickets, orders__sells));

        case -1: // если бы начинали писать операторы в той же строке, то здесь пришлось бы
                 // вставить несколько пробелов-украшений для выравнивания
            int count = ArrayCopy(tickets, orders__buys) +
                ArrayCopy(tickets, orders__sells, count);

            // Общая проверка на ошибку
            if (count == OrdersCount())
                return(count);
            else
                return(0);
                
        default:
            return(0);
    }

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

    // Плохой вариант
    for (int a = 0; a < 10; a++) if (a > b) Print(a, " > ", b);

    // Хороший вариант
    for (int a = 0; a < 10; a++)
        if (a > b)
            Print(a, " > ", b);

    // Лучше, но затратнее
    for (int a = 0; a < 10; a++)
    {
        if (a > b)
        {
            Print(a, " > ", b);
        }
    }
 
    // Как вариант, не ставить скобки на последнем уровне, если там лишь одна строка
    for (int a = 0; a < 10; a++)
    {
        if (a > b)
            Print(a, " > ", b);
    }

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

    // Пустая строка-разделитель - только внутри
    if (a > b)
    {
        if (b > c)
            Print(1);

        if (b < c)
            Print(2);
    }

При использовании else в операторе ветвления выделение внутренних блоков должно быть одинаковым - либо с фигурными скобками везде, либо нигде.

    // Визуальная неравнозначность для равнозначных следствий
    if (a > b)
    {
        Print("a > b");
    }
    else
        Print("a <= b");

    // Плохой вариант, при усложнении ветвления придется переделывать
    if (a > b) Print("a > b");
    else Print("a <= b");

    // Очень хороший вариант, легко добавлять инструкции в блоки.
    if (a > b)
    {
        Print("a > b");
    }
    else
    {
        Print("a <= b");
    }

    // Хороший вариант при возможности (одна строка во всех ветвлениях)
    if (a > b)
        Print("a > b");
    else if (a < b)
        Print("a < b");
    else // a == b
        Print("a = b");

Классы

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

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

Возьмем для примера простейший класс с двумя полями и методом:

class MyClass1
{
private:
    int prop1;
    double prop2;

public:
    void DoSomething(const CObject *object)
};

void MyClass1::DoSomething(const CObject *object)
{
    // do something
}

Это почти типичное оформление кода класса от MetaQuotes, встречающееся в документации и стандартной библиотеке, приведённое в соответствие с правилами предыдущих статей. Здесь получаем несколько проблем, которые попытаемся решить.

Модификаторы доступа выходят из структуры

В коде выше модификаторы public и private находятся на том же уровне, что и название класса, хотя они являются частью структуры (группировка), причем их верхним уровнем является как раз объявление класса. То есть эти модификаторы должны писаться с отступом. Некоторые современные стандарты оформления кода C++ уже учитывают эту ошибку и предлагают делать дополнительный отступ.

class MyClass1
{
    private:
        int prop1;
        double prop2;

    public:
        void DoSomething(const CObject *object);
};

void MyClass1::DoSomething(const CObject *object)
{
    // do something
} 

Дополнение. Это спорный момент. Сам я пока следую за большинством, которое отступы здесь не добавляет.

Изменение модификатора и контроль версий

Если сейчас нужно будет изменить модификатор доступа для prop1, то придется переносить объявление этой переменной в соответствующую группу в структуре модификаторов. Человек это нормально воспримет, если заметит. Здесь приходит на помощь использование системы контроля версий или программ для просмотра сделанных изменений. Но для таких программ перемещение одной строки кода будет выглядеть либо как удаление этой строки в одном месте и вставка в другом, причем вы можете не увидеть, что было произведено перемещение ее под другой модификатор, либо даже удаление и вставка целого блока (если ранее вы пожалели пустых строк на отступы). Нет связи между перемещением строки и сменой доступа в отрыве от контекста.

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

class MyClass1
{
    private: int prop1;
    private: double prop2;
    public: void DoSomething(const CObject *object);
};

void MyClass1::DoSomething(const CObject *object)
{
    // do something
}

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

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

Изменение модификатора доступа для двух полей класса:


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


В этом случае проще обнаружить изменения в отдельной строке.

Вы можете использовать более продвинутые средства сравнения, например TortoiseMerge, Meld и т.п.:


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

Дополнение. Такой стиль ещё более спорный, настолько, что вообще никем не применяется в C, C++, MQL. Считайте это объяснением того, почему в некоторых современных языках вас "заставляют" использовать такой подход. При работе в группе необходимо чётко согласовывать оформление кода и поэтому такой вариант с модификаторами больше не использую даже я сам.

Модификаторы доступа. Порядок

С одной стороны, хочется, чтобы публичные методы были как можно выше, поэтому желательна сортировка типа public-protected-private. С другой стороны, неудобно располагать члены класса (часто приватные) в конце класса и рассеивать их по телу класса.

Есть простой выход из этой ситуации:
  1. Члены класса (или структуры) всегда идут перед методами.
  2. Члены и методы сортируются в порядке public-protected-private.
Получаем такой порядок:
  1. public члены 
  2. protected члены
  3. private члены
  4. public методы
  5. protected методы
  6. private методы
Публичные методы часто разделяются ещё на несколько типов:
  • конструкторы и деструкторы
  • геттеры и сеттеры
  • операторы
  • статические методы
  • виртуальные методы и переопределения
  • обычные методы
Их тоже можно группировать и сортировать в некотором порядке для удобства чтения кода.

Объявление и реализация

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

Нам предлагается писать сначала объявление, а под объявлением класса - реализацию. Такой подход используется, например, в Object Pascal и Delphi, как обязательный. Привожу в пример этот язык, так с ним был получен большой негативный опыт работы с подобным синтаксисом. С введением в процесс разработки системы контроля версий становится понятно, что такой подход даёт не только необходимость выполнения лишней работы, но и неудобство работы с версиями - каждое изменение объявления функции приходится теперь делать в двух местах, что усложняет визуализацию изменений в версии.


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

class MyClass1
{
    private: int prop1;
    private: double prop2;

    public: void DoSomething(const CObject *object)
    {
        // do something
    }
};

В итоге получили редкое для улучшения оформления явление - уменьшение числа строк. Следует отметить важную деталь - если бы в редакторе MetaEditor 5 не было бы навигации по функциям и определениям, то разделение объявления и реализации давало бы некоторое удобство, но навигация есть, и разделение уже не решает проблему её отсутствия. Более того, разделение её усложняет - приходится выбирать между объявлением и реализацией.

Пример излишне усложненной навигации (функция "Перейти к определению"):


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

Прикинем, куда и как можно здесь поставить комментарии.

// Мой класс
class MyClass1
{
 
private:
 
    // Свойство 1
    int prop1;

    // Свойство 2
    double prop2;
 
public:
 
    // Сделать что-нибудь
    void DoSomething(const CObject *object)
    {
        // do something
    }
 
};

Если бы мы сначала писали объявление, а потом реализацию метода класса, то комментарий пришлось бы продублировать, - лишняя работа.

Комментариев нет:

Отправить комментарий