31 июля 2010 г.

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

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

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

Типичные комментарии в программе

Давайте я сначала кратко опишу, что обычно говорят в книгах и учебниках про комментарии. Во-первых, комментарии используются для:
  • пояснения сложного (изощренного) кода - подробный комментарий позволяет разобраться, как функционирует сложный и запутанный код;
  • маркеры – маркеры обычно используются программистами для временных целей, когда нужно отметить отладочные команды или незаконченные фрагменты кода; когда проект завершается, маркеры и, при необходимости, сопутствующий им код, удаляются;
  • разделители – в современных проектах размеры исходных файлов могут быть чрезвычайно большими, и для того, чтобы помочь в отделении структурных частей кода также могут использоваться комментарии;
  • резюмирующий комментарий – этот тип комментария поясняет, каким образом работает код и позволяет без вникания в детали реализации понять алгоритм работы;
  • описание использования кода – данный тип комментария предоставляет информацию, каким образом использовать определенные переменные/процедуры/функции и т.д.;
При этом рекомендуется:
  • помещать комментарий недалеко от начала модуля для пояснения его назначения;
  • помещать комментарий перед объявлением класса;
  • помещать комментарий перед объявлением метода;
  • избегать очевидных комментариев:
    i := i + 1; // добавить к i единицу
    или (сама Delphi тут не безгрешна):
    type
      TForm1 = class(TForm)
      private
        { Private declarations }
      public
        { Public declarations }
      end;
  • помнить, что вводящий в заблуждение комментарий хуже, чем его отсутствие;
  • избегать размещения в комментарий информацию, которая со временем может быть не верна;
  • избегать разукрашивания комментария;
  • стараться создавать, где это имеет смысл, один блочный комментарий доступный для понимания, чем множество разрозненных комментариев, разбросанных по коду и усложняющих его чтение; 
  • комментировать циклы, логические условия – именно эти участки кода являются ключевыми при его изучении;
  • комментировать исправление ошибок;
  • и т.п.

Мысль кратко

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

Распространённый миф: код пишется для того, чтобы его читала (выполняла) машина. Это в корне неверно. Код пишется для того, чтобы его читали люди. Именно поэтому более важно, чтобы код был читабелен и прост в понимании, чем эффективен. Эффективный код, который невозможно проверить на корректность, имеет весьма малую ценность по сравнению с гибким и очевидным кодом, который выполняется несколько медленнее. Да, это пересекается с предыдущей темой.

(регулярные выражения - пример write-only языка; единожды написав регулярное выражение, вы обязательно снабдите его комментарием - ведь вам через месяц или другому человеку будет совершенно не ясно, что же оно делает; кроме того, нет никакой возможности сходу проверить его корректность при чтении кода: вам придётся остановиться и вдумчиво анализировать его корректность; ах, да, и, конечно же, интерпретация этого выражения будет зависеть от движка - have a nice day!)

Примеры

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

Пример 1 - пояснение непонятного кода

Давайте вы попробуете угадать, что делает эта функция:
function DoSomething(const A: Extended): Extended;
begin
  Result := 6.283185307179 * A;
end;
Не сразу можно сообразить, что же тут делается, кроме очевидного и бесполезного смысла: функция умножает число на какую-то константу.

Ну, раз функция непонятна - для этого у нас есть комментарии, разве нет?
// Вычисление длины окружности
function DoSomething(const A: Extended { радиус } ): Extended;
begin
  Result := 6.283185307179 * A; // 2 * Пи * радиус
end;
Во! Совсем другое дело! Теперь стало понятно, что делает эта функция. Но зачем нам комментарии?

По сути, в функции налицо две проблемы: использование волшебных чисел и неинформативные имена. Конечно, комментарии легко решают эти проблемы, но почему бы не написать так:
function CalcCircleLength(const ARadius: Extended): Extended;
const
  Pi = 3.141592653589;
begin
  Result := 2 * Pi * ARadius;
end;
Разве при этом функция не стала ещё более очевидной, чем с использованием комментариев?

Пример 2 - разделители

Вот пример большой и сложной функции (окей, это не самая большая функция - я экономлю место и ваше время, но её размера достаточно для демонстрации):
procedure TClientSocket.Open;
var
  Blocking: Longint;
