Сегодня я хотел бы, по мотивам этого комментария, озвучить (в некотором смысле) противоположную точку зрения.
Типичные комментарии в программе
Давайте я сначала кратко опишу, что обычно говорят в книгах и учебниках про комментарии. Во-первых, комментарии используются для:- пояснения сложного (изощренного) кода - подробный комментарий позволяет разобраться, как функционирует сложный и запутанный код;
- маркеры – маркеры обычно используются программистами для временных целей, когда нужно отметить отладочные команды или незаконченные фрагменты кода; когда проект завершается, маркеры и, при необходимости, сопутствующий им код, удаляются;
- разделители – в современных проектах размеры исходных файлов могут быть чрезвычайно большими, и для того, чтобы помочь в отделении структурных частей кода также могут использоваться комментарии;
- резюмирующий комментарий – этот тип комментария поясняет, каким образом работает код и позволяет без вникания в детали реализации понять алгоритм работы;
- описание использования кода – данный тип комментария предоставляет информацию, каким образом использовать определенные переменные/процедуры/функции и т.д.;
- помещать комментарий недалеко от начала модуля для пояснения его назначения;
- помещать комментарий перед объявлением класса;
- помещать комментарий перед объявлением метода;
- избегать очевидных комментариев:
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. А с комментарием сразу видно, что это так надо.
В общем, если у вас есть кристально ясный код, но его правильность можно поставить под сомнение - добавьте комментарий, который обосновывает его корректность. Можно просто ссылку на статью или тикет в вашем баг-трекере. Это предотвратит появление бага в будущем, если кто-то решит, что это ошибочный код и "исправит" его.
На самом деле...
А на самом деле, всё вышеизложенное - не более чем один из видов рефакторинга: улучшения существующего кода. Для продолжения ознакомления с этой темой я рекомендую книгу Мартина Фаулера "Рефакторинг. Улучшение существующего кода".См. также:
Комментирование конечно нужно. Но только там, где оно действительно нужно.
ОтветитьУдалитьЯ вот например понимаю только свои комментарии. Бывает сижу, смотрю чужой код, вроде там всё и написано и расписано и мордой тыкнули и алгоритм объяснили, а я его не понимаю. И только начав с начала, дописывая где мне нужно свои комменты, разделяя код на логические блоки, я кое-как, таки разбираюсь в том, что написал автор :)
i := i + 1; // добавить к i единицу
ОтветитьУдалитьконечно это не нужно, но если написать, что это за i или сделать пояснение для чего это делается код будет более читабельным. комментарии писать на мой взгляд нужно и пусть их много, но они есть чем их просто нет.
это всего лишь мое мнение.
Пометки об исправлении ошибок скорее вредны чем бесполезны - при чтении кода на ошибку месячной давности глубоко плевать, зато есть риск сделать прокомментированное священной коровой.
ОтветитьУдалитьДля кода гораздо важнее почему эта срока корректна, а не какую ошибку ее добавление исправляет. В приведенном примере лучше прямо написать, почему надо HKEY_CURRENT_USER, а ссылку на ошибку давать при нужде в более развернутом объяснении.
Документация в коде - очень полезна, так как позволяет быстро вычищать лишний код (или не писать зря новый) при изменениях.
Особенно нуждаются в таком документировании интерфейсы - при их использовании без подробных комментариев наломать дров проще простого.
А вот разбиение кода на простые и маленькие подпрограммы вместо комментирования "что делает этот код" можно только приветствовать.
>>> зато есть риск сделать прокомментированное священной коровой
ОтветитьУдалитьНу, я не имел ввиду, что это нужно делать для каждого исправления в обязательном порядке (вроде как "правила компании") - конечно нет. Иначе, действительно, получится загромождение бесполезных комментариев. Я имел ввиду случай, когда такое указание поясняет правильность фрагмента кода (я об этом ниже говорю). Почему ссылкой - да это тоже был просто пример. Мог написать и текстом. Но чаще дать ссылку на баг - проще и короче, чем придумывать краткое пояснение.
Очень понравилась статья!
ОтветитьУдалитьКогда я начал читать пост, то подумал - автор прочел Фаулера ))
ОтветитьУдалитьВсе эти улучшения красивы, безусловно. Добиться кода, который сам себя описывает - хорошая задача. Однако, это сложно. А сложность - это время. А время - это деньги.
Поясню свою мысль про сложность.
Насколько я понимаю, автор поста - участник проекта EurekaLog.
EurekaLog - безусловно, системный код. Допускаю, что этот весьма велик (не имел пока чести ознакомиться с продуктом, хотя думаю, что надо). Системный код, как я думаю, не подвержен болезни прикладного кода - каждый день надо выдавать определенное количество кода, реализующего определенную бизнес логику.
У меня в проекте полно системного кода - я иногда сяду, причешу его, комментарии (между прочим) поудаляю. Есть модули вообще без комментов - были, но удалены за ненадобностью после многочисленных рефакторингов.
А вот прикладной код не может позволить себе многошаговый рефакторинг. Прикладной код - по сути те же регулярные выражения: написал и забыл. А вот, чтобы вспомнить, вполне можно код снабдить комментами.
Резюм. К отсутствию комментов надо стремиться. Но надо делать это не в ущерб всему остальному, например, производительности.
PS. Потом, для хорошего рефакторинга (а без него комменты не удалишь, как я сказал) нужны хорошие средства регрессивного тестирования. Как минимум нужны это тесты!!! Иногда тесты необходимы, иногда их написать сложнее в 10 раз, чем саму программу, которую всегда можно протестировать статически.
Тонко все, тонко )
Ну, Фаулера я читал достаточно давно, чтобы забыть, в какой мере сказанное согласуется с ним. Это озвучивание моей точки зрения (да, сформировавшейся не без участия Фаулера), поводом к озвучиванию которой послужил вышеуказанный комментарий.
ОтветитьУдалить>>> А вот прикладной код не может позволить себе многошаговый рефакторинг. Прикладной код - по сути те же регулярные выражения: написал и забыл.
Я думаю, что это сильно зависит от политики компании. Нацелена ли она на немедленный результат ("всё, сразу и, желательно, студентами за бесплатно") или же немного планирует вперёд. Имеет смысл вкладывать в качественный код, потому что это сокращает расходы на поддержку, исправление ошибок, дальнейшие добавления и т.п. Писать абы как можно, но недолго - в конечном итоге все косяки кода дадут о себе знать. Цена внедрения очередной возможности окажется слишком высока. К сожалению, подобный подход также означает, что тестов у вас нет. И улучшить код будет очень сложно и очень дорого. Признаться, понятия не имею, что обычно делают в таких ситуациях. Видимо, постулируют: это мы сделать не можем, давайте вы руками будете делать?
P.S. Скажем так: со всех предыдущих мест работы я сваливал, когда технических долгов становилось так много, что дальше работать становилось невозможно. Я не думаю, что руководство это понимало. А на мои попытки сказать: ну, мне нужно время на переработку, обычно я встречал недоумение или "но мы не можем это позволить! все сроки горят!". Окей, свалить в таких условиях, может, и не слишком этично, но...
ОтветитьУдалить...ладно, быть может это попросту означает, что я системщик, не способный писать "прикладной код по российским условиям", да...
ОтветитьУдалитьСчитаю абсолютно необходимым комментировать интерфейсные части своих классов, описывая в комментариях как пользоваться классом и его свойствами. Часто бывает так, что реализация класса подразумевает определенные соглашения об инициализации определенных свойств перед вызовом определенных методов. В этом случае прийдется много читать исходный код для понимания логики работы класса. а если есть документация - получается попроще.
ОтветитьУдалитьКроме того, некоторые названия свойств и методов пишутся людьми, которые знают английский язык не очень. Поэтому догадаться что именно делает метод или за что отвечает свойство часто можно только по комментариям!
>>А вот прикладной код не может позволить себе многошаговый рефакторинг.
ОтветитьУдалить>>Прикладной код - по сути те же регулярные выражения: написал и забыл. А вот, чтобы вспомнить, вполне можно код снабдить комментами.
Вообще-то из первого следует не второе, а его противоположность. Прикладной код из-за необходимости непрерывного добавления функционала должен писаться особенно тщательно. А за write only - анальная кара еще на стадии ревью.
>>Все эти улучшения красивы, безусловно. Добиться кода, который сам себя описывает - хорошая задача. Однако, это сложно. А сложность - это время. А время - это деньги.
А деньги уходят на отладку, тестирование, внедрение, сопровождение... причем на порядок бОльшие, чем на тщательное кодирование.
>>Часто бывает так, что реализация класса подразумевает определенные соглашения об инициализации определенных свойств перед вызовом определенных методов.
ОтветитьУдалитьЗа такое разработчика тоже надо карать еще на ревью. Необходимость комментариев к полям и методам в другом - дабы можно было легко удалять ненужный код или избежать добавления дублей при модификациях
>некоторые названия свойств и методов пишутся людьми, которые знают английский язык не очень
ОтветитьУдалитьРусские идентификаторы спешат на помощь!
Так получается, что когда я оставляю комментарии на посты о том, как надо писать код, многие принимают меня за человека, достойного жестокой кары (анальной, как было предложено выше).
ОтветитьУдалитьЯ люблю писать красиво и люблю свой код. Но, согласитесь, у опытного разработчика есть внутреннее чувство, что вот этот конкретный код, будучи написанным раз, таким и останется до скончания проекта. Есть, конечно, код, который надо, видимо, будет дописываться и не раз. Но трудозатраты на абстрагивание и обобщение кода зачастую очень высоки. Нельзя каждый код писать как последний код в твоей жизни - будет еще много взможностей написать красивый код, а вот этот конкретный код поживет и так.
В качестве поясняющей аналогии могу привести пример того, как люди учатся играть музыку. Они играют простые вещи и переходят к более сложным. При этом простые вещи они в большей части никогда не повторяют, а играют уже новые и сложные вещи. Т.е. я хочу пояснить - нельзя ставить задачу доводить каждый код до совершенства - надо и дальше жить и работать: еще много чего интересного можно написать на сэкономленное время.
Поэтому меткий коммент не так и плох. Хотя, конечно, надо знать меру.
> Но, согласитесь, у опытного разработчика есть внутреннее чувство, что вот этот конкретный код, будучи написанным раз, таким и останется до скончания проекта.
ОтветитьУдалитьТакое чувство есть, когда код обладает свойствами нечитабельности и хрупкости. В результате мучает острое желание убить его и переписать, которое не реализуется, так как времени на это никто не дает.
Или когда код с самого начала написан настолько хорошо, что в большинстве случаев для расширения функционала не требует исправления (достаточно писать новый код).
>Но трудозатраты на абстрагивание и обобщение кода зачастую очень высоки. Нельзя каждый код писать как последний код в твоей жизни - будет еще много взможностей написать красивый код, а вот этот конкретный код поживет и так.
Затраты на чтение, отладку, сопровождение и, боже упаси, повторное использование write only кода перебьют любые трудности при написании. Код не обязательно должен быть абстрактным, но он всегда должен максимально легко читаться. Для большинства прикладных областей это вполне достижимо и в теории и на практике.
>Т.е. я хочу пояснить - нельзя ставить задачу доводить каждый код до совершенства
Доводить до совершенства и писать читабельный код - это достаточное и необходимое условия для продакшена. Write only код годится только в одноразовых прототипах.
В примере 4 хотелось бы увидеть откуда берется свойство Calculated.
ОтветитьУдалитьtax, свойсво Calculated должно устанвливаться там, где все параметры будут посчтитаны и готовы.
ОтветитьУдалитьОтлично собрал все загулявшие мысли.
ОтветитьУдалитьЯ для себя недавно обнаружил удобство подпроцедур, пишеш уже не код, а прямо таки сенарий - приятно.
Вот с именами переменных и функций бывают у людей проблемы. Не знаешь как по английски назвать - пиши траслитом, хотябы смысл будет ясен другим.
ОтветитьУдалитьБыл удручающий пример, когда в коде попадались названия переменных на немецком, видимо в школе товарищ учил именно его, а мы долго не могли вникнуть в развернувшиеся в коде баталии.
>>>свойсво Calculated должно устанвливаться там, где все параметры будут посчтитаны и готовы.
ОтветитьУдалитьДа суть примера вобщем-то понятна... Но комментарий перед функцией это своего рода requirements. Если уж на то пошло, то мне кажется именно в функции TFormatter.FormatLine следует устанавливать свойство Calculated, т.е. должна вызываться функция которая проверяет каждый параметр...
Книга Рихтера у меня - единственная настольная (та, что никогда не убирается, в отличии от других, на полку, до которой, сидя на рабочем месте, дотягиваюсь рукой).
ОтветитьУдалитьЗа полгода прочтения удалось все основные рабочие проекты оптимизировать так, что код "похудел" на треть, а быстродействие увеличилось в 2 раза и более...
Конечно, комментарии важны, но еще более "Мысль кратко": пусть сам код станет комментарием!
С примером открытия сокетов не согласен. На мой взгляд, если есть такая вот длинная процедура, которая выполняется строго сверху вниз, то лучше ее так лентой и писать, разделяя логические куски пустыми строчками и комментариями.
ОтветитьУдалитьПо двум причинам:
1. так меньше писанины.
2. Так легче понять, что происходит. Сравните два варианта - вы читаете сверху вниз, или вы постоянно прыгаете вверх-вниз. Когда проще воспринимать?
По поводу документации в коде - это очень удобно при условии, если работают две функции IDE: схлопывание блока и подсветка документации в хинте при выборе через интеллидженс. При этих условиях проблема разбухания изчезает, а профит мы имеем явный
Юрий, не согласен.
ОтветитьУдалитьВы упускаете из виду, что это был простой пример. А теперь умножьте его раз в десять или в сто, чтобы получить пример из жизни. Вам, наверное, просто не приходилось работать с кодом построенному на таких принципах - гигантские методы на несколько страниц.
Конечно, вы можете читать это последовательно, сверху-вниз и это короче, чем разделять на подпрограммы. А вот толку с этого, если когда вы дочитаете до середины, вы забудете, что было в начале? Попробуйте по такому листингу проследить правильность работы, влияние переменных и т.п.
В целом, есть такое правило, к которому стоит стремиться - метод или функция должны выполнять ровно одну задачу. Если они делают больше - нужно подумать, не лучше ли разбить их на два (или более) методов.
Когда у вас перед глазами короткая функция с одной задачей, проверить её корректность - тривиальная задача. "Ага, вот вход, вот выход, вот поля, она делает это". При этом не нужно "постоянно прыгать туда-сюда", потому что вы проверяете одну функцию. Вот когда вы проверите её - вы будете проверять следующую. При этом другой код на проверку не влияет.
Это и есть управление сложностью. Вы разбиваете сложную задачу на несколько мелких. Деление выполняется до тех пор, пока каждый шаг не будет тривиален для проверки.
Возможно, я взял не самый удачный пример. Я взял первый попавшийся под руку подходящий кусок кода.
ОтветитьУдалитьДа нормальный пример.
ОтветитьУдалитьЯ к такому написанию кода (чтобы код легче было читать, а не писать) шёл несколько лет. А когда я начал читать упомянутую книгу, как же пожалел, что не "встретил" её раньше!
На самом деле, поверьте моей практике, я время от времени возвращаюсь к коду, который был написан n-лет назад разными программистами. Читать код, в котором одни вызовы незнакомого API - это просто жуть. Добавить в такой код элементарный функционал превращается в:
а) изучение API и справки,
б) попутно подмечая спорные моменты (насчёт проверок ошибок и возвращаемых результатов,
в) выделение блоков в короткие процедуры (ну т.е. рефакторинг уже),
г) собственно реализация нового функционала.
Так давайте же сразу писать как надо! И тогда сопровождение кода превратиться из рутины в любимую работу.
умник, однако... а еще очки надел!
ОтветитьУдалить