Skip to content

TNT-Bots/lua-style-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 

Repository files navigation

Russian | English

Стиль программирования на LuaJit/Tarantool

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

В качестве ориентира были взяты лучшие практики(субъективно) из следующих руководств:


Оглавление

  1. Типы
  2. Таблицы
  3. Строки
  4. Функции
  5. Циклы
  6. Свойства
  7. Переменные
  8. Условные выражения
  9. Блоки
  10. Комментарии
  11. Пробелы
  12. Запятые
  13. Разделитель
  14. Преобразование типов
  15. Именования
  16. Конструкторы
  17. Модули
  18. Метаметоды
  19. Обработка ошибок
  20. Структура файлов
  21. Статический анализ
  22. Тестирование
  23. Производительность

  • В Lua есть следующие основные типы: nil, boolean, number, string, table, function.
-- Примитивные типы передаются по значению
local foo = 1
local bar = foo -- bar = 1, копия

-- Составные типы передаются по ссылке
local foo = { 1, 2, 3 }
local bar = foo -- bar ссылается на ту же таблицу
  • Для явной проверки типа используйте type().
-- Хорошо
if type(value) == 'function' then
  value(data)
end

if type(value) == 'table' then
  merge(result, value)
end
  • При создании модели заполняйте поля последовательно, а не через литерал.

Обоснование: Последовательное заполнение удобнее для моделей с валидацией — между присваиванием полей можно вставить проверку и обработку ошибки.

-- Плохо
local model = {
  id = tonumber(data.id),
  name = tostring(data.name),
  locked = data.locked or false,
}

-- Хорошо
local model = {}
model.id = tonumber(data.id)
model.name = tostring(data.name)
model.locked = data.locked or false
  • Для словарей и коротких конфигурационных таблиц допустим литерал.
-- Хорошо
local index = {
  name = 'primary',
  options = { parts = { 'id' }, unique = true },
}
  • Используйте setmetatable для задания __serialize = 'map', когда пустая таблица должна сериализоваться как объект, а не как массив.
setmetatable(data.social_url, { __serialize = 'map' })
  • Для копирования таблиц используйте table.copy (поверхностное) и table.deepcopy (глубокое), не пишите свою реализацию.
  • Используйте одинарные кавычки '' для строк.
-- Плохо
local name = "John"

-- Хорошо
local name = 'John'
  • При конкатенации переносите оператор на новую строку.
-- Плохо
local infoStr = 'Info: ' .. 'Host: ' .. '0.0.0.0' .. ' ' .. 'Port: ' .. 8080

-- Хорошо
local infoStr = 'Info: '
  .. 'Host: ' .. '0.0.0.0' .. ' '
  .. 'Port: ' .. 8080
  • Не используйте \z для переноса текста на новую строку.
-- Ужасно
-- Это редкий случай, когда внутри функций необходима генерация -
-- текста с учетом переноса строки, лучше избегать использования \z.
local text = '\z
First Name: Alex\z
\nLast Name: Wazowsky\z
\nAge: 27\z
'

-- Вместо этого лучше вынести текст в двойные квадратные кавычки
local text = [[
First Name: Alex
Last Name: Wazowsky
Age: 27]]
  • Всегда используйте круглые скобки при вызове функций.
-- Плохо
local m = require 'module'
print 'hello'

-- Хорошо
local m = require('module')
print('hello')
  • Предпочитайте следующий стиль функций.
-- Плохо
local foo = function()
  -- ...
end

-- Хорошо
local function foo()
  -- ...
end
  • Для методов сервисов используйте запись через точку, без self.
local service = {}

function service.create(data)
  -- ...
end

function service.read(id)
  -- ...
end

return service
  • Для методов, которым нужен доступ к self, используйте запись через двоеточие.
function Errors:field_missing(field)
  self.count = self.count + 1
  -- ...
end
  • Функции, возвращающие результат с возможной ошибкой, используют паттерн return value, error.
--- Получить пользователя по id
-- @param id (number) id пользователя
-- @return[1] user (table)
-- @return[2] err (table|nil)
function service.read(id)
  local user, err = sql(query, { id = id })
  if err then
    return nil, err
  end

  return user, nil
end
  • Избегайте длинных списков аргументов. Если параметров больше трёх — передавайте таблицу.
-- Плохо
local function createUser(name, email, role, locked, avatarUrl)
  -- ...
end

-- Хорошо
local function createUser(data)
  -- data.name, data.email, data.role, ...
end
  • Для массивов (числовой индекс) предпочитайте for i = 1, #tbl do вместо ipairs.
for i = 1, #routes do
  local route = routes[i]
  httpd:route(route.options, route.handler)
end
  • Для итерации по парам ключ-значение используйте pairs.
