Перейти к основному содержимому

Модули и организация кода

Разработчику Архитектору

Модули и организация кода

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


require

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

local mymodule = require("mymodule")

Разбор:

  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Ключевые вызовы в фрагменте: require().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Принцип работы

  • Lua проверяет, не был ли уже загружен модуль с именем "mymodule" (в таблице package.loaded).
  • Если нет — ищется файл или C-расширение, соответствующее имени, согласно путям, заданным в package.path и package.cpath.
  • Найденный файл выполняется в глобальном окружении, но ожидается, что он вернёт таблицу — интерфейс модуля.
  • Результат кэшируется в package.loaded["mymodule"], чтобы последующие вызовы require возвращали тот же объект без повторного выполнения.

Важно: require гарантирует идемпотентность загрузки — модуль будет выполнен ровно один раз за сессию интерпретатора.


Как ищутся модули?

Lua использует шаблоны путей:

  • package.path — для Lua-файлов (например: ./?.lua;/usr/local/lua/?.lua)
  • package.cpath — для бинарных модулей (C-расширений)

Символ ? заменяется на имя модуля.

Например:

require("utils") -- может загрузить ./utils.lua или /usr/share/lua/5.4/utils.lua

Разбор:

  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Ключевые вызовы в фрагменте: require().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

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


Модуль

Модуль в Lua — это любой файл, возвращающий таблицу, которая представляет собой его публичный интерфейс.

Рекомендуемый способ создания модуля:

Код ITЗагрузка примера кода…

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Загрузка:

local math_utils = require("math_utils")
print(math_utils.square(4)) -- 16

Разбор:

  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Ключевые вызовы в фрагменте: print(), require().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Ранние версии Lua (до 5.2) предлагали функцию module(), автоматически оборачивающую код в глобальный модуль:

module("myoldmodule", package.seeall)
function foo() ... end

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Оператор .. склеивает строки (и автоматически приводит числа к строке при конкатенации).
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

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


Проблемы

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

function init()
counter = 0 -- ОШИБКА: создана глобальная переменная!
end

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

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

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

-- Блокировка создания новых глобальных переменных
setmetatable(_G, {
__newindex = function(_, name, value)
error("Попытка создать глобальную переменную '" .. name .. "'", 2)
end,
__index = function(_, name)
error("Попытка прочитать несуществующую глобальную переменную '" .. name .. "'", 2)
end
})

Разбор:

  • Оператор .. склеивает строки (и автоматически приводит числа к строке при конкатенации).
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: error(), setmetatable().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Теперь любое обращение к необъявленной глобальной переменной вызовет ошибку — отличный способ повысить надёжность. Альтернатива: использовать строгий режим через сторонние библиотеки (strict.lua) или статические анализаторы (например, luacheck). Мы как раз об этом говорили ранее.

Для логической группировки функций и значений применяйте таблицы:

Код ITЗагрузка примера кода…

Разбор:

  • Оператор .. склеивает строки (и автоматически приводит числа к строке при конкатенации).
  • return завершает функцию и может вернуть несколько значений через запятую.
  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Ключевые вызовы в фрагменте: require().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Таким образом, вы создаёте логическое пространство имён, аналогичное пакетам в Python или модулям в JavaScript.


Организация модулей

А как модули организовать? Давайте поговорим об иерархии модулей и организации проекта.

В крупных приложениях модули организуются в древовидную структуру:

project/
├── main.lua
├── utils/
│ ├── string.lua
│ ├── table.lua
│ └── index.lua
├── game/
│ ├── player.lua
│ └── world.lua
└── config.lua

Разбор:

  • Вызовы из стандартных библиотек string, table — готовые функции языка, не нужно писать их с нуля.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Пример иерархического доступа:

local str_util = require("utils.string")
local player = require("game.player")

Разбор:

  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Ключевые вызовы в фрагменте: require().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Если require("utils") — и в директории utils есть init.lua или index.lua, он будет загружен как содержимое модуля:

-- utils/init.lua
return {
string = require("utils.string"),
table = require("utils.table"),
}

Разбор:

  • return завершает функцию и может вернуть несколько значений через запятую.
  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: require().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Теперь можно писать:

local utils = require("utils")
utils.string.trim(" hello ")

Разбор:

  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Вызовы из стандартных библиотек string — готовые функции языка, не нужно писать их с нуля.
  • Ключевые вызовы в фрагменте: require().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Это стандартный паттерн для создания сборных модулей.

Lua допускает циклические зависимости между модулями, но они могут привести к неожиданному поведению.

-- a.lua
local B = require("b")
local A = { value = "A" }
function A.use_b() return B.value end
return A

-- b.lua
local A = require("a") -- a ещё не завершён!
local B = { value = "B" }
return B

Разбор:

  • return завершает функцию и может вернуть несколько значений через запятую.
  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: require().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

В момент require("a") внутри b.lua, модуль a ещё не вернул таблицу — A будет nil.

Решением будет отложенная загрузка или внедрение зависимостей.

  1. Перенос require внутрь функций:
function B.use_a()
local A = require("a")
return A.value
end

Разбор:

  • return завершает функцию и может вернуть несколько значений через запятую.
  • require("имя") загружает модуль один раз и возвращает его публичную таблицу из кэша package.loaded.
  • Ключевые вызовы в фрагменте: require().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.
  1. Инъекция зависимостей:
-- Вместо require — передача через параметры
return function(dependencies)
local A = dependencies.A
...
end

Разбор:

  • Оператор .. склеивает строки (и автоматически приводит числа к строке при конкатенации).
  • return завершает функцию и может вернуть несколько значений через запятую.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Кэширование

Модули в Lua кэшируются — каждый require возвращает один и тот же объект. Это означает, что модули по сути являются синглтонами. Если модуль хранит изменяемое состояние:

-- counter.lua
local count = 0
local M = {}
function M.inc() count = count + 1 end
function M.get() return count end
return M

Разбор:

  • return завершает функцию и может вернуть несколько значений через запятую.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

То все части программы будут делить это состояние. Это может быть полезно (логгер, конфиг), но опасно при неосторожном использовании. По возможности делайте модули stateless (без состояния), а состояние передавайте явно.

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