Ревью Scala-кода

Содержание

Ревью Scala-кода#

Чеклист используется начинающими ревьюверами при проверке Scala-кода в проектах Global ERP. Он помогает определить, на что обращать внимание при ревью: производительность, стабильность, работу с данными, использование стандартных механизмов GSF, безопасность, структуру кода и соблюдение проектных соглашений.

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

Производительность и качество кода#

Качество кода и производительность#

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

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

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

Цель: обеспечить высокую производительность и масштабируемость кода, а также предотвратить проблемы на продуктивной среде.

JVM и Scala-компилятор#

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

Не следует объявлять case class внутри class или trait. Компилятор Scala генерирует для case class вспомогательный код, включая apply, copy, equals и другие методы. При вложении в class или trait такой класс становится нестатическим внутренним классом с неявной ссылкой на экземпляр внешнего класса. Это увеличивает потребление памяти, создает дополнительные объекты и может увеличивать нагрузку на сборщик мусора.

case class следует размещать на верхнем уровне файла или внутри object. Для ограничения видимости используйте модификаторы доступа, например private[packageName].

Неоптимальный вариант:

class Btk_GroupEditPkg extends Pkg {
  case class GroupEdit_AddAttr(
    caption: String,
    fieldType: FieldType
  )

  // ...
}

Оптимальный вариант:

class Btk_GroupEditPkg extends Pkg {
  // ...
}

private[btk] case class GroupEdit_AddAttr(
  caption: String,
  fieldType: FieldType
)

Вложение case class в object допустимо, так как в этом случае создается статический вложенный класс без ссылки на экземпляр внешнего класса.

Цель: снизить объем генерируемого байт-кода, ускорить компиляцию и уменьшить накладные расходы при выполнении кода.

Метаданные и структура классов#

Именование классов#

Имя класса должно быть уникальным, на латинице, в формате <Модуль>_<Имя>. Имя указывается в единственном числе и именительном падеже, например Lbr_Book. Имена сущностей регистрозависимы и должны точно совпадать с ожиданиями фреймворка. Для имен классов используется CamelCase.

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

Именование атрибутов#

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

Цель: улучшить читаемость кода, упростить понимание структуры данных и сохранить соответствие стилю Scala и GSF.

Типы атрибутов#

Для каждого поля должно быть указано имя, тип данных и связь. Простые поля используют типы NLong, NNumber, NString, NDate и другие GSF-типы. Ссылочные поля используют Reference или VariableReference.

При ревью проверяйте, что все Reference указывают на существующие классы, тип связи выбран корректно, для переменной ссылочности используется подходящий тип, типы полей соответствуют структуре БД, а nullable-значения обрабатываются через GSF-типы.

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

Соответствие БД и метаданных#

Названия и типы полей в метаданных должны соответствовать колонкам в БД. Например, NLong должен соответствовать числовому идентификатору, NString — строковому полю, NDate — дате. При join необходимо проверять совместимость типов. Не следует полагаться на неявные преобразования идентификаторов. Если приведение действительно требуется, оно должно быть явным и обоснованным.

Цель: обеспечить корректную работу ORM, избежать ошибок типов и сохранить производительность запросов.

Связи#

Связи должны быть корректно отражены в метаданных. Поля, которые хранят ссылки, должны быть описаны как Reference или VariableReference. Reference используется для ссылки на объект одного заданного класса, а VariableReference — для переменной ссылочности, когда поле может ссылаться на объекты разных классов. Значение переменной ссылки хранится через механизм GID.

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

Цель: обеспечить целостность данных, корректную навигацию по объектной модели и работоспособность связанных функций GSF.

Хранение и извлечение гибких структур данных#

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

Цель: обеспечить эффективное хранение и извлечение гибких структур данных, а при необходимости — повысить производительность поиска.

Отсутствие дубликатов#

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

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

Конфигурация модулей#

Состав модулей и зависимости#