begin
  if FConnected then
    Exit;
  InternalStartup;
  FSocket := socket(PF_INET, FType, IPPROTO_IP);
  if FSocket = INVALID_SOCKET then
    Error(SysErrorMessage(WSAGetLastError));
  FAddr.sin_family := PF_INET;
  FAddr.sin_addr := LookupName(FHost);
  FAddr.sin_port := htons(FPort);
  WSAAsyncSelect(FSocket, 0, 0, 0);
  Blocking := 0;
  ioctlsocket(FSocket, FIONBIO, Blocking);
  if connect(FSocket, FAddr, SizeOf(FAddr)) <> 0 then
    Error(SysErrorMessage(WSAGetLastError));
  FConnected := FSocket <> INVALID_SOCKET;
end;
Ну, тут написано много всего, и часто в таких функциях достаточно сложно сказать, что и в какой последовательности она делает. В данном случае непросто также и быстро выделить отдельные участки кода.

Обычно для этого используется оформление кода: вы визуально "выделяете" участки кода с помощью пробелов, отступов и строк, а также (опционально) подписываете блоки. Как-то так:
procedure TClientSocket.Open;
var
  Blocking: Longint;
begin
  if FConnected then
    Exit;

  InternalStartup;

  // Инициализация сокета
  FSocket := socket(PF_INET, FType, IPPROTO_IP);
  if FSocket = INVALID_SOCKET then
    Error(SysErrorMessage(WSAGetLastError));

  FAddr.sin_family := PF_INET;
  FAddr.sin_addr := LookupName(FHost);
  FAddr.sin_port := htons(FPort);
  WSAAsyncSelect(FSocket, 0, 0, 0);

  // Настройка
  Blocking := 0;
  ioctlsocket(FSocket, FIONBIO, Blocking);

  // Собственно подключение
  if connect(FSocket, FAddr, SizeOf(FAddr)) <> 0 then
    Error(SysErrorMessage(WSAGetLastError));
  FConnected := FSocket <> INVALID_SOCKET;
end;
Да, стало понятнее и визуально легче читается. Но вы помните заголовок этого поста? Во-во. А проблема в этом коде в том, что один метод делает несколько вещей. Вот, как мы можем это исправить:
procedure TClientSocket.Open;

  procedure SocketOpen;
  begin
    FSocket := socket(PF_INET, FType, IPPROTO_IP);
    if FSocket = INVALID_SOCKET then
      Error(SysErrorMessage(WSAGetLastError));
  end;

  procedure SocketSelect;
  begin
    FAddr.sin_family := PF_INET;
    FAddr.sin_addr := LookupName(FHost);
    FAddr.sin_port := htons(FPort);
    WSAAsyncSelect(FSocket, 0, 0, 0);
  end;

  procedure SocketSetup;
  var
    Blocking: Longint;
  begin
    Blocking := 0;
    ioctlsocket(FSocket, FIONBIO, Blocking);
  end;

  procedure SocketConnect;
  begin
    if connect(FSocket, FAddr, SizeOf(FAddr)) <> 0 then
      Error(SysErrorMessage(WSAGetLastError));
    FConnected := FSocket <> INVALID_SOCKET;
  end;

begin
  if FConnected then
    Exit;

  InternalStartup;

  SocketOpen;
  SocketSelect;
  SocketSetup;
  SocketConnect;
end;
Насколько очевиднее стал код - и никаких комментариев. Заметьте, как каждая подпрограмма делает всего одну задачу.

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

Разбиение на более мелкие блоки вместо оформления комментированием - достаточно полезный инструмент, хотя по началу он и кажется "слишком громоздким": "ну посмотрите же, как он плодит мелкие функции!". Конечно, злоупотреблять этим не стоит, но часто это ложное ощущение. Попробуйте повыделять фрагменты кода в подпрограммы - и посмотрите как ваш код станет прозрачнее для понимания! Надо также заметить, что это общий принцип, применимый не только к подпрограммам, но и к объектам (вы можете разбивать один объект на несколько), модулям (разнесение огромного модуля на несколько мелких) или даже исполняемым модулям (разбить большую DLL на несколько).

Пример 3 - условия и циклы

Поехали дальше:
begin
  ...
  if (Pointer(ClassType) <> nil) and
     (IsValidBlockAddr(Pointer(ClassType) + vmtSelfPtr, (vmtCreateObject - vmtSelfPtr))) and
     (TClass((Pointer(ClassType) + vmtSelfPtr)^) = ClassType) and
     (IsValidBlockAddr(Pointer(ClassType) + vmtClassName, SizeOf(Pointer))) and
     (IsValidBlockAddr(PPAddress(Pointer(ClassType) + vmtClassName)^, 1)) and
     (IsValidBlockAddr(PPAddress(Pointer(ClassType) + vmtClassName)^ + 1, PByte(Pointer(ClassType) + vmtClassName)^)) and
     (IsValidSymbolName(ClassType.ClassName)) then
  begin
    ...
  end;
  ...
