Capítulo 18: Atributos Dinámicos
Capítulo 18: Atributos Dinámicos
Introducción
Los atributos dinámicos representan uno de los aspectos más poderosos de Lua: la capacidad de generar propiedades bajo demanda, interceptar accesos, y crear abstracciones que responden inteligentemente a las necesidades del código. En este capítulo exploraremos técnicas avanzadas de metaprogramación que van más allá del simple uso de metatables.
__index como Función: El Motor de la Dinamicidad
Cuando __index es una función en lugar de una tabla, cada acceso a una clave inexistente se convierte en una oportunidad para computar, validar o generar valores dinámicamente.
Ejemplo REPL: Propiedades Computadas Básicas
-- Crear un objeto con propiedades computadas
local Rectangle = {}
Rectangle.__index = function(self, key)
if key == "area" then
return self.width * self.height
elseif key == "perimeter" then
return 2 * (self.width + self.height)
elseif key == "diagonal" then
return math.sqrt(self.width^2 + self.height^2)
end
-- Si no es una propiedad computada, retornar nil
return nil
end
function Rectangle.new(width, height)
local rect = {width = width, height = height}
setmetatable(rect, Rectangle)
return rect
end
-- Crear y usar rectángulo
local r = Rectangle.new(5, 3)
print(r.width) --> 5
print(r.height) --> 3
print(r.area) --> 15 (computado dinámicamente)
print(r.perimeter) --> 16 (computado dinámicamente)
print(r.diagonal) --> 5.830951894845301
-- Las propiedades NO se almacenan
print(rawget(r, "area")) --> nil
-- Se recomputan en cada acceso
r.width = 10
print(r.area) --> 30 (actualizado automáticamente)
Ejemplo REPL: Validación de Accesos
-- Tabla que solo permite acceso a claves permitidas
local function createRestrictedTable(allowedKeys)
local data = {}
local allowed = {}
for _, key in ipairs(allowedKeys) do
allowed[key] = true
end
local mt = {
__index = function(t, key)
if not allowed[key] then
error("Acceso a clave no permitida: " .. tostring(key))
end
return data[key]
end,
__newindex = function(t, key, value)
if not allowed[key] then
error("Asignación a clave no permitida: " .. tostring(key))
end
data[key] = value
end
}
return setmetatable({}, mt)
end
-- Crear tabla restringida
local config = createRestrictedTable({"host", "port", "timeout"})
config.host = "localhost"
config.port = 8080
print(config.host, config.port) --> localhost 8080
-- Esto generará error:
-- config.database = "mydb" --> Error: Asignación a clave no permitida: database
-- print(config.user) --> Error: Acceso a clave no permitida: user
Lazy Loading: Carga Diferida de Propiedades
El lazy loading permite cargar recursos costosos solo cuando se necesitan, mejorando significativamente el rendimiento de inicialización.
Ejemplo REPL: Sistema de Módulos con Lazy Loading
-- Sistema de carga diferida de módulos
local ModuleLoader = {}
function ModuleLoader.new()
local modules = {}
local loaded = {}
local mt = {
__index = function(t, moduleName)
-- Si ya está cargado, retornarlo
if loaded[moduleName] then
return loaded[moduleName]
end
-- Si está registrado, cargarlo
if modules[moduleName] then
print("Cargando módulo: " .. moduleName)
local module = modules[moduleName]()
loaded[moduleName] = module
return module
end
error("Módulo no encontrado: " .. moduleName)
end
}
local loader = setmetatable({}, mt)
-- Método para registrar módulos
function loader.register(name, factory)
modules[name] = factory
end
return loader
end
-- Crear loader
local loader = ModuleLoader.new()
-- Registrar módulos (solo funciones, no se ejecutan aún)
loader.register("math_utils", function()
print(" Inicializando math_utils...")
return {
sum = function(a, b) return a + b end,
multiply = function(a, b) return a * b end
}
end)
loader.register("string_utils", function()
print(" Inicializando string_utils...")
return {
uppercase = function(s) return s:upper() end,
reverse = function(s) return s:reverse() end
}
end)
loader.register("file_utils", function()
print(" Inicializando file_utils (operación costosa)...")
-- Simular carga costosa
local start = os.clock()
while os.clock() - start < 0.1 do end
return {
read = function(path) return "contenido de " .. path end
}
end)
-- Los módulos no se cargan hasta que se acceden
print("=== Inicio del programa ===")
-- Primer acceso: se carga
print(loader.math_utils.sum(5, 3))
--> Cargando módulo: math_utils
--> Inicializando math_utils...
--> 8
-- Segundo acceso: ya está cargado
print(loader.math_utils.multiply(4, 7))
--> 28 (sin mensaje de carga)
-- Otro módulo: se carga solo cuando se necesita
print(loader.string_utils.uppercase("hello"))
--> Cargando módulo: string_utils
--> Inicializando string_utils...
--> HELLO
-- file_utils nunca se carga si no se usa
print("=== Fin del programa ===")
Ejemplo REPL: Lazy Loading de Propiedades Individuales
-- Objeto con propiedades que se calculan y cachean
local function createLazyObject(initializers)
local cache = {}
local mt = {
__index = function(t, key)
-- Si está en cache, retornar
if cache[key] ~= nil then
return cache[key]
end
-- Si hay inicializador, ejecutar y cachear
if initializers[key] then
print("Calculando " .. key .. "...")
local value = initializers[key]()
cache[key] = value
return value
end
return nil
end
}
return setmetatable({}, mt)
end
-- Crear objeto con cálculos costosos
local data = createLazyObject({
fibonacci_1000 = function()
-- Simular cálculo costoso
local function fib(n)
if n <= 2 then return 1 end
return fib(n-1) + fib(n-2)
end
return fib(20) -- Reducido para ejemplo
end,
large_table = function()
local t = {}
for i = 1, 1000000 do
t[i] = i * i
end
return t
end,
timestamp = function()
return os.time()
end
})
-- Primera llamada: se calcula
print(data.fibonacci_1000) --> Calculando fibonacci_1000...
--> 6765
-- Segunda llamada: usa cache
print(data.fibonacci_1000) --> 6765 (sin mensaje)
-- Solo se calcula lo que se usa
print(data.timestamp) --> Calculando timestamp...
--> [número de timestamp]
Proxies: Interceptación Completa
Los proxies son objetos que interceptan todas las operaciones sobre otro objeto, permitiendo validación, logging, transformación y control de acceso.
Ejemplo REPL: Proxy Completo con Logging
-- Crear proxy que registra todos los accesos
local function createLoggingProxy(target, name)
local proxy = {}
local mt = {
__index = function(t, key)
print(string.format("[GET] %s.%s", name, key))
return target[key]
end,
__newindex = function(t, key, value)
print(string.format("[SET] %s.%s = %s", name, key, tostring(value)))
target[key] = value
end,
__pairs = function(t)
print(string.format("[ITERATE] %s", name))
return pairs(target)
end,
__tostring = function(t)
return string.format("Proxy<%s>", name)
end
}
return setmetatable(proxy, mt)
end
-- Crear objeto original
local user = {
name = "Alice",
age = 30
}
-- Crear proxy
local userProxy = createLoggingProxy(user, "user")
-- Todas las operaciones se registran
print(userProxy.name)
--> [GET] user.name
--> Alice
userProxy.email = "[email protected]"
--> [SET] user.email = [email protected]
userProxy.age = 31
--> [SET] user.age = 31
-- Iterar también se registra
for k, v in pairs(userProxy) do
print(k, v)
end
--> [ITERATE] user
--> name Alice
--> age 31
--> email [email protected]
print(userProxy)
--> Proxy<user>
Ejemplo REPL: Proxy de Validación
-- Proxy que valida tipos de datos
local function createTypedProxy(schema)
local data = {}
local mt = {
__index = function(t, key)
return data[key]
end,
__newindex = function(t, key, value)
-- Verificar si la clave está en el schema
if not schema[key] then
error("Campo desconocido: " .. key)
end
-- Validar tipo
local expectedType = schema[key]
local actualType = type(value)
if expectedType == "integer" then
if actualType ~= "number" or math.floor(value) ~= value then
error(string.format(
"Campo '%s' debe ser integer, recibido: %s",
key, actualType
))
end
elseif actualType ~= expectedType then
error(string.format(
"Campo '%s' debe ser %s, recibido: %s",
key, expectedType, actualType
))
end
data[key] = value
end
}
return setmetatable({}, mt)
end
-- Definir schema
local personSchema = {
name = "string",
age = "integer",
email = "string",
active = "boolean"
}
-- Crear objeto tipado
local person = createTypedProxy(personSchema)
-- Asignaciones válidas
person.name = "Bob"
person.age = 25
person.email = "[email protected]"
person.active = true
print(person.name, person.age, person.active)
--> Bob 25 true
-- Asignaciones inválidas (generan errores):
-- person.age = "veinticinco" --> Error: debe ser integer
-- person.age = 25.5 --> Error: debe ser integer
-- person.active = "yes" --> Error: debe ser boolean
-- person.country = "USA" --> Error: Campo desconocido
Wrappers: Extensión de Funcionalidad
Los wrappers envuelven objetos existentes agregando comportamiento sin modificar el original.
Ejemplo REPL: Wrapper de Conversión Automática
-- Wrapper que convierte strings a números automáticamente
local function createAutoConvertWrapper(target)
local wrapper = {}
local mt = {
__index = function(t, key)
local value = target[key]
-- Si es string numérico, convertir
if type(value) == "string" and tonumber(value) then
return tonumber(value)
end
return value
end,
__newindex = function(t, key, value)
target[key] = value
end
}
return setmetatable(wrapper, mt)
end
-- Datos originales con strings
local config = {
port = "8080",
timeout = "30",
host = "localhost",
debug = "true"
}
-- Wrapper con conversión automática
local configWrapper = createAutoConvertWrapper(config)
print(configWrapper.port) --> 8080 (número)
print(configWrapper.timeout) --> 30 (número)
print(configWrapper.host) --> localhost (string)
print(type(configWrapper.port)) --> number
print(type(config.port)) --> string (original no cambia)
-- Operaciones matemáticas funcionan directamente
print(configWrapper.port + 1) --> 8081
Ejemplo REPL: Wrapper de Normalización de Claves
-- Wrapper que normaliza claves (case-insensitive)
local function createCaseInsensitiveWrapper(target)
local keyMap = {}
-- Construir mapa de claves normalizadas
for key in pairs(target) do
keyMap[key:lower()] = key
end
local mt = {
__index = function(t, key)
local normalizedKey = key:lower()
local originalKey = keyMap[normalizedKey]
return originalKey and target[originalKey]
end,
__newindex = function(t, key, value)
local normalizedKey = key:lower()
local originalKey = keyMap[normalizedKey]
if originalKey then
target[originalKey] = value
else
-- Nueva clave: guardar en formato original
target[key] = value
keyMap[normalizedKey] = key
end
end
}
return setmetatable({}, mt)
end
-- Datos con claves en diferentes casos
local headers = {
ContentType = "application/json",
Authorization = "Bearer token123",
UserAgent = "MyApp/1.0"
}
local headersCI = createCaseInsensitiveWrapper(headers)
-- Acceso case-insensitive
print(headersCI.contenttype) --> application/json
print(headersCI.AUTHORIZATION) --> Bearer token123
print(headersCI.useragent) --> MyApp/1.0
-- Modificación case-insensitive
headersCI.CONTENTTYPE = "text/html"
print(headers.ContentType) --> text/html (clave original se mantiene)
Deep Dive: Cadenas de __index
Lua permite crear cadenas de búsqueda complejas combinando funciones y tablas en __index.
Ejemplo REPL: Herencia Multinivel con Mixins
-- Sistema de herencia con mixins dinámicos
local function createClass(name)
local class = {__name = name}
local mixins = {}
class.__index = function(t, key)
-- 1. Buscar en la clase misma
local value = class[key]
if value ~= nil then return value end
-- 2. Buscar en mixins (en orden inverso de inclusión)
for i = #mixins, 1, -1 do
value = mixins[i][key]
if value ~= nil then return value end
end
-- 3. Buscar en clase padre si existe
if class.__parent then
return class.__parent[key]
end
return nil
end
function class.include(mixin)
table.insert(mixins, mixin)
end
function class.extend(parentClass)
class.__parent = parentClass
end
function class.new(...)
local instance = {}
setmetatable(instance, class)
if instance.init then
instance:init(...)
end
return instance
end
return class
end
-- Definir mixins
local Serializable = {
toJSON = function(self)
local result = {}
for k, v in pairs(self) do
if type(v) ~= "function" then
table.insert(result, string.format('"%s": %s', k, tostring(v)))
end
end
return "{" .. table.concat(result, ", ") .. "}"
end
}
local Timestamped = {
touch = function(self)
self.updatedAt = os.time()
end
}
-- Crear clase base
local Entity = createClass("Entity")
function Entity:init(id)
self.id = id
self.createdAt = os.time()
end
-- Crear clase derivada con mixins
local User = createClass("User")
User.extend(Entity)
User.include(Serializable)
User.include(Timestamped)
function User:init(id, name)
Entity.init(self, id)
self.name = name
end
function User:greet()
return "Hola, soy " .. self.name
end
-- Crear instancia
local user = User.new(1, "Carlos")
-- Métodos de la clase
print(user:greet()) --> Hola, soy Carlos
-- Métodos del mixin Timestamped
user:touch()
print(user.updatedAt) --> [timestamp]
-- Métodos del mixin Serializable
print(user:toJSON())
--> {"id": 1, "name": Carlos, "createdAt": [time], "updatedAt": [time]}
-- Propiedades de la clase padre
print(user.id, user.createdAt) --> 1 [timestamp]
Soapbox: Cuándo NO Usar Atributos Dinámicos
Advertencia: Los atributos dinámicos son poderosos pero tienen costos.
Evita usarlos cuando:
- Performance es crítica: Cada acceso ejecuta código adicional
- Debugging es importante: Los valores computados son más difíciles de inspeccionar
- La lógica es simple: No uses un martillo para clavar un tornillo
- Necesitas serialización: Los valores computados no se serializan automáticamente
Úsalos cuando:
- Necesitas abstracción: Ocultar complejidad interna
- Validación es esencial: Garantizar invariantes
- Lazy loading beneficia: Recursos costosos que no siempre se usan
- API uniforme: Propiedades y métodos deben verse igual
-- MAL: Sobre-ingeniería para algo simple
local point = {}
setmetatable(point, {
__index = function(t, key)
if key == "x" then return rawget(t, "_x") end
if key == "y" then return rawget(t, "_y") end
end,
__newindex = function(t, key, value)
if key == "x" then rawset(t, "_x", value) end
if key == "y" then rawset(t, "_y", value) end
end
})
-- BIEN: Simplicidad cuando no necesitas control
local point = {x = 0, y = 0}
Caso Práctico: Sistema de Configuración con Validación y Defaults
Implementaremos un sistema de configuración que combina defaults, validación, y transformación automática.
-- Sistema completo de configuración
local ConfigSystem = {}
function ConfigSystem.new(schema)
local config = {}
local values = {}
-- Validador de tipos
local function validateType(key, value, expectedType)
if expectedType == "integer" then
return type(value) == "number" and math.floor(value) == value
elseif expectedType == "positive_number" then
return type(value) == "number" and value > 0
elseif expectedType == "string_nonempty" then
return type(value) == "string" and #value > 0
else
return type(value) == expectedType
end
end
local mt = {
__index = function(t, key)
-- Si hay valor, retornarlo
if values[key] ~= nil then
return values[key]
end
-- Si hay default en schema, retornarlo
if schema[key] and schema[key].default ~= nil then
return schema[key].default
end
return nil
end,
__newindex = function(t, key, value)
-- Verificar que la clave existe en schema
if not schema[key] then
error("Configuración desconocida: " .. key)
end
local spec = schema[key]
-- Validar tipo
if not validateType(key, value, spec.type) then
error(string.format(
"Tipo inválido para '%s': esperado %s, recibido %s",
key, spec.type, type(value)
))
end
-- Aplicar transformación si existe
if spec.transform then
value = spec.transform(value)
end
-- Validación personalizada si existe
if spec.validate then
local ok, err = spec.validate(value)
if not ok then
error(string.format("Validación fallida para '%s': %s", key, err))
end
end
values[key] = value
end,
__pairs = function(t)
-- Combinar valores y defaults
local combined = {}
-- Agregar valores establecidos
for k, v in pairs(values) do
combined[k] = v
end
-- Agregar defaults no sobreescritos
for k, spec in pairs(schema) do
if combined[k] == nil and spec.default ~= nil then
combined[k] = spec.default
end
end
return pairs(combined)
end
}
config = setmetatable({}, mt)
-- Método para validar configuración completa
function config.validate()
local errors = {}
for key, spec in pairs(schema) do
if spec.required and values[key] == nil and spec.default == nil then
table.insert(errors, "Falta configuración requerida: " .. key)
end
end
if #errors > 0 then
return false, table.concat(errors, "\n")
end
return true
end
-- Método para exportar configuración
function config.toTable()
local result = {}
for k, v in pairs(config) do
result[k] = v
end
return result
end
return config
end
-- Definir schema de configuración
local appSchema = {
host = {
type = "string_nonempty",
default = "localhost",
required = false
},
port = {
type = "integer",
default = 8080,
required = false,
validate = function(value)
if value < 1 or value > 65535 then
return false, "Puerto debe estar entre 1 y 65535"
end
return true
end
},
timeout = {
type = "positive_number",
default = 30,
required = false
},
apiKey = {
type = "string",
required = true,
transform = function(value)
-- Remover espacios y normalizar
return value:gsub("%s+", ""):upper()
end,
validate = function(value)
if #value < 32 then
return false, "API key debe tener al menos 32 caracteres"
end
return true
end
},
debug = {
type = "boolean",
default = false,
required = false
},
maxConnections = {
type = "integer",
default = 100,
required = false,
validate = function(value)
if value < 1 then
return false, "Debe haber al menos 1 conexión"
end
return true
end
}
}
-- Crear configuración
local config = ConfigSystem.new(appSchema)
-- Usar defaults
print("Host:", config.host) --> localhost
print("Port:", config.port) --> 8080
print("Debug:", config.debug) --> false
-- Establecer valores
config.apiKey = " abc123def456ghi789jkl012mno345pqr "
print("API Key:", config.apiKey) --> ABC123DEF456GHI789JKL012MNO345PQR (transformado)
config.port = 3000
print("Port:", config.port) --> 3000
config.maxConnections = 50
print("Max Connections:", config.maxConnections) --> 50
-- Validaciones que fallarían:
-- config.port = 70000 --> Error: Puerto debe estar entre 1 y 65535
-- config.timeout = -5 --> Error: Tipo inválido (debe ser positive_number)
-- config.apiKey = "corta" --> Error: API key debe tener al menos 32 caracteres
-- config.unknown = "value" --> Error: Configuración desconocida
-- Iterar sobre toda la configuración (valores + defaults)
print("\n=== Configuración completa ===")
for key, value in pairs(config) do
print(string.format("%s: %s", key, tostring(value)))
end
-- Exportar como tabla
local configTable = config.toTable()
for k, v in pairs(configTable) do
print(k, v)
end
-- Validar configuración completa
local ok, err = config.validate()
print("Configuración válida:", ok) --> true
Ejercicios
Ejercicio 1: Cache con Tiempo de Expiración
Implementa un sistema de cache donde los valores expiran después de cierto tiempo.
Requisitos:
- Valores se calculan con una función factory
- Cada valor tiene un TTL (time to live)
- Valores expirados se recomputan automáticamente
- Método para invalidar cache manualmente
-- Tu implementación aquí
local function createExpiringCache(ttl)
-- Implementar cache con expiración
end
-- Ejemplo de uso:
local cache = createExpiringCache(2) -- 2 segundos de TTL
local counter = 0
cache.register("value", function()
counter = counter + 1
return "Valor " .. counter
end)
print(cache.value) --> Valor 1
print(cache.value) --> Valor 1 (mismo, en cache)
-- Esperar 3 segundos
os.execute("sleep 3")
print(cache.value) --> Valor 2 (recomputado, expiró)
Solución:
local function createExpiringCache(ttl)
local factories = {}
local cache = {}
local timestamps = {}
local proxy = {}
local mt = {
__index = function(t, key)
local now = os.time()
-- Verificar si está en cache y no ha expirado
if cache[key] ~= nil and timestamps[key] then
if now - timestamps[key] < ttl then
return cache[key]
end
end
-- Recomputar
if factories[key] then
local value = factories[key]()
cache[key] = value
timestamps[key] = now
return value
end
return nil
end
}
function proxy.register(key, factory)
factories[key] = factory
end
function proxy.invalidate(key)
cache[key] = nil
timestamps[key] = nil
end
function proxy.invalidateAll()
cache = {}
timestamps = {}
end
return setmetatable(proxy, mt)
end
-- Test
local cache = createExpiringCache(2)
local counter = 0
cache.register("expensive", function()
counter = counter + 1
print("Calculando valor costoso...")
return counter * 100
end)
print(cache.expensive) --> Calculando valor costoso... 100
print(cache.expensive) --> 100 (cache)
os.execute("sleep 3")
print(cache.expensive) --> Calculando valor costoso... 200 (expiró)
cache.invalidate("expensive")
print(cache.expensive) --> Calculando valor costoso... 300 (invalidado manualmente)
Ejercicio 2: Proxy de Auditoría
Crea un proxy que registre en un log todas las operaciones (lectura, escritura, eliminación) con timestamp.
Requisitos:
- Registrar lectura de propiedades
- Registrar escritura de propiedades
- Registrar cuándo se lee un valor nil (acceso a propiedad inexistente)
- Método para exportar el log de auditoría
-- Tu implementación aquí
local function createAuditProxy(target, auditName)
-- Implementar proxy de auditoría
end
-- Ejemplo de uso:
local data = {count = 0}
local auditData = createAuditProxy(data, "UserData")
auditData.count = auditData.count + 1
auditData.name = "Test"
local x = auditData.missing
local log = auditData.getAuditLog()
for _, entry in ipairs(log) do
print(entry)
end
Solución:
local function createAuditProxy(target, auditName)
local auditLog = {}
local function log(operation, key, value)
local entry = {
timestamp = os.date("%Y-%m-%d %H:%M:%S"),
operation = operation,
target = auditName,
key = key,
value = value
}
table.insert(auditLog, entry)
end
local proxy = {}
local mt = {
__index = function(t, key)
if key == "getAuditLog" then
return function()
local formatted = {}
for _, entry in ipairs(auditLog) do
table.insert(formatted, string.format(
"[%s] %s.%s - %s: %s",
entry.timestamp,
entry.target,
entry.key,
entry.operation,
tostring(entry.value)
))
end
return formatted
end
end
local value = target[key]
if value == nil then
log("READ_NIL", key, nil)
else
log("READ", key, value)
end
return value
end,
__newindex = function(t, key, value)
log("WRITE", key, value)
target[key] = value
end
}
return setmetatable(proxy, mt)
end
-- Test
local data = {count = 0, status = "active"}
local auditData = createAuditProxy(data, "UserData")
-- Operaciones auditadas
auditData.count = auditData.count + 1
auditData.name = "Alice"
local x = auditData.missing
auditData.status = "inactive"
-- Ver log de auditoría
print("=== Audit Log ===")
for _, entry in ipairs(auditData.getAuditLog()) do
print(entry)
end
--> [2025-01-20 10:30:45] UserData.count - READ: 0
--> [2025-01-20 10:30:45] UserData.count - WRITE: 1
--> [2025-01-20 10:30:45] UserData.name - WRITE: Alice
--> [2025-01-20 10:30:45] UserData.missing - READ_NIL: nil
--> [2025-01-20 10:30:45] UserData.status - WRITE: inactive
Ejercicio 3: Builder Pattern con Validación
Implementa un builder pattern usando atributos dinámicos que valide la construcción de objetos complejos.
Requisitos:
- Métodos chainable (retornan el builder)
- Validación al llamar
build() - Soporte para valores opcionales con defaults
- Error descriptivo si faltan campos requeridos
-- Tu implementación aquí
local function createBuilder(schema)
-- Implementar builder pattern
end
-- Ejemplo de uso:
local userSchema = {
name = {required = true, type = "string"},
email = {required = true, type = "string"},
age = {required = false, type = "number", default = 18},
active = {required = false, type = "boolean", default = true}
}
local user = createBuilder(userSchema)
:name("Bob")
:email("[email protected]")
:age(25)
:build()
print(user.name, user.email, user.age, user.active)
Solución:
local function createBuilder(schema)
local values = {}
local builder = {}
-- Crear métodos chainable para cada campo
for key, spec in pairs(schema) do
builder[key] = function(self, value)
-- Validar tipo
if type(value) ~= spec.type then
error(string.format(
"Tipo inválido para '%s': esperado %s, recibido %s",
key, spec.type, type(value)
))
end
values[key] = value
return self
end
end
-- Método build
function builder:build()
local result = {}
-- Validar campos requeridos
for key, spec in pairs(schema) do
if spec.required and values[key] == nil then
error("Campo requerido faltante: " .. key)
end
-- Usar valor o default
result[key] = values[key] or spec.default
end
return result
end
-- Método reset
function builder:reset()
values = {}
return self
end
return builder
end
-- Test
local userSchema = {
name = {required = true, type = "string"},
email = {required = true, type = "string"},
age = {required = false, type = "number", default = 18},
active = {required = false, type = "boolean", default = true}
}
-- Construcción exitosa
local builder = createBuilder(userSchema)
local user1 = builder
:name("Alice")
:email("[email protected]")
:age(30)
:build()
print(user1.name, user1.email, user1.age, user1.active)
--> Alice [email protected] 30 true
-- Usando defaults
local user2 = builder:reset()
:name("Bob")
:email("[email protected]")
:build()
print(user2.name, user2.email, user2.age, user2.active)
--> Bob [email protected] 18 true (age y active usan defaults)
-- Error por campo faltante:
-- local user3 = builder:reset():name("Charlie"):build()
--> Error: Campo requerido faltante: email
Resumen
En este capítulo exploramos:
__indexcomo función: Computación dinámica de propiedades- Lazy Loading: Carga diferida de recursos costosos con caching
- Proxies: Interceptación completa de operaciones con logging y validación
- Wrappers: Extensión de funcionalidad sin modificar originales
- Cadenas de búsqueda: Herencia multinivel con mixins
- Caso práctico: Sistema de configuración con validación, defaults y transformación
Los atributos dinámicos son una herramienta poderosa para crear abstracciones elegantes, pero deben usarse con criterio. En el próximo capítulo exploraremos ambientes y sandboxing para ejecutar código no confiable de forma segura.