В project.yaml необходимо проверять секции конфигурации проекта: версии Scala и Java, параметры сборки, список модулей и настройки публикации. Для модулей должны быть указаны имя, источник, ветка и признак публикации. Также проверяется наличие проектных и пользовательских модулей, если они участвуют в сборке.

Зависимости между модулями должны быть описаны явно в build.sbt конкретного модуля. Не следует полагаться на зависимости, которые фактически доступны только за счет порядка сборки или транзитивного подключения. Вместо этого каждая необходимая зависимость указывается явно в конфигурации сборки модуля. Если для модуля используется module-info.xml, сведения о зависимостях должны быть согласованы с конфигурацией сборки.

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

Структура каталогов#

Структура каталогов должна соответствовать структуре, сгенерированной Configurator: src, resources, тестовые каталоги и другие стандартные директории.

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

Цель: облегчить навигацию по коду, упростить настройку IDE и сборочных скриптов, повысить согласованность между проектами.

Зависимости#

Зависимости между модулями должны быть описаны явно в build.sbt и продублированы в module-info.xml как метаданные. Не следует полагаться на зависимости, которые фактически доступны только за счет порядка сборки или транзитивного подключения. Вместо этого каждая необходимая зависимость указывается явно.

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

Конфигурации сборки и публикации#

Конфигурации сборки, плагины, названия модулей и подпроектов должны соответствовать соглашениям проекта. Изменения версий Scala, Java, sbt-плагинов или параметров публикации должны быть обоснованы. Такие изменения не вносятся случайно, так как они могут повлиять на совместимость и воспроизводимость сборки.

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

Цель: обеспечить стабильность и воспроизводимость процесса сборки и публикации, улучшить читаемость и поддержку проекта.

Использование стандартных компонентов GSF#

Использование сервисов GSF#

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

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

Цель: снизить вероятность ошибок, ускорить разработку, обеспечить единообразие функциональности, упростить поддержку и обновление.

Кэширование и запросы#

Если используется Shared-cache, для часто запрашиваемых сущностей проверяйте настройку cache-index в ORM. При запросах используйте .unique(), если ожидается единственная запись. Объектные запросы OQuery с tryCacheQueryResults() применяются там, где это целесообразно и не нарушает актуальность данных.

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

Цель: повысить производительность за счет снижения обращений к БД и обеспечить актуальность данных в кэше.

Логирование#

Для записи логов в БД используется LogTransaction. При большом количестве записей применяется commitByInterval(). Большой объем логов не следует писать в основной транзакции без необходимости. Вместо этого логирование выносится в отдельную лог-сессию, чтобы откат основной транзакции не приводил к потере диагностической информации и не увеличивал нагрузку на основной процесс.

Цель: изолировать логи от основной транзакции, избежать потери логов при откате, снизить вероятность конфликтов и нагрузку на БД.

Ошибки, исключения и транзакции#

Прикладные исключения#

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

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

Цель: четко разделить бизнес-ошибки и системные сбои, предотвратить скрытие серьезных проблем и нарушение целостности данных.

Объявление исключений#

Собственные исключения создаются как подклассы AppException. Для них заводится фабрика, например object ExceptionName extends ExceptionFactory(new ExceptionName(_)).

Исключения выбрасываются через throw AppException(...) или e.raise(...), если нужно сохранить стек вызовов. Произвольные исключения не используются для бизнес-ошибок. Вместо этого бизнес-ошибки оформляются как прикладные исключения, чтобы они единообразно обрабатывались фреймворком.

Цель: обеспечить единообразную обработку ошибок, упростить идентификацию типа ошибки и сохранить стек вызовов при выбрасывании исключения.

Логирование ошибок#

При необходимости логирования ошибки используется отдельная лог-транзакция. В try/catch должно быть минимальное информативное логирование. Причина исключения не должна скрываться. При этом чувствительные данные не должны попадать в лог.

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

Транзакции и зависимые задачи#

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

При этом flush() и commit() используются осмотрительно. Не следует выполнять неожиданные коммиты внутри server-side методов, если вызывающий код ожидает возможность общего отката.

