Class Abstractor

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
20 Likes

This is actually amazing! I was planning to make something like this but you beat me to it!

1 Like

Thanks! I just added some more documentation that was missing in some new functions I added.

1 Like

Would change this to local Class = require(path_to_class) with dots it got me confused for a second, pretty sure other too. Otherwise great work! Keep it up :blush:

I used dots to show that it may be multiple layers deep for example require(ReplicatedStorage.Utilities.Class)

1 Like

Created an update that should fix the IsA check for instances in interfaces. Also added the ability to check for custom classes with interfaces given the classes are named. You can name a class by passing the name as a parameter in Class.new(). If you pass this parameter, class:IsA("Name") will be available for type checking. If no name is provided class:IsA() will check against nil.

This works with any level of inheritance, so if you had a class which extended a class you could check for the base class in the interface, or the top level class.

I appreciate how you organised your class! It’s quite well-organized. I appreciate how you divided the code up into modules and recursion.

I made an update that allows for function overloading.

Lua does not support function overloading, you’d have to do manual arg length checks which can really bloat your code and make it a mess.

Look at how easy function overloading is now:

-- Disable underlining the duplicate functions
--!nolint DuplicateFunction

local Class = require(path.to.Class)
local Test = Class.NewClass("Test")

function Test:Example()
	print("Hello, world!")
end

function Test:Example(arg)
	print(arg)
end

function Test:Example(arg1, arg2)
	print(arg1, arg2)
end

local t = Test.new()

t:Example() -- "Hello, world!"
t:Example(1) -- 1
t:Example(2, 3) -- 2 3

Caveat:

  • Lua is not a typed language. Due to this reason, overloading only works with different argument length functions. And it does not work with variadic (functions that use ... syntax) functions. I may add an update in the future that allows you to register the types of arguments of functions if you wish to overload so that you could have same length arguments, but have them be type specific. If that interests you, let me know!
1 Like