13 KiB
paths
| paths | |
|---|---|
|
Lua Patterns
This file extends common/patterns.md with Lua specific content.
Module Pattern
The standard Lua module returns a table as its public API. All internal state and helpers are file-local. This is the foundation of all Lua architecture.
-- mymodule.lua
local M = {}
-- Private state — invisible outside this file
local _cache = {}
local _initialized = false
-- Private helper — underscore prefix signals internal use
local function _buildCacheKey(id)
return "key_" .. tostring(id)
end
-- Public API — the only things callers can access
function M.get(id)
return _cache[_buildCacheKey(id)]
end
function M.init()
if _initialized then return end
_initialized = true
-- one-time setup
end
return M
Legacy Module Styles (5.0 and 5.1)
In Lua 5.0, setfenv was used directly to create module environments (there was no module() function). In Lua 5.1, the module() built-in was introduced. Both are deprecated/removed in 5.2+:
-- LEGACY 5.0 style — setfenv only (no module() function exists in 5.0)
local M = {}
setfenv(1, M) -- Sets the current function's environment to M
function get(id) -- Automatically scoped to M
return cache[id]
end
return M
-- LEGACY 5.1 style — module() function (introduced in 5.1, deprecated in 5.2)
module("mymodule") -- Creates a global table, sets the function env
function get(id) -- Automatically added to the module table
return cache[id]
end
-- MODERN style (5.0+) — return-table pattern works on ALL versions
local M = {}
function M.get(id) return cache[id] end
return M
Rule: Always use the return-table pattern. It works on Lua 5.0–5.4, does not depend on setfenv or module(), and does not pollute the global namespace.
Loader / ImportModule Pattern
In large addon systems, a central loader avoids circular require() chains by acting as a module registry with lazy resolution:
-- QuestieLoader.lua
local Loader = {}
local _modules = {}
function Loader:CreateModule(name)
local mod = {}
_modules[name] = mod
return mod
end
function Loader:ImportModule(name)
return _modules[name]
end
-- Usage in a module file:
local QuestieDB = QuestieLoader:CreateModule("QuestieDB")
-- Usage as a consumer:
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
This pattern breaks circular dependencies because modules register themselves at parse time but only call ImportModule at runtime (inside function bodies).
OOP / "Class" Pattern
Simulate classes and inheritance with metatables. This is the most common OOP approach in Lua.
Basic Class
local Animal = {}
Animal.__index = Animal
function Animal:New(name, sound)
return setmetatable({
name = name,
sound = sound,
}, self) -- 'self' is Animal here (or a subclass)
end
function Animal:Speak()
return self.name .. " says " .. self.sound
end
Inheritance
Chain __index through the class hierarchy. Call parent constructors with dot-notation (not colon) to avoid double-wrapping self:
-- Dog extends Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
function Dog:New(name)
-- CORRECT: dot-notation passes 'self' (Dog) explicitly
local instance = Animal.New(self, name, "Woof")
return setmetatable(instance, Dog)
end
function Dog:Fetch(item)
return self.name .. " fetches the " .. item
end
-- BAD: colon-notation would pass Dog as 'self' twice
-- local instance = Animal:New(name, "Woof") -- WRONG
Mixins
Add behavior from multiple sources without full multiple inheritance:
local Serializable = {}
function Serializable:Serialize()
local parts = {}
for k, v in pairs(self) do
table.insert(parts, tostring(k) .. "=" .. tostring(v))
end
return table.concat(parts, ";")
end
-- Mixin application
local function applyMixin(class, mixin)
for k, v in pairs(mixin) do
if class[k] == nil then -- Don't overwrite existing methods
class[k] = v
end
end
end
applyMixin(Dog, Serializable)
-- Now Dog instances have :Serialize()
Singleton Pattern
Wrap initialization in a one-time guard. Common for managers, registries, and services:
local ConfigManager = {}
local _config = nil
function ConfigManager:Get(key)
if not _config then
_config = self:_load()
end
return _config[key]
end
function ConfigManager:_load()
-- expensive one-time load
return { debug = false, maxLevel = 80 }
end
Registry / Plugin Architecture
Use a central registry for runtime plugin discovery without hard coupling. This is the pattern used by QuestiePluginAPI:
local PluginRegistry = { _plugins = {} }
function PluginRegistry:Register(name, pluginData)
assert(type(name) == "string", "Plugin name must be a string")
assert(not self._plugins[name], "Plugin already registered: " .. name)
local plugin = {
name = name,
data = pluginData or {},
stats = { QUEST = 0, NPC = 0, OBJECT = 0, ITEM = 0 },
}
self._plugins[name] = plugin
return plugin
end
function PluginRegistry:Get(name)
return self._plugins[name]
end
function PluginRegistry:IsAnyLoaded()
return next(self._plugins) ~= nil
end
Plugin-Side Registration
Plugins register at parse time (top of file, outside any event handler) so the core can discover them immediately:
-- In plugin Loader.lua (top level, not inside PLAYER_LOGIN)
local plugin = PluginRegistry:Register("WotLKDB", addonTable)
Observer / Event Bus Pattern
Decouple producers from consumers. Essential for addon systems where modules load in unpredictable order:
local EventBus = { _listeners = {} }
function EventBus:On(event, fn)
self._listeners[event] = self._listeners[event] or {}
-- Use table.insert for 5.0 compat (no # operator)
table.insert(self._listeners[event], fn)
end
function EventBus:Off(event, fn)
local listeners = self._listeners[event]
if not listeners then return end
-- Reverse iterate; table.getn for 5.0, # for 5.1+
local n = table.getn and table.getn(listeners) or #listeners
for i = n, 1, -1 do
if listeners[i] == fn then
table.remove(listeners, i)
return
end
end
end
function EventBus:Emit(event, ...)
local listeners = self._listeners[event]
if not listeners then return end
-- ipairs works on all versions (5.0+)
for _, fn in ipairs(listeners) do
fn(...) -- 5.1+: ... is an expression; 5.0: use unpack(arg) instead
end
end
Coroutine-Based Lazy Iterator
Turn expensive database scans into resumable, lazy iterations without building the full result set in memory:
local function filteredNPCs(npcData, predicate)
return coroutine.wrap(function()
for id, data in pairs(npcData) do
if data and predicate(id, data) then
coroutine.yield(id, data)
end
end
end)
end
-- Usage: processes NPCs one at a time, no intermediate table
for npcId, npcData in filteredNPCs(QuestieDB.npcData, function(id, d)
return d[2] and d[2] >= 70 -- level >= 70
end) do
processNPC(npcId, npcData)
end
Version note: coroutine.wrap is available in Lua 5.0+. However, in 5.0 you cannot yield from inside an iterator used in a generic for loop as part of a C boundary. The wrap-based pattern above works because the for loop drives resume directly.
Chunked Processing (Frame-Budgeted Work)
For operations that must not freeze the game client, split work across frames:
local function createChunkedProcessor(items, processFunc, chunkSize)
chunkSize = chunkSize or 100
local keys = {}
-- Use table.insert for 5.0 compat
for k in pairs(items) do table.insert(keys, k) end
local index = 1
local total = table.getn and table.getn(keys) or #keys
return function() -- Call this each frame/tick
local budget = math.min(index + chunkSize - 1, total)
for i = index, budget do
processFunc(keys[i], items[keys[i]])
end
index = budget + 1
return index > total -- Returns true when done
end
end
-- Usage with WoW C_Timer
local processor = createChunkedProcessor(rawData, compileRecord, 200)
local ticker
ticker = C_Timer.NewTicker(0.01, function()
if processor() then
ticker:Cancel()
print("Compilation complete!")
end
end)
Memoization / Caching
Cache expensive computations. Support cache invalidation:
local _zoneCache = {}
local _zoneCacheDirty = false
function ZoneDB:GetZoneAreaId(uiMapId)
if not _zoneCacheDirty and _zoneCache[uiMapId] ~= nil then
return _zoneCache[uiMapId]
end
local result = self:_computeZoneAreaId(uiMapId)
_zoneCache[uiMapId] = result
return result
end
function ZoneDB:InvalidateCache()
wipe(_zoneCache)
_zoneCacheDirty = false
end
Caution: Use ~= nil for the cache check, not truthiness. Cached false or 0 values are valid and must not trigger recomputation.
AddonTable Pattern (WoW-Specific)
The WoW client passes a private shared table to every file listed in the addon's .toc:
-- File 1: data.lua
local addonName, addonTable = ...
addonTable.npcData = { [1] = { "Ragnaros", 63, 1 } }
-- File 2: init.lua — same addonTable reference
local addonName, addonTable = ...
local npc = addonTable.npcData[1]
print(npc[1]) -- "Ragnaros"
Critical rule: Use addonTable as the sole inter-file communication channel. Never use _G for data sharing between files — it pollutes the namespace, causes taint, and is visible to every addon in the client.
Lua 5.0 note: The vararg ... syntax to capture addonName, addonTable works differently in 5.0. In 5.0, you must use the implicit arg table:
-- Lua 5.1+:
local addonName, addonTable = ...
-- Lua 5.0:
local addonName = arg[1]
local addonTable = arg[2]
-- Universal (works on 5.0–5.4):
local addonName = select and select(1, ...) or (arg and arg[1])
local addonTable = select and select(2, ...) or (arg and arg[2])
Proxy / Facade Pattern
Wrap a complex subsystem behind a simplified interface:
local QuestieAPI = {}
function QuestieAPI:GetQuestName(questId)
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
local data = QuestieDB and QuestieDB:GetQuest(questId)
return data and data.name or "Unknown Quest"
end
Functional Patterns
Lua supports higher-order functions. Use them for data transformations:
-- map: Transform each element (uses ipairs for 5.0 compat)
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do result[i] = fn(v, i) end
return result
end
-- filter: Keep elements matching predicate
local function filter(t, predicate)
local result = {}
for i, v in ipairs(t) do
if predicate(v, i) then table.insert(result, v) end
end
return result
end
-- reduce: Fold elements into a single value
local function reduce(t, fn, initial)
local acc = initial
for i, v in ipairs(t) do acc = fn(acc, v, i) end
return acc
end
-- Compose: Right-to-left function composition
-- Note: Uses arg.n in 5.0 since select() doesn't exist
local function compose(...)
local fns = select and { ... } or arg
local n = select and select("#", ...) or fns.n
return function(x)
for i = n, 1, -1 do x = fns[i](x) end
return x
end
end
Enumerations
Lua has no native enums. Simulate with constant tables. Optionally freeze with readOnly() (see coding-style.md):
local DebugLevel = {
CRITICAL = 1,
INFO = 2,
DEVELOP = 3,
}
local QuestFlags = {
SHARABLE = 0x0008,
DAILY = 0x1000,
WEEKLY = 0x8000,
AUTO_ACCEPT = 0x80000,
}
Weak Tables
Use weak references for caches that should not prevent garbage collection:
-- Values are weak: GC can collect them when no other references exist
local textureCache = setmetatable({}, { __mode = "v" })
function getTexture(path)
local cached = textureCache[path]
if cached then return cached end
local tex = loadTexture(path)
textureCache[path] = tex
return tex
end
| Mode | Meaning |
|---|---|
__mode = "v" |
Weak values — GC collects values with no other refs |
__mode = "k" |
Weak keys — GC collects keys with no other refs |
__mode = "kv" |
Both weak — GC collects either direction |
Ephemeron tables (5.2+): In Lua 5.2, weak tables with weak keys behave as ephemeron tables. In an ephemeron table, a value is considered reachable only if its key is reachable. If the only reference to a key comes through its value (e.g., a table used as both key and value), the entry is removed. This prevents reference cycles from keeping entries alive and is the correct behavior for cache patterns. In 5.0/5.1, a strong value could keep a weak-keyed entry alive even if the key was unreachable.
Mode reference:
| Mode | When entry is removed |
|---|---|
"v" |
Value is garbage-collectable and no other references exist |
"k" |
Key is garbage-collectable and no other references exist |
"kv" |
Either key or value is independently collectable |