Цель: снизить вероятность конфликтов, уменьшить рост WAL, улучшить общую производительность БД, облегчить откат и сохранить согласованность данных.

Структура Scala-кода#

Соглашения по стилю#

Код должен соответствовать соглашениям Scala и GSF: классы именуются в CamelCase, методы и переменные — в camelCase, имена сущностей, атрибутов и методов должны точно совпадать с объявлением. Служебные символы в идентификаторах не используются, а подчеркивание применяется только там, где оно предусмотрено соглашениями GSF, например как разделитель модуля и имени класса.

Не следует смешивать разные стили именования в одном модуле. Вместо этого используется единый стиль, принятый для Scala-кода и GSF.

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

D/A-паттерн кода#

Фреймворк генерирует две иерархии: Domain- и Application-части. Доменные классы, например ClassDpi и ClassDvi, перезаписываются при генерации и содержат сгенерированную основу. Прикладные классы, например ClassApi и ClassAvi, расширяют доменные и используются для ручной бизнес-логики.

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

Цель: обеспечить стабильность при регенерации кода и изолировать бизнес-логику от автоматически генерируемого кода.

Null-типы GSF#

Для работы со значениями из БД используются расширенные типы GSF: NLong, NNumber, NGid, NDate, NString и другие.

Арифметику с N-типами нельзя выполнять без проверки nullable-значения. Перед вычислением проверяйте значение через .isNotNull или используйте безопасную обработку nullable-типов. При сравнении nullable-значений в скриптах GSF используется оператор ===, а не стандартный ==.

Цель: защитить код от NullPointerException, обеспечить корректную обработку null и безопасность при сравнениях.

Комментарии#

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

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

Именование переменных и методов#

Переменным, методам и локальным значениям необходимо давать осознанные имена. Транслита нужно избегать. Имена должны соответствовать Scala-конвенциям и отражать назначение сущности.

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

Форматирование#

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

Цель: улучшить читаемость, облегчить понимание и сопровождение кода.

Официальная документация к API#

Для публичных методов, классов и значимых переменных оформляется scaladoc, если они используются как API или предназначены для повторного использования. Публичный API не следует оставлять без описания назначения, параметров и результата, если метод не является очевидным внутренним вспомогательным кодом.

Цель: предоставить документацию к API, облегчить поддержку и использование кода другими разработчиками.

Безопасность и надежность#

Исключение использования null#

Использование null для ссылочных типов должно быть исключено, если есть безопасная альтернатива. Вместо null используются Option, пустые экземпляры, GSF N-типы или другие безопасные представления отсутствия значения.

Цель: предотвратить NullPointerException и использовать безопасные механизмы обработки отсутствующих значений.

Обработка пустых значений#

Для значимых типов, например JObject, запрещено использовать null или приведение null.asInstanceOf[JObject]. Вместо этого используется пустой экземпляр, например JObject(), а проверка наличия данных выполняется через .nonEmpty.

Цель: предотвратить NullPointerException при работе со значимыми типами и сделать обработку пустых значений явной.

Безопасный доступ к данным#

Не следует использовать head и get, если отсутствие значения является возможным сценарием. Вместо этого используются headOption, getOption или явная обработка отсутствующего значения.

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

Обработка всех сценариев#

При ветвлении if-else должна быть обработана альтернативная ветка, если результат ветвления влияет на бизнес-логику. Для pattern matching должен быть обработан общий случай, если набор вариантов не является исчерпывающим.

Необработанные сценарии, которые могут привести к MatchError или некорректному состоянию, недопустимы. Вместо этого добавляется явная ветка else или case _ => с понятной обработкой: исключением, сообщением, логированием или безопасным значением по умолчанию.

Цель: гарантировать обработку всех возможных сценариев, предотвратить MatchError и сделать код более надежным.

Чувствительные данные#

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

Цель: защитить конфиденциальную информацию от утечки через логи и сообщения об ошибках.

Валидации#

Валидации на уровне приложения дополняют ограничения БД, но не заменяют их. Если целостность должна гарантироваться на уровне данных, соответствующее ограничение должно быть зафиксировано в БД и отражено в метаданных.

