четверг, 11 сентября 2014 г.

Стиль написания кода на C++

1. Общие замечания.
Ни один адекватный человек не использует все возможности C++ в одной программе. С++ это настолько мощный и гибкий язык, что все его возможности не то что использовать, а просто запомнить невозможно.
Я как-то сопровождал программу, где дикие украинские студенты использовали макросы, шаблоны, статическое и динамическое подключение библиотек, глобальные, внешние и статические переменные без каких либо префиксов, закрытые конструкторы, конфигурацию в константах и в XML-файле одновременно, а еще вперли туда самодельный упрощенный интерпретатор LUA. Это просто ужас.  Примерно 97% кода в той программе можно выбросить и ничего не изменится.

Прикола ради, расскажу историю из своего опыта. У меня у самого была такая ситуация, когда я был наемным программистом, один мой работодатель, мягко выражаясь, кинул меня через пенис. Но, чтобы хоть какую-то часть денег забрать (как у нас это водится, с черной бухгалтерии), мне надо было закончить одну программу.  Это был сервер обработки USSD запросов для операторов мобильной связи. Тогда я специально писал read only код. Например, чтобы преобразовать строку в число, в программе создавалось три параллельных потока, в одном строилось число в неинициализированном фрагменте памяти, в другом строка, третий синхронизировал те два, а чтение значений между потоками выполнялось через общий файл, проецируемый в память. Отладить это было невозможно (т.к. точка останова сбивала напрочь всю синхронизацию потоков). Понять что там делается тоже крайне сложно, т.к три тысячи строк кода в трех файлах выполняют ту же работу что и один вызов стандартной библиотечной функции. И в таком стиле была написана вся программа.  Это была самая большая по количеству кода программа, которую я когда либо писал. Затем мой работодатель продекларировал своему заказчику готовность. Что было дальше, мне не известно. Почему в русскоговорящих странах такое бывает — тема для отдельной большой статьи.
Старайтесь так не делать.
2. Общее оформление кода.
Альберт Эйнштейн однажды сказал: «Сделай настолько просто, насколько это возможно. Но не проще.». Друзья! Придерживайтесь этого принципа. Или Вы умнее автора приведенной выше фразы? Каждая строка кода, каждая функция, каждый класс, каждый цикл и оператор условного перехода — всё это должно быть очень и очень простым. Оно должно читаться как стихотворение, просто без рифмы. Имена переменных должны способствовать этому. Всё должно способствовать этому. Код должен легко читаться, без комментариев. Комментируйте только не очевидные приемы и константы. Когда пишите код от которого будут наследоваться, его максимально подробно комментируйте.
3. Структура программы.
Хорошая программа состоит из:
  • одного главного класса программы (обычно это ядро программы);
  • набора менеджеров и брокеров, которыми управляет ядро;
  • набора сущностей для хранения данных в удобном для обработки виде;
  • набора сущностей из предметной области (зависит от решаемой программой задачи), которыми управляют менеджеры и брокеры;
  • набора системных сущностей (для работы с файлами, звуками, сетью и т.д.), которыми управляет, непосредственно или через брокеры, ядро;
  • набора внешних библиотек (если требуется), которыми управляет использующая библиотеку сущность или брокер этой сущности;
  • набора графических интерфейсов (если требуются), которые взаимодействуют с ядром и никогда не обрабатывают никаких данных, а лишь принимают и отображают их;
  • интерфейса для работы с оборудованием (если этого требует решаемая задача);
  • интерфейса для работы с базами данных (если требуется);
  • журнала сообщений;
  • сторожевого таймера (если программа должна быть надежной).
Это я описал сложную систему. Но даже если Вы пишите простую программку, никогда не лепите логику обработки данных в графический интерфейс. Не делайте кашу из кода. Если есть обработчик нажатия кнопки (функция OnButtonOKClick(), например) — его задача сказать ядру или брокеру (даже не сущности напрямую), что пользователь нажал кнопку. И всё! Это одна строка кода. Не тулите в обработчик кнопки 200 строчек кода.
4. Структура файлов.
Для простой программы хватит одной директории с файлами исходных кодов. Для сложной, отдельные сущности и их менеджеры/брокеры лучше раскладывать в отдельные директории.
Запомните главное! Один файл содержит только один класс. Название файла соответствует названию класса. В заголовочном файле может быть только определение класса или структуры, определение пользовательских типов данных и перечислимые типа данных. Все константы лучше выносите в отдельный заголовочный файл, в котором ничего кроме констант не будет. Запомните! Никаких реализаций в заголовочном файле. Только определения. И никаких определений классов в файле реализации класса.
Вот пример хорошего заголовка:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*******************************************************************/
/* Original File Name: Canvas.h                                    */
/* Date: 2013-12-10                                                */
/* Developer: Mike Tyson                                           */
/* Copyright: Mike Tyson LTD                                       */
/* Description: Graphics preprocessor for monohrome indicator      */
/*******************************************************************/
 