for key, val in pairs(tbl) do
  -- ...
end
  • Если значение индекса не используется, именуйте его _.
for _, val in pairs(tbl) do
  process(val)
end
  • Используйте точечную нотацию для доступа к известным свойствам.
-- Хорошо
local name = user.first_name
local id = user.id
  • Используйте скобочную нотацию [] для доступа по динамическому ключу.
local field = 'first_name'
local value = user[field]
  • Всегда используйте local для объявления переменных. Глобальные переменные не допускаются (за исключением Tarantool API: box, _TARANTOOL).
-- Плохо
name = 'John'

-- Хорошо
local name = 'John'
  • Объявляйте каждую переменную на отдельной строке.
-- Плохо
local a, b = 1, 2

-- Хорошо
local a = 1
local b = 2
  • Объявляйте переменные как можно ближе к месту использования.

  • Используйте box.NULL вместо nil при работе с Tarantool-данными, где нужно различать "нет значения" и "значение не задано".

if data.username ~= box.NULL then
  model.username = tostring(data.username)
end
  • Используйте ранний выход (guard clause) вместо глубокой вложенности.
-- Плохо
local function process(data)
  if data then
    if data.id then
      -- ...
    end
  end
end

-- Хорошо
local function process(data)
  if not data then
    return nil
  end

  if not data.id then
    return nil
  end

  -- ...
end
  • Для значений по умолчанию используйте or.
local locked = data.locked or false
local name = data.name or 'Unknown'
  • Для проверки наличия опционального параметра используйте and.
local init = opts and opts.init
  • При работе с данными из Tarantool используйте явные проверки на nil и box.NULL.

Обоснование: box.NULL является truthy в условных выражениях, но семантически означает "нет значения". if x then не отловит box.NULL, что приведёт к неожиданному поведению.

-- Опасно при работе с Tarantool
if data.field then
  -- box.NULL пройдёт эту проверку
end

-- Хорошо
if data.field ~= nil and data.field ~= box.NULL then
  -- ...
end
  • Предпочитайте позитивные проверки негативным, если есть обе ветки.
-- Плохо
if not thing then
  handle_missing()
else
  handle_present()
end

-- Хорошо
if thing then
  handle_present()
else
  handle_missing()
end
  • Допускается использование not (x == y) вместо x ~= y, когда это делает код читабельнее.
  • Блоки if, for, while, function всегда завершаются end на отдельной строке.
if condition then
  -- ...
end

for i = 1, 10 do
  -- ...
end
  • Не используйте однострочные блоки.
-- Плохо
if condition then return end

-- Хорошо
if condition then
  return
end
  • Язык комментариев — русский.

  • Пробел после --.

--плохо
-- хорошо
  • Документация функций оформляется в стиле LuaDoc: первая строка через ---, остальные через --.
--- Получить пользователя по id
-- @param id (number) id пользователя
-- @return[1] user (table)
-- @return[2] err (table|nil)
function service.read(id)
  • Доступные LuaDoc-теги:
Тег Формат Описание
@module @module path.to.module Идентификатор модуля
@param @param name (type) описание Параметр функции
@param[optchain] @param[optchain] name (type) описание Опциональный параметр
@return[1] @return[1] name (type) описание Первое возвращаемое значение
@return[2] @return[2] name (type) описание Второе возвращаемое значение (ошибка)
@usage блок с примером Пример использования
  • Пример с @usage:
--- Выполнить SQL-запрос
-- @param sql_query (string) SQL-строка
-- @param values (table) таблица значений
-- @return[1] result
-- @return[2] error
-- @usage
  -- local rows = sql.execute("SELECT * FROM users WHERE name = ${name}", { name = 'Alex' })
  -- if rows == nil then
  --   log.error('No rows')
  -- end
  • Для визуального разделения секций используйте блоки комментариев.
-- ----------------------------
-- Точка входа
-- ----------------------------
  • Для примеров данных (JSON-запросы/ответы) используйте блочные комментарии --[[ ... --]].
--[[ Данные для регистрации
{
  "phone": "+71234567890",
  "email": "foo@bar.baz", -- Опционально
  "password": "qwerty",
  "first_name": "John",
  "last_name": "Doe"
}
--]]
  • Используйте TODO и FIXME теги. TODO — для запланированной доработки, FIXME — для известной проблемы.
-- TODO: реализовать пагинацию
-- FIXME: неэффективный запрос, нужна оптимизация
  • Для ссылок на внешние ресурсы используйте See:.
-- See: https://www.tarantool.io/en/doc/latest/reference/...
  • Директивы luacheck оформляются как комментарии.
-- luacheck: ignore req
-- luacheck: push ignore 631
-- ... код ...
-- luacheck: pop
  • Пустая строка-комментарий -- используется как визуальный разделитель внутри блока кода.