end;
Угх... увидеть такое при чтении кода равносильно удару под дых. Единственное, что можно сказать про этот код - он что-то делает с классом. Комментарии спешат на помощь:
begin
  ...
  // Является ли ClassType допустимым классом?
  if
     // Допустимый указатель...
     (Pointer(ClassType) <> nil) and
     // Читабельные данные класса...
     (IsValidBlockAddr(Pointer(ClassType) + vmtSelfPtr, (vmtCreateObject - vmtSelfPtr))) and
     // Корректное поле Self...
     (TClass((Pointer(ClassType) + vmtSelfPtr)^) = ClassType) and
     // Читабельный указатель ClassName...
     (IsValidBlockAddr(Pointer(ClassType) + vmtClassName, SizeOf(Pointer))) and
     // Читабельный размер ClassName...
     (IsValidBlockAddr(PPAddress(Pointer(ClassType) + vmtClassName)^, 1)) and
     // Читабельное значение ClassName...
     (IsValidBlockAddr(PPAddress(Pointer(ClassType) + vmtClassName)^ + 1, PByte(Pointer(ClassType) + vmtClassName)^)) and
     // Корректное значение ClassName...
     (IsValidSymbolName(ClassType.ClassName)) then
  begin
    ...
  end;
  ...
end;
Ну... хотя назначение кода прояснилось, но пока вы дочитаете его до конца, вы забудете, что вы читали до этого. Делаем как обычно:
...

  function IsValidClass(const ClassType: TClass): Boolean;
  var
    ClassPntr: PAddress;
  begin
    ClassPntr := Pointer(ClassType);
    Result :=
      // Допустимый указатель...
      (ClassPntr <> nil) and
      // Читабельные данные класса...
      (IsValidBlockAddr(ClassPntr + vmtSelfPtr, (vmtCreateObject - vmtSelfPtr))) and
      // Корректное поле Self...
      (TClass((ClassPntr + vmtSelfPtr)^) = ClassType) and
      // Читабельный указатель ClassName...
      (IsValidBlockAddr(ClassPntr + vmtClassName, SizeOf(Pointer))) and
      // Читабельный размер ClassName...
      (IsValidBlockAddr(PPAddress(ClassPntr + vmtClassName)^, 1)) and
      // Читабельное значение ClassName...
      (IsValidBlockAddr(PPAddress(ClassPntr + vmtClassName)^ + 1, PByte(ClassPntr + vmtClassName)^)) and
      // Корректное значение ClassName...
      (IsValidSymbolName(ClassType.ClassName));
  end;

begin
  ...
  if IsValidClass(ClassType) then
  begin
    ...
  end;
  ...
end;
(Замечу, что саму функцию IsValidClass тоже можно попробовать улучшить, но это выходит за рамки примера)

Насколько проще стало условие! Всего одна строка вместо почти десятка (и даже больше, если это комментировать). Код упростился и его стало проще проверять на корректность: вы можете сначала проверить корректность основной функции, а потом проверить корректность под-функции IsValidClass.

Отсюда следует простое правило: если в if, for, while или until у вас стоит нечто большее, чем идентификатор (как вариант - вызов функции) - рассмотрите возможность выделения этого кода в отдельную функцию.

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

Ещё одно использование комментариев - пояснение, как должна использоваться подпрограмма/класс. Вот один из вариантов:
// Должна вызываться только после того, как все параметры были вычислены
function TFormatter.FormatLine(...): String;
begin
  ...
end;
Не кажется ли вам, что такой вариант будет лучше?
function TFormatter.FormatLine(...): String;
begin
  Assert(Calculated);
  ...
end;
Во-первых, код описывает сам себя: функция может вызываться только когда свойство Calculated = True. Во-вторых, этот вариант имеет и другое преимущество: указанное ограничение не только описано на словах, но и насильно выполняется (принцип "если вы не хотите делать правильно - вас заставят это делать" - вообще очень правильный). Если вы ошибочно вызовете этот метод до вычисления параметров форматирования, то вместо неверных результатов вы получите ошибку (исключение).

