Qué resuelve
El patrón manual, hecho consistente
Una clase manual de Roblox empieza con Class.__index = Class. Funciona para objetos
chicos, pero no te da lifecycle ni cleanup. Clases cubre ese espacio sin meter un framework.
Lifecycle real
Constructores y destructores encadenados, Destroy idempotente y cleanup LIFO sin reescribirlo en cada clase.
Herencia limpia
extend, super, IsA y ancestros precomputados. Herencia simple más mixins, sin ambigüedad múltiple.
Tipado honesto
Clases.Object y Clases.Class<T, A...> tipan instancias y argumentos del constructor, sin fingir inferencia mágica.
Tipado
Estricto, en un solo cast
Declara el tipo de instancia, declara el tipo de clase y castea Clases.define una vez. El
pack de argumentos A... tipa Class.new(...) y Class(...), así que
no escribes new a mano.
export type Door = Clases.Object & {
part: BasePart,
open: (self: Door) -> (),
}
export type DoorClass = Clases.Class<Door, (BasePart)>
local Door: DoorClass = Clases.define("Door", {
constructor = function(self: Door, part: BasePart)
self.part = part
end,
methods = {
open = function(self: Door)
print("open", self.part.Name)
end,
},
})
-- assert refina: el retorno es Door, no any.
local door = Clases.assert(workspace.Door:GetAttribute("ref"), Door)
door:open()
Por qué un cast. Luau no infiere la forma exacta de una clase construida desde una
tabla dinámica. El cast es el puente normal entre un factory en runtime y el typechecker estructural;
deja el any aislado en ese único punto.
Cleanup & lifecycle
Los recursos se sueltan solos
En Roblox, conexiones, instancias, threads y promesas no se liberan cuando un objeto Luau deja de referenciarse. Clases trae un stack LIFO que cubre el caso común, con helpers para los patrones más frecuentes.
local Hatch = Clases.define("Hatch", {
constructor = function(self, model: Model)
-- conexión: se desconecta al destruir
self:connect(model.PrimaryPart.Touched, function(hit)
self:onTouched(hit)
end)
-- auto-Destroy cuando el modelo deja de existir
self:bindToInstance(model)
-- recurso con llave: re-registrar reemplaza y limpia el anterior
self:addCleanup(startAmbience(model), nil, "ambience")
-- promesas: se cancelan al destruir, se sueltan al resolverse
self:addPromise(preload(model))
end,
})
local hatch = Hatch.new(workspace.Hatch)
hatch:Destroy() -- desconecta, cancela y destruye en orden LIFO
Idempotente. Llamar Destroy() dos veces corre destructores y cleanups
una sola vez. Si un constructor falla, lo registrado hasta ahí se limpia antes de relanzar.
Library-agnostic. addPromise hace duck-typing del promise (sin
dependencia), y addCleanup también acepta objetos con Destroy,
Disconnect o cancel.
Herencia & mixins
Identidad por herencia, capacidades por mixin
Los constructores corren de base a derivada y los destructores al revés. super llama al
método heredado más cercano. Los mixins copian métodos y fallan al definirse si chocan.
local Animal = Clases.define("Animal", {
constructor = function(self, name: string)
self.name = name
end,
methods = {
speak = function(self)
return self.name
end,
},
})
local Dog = Animal:extend("Dog", {
methods = {
speak = function(self)
return `{self:super("speak")} bark`
end,
},
})
Dog.new("Kira"):IsA(Animal) -- true
Comparación
Dónde encaja
Qué trae el núcleo de Clases frente a una clase manual y a soluciones de mayor alcance. No es un veredicto: Component y Classe apuntan a problemas distintos.
| Capacidad | Clases | Metatables manuales | RbxUtil Component | Classe |
|---|---|---|---|---|
| Constructores/destructores encadenados | Sí | Manual | Parcial | Sí |
super integrado |
Sí | Manual | No | Sí |
| Cleanup de recursos (LIFO) | Sí | No | Extensiones | No |
| Bind a Instance (auto-Destroy) | Sí | No | Sí | No |
| Mixins con conflicto explícito | Sí | No | No | Parcial |
| Clases abstractas | Sí | No | No | Parcial |
Tipado del constructor (Class<T, A...>) |
Sí | Manual | Parcial | Parcial |
| Testeable headless (sin Studio) | Sí | Sí | Roblox | Parcial |
| Sin dependencias ni globals | Sí | Sí | Framework | Sí |
Instalación
Rojo, vendorizado o paquete
[dependencies]
Clases = "elwapotedev/clases@0.2.1"
rojo build default.project.json \
-o build/Clases.rbxm
local Clases = require(
ReplicatedStorage.Shared.Clases
)
Publicado en Wally. Guías completas:
Alcance
Lo que Clases no es
Clases no es un contenedor de inyección de dependencias, ni decoradores, ni un ECS,
ni metaprogramación opaca. El núcleo es clases, lifecycle y cleanup. Las salidas de emergencia son
explícitas: freeze = false para mutar una clase, y las funciones libres
Clases.is, Clases.assert, Clases.getClass e
Clases.isInstance para introspección.
Úsala donde haya estado y ciclo de vida: controllers, componentes de mundo, objetos con conexiones, armas, NPCs, servicios. Para datos constantes o funciones puras, un módulo normal es mejor.
FAQ
Preguntas frecuentes
¿Es más rápido que una clase manual?
Las llamadas de método quedan muy cerca del patrón manual, porque las instancias usan
__index directo a la clase concreta (profundidad 1). Crear, destruir,
super e IsA tienen coste extra por el lifecycle y la introspección.
¿Reemplaza a Trove o Janitor?
No siempre. Cubre el cleanup normal de una instancia. Si ya necesitas su API completa, puedes
registrarlos con self:addCleanup(trove).
¿Por qué Class.new tipa mejor que Class(...)?
Ambas funcionan en runtime y ahora ambas respetan el pack A.... Class.new
es la forma recomendada cuando quieres que Luau revise los argumentos del constructor.
¿Hay herencia múltiple?
No. En Luau suele generar orden ambiguo y conflictos silenciosos. Clases usa herencia simple más mixins con conflicto explícito.
¿Puedo usarla en servidor y cliente?
Sí. Es realm shared y no usa APIs exclusivas de servidor o cliente.