//-------------------------------------------------------------------
 
#ifndef CANVAS_H_
#define CANVAS_H_
//-------------------------------------------------------------------
 
#include
//-------------------------------------------------------------------
 
class Canvas : public BaseCanvas
{
private:
    bool        m_active;
    std::string m_text;
    int         m_width;
    int         m_height;
 
public:
    Canvas();
    virtual ~Canvas();
 
    void ShowText(const std::string& Text);
 
protected:
    void Clear();
 
};
//-------------------------------------------------------------------
 
#endif /* CANVAS_H_ */
А вот отличная реализация:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*******************************************************************/
/* Original File Name: Canvas.cpp                                  */
/* Date: 2013-12-10                                                */
/* Developer: Mike Tyson                                           */
/* Copyright: Mike Tyson LTD                                       */
/* Description: Graphics preprocessor for monohrome indicator      */
/*******************************************************************/
 
//-------------------------------------------------------------------
 
#include
#include "Canvas.h"
//-------------------------------------------------------------------
 
Canvas::Canvas()
: m_active(false), m_text(""), m_width(0), m_height(0)
{
    this->AddReference();
}
//-------------------------------------------------------------------
 
/*virtual*/ Canvas::~Canvas()
{
    this->Release();
}
//-------------------------------------------------------------------
 
void Canvas::ShowText(const std::string& Text)
{
    if(!Core::Instance()->ValidateText(Text))
    {
        Core::Instance()->ErrorHere("Canvas::ShowText", ERROR_MSG_INVALID_TEXT);
        return;
    }
 
    Clear();
 
    m_width  = Core::Instance()->Tools->CalculateWidth(Text.c_str(), false);
    m_height = Core::Instance()->Tools->CalculateHeight(Text.c_str(), false);
    m_text   = Text;
 
    m_driverOfIndicator->ShowText(Text);
}
//-------------------------------------------------------------------
 
void Canvas::Clear()
{
    m_driverOfIndicator->Clear();
    m_width  = 0;
    m_height = 0;
    m_text   = "";
}
//-------------------------------------------------------------------
Это, конечно, тривиально, но именно так должна выглядеть вся программа. У меня это получается. А у Вас?
5. Названия классов.
Читая название любого из ваших классов я не должен спрашивать, что делает этот класс. Эта информация должна содержаться в названии класса или структуры.
Например, классов вроде class MyAlgorithm в программе не должно быть. Вместо этого назовите его, например, class NoiseLevelCalculator, если этот класс вычисляет уровень шума в сигнале.
Названия классов всегда пишите с большой буквы в стиле CamelCase.
Вот примеры откровенно херовых названий:
1
class Class1;
1
class Data;
1
class Algorithm1;
1
class Form1;
Можно сколько угодно много примеров привести. Что эти классы делают?
А вот хорошие названия классов:
1
class BaseObject;
1
class GameCore;
1
class PlayersBroker;
1
class Player;
1
class FileManager;
6. Названия переменных.
Переменные называйте так:
  • Закрытые и защищенные члены класса начинаются с маленькой буквы m и подчеркивания, за которым следует первое слово названия с маленькой буквы, а остальные названия в стиле CamelCase. Например: intm_usersCount, int m_width, int m_tempDataDir. Открытых переменных у класса не должно быть. Если в них есть необходимость — что-то спроектировано не так.
  • Названия параметров функций пишите с большой буквы в стиле CamelCase. Например: OpenFile(const std::string FileName);
  • Названия локальных переменных начинайте с маленькой буквы и продолжайте в стиле CamelCase. Например: int maxPortNumber;
  • Названия глобальных переменных начинайте с буквы g с последующим знаком подчеркивания и дальше первое слово с маленькой буквы, а остальные в стиле CamelCase. Например: GameCore g_gameCore; Кстати, если у Вас в коде есть глобальные переменные — повод задуматься.
  • названия констант членов класса начинайте с символа k и подчеркивания и дальше как в обычных закрытых членах класса. Например, intk_maxFileSize. Все остальные константы (за пределами класса) лучше выносите в отдельный заголовочный файл (constants.h, например) и старайтесь определять их (константы) с помощью директивы препроцессора. При этом названия переменных пишите полностью большими буквами через подчеркивание. Например:
    #define MAX_FILE_NAME_LEN 32
