Commonly used metatable-based OOP
There is a common method followed by developers when using OOP in Luau, and it usually looks something like this, as an example:
local Person = {}
Person.__index = Person
function Person.new(name: string, age: number)
return setmetatable({
_name = name,
_age = age
}, Person)
end
function Person:PrintName()
print(self._name)
end
function Person:IncrementAge()
self._age += 1
end
return Person
This is a common example of an OOP class in Luau. However, there are several flaws with it:
-
When requiring the module, you’ll notice that the class methods (:IncrementAge(), :PrintName() ) are exposed despite not having constructed a Person yet.
-
When you have constructed a Person with Person.new(), you’ll notice that you can call .new() again. ( Person.new().new().new()…), this is really quite odd and not really desirable.
-
Metatables have overhead, which, combined with the existing table overhead, can make these classes unecessarily slower than they need to be.
-
Metatables are vague and if your class is big it can get messy to find bugs or something else real fast.
Luckily, there is quite a simple solution that solves all of these flaws with metatable-based OOP,
Prototype-based OOP
What is prototype-based OOP? At its core, it is essentially just making the things you need for the Object beforehand, and then combining them when you construct it.
As an example using the previous Person class, but this time using prototype-based OOP:
local function person_printName(self)
print(self._name)
end
local function person_incrementAge(self)
self._age += 1
end
local function person_new(name: string, age: number)
return {
_name = name,
_age = age,
PrintName = person_printName,
IncrementAge = person_incrementAge
}
end
return {
new = person_new
}
Pre-construction
As you can see, we are constructing the PrintName and IncrementAge functions beforehand, nullifying any overhead we would’ve gotten with closure-based OOP.
We can do this because both take in a self argument, which allows it to reference the Person itself without using upvalues/global variables.
Increased clarity
However, you may also notice that it’s much clearer what is being done when constructing the Person.
We are constructing a table, putting our name, age, and functions inside it, then returning it.
This differs much from the previous commonly used method, where you have to think about what the metatable is doing.
Full control over public and private functions
Another thing you may have noticed is that now we have full, explicit control over what functions we want to expose.
We can now choose what functions we want to keep private to the methods of Person, and which ones we want the user to be able to use.
Less overheads
This method of doing OOP also has a benefit of less overheads.
Since there are no hidden metatables or unused carry-over functions/members, classes made using this method will only be as big as you want them to be.
Flaws
With how good prototype-based OOP is, it is not without its flaws. Unlike metatable-based OOP which are proxies of another table, and thus only need to hold a pointer to that table, prototype-based Objects need to store references for each individual method, which makes it take up slightly more memory than metatable-based OOP.
There is also the flaw that you can’t really do inheritance with prototype-based OOP (however I personally consider this less of a flaw and more of a benefit.)
Conclusion
In conclusion, prototype-based OOP is definitely an alternative to the usual metatable-based OOP that you should try using for any future classes you plan to make.
Hopefully you enjoyed this post, and may your scripts always be blessed with no bugs
SimpleSignal
Also, I’ve designed a version of Stravants GoodSignal called SimpleSignal, which utilizes this prototype-based approach and replaces all of the linked lists with dictionaries.
Download it here! →
SimpleSignal.rbxm (3.3 KB)