All streams
Search
Write a publication
Pull to refresh
23
0.1
Виктор Поморцев @SpiderEkb

Консультант направления по разработке

Send message

Ага. Сколько может стоить 400Tb SSD Array?

И где его можно купить?

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

По ошибкам с прода идет рассылка ежедневная типа:

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

И ошибка - это не обязательно падение всего и вся. И не обязательно ошибка в коде.

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

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

Вы не работаете с БД? Текстовые файлы парсить чтобы что-то там найти... Ну такое себе.

Есть ли у вас уровни логирования?

У нас есть отдельный сервис логирования. Поскольку вся работа у нас идет с БД, то и лог - это отдельная таблица в БД.

Каждая запись содержит

  • т.н. "узел логирования" - некий литерал, означающий к какому модулю (или части модуля) относится эта строка,

  • временную метку,

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

  • уровень логирования (сейчас три уровня - ошибка, бизнес-сообщение, трейс)

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

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

  • собственно блок логируемой информации

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

Плюс есть настроечная таблица где каждому узлу логирования сопоставляется допустимый уровень. Например, если для узла 'ABC' прописан уровень 'E', то в лог будут записываться только сообщения с уровнем 'E' - ошибки. Все остальное будет игнорироваться. А если поменять 'E' на 'T', то будут писаться все сообщения - ошибки, бизнес-сообщения, трейсы... Это позволяет "на лету" включать или выключать вывод в лог нужной информации. Т.е. когда в коде прописано логирование и ошибок, и бизнес-сообщений и трейсов, через настройки можно управлять что реально будет писаться в лог, а что нет.

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

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

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

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

На госуслугах есть возможность заказать выписку с пенсионного счета. И там все очень подробно расписано.

В-третьих, Вы говорите 20 баллов от 2.3х от средней. Насколько я понимаю пенсии в России не велики даже если человек набирал по 10 баллов. При этом за него делались ломовые отчисления по ставке 20% (точно не знаю, но думаю близко).

Потому что это ЕСН - "Единый Социальный Налог". Это не только пенсия, но и медицина - полис ОМС (Обязательное Медицинское Страхование) - тоже из этих же денег. Получаешь его бесплатно в страховой кампании. Там же еще пособия всякие и матпомощи.

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

Ну и плюс моменты. Если не оформлять пенсию по достижении пенсионного возраста, то за "переработку" вводятся повышающие коэффициенты для k (индивидуальный пенсионный коэффициент - стоимость пенсионного балла) и b (фиксированная выплата).

"Ежегодная индексация пенсии" - это и есть повышение k (в этом году 145,69р) и b (8907,7р сейчас).

Т.е. условно, имея на пенсионном счету 100 баллов (это мало) получишь пенсию 149.69 * 100 + 8907.70 = 23 876.70р

Повышающие коэффициенты зависят от того, сколько лет "переработал" после наступления пенсионного возраста не оформляя пенсию

1 год - 1,07 для k и 1,056 для b

2 года - 1,15 для k и 1,12 для b

3 года - 1,24 для k и 1,19 для b

4 года - 1,34 для k и 1,27 для b

5 лет - 1,45 для k и 1,36 для b

ну и так далее до 10ти лет когда для k будет 2,32 а для b 2,11

Плюс там еще всякие добавки (в основном для женщин - 2 и более детей, рождение детей в период учебы в ВУЗе и т.п.)

У жены вышло именно так. По баллам не скажу что было, но получалось что-то около 36тр. А с переработкой в 5 лет вышло уже ближе к 60-ти...

С начислением балов - вот там сложнее. Количество балов исчисляется исходя из сумм ЕСН который работодатель отчисляет как процент от начисленной работнику з/п. И это не вычитается от з/п, а исчисляется как процент от нее. Вычитается только НДФЛ.

Формулу не скажу, на память не помню. Но есть тонкость - за год не может быть начислено более 10ти пенсионных баллов. И поэтому начиная с некоторой суммы э/п (которая в итоге даст 10 баллов) ЕСН отчисляемый работодателем становится фиксированным в том размере, который обеспечивает те самые максимальные 10 баллов в год.

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

И еще момент. Жена продолжает работать и баллы продолжают копиться. Но пенсия начисляется от того количества баллов, которое было на момент оформления. А вот когда она окончательно перестанет работать, пенсия будет пересчитана уже на окончательное количество баллов (т.е. еще подрастет).

