This is a class abstractor for object oriented programming. It simplifies the process of creating a class, extending classes, and even allows you to implement interfaces into the classes.
Example usage:
local Class = require(path.to.Class)
local Animal = Class.NewClass():Implements({"number Health", "number MaxHealth", "BasePart RootPart"})
local Bear = Class.NewClass():Extends(Animal):Implements({"function Attack"})
function Animal:Constructor(health, rootPart)
self.Health = health
self.MaxHealth = health
self.RootPart = rootPart
end
function Bear:Constructor(health, rootPart, damage)
self:super(health, rootPart)
-- You can add stuff that isn't in the interfaces, you just have to include all members of the interface alongisde anything else
self.Damage = damage
end
function Animal:Example()
print("Hello, world!")
end
function Bear:Attack(target)
target.Health -= self.Damage
end
local BearInstance = Bear.new(100, workspace.Bear.PrimaryPart, 50)
BearInstance:Attack(BearInstance) -- It hurt itself in its confusion
BearInstance:Example() -- Inherited method
print(BearInstance.Health) -- 50
In this example, you can see just how easy it is to make a new class, add functions and make constructors for the classes. This example also utilizes interfaces, but they are completely optional, just an extra thing that can help you solve some easy bugs.
One important thing, if you want your classes to inherit the properties of the constructors of the classes they extend, you must call self:super()
in the classes constructor and pass any parameters. The example showcases this as the constructor for Bear
passes the values to the constructor of Animal
To add a new function to a class:
local Class = require(path.to.Class)
local ExampleClass = Class.NewClass()
function ExampleClass:Function()
-- In here you can use "self" to refer to the instance.
end
-- To call it, you need an instance:
local instance = ExampleClass.new()
instance:Function() -- You should call it with ":"
You can also add static functions and variables which are the same among all classes using .
instead of :
like so:
local Class = require(path.to.Class)
local ExampleClass = Class.NewClass()
ExampleClass.StaticInt = 5
function ExampleClass.StaticFunction()
-- "self" is not available here.
end
-- To call/access it use the class directly:
ExampleClass.StaticFunction()
print(ExampleClass.StaticInt)
You can use instance.base
to access the class which the instance’s class extends. You can chain base
as many times as you want depending on how many times you extend (instance.base.base.base
, etc). This allows you to access static methods and properties of the extended classes without a direct reference to the class itself.
Using .base
will also allow you to call the instance functions of the class if you override them, but you’ll have to explicitly pass the instance:
local Class = require(path.to.Class)
local Animal = Class.NewClass()
local Bear = Class.NewClass():Extends(Animal)
function Animal:Example()
print("Hello, world!")
end
function Bear:Example() -- This overrides "Animal:Example()"
print("Goodbye, world!")
end
local BearInstance = Bear.new()
BearInstance:Example() -- "Goodbye, world!"
BearInstance.base.Example(BearInstance) -- "Hello, world!"
It’s very important to pass the instance as the first parameter which gives you access to self
.
Finally, if you want to destroy an instance or class, use Class.DestroyInstance(instance)
or Class.DestroyClass(class)
. And make sure to remove all references to it in your code so that it can be garbage collected if you don’t plan on instantiating it again.
The code is fully documented so you can refer to that first with any questions:
--[[
Class abstractor made by Steven4547466.
Allows easy creation of classes and provides built in extension and interface support.
Example:
local Class = require(path.to.Class)
local Animal = Class.NewClass():Implements({"number Health", "number MaxHealth", "BasePart RootPart"})
local Bear = Class.NewClass():Extends(Animal):Implements({"function Attack"})
function Animal:Constructor(health, rootPart)
self.Health = health
self.MaxHealth = health
self.RootPart = rootPart
end
function Bear:Constructor(health, rootPart, damage)
self:super(health, rootPart)
-- You can add stuff that isn't in the interfaces, you just have to include all members of the interface alongisde anything else
self.Damage = damage
end
function Animal:Example()
print("Hello, world!")
end
function Bear:Attack(target)
target.Health -= self.Damage
end
local BearInstance = Bear.new(100, workspace.Bear.PrimaryPart, 50)
BearInstance:Attack(BearInstance) -- It hurt itself in its confusion
BearInstance:Example() -- Inherited method
print(BearInstance.Health) -- 50
]]
local Class = {}
--- Finds a value in a table
-- @param haystack The table
-- @param needle The tester function
-- @return The value, or nil
local function Find(haystack, needle)
for k, v in pairs(haystack) do
if needle(v, k, haystack) then
return v
end
end
return nil
end
--- Counts how many values in a table pass a function
-- @param haystack The table
-- @param tester The tester function
-- @return The number of values which pass the function
local function Count(haystack, tester)
local num = 0
for k, v in pairs(haystack) do
if tester(v, k, haystack) then
num += 1
end
end
return num
end
--- Counts all functions which need be overloaded
-- @param class The class to count overloads on
-- @param index The name of the index to count
-- @return The number of overloads for the index
local function FindAllOverload(class, index)
if not class then
return 0
end
return Count(class, function(t) return typeof(t) == "table" and t.__name and t.__nParams and string.sub(t.__name, 3, -3) == index end) + FindAllOverload(class.__base, index)
end
--- Gets a member from the class, or any of its extensions
-- @param class: table The class to get the member from
-- @param index The name of the member to retrieve
-- @return The value of the member, or nil if non-existent
local function GetMember(class, index)
if not class then
return nil
end
if class[index] then
return class[index]
end
local val = Find(class, function(t) return typeof(t) == "table" and t.__name and not t.__nParams and t.__name == index end)
if val then
return val
end
return GetMember(class.__base, index)
end
--- Clears an instance from memory
-- @param instance The instance to destroy
function Class.DestroyInstance(instance)
table.clear(instance)
setmetatable(instance, nil)
end
--- Clears a class from memory
-- @param class The class to destroy
function Class.DestroyClass(class)
table.clear(class)
setmetatable(class, nil)
end
--- Creates a new class
-- @param name The name of the class (used in "IsA")
-- @return Class The new class which members can be added to
function Class.NewClass(name)
local class = {
__interfaces = {};
__type = name;
}
class.__index = function(tbl, index)
return GetMember(class, index)
end
--- Creates a new instance of this class
-- @param ... Any parameters to pass to the constructor
-- @return The new instance
class.new = function(...)
local self = setmetatable({}, class)
local supers = 0
--- Calls the constructor of the extended class
-- @param ... Any parameters to pass to the constructor
function self:super(...)
local extensionClass = class.__base
for i = 1, supers do
extensionClass = extensionClass.__base
end
supers += 1
if extensionClass then
local constructor = GetMember(extensionClass, "__Constructor")
if constructor then
constructor(self, ...)
end
end
end
local constructor = GetMember(class, "__Constructor")
if constructor then
constructor(self, ...)
end
--- Checks if the instance implements all interfaces correctly
-- @param self The instance
-- @param classToCheck the class to check the interfaces of
-- @return A boolean value that indicates whether or not the interfaces are implemented
local function implementsInterfaces(self, classToCheck)
if classToCheck == nil then
return true
end
for _, interface in ipairs(classToCheck.__interfaces) do
for _, Imember in ipairs(interface) do
local split = Imember:split(" ")
local _type, name = split[1], split[2]
local member = GetMember(self, name) or GetMember(self, "__"..name)
local memberType = typeof(member)
if memberType == "table" then
if typeof(memberType.__value) == "function" then
memberType = "function"
end
end
local isProperClass = true
if memberType == "Instance" then
isProperClass = member:IsA(_type)
elseif memberType == "table" and GetMember(member, "IsA") then
isProperClass = member:IsA(_type)
while not isProperClass and member.__base do
member = member.__base
isProperClass = member:IsA(_type)
end
else
isProperClass = memberType == _type
end
if member == nil or not isProperClass then
return false
end
end
end
return implementsInterfaces(self, classToCheck.__base)
end
assert(implementsInterfaces(self, self), "Class does not implement all of its interfaces correctly.")
return self
end
--- Extends this class from another class, inheriting its functions
-- @param class The class which is getting the extension
-- @param extendedClass The class to extend
-- @return The class that was passed as the first parameter for daisy chaining
class.Extends = function(class, extendedClass)
assert(class ~= extendedClass, "Class extends itself.")
class.__base = extendedClass
class.base = class.__base
--- Checks if the extendedClass is cyclic, or in other words, "class" already extends "extendedClass"
-- @param cls The class to check against the extendedClass
-- @return true if the extended class is cyclic, false otherwise
local function checkCyclic(cls)
return if cls == nil then false
elseif cls == extendedClass then true
else checkCyclic(cls.__base)
end
assert(not checkCyclic(extendedClass.__base), "Cyclic class inheritance detected.")
return class
end
--- Implements an interface, ensuring all the members in it are correctly implemented
-- @param class The class which the interfaces are applied to
-- @param ... The interfaces to implement, which are arrays of strings that follow a "type memberName" format
-- @return The class that was passed as the first parameter for daisy chaining
class.Implements = function(class, ...)
local interfaces = table.pack(...)
for _, interface in ipairs(interfaces) do
table.insert(class.__interfaces, interface)
end
return class
end
--- Checks whether the function type matches the given type
-- @param class The class which to check
-- @param name The name of the type that is expected
-- @return true if the type of the class and the name are the same
class.IsA = function(class, name)
return class.__type == name
end
local overloadMetatable = {
__call = function(tbl, ...)
local nParams = #table.pack(...)
local val = Find(class, function(t) return typeof(t) == "table" and t.__name and string.sub(t.__name, 3, -3) == tbl.__name and t.__nParams == nParams end)
if val then
return val.__value(...)
else
return Find(class, function(t) return typeof(t) == "table" and t.__name and string.sub(t.__name, 3, -3) == tbl.__name end).__value(...)
end
end,
}
setmetatable(class, {
__newindex = function(tbl, index, value)
local isString = typeof(index) == "string"
local isFunction = typeof(value) == "function"
if isFunction and isString then
local nParams, variadic = debug.info(value, "a")
if not variadic then
local count = FindAllOverload(class, index) + 1
local suffix = "_"..count
local toSet = {
__name = "__"..index..suffix;
__value = value;
__nParams = nParams;
}
rawset(class, "__"..index, setmetatable({__name=index}, overloadMetatable))
return rawset(class, "__"..index..suffix, toSet)
end
else
rawset(class, index, value)
end
end,
})
return class
end
return Class