The way I made my maid is by giving it a function to run that does the cleaning instead of supplying an (object, method, args)
type of deal, which is incredibly flexible, lightweight, doesn’t need type checks, and is great for things that aren’t objects.
My maid module looks like this:
local Maid = {}
Maid.__index = Maid
-- creates a maid
function Maid.new()
return setmetatable({ Tasks = {} }, Maid)
end
-- assigns maid with a task
function Maid:Give(task)
table.insert(self.Tasks, task)
end
-- does the tasks assigned
function Maid:Clean()
for _, task in ipairs(self.Tasks) do
xpcall(task, function(msg)
warn(debug.traceback(msg))
end)
end
self.Tasks = {}
end
return Maid
And in practice, it would look like this:
-- make a maid
local maid = Maid.new()
-- do yo thang
local var = "a value"
local t = {1, 2, 3}
local part = Instance.new("Part")
local touchedConn = part.Touched:Connect(print)
-- after doing the thangs,
maid:Give(function()
var = nil
t = {}
part:Destroy()
touchedConn:Disconnect()
end)
-- when you want all yo thangs to get cleaned up,
maid:Clean()
This design pattern could be even more generalized as more of a place to dump functions with “workloads” rather than cleaning tasks, which is exactly what bindable events do:
-- new
local bindable = Instance.new("BindableEvent")
-- give
bindable.Event:Connect(task)
-- clean
bindable:Fire()
bindable:Destroy() -- for disconnecting the events
-- in module form
local RunService = game:GetService("RunService")
local Connection = {}
Connection.__index = Connection
function Connection.new(listeners)
local self = {
Connected = true,
Listeners = listeners
}
return setmetatable(self, Connection)
end
function Connection:Disconnect()
self.Connected = false
self.Listeners[self] = nil
end
local Event = {}
Event.__index = Event
function Event.new()
local self = {
Listeners = {},
Waiting = {}
}
return setmetatable(self, Event)
end
function Event:Fire(...)
for _, fn in pairs(self.Listeners) do
coroutine.wrap(fn)(...)
end
for _, thread in pairs(self.Waiting) do
coroutine.resume(thread, ...)
end
self.Waiting = {}
end
function Event:Connect(fn)
local connection = Connection.new(self.Listeners)
self.Listeners[connection] = fn
return connection
end
function Event:Wait(timeout)
local key = newproxy()
local thread = coroutine.running()
self.Waiting[key] = thread
if timeout then
coroutine.wrap(function()
local i = 0
while i < timeout do
i += RunService.Heartbeat:Wait()
end
self.Waiting[key] = nil
coroutine.resume(thread, i)
end)()
end
return coroutine.yield()
end
return Event
This is obviously the same as just adding a destroy
method to an object or a done
method to a state, this is just more generalized toward more varied concepts.
To be honest, I will probably be using the event abstraction in my project now, it just seems like too good an idea.