На самом деле порядок выполнения решается и проще

      // Этапы вполнения
      dcl-enum enStages qualified;
        stageAccounts   1;
        stageNames      2;
        stageUnconfGRF  3;
        stageChngUID    4;
        stageChngCBH    5;
        stageCLJ        6;
        stageCrdHldr    7;
        stageSend       8;
      end-enum;

И далее

        for-each stage in enStages;
          select stage;
            when-is enStages.stageAccounts ; // Отбор по счетам
              procAccounts(procDte: dsError);

            when-is enStages.stageNames    ; // Отбор по изменению Имени
              procNames(procDte: dsError);

            when-is enStages.stageUnconfGRF; // Отбор по неподтвержденному ИНН
              procUnconfCRF(finnstFrom: finnstTo);

            when-is enStages.stageChngUID  ; // Отбор по изменению идентификации
              procUID(finnstFrom: finnstTo);

            when-is enStages.stageChngCBH  ; // Отбор по изменению статуса самозанятого
              procCBH(procDte);

            when-is enStages.stageCLJ      ; // Отбор из витрины ЖМ
              procCLJ(procDte);

            when-is enStages.stageCrdHldr  ; // Отбор держателей карт из МПК
              procCardHolders(procDte);

            when-is enStages.stageSend     ; // Отсылка запросов
              sendRequests(dsError);
          endsl;

          // Если на каком-то из этапов вернулась ошибка - завершаем работу
          if dsError <> *blanks;
            leave;
          endif;
        endfor;

Порядок выполнения определяется порядком следования в перечислении. Даже не значениями (они могут быть любыми - цифры 1, 2, 3..., буквы 'A', 'B', 'C', ... или вообще любые символы...) а именно порядком т.к. for-each идет по порядку объявления.

Но если порядок имеет значение - это необходимо указать в комментарии. Чтобы кому-то не пришло в голову что-то поменять "потому что так красивее".

В данном случае sendRequests всегда должен идти последним - он работает уже с заполненным на предыдущих этапах списком.

Понятно, что логика в вашем реальном приложении сильно сложнее моих упрощенных примеров. На то они и упрощенные

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

В реальной жизни опыт - это не знание "как надо делать", а понимание "как не надо делать в данном конкретном случае".

"Как надо" - это упрощенный пример. Который часто сознательно или подсознательно подгоняется под иллюстрируемую концепцию.

А в жизни приходится ровно наоборот - выбирать из нескольких возможных вариантов наиболее подходящую под данную ситуацию концепцию. И вот тут уже вступает в силу "как не надо..."

Плюс страдает читаемость. Если в функции заложена какая-то логика сложнее чем проверка флага или 2+2, а эта логика используется внутри другой логики, то чтобы понять что скрывается за "проверка_чего-то_там" - как оно проверяется, правильная ли там логика проверки, придется прыгать по коду туда-сюда.

Понятно, что если оно в 10-ти местах используется - тогда да. А ради одного места - таки нет.

Все верно. SQL сразу отбирает тех у кого

  • счет в статусе «Открыт»

  • счет открыт в день

  • маска счета входит в список разрешенных

  • исключить запрещенные маски счетов

  • ограничить выборку классом клиента ФЛ

А

  • ограничить  выборку условием пустого ИНН или не подвержденного

Реализуется в chkCRFNotConf Вот только

Дополнительные данные для проверок (например, данные из других таблиц), если они нужны, можно загрузить заблаговременно и передать их в строитель тем или иным способом

