Не пишите комментарии к коду!

в 12:54, , рубрики: комментарии, Программирование, философия, метки: ,

Вступление

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

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

Для понимания проблемы обратимся к Вики, а после перейдем к примерам:

Коммента́рии — пояснения к исходному тексту программы, находящиеся непосредственно внутри комментируемого кода.

Пример комментариев к коду

public static void main(String[] args) throws IOException {
    // "ua.in.link.rest.server" - name of package with classes for Jersey server
    ResourceConfig rc = new PackagesResourceConfig("ua.in.link.rest.server");
    // creating httpServer for url "http://localhost:8080"
    HttpServer httpServer = GrizzlyServerFactory.createHttpServer("http://localhost:8080", rc);    
    InetSocketAddress socketAddres = httpServer.getAddress();    
    doSomethisWithSocket(socketAddres);        // waiting for Enter before stoping the server
    System.in.read();
    httpServer.stop();
}

Константы

Давайте поглядим на данный пример. Начнем с таких строк:

    // "ua.in.link.rest.server" - name of package with classes for Jersey server
    ResourceConfig rc = new PackagesResourceConfig("ua.in.link.rest.server");

Для человека, который не работал с локальным сервером Jersey, может быть действительно неочевидно, что за строка передается в конструкторе («ua.in.link.rest.server»). И автор, наверняка, хотел сделать данную часть более понятной. Однако, теперь, если по каким-либо причинам имя пакета в конструкторе будет изменено существует вероятность того, что комментарий останется старый, а, как известно, устаревший комментарий хуже отсутствующего. Более того, как уже говорил ранее, желание вставить комментарий в код свидетельствует (почти всегда) об проблемах с кодом. В данном примере думаю понятно, что проблемой является «захардкоденная» строка пакета(«ua.in.link.rest.server»). И если ее вынести в отдельную сущность, мы будем вынуждены именовать ее, для связки ее с данным кодом. Например, мы вынесли ее как константу:

private static final String JERSEY_CLASSES_PACKAGE_NAME = "ua.in.link.rest.server";
 
public static void main(String[] args) throws IOException {
    ResourceConfig rc = new PackagesResourceConfig(JERSEY_CLASSES_PACKAGE_NAME);
    // creating httpServer for url "http://localhost:8080"
    HttpServer httpServer = GrizzlyServerFactory.createHttpServer("http://localhost:8080", rc);    
    InetSocketAddress socketAddres = httpServer.getAddress();    
    doSomethisWithSocket(socketAddres);    
    // waiting for Enter before stoping the server
    System.in.read();
    httpServer.stop();
}

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

Разделяй и властвуй

Идем далее… Обратим внимание на вот эти строки:

    // creating httpServer for url "http://localhost:8080"
    HttpServer httpServer = GrizzlyServerFactory.createHttpServer("http://localhost:8080", rc);    
    InetSocketAddress socketAddres = httpServer.getAddress();    
    doSomethisWithSocket(socketAddres);

И тут комментарий наносит непоправимую пользу=). Вроде и так понятно, что делаю строки, комментарий дописан, так как метод в данном примере решает более чем одну задачу и нужно разграничить код, очень часто это делают комментариями, как тут. Что мы в итоге имеем. Допустим, что сервер стартует в каком-либо другом месте и из данного места старт сервера необходимо убрать. Нам нужно точно знать какой код отвечает за его старт. И вот проблема. Отвечает за старт только эта строка:

    HttpServer httpServer = GrizzlyServerFactory.createHttpServer("http://localhost:8080", rc);    

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

    HttpServer httpServer = GrizzlyServerFactory.createHttpServer("http://localhost:8080", rc);    
    InetSocketAddress socketAddres = httpServer.getAddress();    
    doSomethisWithSocket(socketAddres);

Непонятно, какие строки необходимо убрать в случаи, если сервер запускается где-то на стороне. Более того, при модификации кода очень легко оставить строку:

    ResourceConfig rc = new PackagesResourceConfig(JERSEY_CLASSES_PACKAGE_NAME);