Цель: обеспечить дополнительный уровень защиты и пользовательскую обратную связь, не подменяя ограничения целостности на уровне БД.

Работа с API и ROP#

HTTP-пакеты#

Прямое обращение к HTTP-пакетам запрещено. Вместо этого используется Btk_HttpPkg.

Цель: обеспечить централизованное управление HTTP-вызовами, соблюдение стандартов безопасности и логирования.

Файлы#

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

Цель: гарантировать корректную обработку файлов, безопасность и совместимость с инфраструктурой.

Оптимизация расхода памяти#

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

Цель: предотвратить чрезмерный расход памяти, особенно при работе с большими объектами или в циклах.

Загрузка ROP по gid#

При загрузке ROP по gid используется Btk_Pkg.loadByGid, обернутый в TryApp. Полученный rop должен сопоставляться с нужным API через pattern matching, например case Btk_ClassApi(rop) =>. Загружать объект по gid без проверки типа не следует: после загрузки нужно явно проверить, что получен объект ожидаемого API.

Цель: обеспечить безопасную загрузку объектов и правильную типизацию результата.

Поиск по коду#

Для поиска по коду используются методы вида ____Api().findByMnemoCode(""), а не прямые запросы. Объекты API должны быть объявлены через lazy val.

Цель: использовать встроенные механизмы GSF, повысить читаемость и поддерживаемость кода.

Надежное получение данных#

Вместо конструкции api().load(getVar("idAnyClass").asNLong) используется безопасное получение значения через get или другой механизм, который учитывает отсутствие идентификатора. Такой подход нужен, потому что нет гарантии, что getVar вернет значение вместо null.

При этом нужно учитывать ограничение: get вернет пустое значение только если id == null. Если id задан, но в таблице нет соответствующей записи, метод загрузки все равно вернет ошибку NoObjectLoadException, так как запись не найдена.

Цель: безопасно обрабатывать отсутствие идентификатора и явно учитывать сценарий, когда идентификатор задан, но объект по нему не найден.

Работа с БД и SQL#

Изменение данных#

DML, например INSERT, UPDATE, DELETE, не должен выполняться через ASQL, если это приводит к изменению прикладных данных вне штатного механизма. Вместо этого используются штатные API, ORM/ROP или другой транзакционный механизм, предусмотренный для изменения данных в системе.

Цель: предотвратить нарушение целостности данных из-за обхода прикладной логики, проверок, аудита и обработчиков.

SQL-инъекции#

SQL-строки не должны формироваться конкатенацией с пользовательскими или внешними данными. Основной способ защиты — параметризованные запросы, при которых шаблон SQL-запроса и значения передаются отдельно. Подробнее в разделе «SQL-инъекции».

В ASQL/ATSQL параметры передаются через on, например SQL("... WHERE t.idProject = {idProject}").on(Symbol("idProject") -> idProject). В onRefresh, если параметр берется из текущей или мастер-выборки, используются бинды через :, например :idObjectType, :super$$idObjectType или :idObjectType#. Для sql"""...""" допускается интерполяция, так как она работает как bind-подстановка.

Если без интерполяции в обычной SQL-строке нельзя обойтись, значения необходимо безопасно преобразовывать. Для дат нужно явно указывать маску как при преобразовании значения в строку, так и в самом SQL-запросе, например через NDate#toNString и to_timestamp(..., 'DD.MM.YYYY HH24:MI:SS').

Цель: защитить код от SQL-инъекций и использовать безопасную передачу параметров в зависимости от типа SQL-запроса.

Ограничения целостности#

Ограничения целостности в БД, например foreign key и unique, должны соответствовать метаданным. Прикладные проверки не заменяют ограничения БД. Если целостность должна гарантироваться на уровне данных, соответствующее ограничение должно быть зафиксировано в БД и отражено в метаданных.

Цель: обеспечить корректность и согласованность данных на уровне БД.

Вложенные циклы#

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

Цель: избежать кратного роста числа запросов к БД и сохранить производительность.