не прокатит - в "других таблицах" могут быть миллионы записей. И не всегда их нужно читать. В том же chkCRFNotConf:

      //==============================================================================
      // проверка неподтвержденности ИНН
      //==============================================================================
      dcl-proc chkCRFNotConf ;
        dcl-pi *n ind;
          dsData  likeds(t_dsData);
        end-pi;

        dcl-f FINNST20LF disk(*ext)
                         usage(*input)
                         usropn
                         keyed
                         qualified
                         static;
        dcl-f GF01LF     disk(*ext)
                         usage(*input)
                         usropn
                         keyed
                         qualified
                         static;

        dcl-ds dsGFRec       likerec(GF01LF.GFPFR:         *all);
        dcl-ds dsGFKey       likerec(GF01LF.GFPFR:         *key);
        dcl-ds dsFINNST20Key likerec(FINNST20LF.FINNSTPFR: *key);
        dcl-s  result        ind;
        dcl-s  CRF           like(t_dsData.CRF);

        // Если ИНН не определен, нужно его получить из карточки
        if dsData.CRF = cNoCRF;
          if not %open(GF01LF);
            open GF01LF;
          endif;

          dsGFKey.GFCUS = dsData.CUS;
          dsGFKey.GFCLC = dsData.CLC;

          chain %kds(dsGFKey) GF01LF.GFPFR dsGFRec;

          if %found(GF01LF);
            CRF = dsGFRec.GFCRF;
          endif;
        else;
          CRF = dsData.CRF;
        endif;

        if CRF = *blanks;
          result = *on;
        else;
          if not %open(FINNST20LF);
            open FINNST20LF;
          endif;

          dsFINNST20Key.STCUS  = dsData.CUS;
          dsFINNST20Key.STCLC  = dsData.CLC;
          dsFINNST20Key.STINN  = CRF;  
          dsFINNST20Key.STHIST = 'A';
          dsFINNST20Key.STFROM = 'Y';
          

          setll %kds(dsFINNST20Key) FINNST20LF;
          result = not %equal(FINNST20LF);
        endif;
      
        return result;
      
        begsr *pssr;
          dump;
        endsr;
      end-proc;

Во-первых, он на вход может принимать уже прочитанный ИНН. Тогда его нужно просто проверить (он может быть пустым, подтвержденным или неподтвержденным). В данном случае он "бесплатно" приходит их SQL - там все равно эта таблица задействована. Но есть выборки где она не задействована, а цеплять ее только ради ИНН дорого - она достаточно большая - более 50млн записей. В этом случае дешевле прочитать из нее одну запись по индексу прямым доступом к БД (chain).

Если ИНН в итоге пустой - все понятно. Нужно отправлять запрос. А если нет - смотрим подтвержден он или нет. Это уже другая таблица FINNST. И по ней есть индекс FINNST20LF в который входит признак подтвержденности ИНН - STFROM = 'Y'. И вот тут нам не надо читать запись из таблицы. Нам достаточно знать есть такая запись в индексе или нет - setll позиционируется на запись с заданным или меньшим значением ключа (но не читает ее из таблицы). А %equal говорит нам о том, попали на точное значение, или нет. И чтение (read) тут не нужно - лишнее время. Это уже тонкости языка (аналитик их знать не обязан, а разработчик - должен знать).

Но в целом - да. Все что можно отбираем на уровне выборки - тут баланс между полнотой критериев отбора и сложностью (и скоростью) запроса (если отбор по одной таблице, быстрее его сделать прямым доступом, но потом проверок будет больше). Что не удалось - отдельными проверками, возвращающими логику "да/нет".

А еще в ТЗ есть такой пункт

Дополнить выборку из пункта «Отбор клиентов для актуализации ИНН» уникальными записями из файла CLJMNPF где CLJDT = $Date и CLEJMNPF где CLJCUS = CLEJCUS и CLJDT = CLEJDT;

По каждой отобранной записи:

  • если есть запись в файле CLEJMNPF, то выполнить поиск записи в файле указанном в поле CLEJFILE, вызвав Get_File_Record передав на вход CLEJFILE:CLEJKYL:CLEJKYE

  • заполнить структуру customerDUL найденной записью RDKPF

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

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

А если его там еще нет - добавляем новую запись с заполненным ДУЛ.

Так что реальная логика оказывается сложнее того, что написано в ТЗ.

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

Например, сборка на нашем тестовом сервере с использованием нашей же системы сборки (на основе gradle) из VSCode выглядит в терминале примерно так:

PS C:\Users\***\GIT\vpom> chcp 65001
Active code page: 65001
PS C:\Users\***\GIT\vpom> gradle as400syncanddeploy -PreleaseFile="VPOM#170"
Starting a Gradle Daemon, 4 busy and 1 incompatible Daemons could not be reused, use --status for details

> Configure project :
Found config file source:  https://***/config_20241112152851.zip modified on 2024-11-12T16:28:51.358+0400
  setting.yml
Current plugin version: 1.1.55.6 created on 08/07/2025 11:35:03
Log location: C:\Users\***\GIT\vpom\2025-07-23_15-41-05.log
Temp files location: C:\Users\***\GIT\vpom\build
add folder <DTAQ>
add folder <USRIDX>
add folder <ERRORS>
add folder <CONTAIN>
Include packages for deploy ...
[buildinfo] Not using buildInfo properties file for this build.