-- Проверка что пользователя с таким телефоном нет
--
local existing, _ = authService.findByPhone(data.phone)
--
  • Не комментируйте синтаксис — читатель должен знать Lua. Комментируйте намерение и поведение.
-- Плохо
-- Присваиваем переменной x значение 1
local x = 1

-- Хорошо
-- Начальное смещение для пагинации
local x = 1
  • Используйте 2 пробела для отступов. Не используйте табуляцию.
-- Хорошо
local function foo()
  if condition then
    bar()
  end
end
  • Ставьте пробелы вокруг операторов.
-- Плохо
local x=1+2

-- Хорошо
local x = 1 + 2
  • Ставьте пробел после запятой.
-- Плохо
local tbl = {1,2,3}

-- Хорошо
local tbl = { 1, 2, 3 }
  • Не ставьте пробелы внутри скобок вызова функции.
-- Плохо
print( 'hello' )

-- Хорошо
print('hello')
  • Оставляйте одну пустую строку между функциями.

  • Максимальная длина строки — 130 символов.

  • Ставьте запятую после каждого элемента таблицы, включая последний (trailing comma).
-- Хорошо
local tbl = {
  name = 'John',
  age = 27,
}
  • В однострочных таблицах trailing comma не нужна.
local point = { x = 1, y = 2 }
  • Не используйте точку с запятой ; для разделения выражений.
-- Плохо
local a = 1; local b = 2;

-- Хорошо
local a = 1
local b = 2
  • Используйте tonumber() для явного приведения к числу.
model.id = tonumber(data.id)
  • Используйте tostring() для явного приведения к строке.
model.first_name = tostring(data.first_name)
  • Не полагайтесь на неявное приведение типов.
-- Плохо
local result = '5' + 3

-- Хорошо
local result = tonumber('5') + 3
  • camelCase — для переменных и функций.
local userId = 1
local function getUserById(id) end
  • В отдельных случаях допустимо использование snake_case для методов (например, Errors:field_missing), когда это обусловлено стилем API объекта.

  • PascalCase — для конструкторов и классов.

local function User(data) end
local function Errors() end
  • UPPER_CASE — для констант и значений перечислений.
local roles = {
  USER = 1,
  GUIDE = 2,
  ADMIN = 3,
}
  • _snake_case — для приватных методов и внутренних переменных модуля.
function MyClass._internal_method(self)
  -- ...
end
  • Для булевых функций используйте префиксы is / has.
-- Плохо
local function valid(data)
  return data.id ~= nil
end

-- Хорошо
local function isValid(data)
  return data.id ~= nil
end

local function hasErrors(self)
  return self.count > 0
end
  • Аргументы функций допустимо писать в snake_case, но предпочтительнее camelCase.
-- Допустимо
local function createUser(avatar_url)
  -- ...
end

-- Лучше
local function createUser(avatarUrl)
  -- ...
end
  • Поля объектов хранилища (Tarantool spaces) пишутся в snake_case.
-- Поля из БД
model.first_name = tostring(data.first_name)
model.avatar_url = data.avatar_url
  • Имена файлов: PascalCase для моделей и сервисов (User.lua, Errors.lua, UserService.lua), snake_case для утилит (find.lua, merge.lua).

  • Неиспользуемые переменные именуйте _.

for _, val in pairs(tbl) do
  • Избегайте однобуквенных имён, кроме итераторов (i, k, v, _).
  • Конструкторы именуются в PascalCase и возвращают model, errors.
--- Конструктор модели User
-- @param data (table) данные пользователя
-- @param opts (table) опции
-- @return[1] model (table)
-- @return[2] errors (table|nil)
local function User(data, opts)
  local init = opts and opts.init
  local errors = Errors:new({ space = 'users' })
  local model = {}

  -- Валидация и заполнение полей
  if tonumber(data.id) then
    model.id = tonumber(data.id)
  else
    errors:field_missing('id')
  end

  if errors:has_errors() then
    return nil, errors:get_compact()
  end

  return model, nil
end

return User
  • Для OOP-конструкторов используйте setmetatable и метод new.
local MyClass = {}
MyClass.__index = MyClass

function MyClass:new(params)
  local obj = {}
  obj.field = params.field
  setmetatable(obj, self)
  return obj
end
  • Каждый файл — один модуль. Модуль заканчивается одним return.
-- Утилита — возвращает функцию
local function find(tbl, value)
  -- ...
end

return find
-- Сервис — возвращает таблицу с методами
local service = {}

function service.create(data) end
function service.read(id) end
function service.update(fields, where) end
function service.delete(id) end

