C Clases
Luau · Roblox · --!strict

Clases con lifecycle real, sin magia.

Una librería Luau para Roblox que convierte el patrón de metatables en un sistema de objetos tipado: herencia, super, Destroy idempotente, cleanup de recursos y herramientas headless. El núcleo se mantiene pequeño y explicable.

3 módulos de núcleo 0 dependencias __index plano, profundidad 1
Counter.luau
local Clases = require(ReplicatedStorage.Shared.Clases)

export type Counter = Clases.Object & {
	value: number,
	add: (self: Counter, amount: number) -> number,
}

-- Un solo tipo: el pack tipa Counter.new(...)
export type CounterClass = Clases.Class<Counter, (number)>

local Counter: CounterClass = Clases.define("Counter", {
	constructor = function(self: Counter, value: number)
		self.value = value
	end,

	methods = {
		add = function(self: Counter, amount: number): number
			self.value += amount
			return self.value
		end,
	},
})

local counter = Counter.new(10)
counter:add(5)
counter:Destroy()

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.

Door.luau
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.

Hatch.luau
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.

Animal.luau
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 Manual Parcial
super integrado Manual No
Cleanup de recursos (LIFO) No Extensiones No
Bind a Instance (auto-Destroy) No No
Mixins con conflicto explícito No No Parcial
Clases abstractas No No Parcial
Tipado del constructor (Class<T, A...>) Manual Parcial Parcial
Testeable headless (sin Studio) Roblox Parcial
Sin dependencias ni globals Framework

Instalación

Rojo, vendorizado o paquete

wally.toml
[dependencies]
Clases = "elwapotedev/clases@0.2.1"
Modelo Rojo
rojo build default.project.json \
  -o build/Clases.rbxm
Vendorizado
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.