(Я сильно завидую в этом плане Microsoft-ской студии, новые версии которой умеют форсировать достаточно сложные условия (типа "параметр три указывает размер буфера в параметре два") и просто не позволит вам вызвать метод с неверными параметрами)

Документация в коде

Ну, комментарии также используются для более подробного комментирования кода (когда в комментариях приводятся инструкции и даже философия архитектуры кода) и даже ведения документации, например:
{*------------------------------------------------------------------------------
  Конвертирует дату-время из отчёта в TDateTime.

  @param  AValue Строка, представляющая дату из отчёта. Имеет формат вида 'Sun, 25 Jan 2009 12:48:15 +0300'.
  @return Значение TDateTime (по местному времени), которое соответствует параметру AValue, или 0, если AValue не содержит корректного представления даты.
  @throws нет
  @see    GetDate
-------------------------------------------------------------------------------}
function GetDateTime(const AValue: String): TDateTime;
begin
  ...
end;
Последнее, при использовании специального оформления, позволяет автоматически генерировать файл справки, содержащий описания подпрограмм кода.

Хорошо это или плохо - момент достаточно спорный, и каждый решает его для себя сам.

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

Примечание: я поменял своё мнение относительно этого вопроса - см. этот пост.

Конечно, информация в таких комментариях весьма полезна, и её не следует выкидывать (ну, описания подобного рода вы не зафиксируете переписыванием кода, как мы делали это в примерах выше) - поэтому зафиксируйте её в файле справки. Оставьте в коде только минимально необходимые описания (по возможности избавившись от них изменением кода). "Файл справки" - в смысле не справки для пользователей, а документе для разработчиков. Да, его придётся писать вручную (что означает, что вы не сможете сгенерировать его автоматически, откуда следует и раздельное написание кода/справки и потенциальная возможность рассогласования), но зато вручную собранный "хэлп по коду" и выглядит приятнее (отшлифовкой моментов), и читается удобнее (гиперссылки, которых нет в исходниках), и не "засоряет" исходный код, который от этого только выигрывает в плане читабельности.

Но ведь комментарии зачем-то нужны?

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

Вот пример нужного комментария:
procedure NameThreadForDebugging(const ATID: Cardinal; const AThreadName: AnsiString);
type
  TThreadNameInfo = record
    InfoType: LongWord;     
    Name: PAnsiChar;    
    ThreadID: LongWord; 
    Reserved: LongWord;    
  end;
const
  cSetThreadNameExcep = $406D1388;
  DefaultInfoType     = $1000;
var
  ThreadNameInfo: TThreadNameInfo;
begin
  if IsDebuggerPresent then
  begin
    // См. "Setting a Thread Name (Unmanaged)":
    // http://msdn.microsoft.com/en-us/library/xcb2z8hs(VS.71).aspx

    FillChar(ThreadNameInfo, SizeOf(ThreadNameInfo), 0);

    ThreadNameInfo.InfoType := DefaultInfoType;
    ThreadNameInfo.Name := PAnsiChar(AThreadName);
    ThreadNameInfo.ThreadID := ATID;

    try
      RaiseException(cSetThreadNameExcep, 0, SizeOf(ThreadNameInfo) div SizeOf(LongWord), @ThreadNameInfo);
    except
    end;
  end;
end;
Данная функция устанавливает имя потока (нити) по её ID. Комментарий нужен затем, чтобы указать на допустимость кода. Дело в том, что правильный способ задания имени потока выглядит очень странно: вам нужно возбудить исключение со странным (волшебным) кодом и параметрами. Если вы не знакомы с этим способом, то можете подумать, что это хак, а не документированная возможность. Вот чтобы указать на то, что этот код допустим - и используется комментарий, который указывает ссылку на документацию, где описан этот способ. Заодно вы сможете проверить корректность кода по указанной документации.

Другой пример - пометки о исправлениях ошибок или предотвращении их появления. К примеру, исправили вы ошибку - пометили исправление:
// было:

ReadKey(HKEY_LOCAL_MACHINE, ...);

// Стало:

// http://localhost/view.php?id=85
ReadKey(HKEY_CURRENT_USER, ...);
Ошибка заключалась в чтении глобальных настроек вместо настроек пользователя. Комментарий содержит ссылку на описание этой ошибки в вашем баг-трекере (вы же используете баг-трекер, да?). Это позволяет вам узнать историю ошибки, её обсуждение и другие вещи. Кроме того, этот комментарий блокирует обратное изменение - без него кто-то может решить, что вариант с HKCU - неверный, и заменить его обратно на HKLM. А с комментарием сразу видно, что это так надо.

