IT Notes

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

Введение

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

Правило 1. Пишите комментарии на высоком уровне абстракции

Рассмотрим такой пример кода на C++:

class Person {
    …
};

class Registrator {
public:
    …
    // Добавляет объект класса Person в вектор m_persons.
    // Возвращает false, если person уже занесен в m_persons,
    // иначе возвращает true.
    bool registerPerson( const Person& person );
    …

private:
    std::vector< Person > m_persons;
    …
};

Основная его проблема - нарушение инкапсуляции. Пользователю класса Registrator не интересно то, каким образом будет храниться информация. А если в будущем m_persons станет множеством std::set или просто поменяются имена переменных? В конечном итоге автор класса вообще перейдет на хранение записей в базе данных и комментарий в текущей формулировке потеряет всякий смысл.

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

Лучше было бы дать такое описание функции-члену registerPerson():

// Осуществляет регистрацию сведений о личности.
// Возвращает false, если регистрация окончилась неудачей,
// иначе возвращает true.
bool registerPerson( const Person& person );

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

enum RegistrationResult {
    REG_OK,
    REG_FAILED
};

RegistrationResult registerPerson( const Person& person );

Правило 2. Не пишите избыточные комментарии

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

// Очищает страницу.
void clearPage();

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

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

// Предусловие:
//     a >= 0
//
// Постусловие:
//     | a - sqrt( a ) * sqrt( a ) | <= eps
double sqrt( double a );

Правило 3. Не устраняйте недостатки кода с помощью комментариев

Предположим, что перед нами код ужасного качества. Кто-то может попытаться выправить ситуацию с помощью комментариев. Тогда те, кто в будущем будут работать с этим кодом, вероятно, смогут с ним разобраться, но зачем тратить время таким образом? Рассмотрим пример плохого кода:

FigPtr fma( const std::vector< FigPtr >& a ) {
    if( a.empty() ) {
        return nullptr;
    }

    FigPtr r = a[ 0 ];
    int m = r->ar();
    for( size_t i = 1; i < a.size(); ++i ) {
        int ar = a[ i ]->ar();
        if( ar < m ) {
            m = ar;
            r = a[ i ];
        }
    }
    return r;
}

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

FigurePointer findMinimumAreaFigure( const std::vector< FigurePointer >& figures ) {
    if( figures.empty() ) {
        return nullptr;
    }

    FigurePointer minAreaFigure = figures[ 0 ];
    int minArea = minAreaFigure->getArea();
    for( size_t i = 1; i < figures.size(); ++i ) {
        int area = figures[ i ]->getArea();
        if( area < minArea ) {
            minArea = area;
            minAreaFigure = figures[ i ];
        }
    }
    return minAreaFigure;
}

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

Но и это решение не является наилучшим. Удобнее и правильнее будет воспользоваться стандартной STL-функцией std::min_element():

FigurePointer findMinimumAreaFigure( const std::vector< FigurePointer >& figures ) {
    if( figures.empty() ) {
        return nullptr;
    }

    return *std::min_element(
                figures.begin(),
                figures.end(),
                []( const FigurePointer& first, const FigurePointer& second ) {
                    return first->getArea() < second->getArea();
                }
    );
}

Правило 4. Следите за актуальностью комментариев

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

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

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

// Наш крутой потоково-безопасный класс
class MyClass {
    …
};

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

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

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

Правило 5. Пишите комментарии только для функций и классов

Что можно сделать с очень длинной функцией, чтобы упростить работу с ней? Один из вариантов - добавить комментарии в каждый ее блок, чтобы логически выделить структуру. Например:

void veryLongFunction() {
    // Инициализация подключения к БД
    …

    // Чтение данных из БД
    …

    // Завершение сеанса работы с БД
    …

    // Инициализация сетевого подключения
    …

    // Отправка прочитанных данных из БД на удаленный сервер
    …

    // Закрытие сетевого соединения
    …
}

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

void veryLongFunction() {
    try {
        initDB();
        Data data = readDataFromDB();
        closeDB();

        initNetworkConnection();
        sendDataToServer( data );
        closeNetworkConnection();
    } catch( … ) {
        // Обработка исключений…
    }
}

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

Заключение

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

  1. Пишите комментарии на высоком уровне абстракции;
  2. Не пишите избыточные комментарии;
  3. Не устраняйте недостатки кода с помощью комментариев;
  4. Следите за актуальностью комментариев;
  5. Пишите комментарии только для функций и классов.

Похожие публикации