> Task :as400generateMemberList   
Getting files list for release ...
 add file C:\Users\***\vpom\src\main\ERRORS\DSERR37.RPGLE
 add file C:\Users\***\GIT\vpom\src\main\ERRORS\RPGPSDS.RPGLE
 add file C:\Users\***\GIT\vpom\src\main\ERRORS\ONERROR.RPGLE
  git: ssh://git@***/vpom.git
Using enviroment from class: ***.gradle.as400pluginEnvironment.EQEnviroment
[maven][0][jar][0]pgm
srvpgm
cpysrc
command
display
printer
panelgroup
bnddir

> Task :as400validate
Validating project structure ...
OK
Validating data ...
Validation completed

> Task :as400connect
   ...
Connecting to server *** using credentials for ***
IBMi job: 441572/QUSER/QZRCSRVS
jobUserID: ***


> Task :as400enviroment
Calling RTVATTR        
Creating eviroment completed

> Task :as400sendFilesToServer
Previous copy failed or status unknown. Copying all
Add generated installer @CRVPOM to files list
Copy file C:\Users\***\GIT\vpom\src\main\ERRORS\DSERR37.RPGLE to member VPOMSRC170/DSERR37
Copy file C:\Users\***\GIT\vpom\src\main\ERRORS\RPGPSDS.RPGLE to member VPOMSRC170/RPGPSDS
Copy file C:\Users\***\GIT\vpom\src\main\ERRORS\ONERROR.RPGLE to member VPOMSRC170/ONERROR
Copy file C:\Users\***\GIT\vpom\src\main\@CRVPOM.CLLE to member VPOMSRC170/@CRVPOM

> Task :lockObjects
Allocating necessary objects for deploy ...
OK

> Task :as400disconnect
Disconnected on AS400  

> Task :as400result
BUILD SUCCESSFUL

BUILD SUCCESSFUL in 1m 4s
14 actionable tasks: 14 executed

По gradle скриптам автоматически создается программа инсталлятор которая собирает на сервере все объекты поставки.

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

В принципе, плагины для VSCode позволяют работать с кодом прямо с сервера, без копирования его на локал.

Задача решается, например, применением строителя условий

В данном случае нет. Потому что вся выборка делается одним SQL запросом

        exec sql declare curProcAccounts cursor for
                   select GF.GFCUS,
                          GF.GFCLC,
                          GF.GFCRF,
                          cast('F' as char(1))
                     from ABALPF ABAL

                     join SCPF SC
                      on (SC.SCNANC, SC.SCOAD) = (ABAL.ABALNBR, :dte)

                     join GFPF GF
                       on GF.GFCPNC = SC.SCAN

                    where ABAL.ABALMSGT          = 'FNSFMSG'
                      and ABAL.ABALOPEN          = 'Y'
                      and SC.SCCAD               = 0
                      and SC.SCAI80             <> 'Y'
                      and GET_CTP_TYPE(GF.GFCTP) = 'F'
                      and not exists (select EXCPT.EXCABAL
                                        from EXCPTPF EXCPT
                                       where EXCPT.EXCABAL  = SC.SCNANC
                                         and EXCPT.EXCGROUP = SC.SCSAC);

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

        dou lastRow;
          exec sql fetch curProcAccounts for :C_MAX_SQL_ROWS rows into :dsData;

          exsr srChkSQLError;

          lastRow = sqlGetRows(rowsRead);

          for row = 1 to rowsRead;
            // Если ИНН требует подтверждения и тип идентификации входит в список разрешенных,
            // заносим данные в список
            eval-corr dsListKey  = dsData(row);

            if chkCRFNotConf(dsData(row)) and chkUIDTPI(dsListKey);
              SKPL_InsertData(pDataList: %addr(dsListKey): %addr(dsListData): %size(cusDULt));
            endif;
          endfor;
        enddo;

нужно, чтобы либо ТЗ было окончательным (что бывает редко), либо ссылаться нужно на конкретную версию ТЗ (чего я тоже не встречал), иначе любое изменение в ТЗ, влияющее на нумерацию, прведет к тому, что комментарии начнут вводить в заблуждение

Да, без номеров, но с максимально близко к тексту ТЗ. Чтобы было можно связать кодл с ТЗ.

Скажите, а чем это лучше Rocket COBOL для Visual Studio?

И тут еще такой момент - COBOL в большинстве случаев работает не на десктопах, а на "больших машинах". Серверах с совершенно другой архитектурой. А там процесс разработки несколько иной. Пишем на локалке, потом отправляем на сервер, там запускаем сборку (с получением ошибок и предупреждений обратно на локалку)... Отладка - тоже на сервере (с подключением локалки к серверу в режиме отладки).