Эффективное обновление кэша#

В onRefresh для обновления данных по ключу используется refreshByKey(parent). byKey(parent) не следует использовать там, где требуется именно обновить данные в кэше. В таких случаях применяйте refreshByKey(parent), чтобы получить актуальное состояние объекта.

Цель: обновить данные в кэше и повысить эффективность работы с объектами.

Предотвращение конфликтов сессии#

Если в детализации документа или LookUp-отображении используется onRefresh на основе selectStatement, при необходимости отключается автоматический сброс сессии через @FlushBefore(mode = FlushBeforeMode.Disabled). Автоматический flush не следует оставлять, если он приводит к ошибкам при создании документа с обязательными атрибутами или к нежелательным побочным эффектам при обновлении данных. В таком сценарии flush отключается на конкретном методе.

Цель: предотвратить конфликты сессии и ошибки при обновлении данных.

Архитектура и дизайн#

Эффективная инициализация полей#

При объявлении полей класса, которые не нужны сразу при создании объекта, используется lazy val. Тяжелую инициализацию не следует выполнять при создании объекта, если значение может не понадобиться. В таком случае поле инициализируется лениво при первом обращении.

Цель: улучшить производительность и избежать ненужных вычислений.

Читаемость кода установки#

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

Цель: улучшить читаемость и поддержку кода установки данных.

Изменение параметров#

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

Цель: сохранить стабильность API и предотвратить ошибки в использующем коде.

Новые параметры#

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

Цель: избежать необходимости изменять весь код, который вызывает этот метод.

Сложная логика в AVI#

Сложная бизнес-логика не должна размещаться непосредственно в операции AVI. Вместо этого логика выносится в Api, Pkg или Lib, а AVI-операция вызывает готовый метод.

Цель: улучшить поддерживаемость кода, разделить ответственности и тестировать бизнес-логику отдельно от UI-логики.

Scala-классы с компаньоном#

При создании Scala-классов с объектом-компаньоном используется решение, позволяющее проектное переопределение: def list(): List = { new List { ... } }. Структуру не следует проектировать так, что проектное переопределение становится невозможным без изменения типовой поставки. Вместо этого должна быть предусмотрена точка расширения или способ заменить поведение на проекте.

Цель: обеспечить более гибкую настройку поведения классов на уровне проекта.

Работа с атрибутами и полями#

Source-generated доступ к атрибутам#

Для доступа к атрибутам необходимо использовать конструкции, сгенерированные генератором источников, например rop.get(_.attr), а не методы getByAttrName и getAttrByName. В выборках и AVI вместо строковых имен используется объект A. Строковые методы работают с именем атрибута и используют рефлексию, поэтому ошибки могут быть обнаружены только во время выполнения.

Цель: обеспечить безопасность на этапе компиляции и предотвратить ошибки, связанные с рефлексией.

Безопасность на этапе компиляции#

В выборках при обращении к стандартным атрибутам необходимо использовать getSelfVar(A.idPerson.name) или A.idPerson.asNLong вместо getSelfVar("idPerson"). Такой подход позволяет поймать ошибку во время компиляции при удалении или переименовании атрибута.

Цель: обеспечить безопасность типов на этапе компиляции.

Хранимые поля в AVI#

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

Цель: сделать код устойчивым к переименованию атрибутов.

Корректность обновления данных#

В case class, которые возвращаются в onRefresh, атрибуты должны быть мутабельными, если UI должен обновлять их значения. Неизменяемые поля не следует использовать там, где интерфейс ожидает возможность обновления значения. В таких случаях поля объявляются через var.

Цель: обеспечить корректную работу UI и обновление данных.

Строки и JSON#

Интерполяция строк#

При сборке строк из статических и динамических элементов используется интерполяция строк, например s"User: $userName, status: $status", вместо конкатенации строк.

Цель: повысить читаемость кода и использовать более выразительный способ сборки строк.

Читаемость многострочных строк#