return service
-- Перечисление — возвращает таблицу констант
return {
  USER = 1,
  GUIDE = 2,
  ADMIN = 3,
}
  • require вызовы группируйте в начале файла.
  • Сортируйте порядок require-ов от системных/встроенных модулей к модулям приложения.
local log = require('log')
local datetime = require('datetime')
local sql = require('src.libs.sql')
local User = require('src.models.User')
local Errors = require('src.models.Errors')
local roles = require('src.enums.roles')
  • Именуйте переменную require по имени модуля. Не переименовывайте произвольно.

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

-- Плохо
local skt = require('socket')
local j = require('json')

-- Хорошо
local socket = require('socket')
local json = require('json')
  • Подключение модуля через require не должно вызывать побочных эффектов (запись в БД, вывод в консоль, изменение глобального состояния). Единственное допустимое действие — загрузка зависимостей и возврат таблицы модуля.

  • Не используйте module(). Всегда используйте паттерн с return.

  • Используйте __index для наследования.
local Errors = {}
Errors.__index = Errors
  • Используйте __serialize для контроля сериализации (Tarantool).
setmetatable(result, { __serialize = 'map' })
  • Используйте __tostring для отладочного вывода объектов.

  • Не злоупотребляйте метаметодами. Если задачу можно решить без них — решайте без них.

  • Файл организуется в следующем порядке:
-- 1. Подключение модулей (require)
local log = require('log')
local sql = require('src.libs.sql')
local User = require('src.models.User')

-- 2. Локальные переменные и константы
local MAX_RETRIES = 3

-- 3. Локальные функции (вспомогательные)
local function helper()
  -- ...
end

-- 4. Основная логика / определение модуля
local service = {}

function service.create(data)
  -- ...
end

-- 5. Возврат модуля
return service
  • Правила оформления комментариев и документации описаны в секции Комментарии.
  • Функции, которые могут завершиться с ожидаемой ошибкой (I/O, валидация, запросы к БД), возвращают nil, err.
local user, err = service.read(user_id)
if err then
  return nil, err
end
  • При ошибке API (неправильное использование функции) используйте error() или assert().
function service.read(id)
  assert(type(id) == 'number', 'id must be a number')
  -- ...
end
  • Не возвращайте больше двух значений. Если нужно вернуть дополнительные данные — оберните их в таблицу.
-- Плохо
return result, err, details, code

-- Хорошо
return result, { message = err, details = details, code = code }
  • При проверке ошибки сначала проверяйте err / nil, затем продолжайте работу.
-- Плохо
local user, err = service.read(id)
if user then
  -- ...
end

-- Хорошо
local user, err = service.read(id)
if err then
  log.error(err)
  return nil, err
end

-- работа с user
  • Для накопления ошибок валидации используйте объект ошибок.
local errors = Errors:new({ space = 'users' })

if not data.id then
  errors:field_missing('id')
end

if not data.name then
  errors:field_missing('name')
end

if errors:has_errors() then
  return nil, errors:get_compact()
end
  • Код должен проходить проверку luacheck.

  • Настройки проекта описаны в .luacheckrc.

  • Основные параметры:

Параметр Значение
Стандарт min
Макс. длина строки 130 символов
Допустимые глобальные box, _TARANTOOL, p
  • Предупреждения luacheck, которые допустимо игнорировать (настроены в .luacheckrc):
Код Описание
411 Переопределение локальной переменной
412 Переопределение аргумента
413 Переопределение переменной цикла
421–423 Затенение (shadowing) локальных переменных
431–433 Затенение upvalue
581 Использование not (x == y)
  • Если luacheck выдаёт ложное срабатывание — используйте директивы в комментариях (см. Комментарии).
  • Тестовые файлы размещаются в директории tests/.

  • Именование тестовых файлов: *_test.lua.

  • Используйте luatest в качестве фреймворка для тестирования.

local t = require('luatest')
local g = t.group('group_name')

g.before_all(function()
  -- подготовка
end)

g.test_example = function()
  local result = service.read(1)
  t.assert_is_not(result, nil)
  t.assert_equals(result.id, 1)
end
  • Каждый тест должен быть независимым от других.

  • Тестируйте граничные случаи и ошибки, а не только "happy path".

  • Избегайте создания лишних замыканий и таблиц в циклах.
-- Плохо: новая функция на каждой итерации
for i = 1, #items do
  items[i]:on('event', function() handle(i) end)
end

-- Хорошо: функция создаётся один раз
local function handler(i)
  return function() handle(i) end
end

for i = 1, #items do
  items[i]:on('event', handler(i))
end
  • Используйте table.new(narr, nrec) для предаллокации таблиц, если известен примерный размер (LuaJIT / Tarantool).

About

TNT-bots Lua style guide

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors