485 lines
13 KiB
Markdown
485 lines
13 KiB
Markdown
---
|
||
paths:
|
||
- "**/*.lua"
|
||
---
|
||
# Lua Patterns
|
||
|
||
> This file extends [common/patterns.md](../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.
|
||
|
||
```lua
|
||
-- 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+:
|
||
|
||
```lua
|
||
-- 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:
|
||
|
||
```lua
|
||
-- 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
|
||
|
||
```lua
|
||
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`:
|
||
|
||
```lua
|
||
-- 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:
|
||
|
||
```lua
|
||
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:
|
||
|
||
```lua
|
||
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`:
|
||
|
||
```lua
|
||
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:
|
||
|
||
```lua
|
||
-- 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:
|
||
|
||
```lua
|
||
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:
|
||
|
||
```lua
|
||
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:
|
||
|
||
```lua
|
||
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:
|
||
|
||
```lua
|
||
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`:
|
||
|
||
```lua
|
||
-- 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
|
||
-- 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:
|
||
|
||
```lua
|
||
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:
|
||
|
||
```lua
|
||
-- 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):
|
||
|
||
```lua
|
||
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:
|
||
|
||
```lua
|
||
-- 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 |
|