В общем, если у вас есть кристально ясный код, но его правильность можно поставить под сомнение - добавьте комментарий, который обосновывает его корректность. Можно просто ссылку на статью или тикет в вашем баг-трекере. Это предотвратит появление бага в будущем, если кто-то решит, что это ошибочный код и "исправит" его.

На самом деле...

А на самом деле, всё вышеизложенное - не более чем один из видов рефакторинга: улучшения существующего кода. Для продолжения ознакомления с этой темой я рекомендую книгу Мартина Фаулера "Рефакторинг. Улучшение существующего кода".

См. также:

26 комментариев:

  1. Комментирование конечно нужно. Но только там, где оно действительно нужно.

    Я вот например понимаю только свои комментарии. Бывает сижу, смотрю чужой код, вроде там всё и написано и расписано и мордой тыкнули и алгоритм объяснили, а я его не понимаю. И только начав с начала, дописывая где мне нужно свои комменты, разделяя код на логические блоки, я кое-как, таки разбираюсь в том, что написал автор :)

    ОтветитьУдалить
  2. i := i + 1; // добавить к i единицу
    конечно это не нужно, но если написать, что это за i или сделать пояснение для чего это делается код будет более читабельным. комментарии писать на мой взгляд нужно и пусть их много, но они есть чем их просто нет.

    это всего лишь мое мнение.

    ОтветитьУдалить
  3. Пометки об исправлении ошибок скорее вредны чем бесполезны - при чтении кода на ошибку месячной давности глубоко плевать, зато есть риск сделать прокомментированное священной коровой.
    Для кода гораздо важнее почему эта срока корректна, а не какую ошибку ее добавление исправляет. В приведенном примере лучше прямо написать, почему надо HKEY_CURRENT_USER, а ссылку на ошибку давать при нужде в более развернутом объяснении.
    Документация в коде - очень полезна, так как позволяет быстро вычищать лишний код (или не писать зря новый) при изменениях.
    Особенно нуждаются в таком документировании интерфейсы - при их использовании без подробных комментариев наломать дров проще простого.
    А вот разбиение кода на простые и маленькие подпрограммы вместо комментирования "что делает этот код" можно только приветствовать.

    ОтветитьУдалить
  4. >>> зато есть риск сделать прокомментированное священной коровой

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

    ОтветитьУдалить
  5. Когда я начал читать пост, то подумал - автор прочел Фаулера ))

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

    Поясню свою мысль про сложность.

    Насколько я понимаю, автор поста - участник проекта EurekaLog.

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

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

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

    Резюм. К отсутствию комментов надо стремиться. Но надо делать это не в ущерб всему остальному, например, производительности.

    PS. Потом, для хорошего рефакторинга (а без него комменты не удалишь, как я сказал) нужны хорошие средства регрессивного тестирования. Как минимум нужны это тесты!!! Иногда тесты необходимы, иногда их написать сложнее в 10 раз, чем саму программу, которую всегда можно протестировать статически.
    Тонко все, тонко )

    ОтветитьУдалить
  6. Ну, Фаулера я читал достаточно давно, чтобы забыть, в какой мере сказанное согласуется с ним. Это озвучивание моей точки зрения (да, сформировавшейся не без участия Фаулера), поводом к озвучиванию которой послужил вышеуказанный комментарий.

    >>> А вот прикладной код не может позволить себе многошаговый рефакторинг. Прикладной код - по сути те же регулярные выражения: написал и забыл.

    Я думаю, что это сильно зависит от политики компании. Нацелена ли она на немедленный результат ("всё, сразу и, желательно, студентами за бесплатно") или же немного планирует вперёд. Имеет смысл вкладывать в качественный код, потому что это сокращает расходы на поддержку, исправление ошибок, дальнейшие добавления и т.п. Писать абы как можно, но недолго - в конечном итоге все косяки кода дадут о себе знать. Цена внедрения очередной возможности окажется слишком высока. К сожалению, подобный подход также означает, что тестов у вас нет. И улучшить код будет очень сложно и очень дорого. Признаться, понятия не имею, что обычно делают в таких ситуациях. Видимо, постулируют: это мы сделать не можем, давайте вы руками будете делать?

    ОтветитьУдалить
  7. P.S. Скажем так: со всех предыдущих мест работы я сваливал, когда технических долгов становилось так много, что дальше работать становилось невозможно. Я не думаю, что руководство это понимало. А на мои попытки сказать: ну, мне нужно время на переработку, обычно я встречал недоумение или "но мы не можем это позволить! все сроки горят!". Окей, свалить в таких условиях, может, и не слишком этично, но...

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

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

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

    ОтветитьУдалить
  10. >>А вот прикладной код не может позволить себе многошаговый рефакторинг.
    >>Прикладной код - по сути те же регулярные выражения: написал и забыл. А вот, чтобы вспомнить, вполне можно код снабдить комментами.

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

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

    ОтветитьУдалить
  11. >>Часто бывает так, что реализация класса подразумевает определенные соглашения об инициализации определенных свойств перед вызовом определенных методов.

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

    ОтветитьУдалить
  12. >некоторые названия свойств и методов пишутся людьми, которые знают английский язык не очень

    Русские идентификаторы спешат на помощь!

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

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

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

    Поэтому меткий коммент не так и плох. Хотя, конечно, надо знать меру.

    ОтветитьУдалить
  14. > Но, согласитесь, у опытного разработчика есть внутреннее чувство, что вот этот конкретный код, будучи написанным раз, таким и останется до скончания проекта.

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

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

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

    >Т.е. я хочу пояснить - нельзя ставить задачу доводить каждый код до совершенства

    Доводить до совершенства и писать читабельный код - это достаточное и необходимое условия для продакшена. Write only код годится только в одноразовых прототипах.

    ОтветитьУдалить
  15. В примере 4 хотелось бы увидеть откуда берется свойство Calculated.

    ОтветитьУдалить
  16. tax, свойсво Calculated должно устанвливаться там, где все параметры будут посчтитаны и готовы.

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

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

    ОтветитьУдалить
  19. >>>свойсво Calculated должно устанвливаться там, где все параметры будут посчтитаны и готовы.
    Да суть примера вобщем-то понятна... Но комментарий перед функцией это своего рода requirements. Если уж на то пошло, то мне кажется именно в функции TFormatter.FormatLine следует устанавливать свойство Calculated, т.е. должна вызываться функция которая проверяет каждый параметр...

    ОтветитьУдалить
  20. Книга Рихтера у меня - единственная настольная (та, что никогда не убирается, в отличии от других, на полку, до которой, сидя на рабочем месте, дотягиваюсь рукой).
    За полгода прочтения удалось все основные рабочие проекты оптимизировать так, что код "похудел" на треть, а быстродействие увеличилось в 2 раза и более...
    Конечно, комментарии важны, но еще более "Мысль кратко": пусть сам код станет комментарием!

    ОтветитьУдалить
  21. С примером открытия сокетов не согласен. На мой взгляд, если есть такая вот длинная процедура, которая выполняется строго сверху вниз, то лучше ее так лентой и писать, разделяя логические куски пустыми строчками и комментариями.
    По двум причинам:
    1. так меньше писанины.
    2. Так легче понять, что происходит. Сравните два варианта - вы читаете сверху вниз, или вы постоянно прыгаете вверх-вниз. Когда проще воспринимать?

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

    ОтветитьУдалить
  22. Юрий, не согласен.

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

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

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

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

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

    ОтветитьУдалить
  23. Возможно, я взял не самый удачный пример. Я взял первый попавшийся под руку подходящий кусок кода.

    ОтветитьУдалить
  24. Да нормальный пример.
    Я к такому написанию кода (чтобы код легче было читать, а не писать) шёл несколько лет. А когда я начал читать упомянутую книгу, как же пожалел, что не "встретил" её раньше!

    На самом деле, поверьте моей практике, я время от времени возвращаюсь к коду, который был написан n-лет назад разными программистами. Читать код, в котором одни вызовы незнакомого API - это просто жуть. Добавить в такой код элементарный функционал превращается в:
    а) изучение API и справки,
    б) попутно подмечая спорные моменты (насчёт проверок ошибок и возвращаемых результатов,
    в) выделение блоков в короткие процедуры (ну т.е. рефакторинг уже),
    г) собственно реализация нового функционала.
    Так давайте же сразу писать как надо! И тогда сопровождение кода превратиться из рутины в любимую работу.

    ОтветитьУдалить
  25. Анонимный4 июля 2012 г., 19:01

    умник, однако... а еще очки надел!

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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