2023 Edit: This post has not been maintained in a long time and is based on V.1.X.X of Nexus Instance with most probably working on V.2.X.X. V.3.X.X is incompatible with most or all of the examples below.
Nexus Instance is an object oriented (OO) library designed for Roblox. The goal to provide some of the functionality of Roblox’s Instance
object, as well as providing the context of “super
” classes when extending a class.
Why Object Oriented?
This thread is meant to be a tutorial about using Nexus Instance; not Object Oriented programming in general. There are many different articles, books, and videos explaining why Object Oriented is “better” compared to functional programming. As a TL;DR of the benefits, I will pull the 4 main features of object oriented design from the answer key from the second assignment from one of my several object oriented university courses:
- Identity - Classes are templates. Objects are instances of those templates. Every object is uniquely identifiable.
- Encapsulation - State and behavior are grouped together inside an object. The object protects and controls access to them using access modifiers.
- Inheritance - One class may establish a parent-child relationship with another. The child class inherits the accessible state and behavior of its parent.
- Polymorphism - Any instance of a child class may be used where an instance of its parent class is expected. A reference may refer to an instance of any compatible type. What appears to be one method at compile time may be one of many different implementations at runtime (overriding).
Of course, object oriented is not a “silver bullet” that will make your code instantly better. You need to properly design for it, as well as practice. I wouldn’t make your first project with object oriented a large-scope project spanning months.
Why Use Any Library?
The main reason object oriented libraries get made for Lua is because the built-in method (metatables) can be very confusing for new users. Here is an example class with a constructor using Lua’s metamethods:
local TestClass = {}
--[[
Constructor for TestClass.
--]]
function TestClass.new(Value)
--Create the base object.
local Object = {Value = Value}
setmetatable(Object,TestClass)
TestClass.__index = TestClass
--Return the object.
return Object
end
--[[
Returns the value of the class.
--]]
function TestClass:GetValue()
return self.Value
end
--Create an instance of the class.
local TestObject = TestClass.new(2)
print(TestObject:GetValue()) --2
The metatables used above are something that can be generalized so it doesn’t have to be repeated. Inheritance is also a bit complicated because you need to make several metatable calls, and you still won’t have a good way to reference a super function. It can be done, but makes refactoring difficult if you change the inheritance.
Why Nexus Instance?
One thing I didn’t know going into the project is that there is a lot of different object oriented libraries that exist for Lua, and the syntax is different between them. Syntax mostly comes down to which you prefer. The functionality that is implemented into Nexus Instance includes:
- Easy extending of classes.
- Proper hierarchy with a “super” property.
- Class names and a built in “IsA” function.
- Property change signalling. (Note: The current implementation uses Roblox’s
BindableEvent
object, so it is Roblox specific).
Using Nexus Instance
Most of this tutorial will mirror the “Why Nexus Instance” on the GitHub pages documentation. In this case, I will be using shapes as an example.
Creating a Base Class
Most objects in Roblox inherit from a common class (Instance
). Nexus Instance is a same, except NexusObject
is the common class. To create an empty class, NexusObject::Extend
needs to be called on the base class. It is also recommended to set the class name using NexusObject::SetClassName
. As an example, here is how you would create a “Rectangle” class:
local Sources = game:GetService("ReplicatedStorage"):WaitForChild("Sources")
local NexusObjectFolder = Sources:WaitForChild("NexusObject")
local NexusObject = require(NexusObjectFolder:WaitForChild("NexusObject"))
--Extend NexusObject for a Rectangle class.
local Rectangle = NexusObject:Extend()
Rectangle:SetClassName("Rectangle")
--Create the object.
local RectangleObject = Rectangle.new()
--Return the class (assuming it is a ModuleScript).
return Rectangle
Overriding the Constructor
Some classes will require some type of arguments given as part of the constructor to create the object. An example of this is Instance.new
requiring the class name to create. For this, you need to override the __new
in the class. To simplify setting parameters, the function gets called on the instantiated object so a self
exists. Here is the same example with “Width” and “Height” parameters:
local Sources = game:GetService("ReplicatedStorage"):WaitForChild("Sources")
local NexusObjectFolder = Sources:WaitForChild("NexusObject")
local NexusObject = require(NexusObjectFolder:WaitForChild("NexusObject"))
--Extend NexusObject for a Rectangle class.
local Rectangle = NexusObject:Extend()
Rectangle:SetClassName("Rectangle")
--The constructor of the rectangle.
--The following is functionally identical to Rectangle.__new = function(self,...)
function Rectangle:__new(Width,Height)
self.Width = Width
self.Height = Height
end
--Create the object.
local RectangleObject = Rectangle.new(2,4)
print(RectangleObject.Width) --2
print(RectangleObject.Height) --4
Initializing the Super Class
In most cases, you will want to initialize the super class to initialize the behavior. If the constructor isn’t overridden, this is done implicitly using the InitializeSuper
function. If you do override the constructor, it is recommended (or potentially required) to explicitly call this. This function will call the constructor of the superclass with self.super
as self
, as well as any parameters. For the Rectangle example, a Square
is a reasonable class to extend from it, with only a side length being the constructor.
local Sources = game:GetService("ReplicatedStorage"):WaitForChild("Sources")
local NexusObjectFolder = Sources:WaitForChild("NexusObject")
local NexusObject = require(NexusObjectFolder:WaitForChild("NexusObject"))
--Extend NexusObject for a Rectangle class.
local Rectangle = NexusObject:Extend()
Rectangle:SetClassName("Rectangle")
--The constructor of the rectangle.
--The following is functionally identical to Rectangle.__new = function(self,...)
function Rectangle:__new(Width,Height)
self.Width = Width
self.Height = Height
end
--Extend Rectangle for a Square class.
local Square = Rectangle:Extend()
Square:SetClassName("Square")
--The constructor of the rectangle.
function Square:__new(Length)
self:InitializeSuper(Length,Length)
end
--Create the Rectangle object.
local RectangleObject = Rectangle.new(2,4)
print(RectangleObject.Width) --2
print(RectangleObject.Height) --4
--Create the Square object.
local SquareObject = Square.new(2,2)
print(SquareObject.Width) --2
print(SquareObject.Height) --2
print(SquareObject.ClassName) --"Square"
print(SquareObject:IsA("Rectangle")) --true
Adding Functions
A key feature of object oriented design is being able to abstract functionality behind certain methods. It is important to be able to either inherit the behavior or to override the behavior while keeping the same name (Polymorphism). In the Rectangle example, Rectangle::GetArea
can be added, and the Square will inherit the method.
local Sources = game:GetService("ReplicatedStorage"):WaitForChild("Sources")
local NexusObjectFolder = Sources:WaitForChild("NexusObject")
local NexusObject = require(NexusObjectFolder:WaitForChild("NexusObject"))
--Extend NexusObject for a Rectangle class.
local Rectangle = NexusObject:Extend()
Rectangle:SetClassName("Rectangle")
--The constructor of the rectangle.
--The following is functionally identical to Rectangle.__new = function(self,...)
function Rectangle:__new(Width,Height)
self.Width = Width
self.Height = Height
end
--Returns the area of the rectangle.
function Rectangle:GetArea()
return self.Width * self.Height
end
--Extend Rectangle for a Square class.
local Square = Rectangle:Extend()
Square:SetClassName("Square")
--The constructor of the rectangle.
function Square:__new(Length)
self:InitializeSuper(Length,Length)
end
--Create the Rectangle object.
local RectangleObject = Rectangle.new(2,4)
print(RectangleObject:GetArea()) --8
--Create the Square object.
local SquareObject = Square.new(2,2)
print(SquareObject:GetArea()) --4
Changed Event
A built in extension of NexusObject
is NexusInstance
. The primary purpose of NexusInstance
is to add a Changed
event signal for when an property gets changed. This does add some overhead to run the event, but it should be insignificant. Going back to the Rectangle and removing the Square, here is an example with the GetPropertyChangedSignal
function and Changed
event.
local Sources = game:GetService("ReplicatedStorage"):WaitForChild("Sources")
local NexusObjectFolder = Sources:WaitForChild("NexusObject")
local NexusInstance = require(NexusObjectFolder:WaitForChild("NexusInstance"))
--Extend NexusInstance for a Rectangle class.
local Rectangle = NexusInstance:Extend()
Rectangle:SetClassName("Rectangle")
--The constructor of the rectangle.
--The following is functionally identical to Rectangle.__new = function(self,...)
function Rectangle:__new(Width,Height)
self:InitializeSuper() --This is REQUIRED or signaling won't be set up.
self.Width = Width
self.Height = Height
end
--Create the Rectangle object.
local RectangleObject = Rectangle.new(2,4)
RectangleObject.Changed:Connect(function(Property)
print(Property..": "..tostring(RectangleObject[Property])) --"Width: 4"
end)
RectangleObject:GetPropertyChangedSignal("Width"):Connect(function()
print("Width: "..tostring(RectangleObject.Width)) --"Width: 4"
end)
RectangleObject.Width = 4
Locking Properties
If you need a property to be immutable (read-only), the function NexusInstance::LockProperty
can be used to make a property immutable. An example where this may be useful is any of Roblox’s datatypes, where they need to be re-instantiated rather than changing a component of the userdata. For the Rectangles example, the Width and Height may need to be immutable for performance reasons or how they are used by other parts of the software.
local Sources = game:GetService("ReplicatedStorage"):WaitForChild("Sources")
local NexusObjectFolder = Sources:WaitForChild("NexusObject")
local NexusInstance = require(NexusObjectFolder:WaitForChild("NexusInstance"))
--Extend NexusInstance for a Rectangle class.
local Rectangle = NexusInstance:Extend()
Rectangle:SetClassName("Rectangle")
--The constructor of the rectangle.
--The following is functionally identical to Rectangle.__new = function(self,...)
function Rectangle:__new(Width,Height)
self:InitializeSuper() --This is REQUIRED or signaling won't be set up.
self.Width = Width
self.Height = Height
--Lock the properties.
self:LockProperty("Width")
self:LockProperty("Height")
end
--Create the Rectangle object.
local RectangleObject = Rectangle.new(2,4)
RectangleObject.Width = 4 --"Width is read-only."
Changes
Version V1.1.0 (Interfaces, Property Validation, Metamethod Passthrough)