Для многострочных строк необходимо использовать | и .stripMargin, чтобы сохранить форматирование и структуру файла. В SQL-примерах пользовательские значения должны передаваться параметрами, например where sUserName = :userName, а не подставляться в строку через интерполяцию.

Цель: улучшить читаемость многострочных строк и не допускать небезопасной сборки SQL.

Эффективная работа со строками#

При сборке очень больших строк используется StringBuilder. Большие строки не следует собирать через pos.map(...).mkString(), если это приводит к избыточным временным объектам и расходу памяти.

Цель: обеспечить более эффективную работу с большими строками.

Выбор безопасных JSON-типов#

scala.util.parsing.json использовать нельзя. Вместо этого используется JObject.

Цель: использовать интегрированный с GSF и более безопасный тип JSON.

Предотвращение несовместимости#

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

Цель: предотвратить неожиданное поведение и ошибки при работе с JSON.

UI, AVI и списочные отображения#

Безопасное получение значений в списках#

В Avi.checkWorkAbility и других AVI-операциях списочных отображений значения полей нужно получать через getVar, getSelfVar или source-generated объект A. thisRop() не следует использовать в списочных отображениях, если список может быть пустым. Вместо этого значения берутся из текущей строки отображения.

Цель: предотвратить ошибку load id = null при работе с пустыми списками.

Поля с большим объемом данных#

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

Цель: повысить производительность UI и улучшить пользовательский опыт.

Корректная активация и деактивация операций#

Для деактивации операции при открытии выборки в методах beforeFirstOpen, beforeOpen, afterOpen, onLoadMeta или onLoadAdminMeta необходимо использовать метод DefaultRep#deactivateOper. Прямое присваивание oper.isActive = false в этих методах не используется.

Для динамического изменения активности операции в checkWorkAbility необходимо использовать метод DefaultRep#setOperActive. Прямое присваивание oper.isActive = someCondition в checkWorkAbility не используется.

Методы deactivateOper и setOperActive корректно работают при различных перекрытиях и сохраняют предсказуемое поведение операций.

Цель: использовать встроенные механизмы фреймворка для корректной активации и деактивации операций.

Управление доступностью кнопок в списках#

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

Кнопка должна оставаться в интерфейсе. Для управления её доступностью в checkWorkAbility необходимо изменять активность операции с помощью метода DefaultRep#setOperActive.

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

Структурирование настроек выборки#

Сортировка по умолчанию и передача макросов фильтрации указываются в prepareSelectStatement, а не в selectStatement. prepareSelectStatement используется для настройки выборки, а selectStatement — для формирования запроса.

Цель: улучшить структуру и читаемость кода настройки выборки.

Переходы состояний#

Переходы состояний должны определяться сравнением nOrder и условием из двух частей: из какого состояния выполняется переход и в какое состояние он идет, например nvStateFrom < 100.nn && nvStateTo >= 100.nn. Логику не следует привязывать только к конкретной записи перехода, если этот переход может быть удален или изменен.

Цель: сделать логику переходов устойчивой к изменениям структуры состояний.

Синхронизация данных с интерфейсом#

Если используется аннотация @Setter(refreshAfter = true), в конце сеттера должен выполняться selection.refreshItem(). Значение не следует изменять без обновления строки интерфейса, если после изменения пользователь должен видеть актуальное состояние.

Цель: обеспечить обновление UI после изменения значения.

Кастомные выборки#

Редактируемые кастомные выборки должны быть реализованы объектным запросом, чтобы пул изменений отправлялся в БД одной транзакцией. Редактируемую выборку не следует реализовывать так, чтобы изменения выполнялись разрозненно и не контролировались общей транзакцией.

Цель: повысить эффективность и обеспечить целостность данных.

Идентификатор в результатах выборки#

При реализации кастомных выборок на объектном запросе результат должен содержать поле id, если используется onRefreshExt. Результат без идентификатора не подходит для сценариев, где механизм обновления расширенных данных ожидает id.

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

Предпочтительные способы получения данных#

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

Цель: улучшить структуру и производительность, избежать избыточного кода.