Все переменные должны называться осмысленно. Глядя на вашу переменную я должен сразу же понимать что эта переменная хранит. Например, int a; — это пример на два балла из 5. А вот так правильно: int index;
7. Количество кода в классе.
Не надо писать километровые простыни с тысячами закрытых переменных в одном классе. Но и не надо каждую переменную выносить в отдельный класс.
В идеале должно быть так: 20…150 строчек в заголовочном файле и 50…1000 строк в файле реализации. Если выходит больше — скорее всего, что-то не так в архитектуре.
8. Исключения.
Это очень тонкая тема. Тут надо без фанатизма.
Исключения используйте только в крайнем случае. Особенно в библиотеках. Я рекомендую использовать исключения только тогда, когда другими средствами невозможно или неудобно передавать информацию.
Не надо бросать исключения всякий раз, как только Вы считаете что где-то что-то не так введено или нет нужного файла.
Например, вот это ужасно:
1
2
3
4
5
6
...
if(massa < 11.7)
{
    throw new Exception("blablabla");
}
...
А вот это, иногда, единственный способ сообщить об ошибке:
1
2
3
4
5
6
7
8
Canvas::Canvas()
{
    FILE* configFile = fopen("/usr/config/monitor/.config", "rb");
    if(configFile == NULL)
    {
        throw std::runtime_error("Can not open the config");
    }
}
9. Форматирование кода блоков.
Друзья! Не гоните! Не издевайтесь!
Не пишите такого:
1
2
3
4
Canvas::Canvas ( ) {
    FILE* f = fopen("/usr/config/monitor/.config", "rb");
    if(!f)
        throw std::runtime_error("Can not open the config"); }
Я такое даже во многих книгах встречаю. Ну, не отнимайте мое (и других программистов) время. Ладно три строчки так написать. Но некоторые геннии умудряются так всю программу написать… Откройте Страуструпа, посмотрите как он пишит примеры кода.
Еще раз. Вот правильный формат того же кода:
1
2
3
4
5
6
7
8
Canvas::Canvas()
{
    FILE* configFile = fopen("/usr/config/monitor/.config", "rb");
    if(configFile == NULL)
    {
        throw std::runtime_error("Can not open the config");
    }
}
Ключевые моменты:
  • На одной строке с фигурной скобкой ничего нет. На начало и конец блока отводим по одной отдельной строке.
  • В одной строке тела функции не может (оно то может, но так писать не нужно) быть более одной точки с запятой. Т.е. одна строка = одна операция.
  • Если в условном блоке или цикле выполняется только одна строка, всё равно выделяйте ее в блок (фигурными скобками).
  • Не сокращайте операторы сравнения по самые помидоры.
  • Давайте осмысленные имена всем переменным
Комментарии.
Не надо писать поэмы в комментариях. Войну и мир в исходники переписывать не надо. Но и совсем без комментариев тоже не надо.
Пишите короткое описание файла в начале:
1
2
3
4
5
6
7
/*******************************************************************/
/* Original File Name: Canvas.cpp                                  */
/* Date: 2013-12-10                                                */
/* Developer: Mike Tyson                                           */
/* Copyright: Mike Tyson LTD                                       */
/* Description: Graphics preprocessor for monohrome indicator      */
/*******************************************************************/
Комментируйте неочевидные приемы:
1
int angle = angle / 57.3; // Convert degrees to radians.
Хотя это плохой пример кода. Тут константу лучше вынести в файл констант. Да и вообще, правильно в данном случае написать функцию или макрос, примерно так:
1
2
3
4
float DegrToRad(const float& Angle)
{
    return Angle * M_PI / 180;
}
Законченные куски кода (функции, классы, библиотеки) отделяйте горизонтальной линией:
1
//-------------------------------------------------------------------
Друзья! Эту тему можно продолжать до бесконечности. Можно пунктов 100 написать. А то и больше. В общем, у кого есть желание и позволяет опыт — пишите в комментарии к этой статье, что бы Вы еще добавили.