Если интересно - можно попробовать взять VSCode, установить IBM i Development Pack (там есть в т.ч. и COBOL, зарегистрироваться на бесплатном публичном AS/400 сервере PUB400 и попробовать как все это работает на самом деле.

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

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

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

Все верно. Но тут нет сколь бы то ни было сложной логики. Все на уровне 2+2.

Увы, но не могу привести примеры из тех ТЗ с которыми работаю (во-первых, слишком длинно - там одна функция описывается н 3-5 страницах), во-вторых, там слишком много конкретики "не для публикации".

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

  • счет в статусе «Открыт»

  • счет открыт в день

  • маска счета входит в список разрешенных

  • исключить запрещенные маски счетов

  • ограничить выборку классом клиента ФЛ

  • ограничить  выборку условием пустого ИНН или не подвержденного

И это только одна выборка. Остальные не проще.

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

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

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

Но факт в том, что аналитик не способен сразу расписать все тонкости эффективного алгоритма - он не настолько хорошо погружен в технические особенности стека.

Увы, но нет.

При работе с долгоживущими и постоянно развивающимися проектами вам на доработку придет код, написанный не вами и 5 лет назад. И ТЗ где написано что "в п.3.9.10 нужно внести вот такие изменения". И, поверьте, если в коде нет комментариев связывающих блок кода с пунктом в ТЗ, вы потратите немало времени чтобы понять - а где именно в коде нужны эти изменения внести.

А "приемочные тесты" - это вообще не про ТЗ (FSD), а про бизнес-требования (BRD). А это совсем другой документ, написанный совсем другим языком и не для разработчика, а для аналитика. И в этих тестах в код вообще никто не смотрит. Там прогоняют тестовые сценарии на предмет соответствия тому что подается на вход и что получается на выходе ожидаемым результатам (если на вход подаём это, на входе должны получить это). Ну плюс нагрузочное и интеграционное тестирование еще.

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

Про осмысленность нейминга вопросов вообще нет.

Но назначение комментариев не в том чтобы писать "тут вычисляем ставку налога" - это бессмысленный комментарий. Назначение комментария - кратко описать какую-то сложную и неочевидную логику, суть неочевидного алгоритма. Это очень облегчает понимание кода. Особенно, когда (а лично мне приходится очень много работать именно с таким кодом) на доработку (изменение бизнес-логики, оптимизация) приходит код, написанный 5 и более лет назад. А если я пишу что-то новое, свое, то всегда думаю о том, что через те же 3-5 лет кому-то другому возможно придется что-то в этом коде менять и разбираться в том, как это реализовано и почему реализовано именно так, а не иначе.

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

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

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

В моей Вселенной для поиска ошибок придумали отладку (debug). Искать причину баги глазами по коду??? Увольте.

Ошибка возникла на проде. Куда доступа у вас нет. На тестовой среде не воспроизводится. Видно ее только в joblog'е. Ошибка возникла в фоновом процессе, который запускается автоматически и автоматически же завершается (ну хорошо, там есть способы подключиться к фоновому заданию отладчиком через сервисное задание и сервисную точку останова, но то это та еще развлекуха).

Но бывают ситуации, когда задание работает в режиме реального времени, с таймаутами. И попытка подебажить его приводит к вылету по таймауту.

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

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

На одном из проектов, где я техлидил, комментарии были просто запрещены (с парой исключений), и при этом никто не страдал, в том числе новички, код был ясен и понятен.

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

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

в большинстве проектов производительность — не первое, о чём стоит беспокоиться

Ну это моя профессиональная деформация... Мне всю жизнь приходилось думать про эффективность. Ну так сложилось...

Разбиение метода на 2–3 вспомогательных почти никогда не станет узким местом.

В целом соглашусь, тут уже утрировал.

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

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

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

Синтаксически язык RPG достаточно простой и легко читаемый. Но там очень много тонкостей именно в плане эффективности. И много тонкостей в самой платформе. И логика у нас достаточно сложная может быть. И много работы с БД (это вообще основное) где все завязано на структуру данных, реализацию индексов и т.п. Т.е. не все является очевидным только по коду.

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

Просто одно другое дополняет, а не отменяет.

Неправда. Если вы даёте методам осмысленные, качественные имена, код становится сильно понятнее

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

К счастью, производительность требуется достаточно редко – я не говорю о каких-то крайних применениях ПО - управление производством, геймдев и т.д., а об "обычных" системах.

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

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

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

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

Ну т.е. Вы предлагаете в код в виде комментариев вносить требования (ТЗ)? Ну это такое.

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

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

вынести их в отдельные именованные функции может быть плохой идеей из-за большого количества совместно используемых переменных

Да, тоже хотел упомянуть, но не получилось так точно сформулировать.

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

Затруднительно - это еще мягко сказано. Писать имена переменных "параметр_который_не_может_быть_больше_5_и_меньше_1" - ну такое себе...

Наконец, исключение "чтобы объяснить почему сделано именно так, а не иначе" - очень резиновое. Ведь такие объяснения возможны в любой сколько-то нетривиальной функции

Именно. И эти объяснения опять таки в имя никак не внести. В целом это может быть краткое описание алгоритма которое сильно помогает понимать что именно делает код.

Как-то пришлось делать модуль для транслитерации. Особенность была в том, что "ЯКОВ" транслитерируется в "YAKOV", а "Яков" в "Yakov" Или "Я." в "Ya." Т.е. регистр второго знака в сочетании зависел от контекста.

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

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

Не всегда. Когда пишется что-то реально объемное и реально сложное, каждые 2-3 строки в метод выносить - только запутывать код. Не говоря уже о том, что это снижение производительности (каждый вызов метода - создание, а потом свертывание еще одного уровня стека). Если все это вас не волнует - вам повезло.

Метод - это некая законченная логическая единица. И она может быть достаточно сложной логически и неочевидной. Метод "каким клиентам нужно посылать уведомление" описывается в ТЗ на 10-15-ти страницах, а в коде выливается в 5 выборок (каждая из которых есть SQL почти на станицу по нескольким таблицам с кучей условий), а потом еще каждый элемент выборки проверяется по 3-4 дополнительным условиям (если еще и их в запрос включать, он будет непозволительно долго выполнятся т.к. станет безумно сложным).

И всегда останется вопрос - почему в методе needClientSendNotification эта самая необходимость проверяется именно по признакам А, Б и В именно в этой таблице, на не признакам Г, Д и Е в другой таблице (потому что можно и так и этак).

Вот простой пример. Нужно

Найти в таблице HDAPF наличие записи по условию
• HDACUS = CUS
• HDACLC = CLC
• HDATYP = ‘DOC’
• HDAMBN = 1,4,5
• И максимальным HDACRD

       SetGT ($Cus: $Clc: 'DOC') HDA02LF;
       readp HDA02LF;

       dow not %eof(HDA02LF) and 
           HDACUS = $Cus and 
           HDACLC = $Clc and 
           HDATYP = 'DOC';
         if HDAMBN in %list('1': '4': '5');
           @DAT = HDADAT;
           leave;
         endif;

         readp HDA02LF;
       enddo;

Чтобы понять что этот код работает правильно (а первая реакция человека будет - "а где проверка на максимальный CRD - его тут вообще нигде нет"), вам потребуется заглянуть в структуру индекса HDA02LF

И только там вы увидите что

     A                                      UNIQUE
     A          R HDAPFR                    PFILE(HDAPF)
     A          K HDACUS
     A          K HDACLC
     A          K HDATYP
     A          K HDACRD

Последнее поле - HDACRD. Т.е. все записи группы HDACUS-HDACLC-HDATYP отсортированы по возрастанию CRD. И поэтому поставив указатель на "максимальное значение в группе" (SetGT) и прочитав "запись назад" мы получим именно запись с максимальным CRD. Ну а дальше проверяем допусловие по HDAMBN и если онj не выполнятся - читаем еще назад пока не найдем нужное (а как нашли - выходим из цикла).

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

Но достаточно добавить комментарий

       // HDA02LF отсортирован по HDACRD. 
       // Посему ставим на конец цепочки CUS-CLC-DOC и идет назад пока не найдем запись с HDAMBN = 1,4,5

Как все становится очевидно.

Комментировать что делает одна строка кода малоосмысленно.

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

Или когда есть достаточно объемное ТЗ со сложной логикой. И вам нужно привязать определенные блоки кода к определенным пунктам ТЗ. Тут без комментариев не обойтись.

И да. Комментарии нужно актуализировать вместе с кодом. Ну и осмысленные имена переменных и функций тоже никто не отменял.

Information

Rating
3,121-st
Location
Екатеринбург, Свердловская обл., Россия
Works in
Date of birth
Registered
Activity