Структура данных при переопределении#

При переопределении отображения, в котором уже используется AdditionalInfo, новые вычисляемые атрибуты не должны добавляться в новый AdditionalInfo2 без необходимости. Вместо этого в onRefresh используются несколько case-классов или единая структура получения дополнительных данных, например thisApi().byParent(getIdMaster).map { rop => (rop, getAdditionalInfo(rop), getAdditionalInfo2(rop)) }.

Цель: улучшить структуру и читаемость кода обновления данных.

Отражение и ввод-вывод#

Отражение#

Использование reflection должно быть оправдано. В общем случае оно запрещено. Вместо reflection используются типобезопасные source-generated конструкции, стандартные API и явные вызовы методов. Reflection допустим только при наличии технического обоснования, когда задачу нельзя решить штатным типобезопасным способом.

Цель: повысить безопасность типов, производительность и упростить сопровождение кода.

Выбор стабильных библиотек ввода-вывода#

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

Цель: обеспечить стабильность, совместимость и предсказуемое поведение кода при работе с вводом-выводом.

Модульные и проектные особенности#

Модуль STK#

Для получения остатков используется ru.bitec.app.stk.Stk_Pkg#getRemainsMulti. Для получения цены используется ru.bitec.app.stk.Stk_Pkg#getnPrCostConsSum. Собственные расчеты остатков или цен не следует реализовывать, если задача закрывается стандартными методами модуля STK.

Цель: использовать стандартные оптимизированные методы для получения данных из модуля STK.

Модуль ACT#

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

Цель: гарантировать корректность и полноту данных об оборотах.

Проектные модули#

При открытии MR необходимо указывать соответствующую ветку с учетом проекта, на котором выполняется разработка. Например, для проекта СНГ разработка ведется на sng-internal-dev > ветка проектного модуля gs; pdev > dev. MR не следует открывать в ветку, которая не соответствует проектному процессу. Перед созданием MR нужно проверить целевую ветку и правила проекта.

Цель: обеспечить правильную интеграцию изменений в проектную ветку.

Блокирующие замечания#

Блокирующие замечания — это ошибки, при которых MR не принимается до исправления. В разделе перечислены только критические ошибки из чеклиста. Остальные правила документа используются при ревью, но сами по себе не относятся к блокирующим замечаниям, если это не определено отдельно.

Замечание

Почему блокирующее

Как исправить

Подробнее

DML, например INSERT, UPDATE, DELETE, выполняется через ASQL

Изменение может обойти прикладную логику, проверки, аудит и обработчики

Использовать штатные API, ORM/ROP или другой транзакционный механизм изменения данных

Подробнее в разделе «Изменение данных»

Долгая транзакция захватывает большое число строк и не разбита на партии

Увеличиваются блокировки, нагрузка на WAL и риск конфликтов

Разбить обработку на логические блоки и выполнять session.commitWorkAuto() после каждой партии

Подробнее в разделе «Транзакции и зависимые задачи»

В больших выборках или отчетах массово используются getattribute, getmnemocode, getheadline

Создается чрезмерная нагрузка на БД

Получать данные заранее через запрос, join, пакетную выборку или подготовленное отображение

Подробнее в разделах «Качество кода и производительность» и «Вложенные циклы»

Арифметика с N-типами выполняется без проверки nullable-значений

Возможны ошибки времени выполнения и некорректные расчеты

Проверять значения через .isNotNull или использовать безопасную обработку nullable-типов

Подробнее в разделе «Null-типы GSF»

SQL-строка формируется конкатенацией с пользовательскими или внешними данными

Возникает риск SQL-инъекции и некорректного выполнения запроса

Использовать параметризованные запросы

Подробнее в разделе «SQL-инъекции»

Логика дублируется в разных модулях без объяснения и согласованности

Исправления придется вносить в несколько мест, повышается риск расхождения поведения

Вынести общую логику в общий метод, API, Pkg или другой переиспользуемый компонент

Подробнее в разделах «Сложная логика в AVI» и «Использование сервисов GSF»