Не заметив что она так же относится к блоку создания сервера.

Давайте теперь отрефакторим данный код.

private static final String JERSEY_CLASSES_PACKAGE_NAME = "ua.in.link.rest.server";
 
private static final String SERVER_URL = "http://localhost:8080";
 
public static void main(String[] args) throws IOException {    
    HttpServer httpServer = startServer();    
    
    InetSocketAddress socketAddres = httpServer.getAddress();    
    prepareSocket(socketAddres); 
    
    // waiting for Enter before stoping the server
    System.in.read();
    httpServer.stop();
}
 
private static HttpServer startServer() {
    ResourceConfig rc = new PackagesResourceConfig(JERSEY_CLASSES_PACKAGE_NAME);
    return GrizzlyServerFactory.createHttpServer(SERVER_URL, rc);    
}
 
private static void prepareSocket(InetSocketAddress socketAddres){
    doSomethisWithSocket(socketAddres);       
}

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

private static HttpServer startServer() {
    ResourceConfig rc = new PackagesResourceConfig(JERSEY_CLASSES_PACKAGE_NAME);
    HttpServer httpServer = GrizzlyServerFactory.createHttpServer(SERVER_URL, rc);    
    InetSocketAddress socketAddres = httpServer.getAddress();    
    prepareSocket(socketAddres); 
    return httpServer;
}

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

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

private static final String JERSEY_CLASSES_PACKAGE_NAME = "ua.in.link.rest.server";
 
private static final String SERVER_URL = "http://localhost:8080";

private static final String PRESS_ENTER__TO_STOP_STRING = "Press Enter to stop server";
 
public static void main(String[] args) throws IOException {    
    HttpServer httpServer = startServer();    
    
    InetSocketAddress socketAddres = httpServer.getAddress();    
    prepareSocket(socketAddres); 
    
    userStopsServer(httpServer);
}
 
private static HttpServer startServer() {
    ResourceConfig rc = new PackagesResourceConfig(JERSEY_CLASSES_PACKAGE_NAME);
    return GrizzlyServerFactory.createHttpServer(SERVER_URL, rc);    
}
 
private static void prepareSocket(InetSocketAddress socketAddres){
    doSomethisWithSocket(socketAddres);       
}
 
private static void userStopsServer(HttpServer httpServer){
    System.out.println(PRESS_ENTER__TO_STOP_STRING + httpServer.toString());
    System.in.read();
    httpServer.stop();
}

А где нужны комментарии?

Очень удачно по поводу тестов сказано в Вики, которую я и процитирую:

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

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

И все же, иногда нету иного пути, кроме как прибегнуть к неочевидному решению. Пример этому — использование более старой версии библиотеки или @Depricated методов. Но все эти случаи на практике встречаются куда реже, чем кажется.

JavaDoc

Множество проектов, уже на пред-релизном этапе, изобилуют стандартными комментариями вида:

/**
* Имя или краткое описание объекта
* 
* Развернутое описание
* 
* @имя_дескриптора значение
* @return тип_данных
*/

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

Вместо выводов

Само собой комментарии — это весьма холиварная тема. Много, кто может полезть в открытые проекты, которыми я заведую и, указав на какой -то код, сказать — «без комментариев он не очевиден» и будет абсолютно прав. Однако, если у меня будет время заняться этой частью кода, я потрачу это время на рефакторинг а не на написание комментариев, и лишь будучи загнан в угол и не придумав как применить принцип KISS, покорно напишу комментарий, но не ранее. Так же хочется сказать, что о комментариях часто напоминают молодые программисты, которым на самом деле банально не хватает практики, что бы увидеть в том или ином коде всем известный паттерн, подход или незнание работы каких-либо библиотек. Но это не проблема (и точно не необходимость) комментариев, а просто отсутствие должного опыта.

Автор: b0noII

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js