Efficient Object Oriented Programming Tutorial



Efficient Object Oriented Programming Tutorial



This tutorial will not be explaining what OOP (aka Object Oriented Programming) is.

I’ll be showing how to create an efficient and organized OOP System.



  • Note: I start to create the efficient OOP system at the “what do we do now?” section below

All module scripts in this tutorial will be parented to ServerStorage

The scripts that we will test module scripts with will be parented to ServerScriptService


Things you should know before continuing this tutorial

  • Basic OOP Concepts (objects, classes, inheritance)
  • Module Scripts
  • Metatables
  • . vs : function calls | example: Person.shout() vs Person:shout()


Let’s Get Started

There are lots of tutorials on OOP in Roblox Lua. However, there seems to be key issues consisting of some combination of the following:

  • Memory usage (including memory leaks)
  • Complexity
  • Readability
  • Flexibility
  • Issues with inheritance and event connections
  • Lack of using DRY method (Do not repeat yourself)

I’ve been developing Lua OOP systems for a couple years now. They’re not easy to conceptualize. I’ve scrapped countless ideas either having issues with inheritance, memory leaks, or just not liking the structure or looks of the system.

The best post I could find about OOP was a post first written in… 2014. (edited in 2020 Jul.)

I would advise you to look it over first since it’s much better at explaining the concepts of OOP.


Bad OOP Implementation Example


First let’s explore the issues that I’ve seen with other OOP systems

Here’s an example of a very simple Person class represented inside a module script:

local Person = {}

function Person.new(name, age)
    local self = {}
    self.name = name
    self.age = age

    function self.shout()
        print(self.name .. ": AHHHHHH")
    end
    return self
end

return Person

This is what I normally see when people try to implement OOP. in this example, we have a Person class, which creates an object through a function called new.

Our object has a:

  • name property
  • age property
  • function which lets the person shout in the output bar called shout.

First thing I want to make clear is that this OOP system does work. It’s great for beginners since it’s simple and easy to read.

However, it sacrifices one thing: Memory usage



Why is that?

The issue at hand is that we have created a function inside the object itself.

Let’s say we create two objects. That means both objects have its own shout function.

Two functions that do exactly the same thing

This uses up memory that could otherwise be avoided.

Here’s what we want:

  • To be able to call the function from the object
  • Not have the function located directly inside the object
  • Only create one function that is used by all objects
  • Still is easy to read and execute (in a perfect world)


How do we solve this?

With metatables

Specifically with the __index metamethod.

Metatables are awesome, but they can get pretty confusing quickly. I hope you already have a basic understanding with the concept of metatables and metamethods.

Instead of adding the shout function into the Person object, we’ll add them to the Person class, which is the same place the new function is located. In this example, that would be Person.

Example:

local Person = {}

function Person.new(name, age)
    local self = {}
    self.name = name
    self.age = age

    return self
end

function Person.shout(self)
    print(self.name .. ": AHHHHHH")
end

return Person

Notice how the shout function is outside of the new function.

Also note how the shout function includes self as a parameter. The self lets us target a specific object that calls the function. However, there’s a better way to write it.

This does the exact same thing as the shout function in the above example:

function Person:shout()
    print(self.name .. ": AHHHHHH")
end

We replace the . with a : in the function definition. This passes self as the first parameter without needing to write it. And when we call the function from our object, well use : as well

object:shout()



Metatable Time

You might notice with the above example we have no way to call the function directly from a Person object, since the function is located inside the Person class. You would be correct.

Now it’s time to play with metatables.

Like I said before, I’m not here to explain how metatables work. So i’m just going to provide a working scenario here with an updated Person module script

local Person = {}
Person.__index = Person

function Person.new(name, age)
    local self = {}
    self.name = name
    self.age = age

    return setmetatable(self, Person)
end

function Person:shout()
    print(self.name .. ": AHHHHHH")
end

return Person

Here are the changes:

  • Person.__index = Person | We added a field __index to Person

  • setmetatable(self, Person) returns self after setting the Person class as its metatable.

To sum it up, whenever you try to access a field that the Person object does not have, it will look inside the Person class to see if it has it.



But is it good enough?


Now we can access the class functions from our objects! And when we create multiple objects, it does not create more functions, reducing the amount of memory we use!

This is very similar to the OOP tutorial I listed above, with a little of my personal coding style mixed in. This system is a great option for developers with larger games, since the metatables save us memory.

We can add inheritance to this system, however, the features of this system are lacking. Here’s problems with this system:

  • If you want to add metamethods to classes, you will have to create one in every class.
  • The new function (the new function should not be located directly inside the class)
  • The inability to use functions of the class inside the new function through self
  • Just not cool enough


What do we do now?


Here’s where the real tutorial starts. If you got through here, good job.

If you jumped here without reading the above, I hope you understand basic OOP systems in Lua.


We are now going to scrap everything we just made


Let’s create a new module script inside ServerStorage. This is going to be where we create the concept of a class. I’m going to call it “Rcade” (random name I thought of).

Lets add a function called class to the module script with the following logic inside:

local Rcade = {}

function Rcade.class(newClass)
    newClass.__index = newClass
    return setmetatable(newClass, Rcade)
end

return Rcade

Notice how the function receives a parameter called newClass. This is where we receive default values for a class. I’ll explain it more in detail later on. The function also sets Rcade as the metatable. Finally it returns newClass

Now we need to add a metamethod to Rcade. First, we will add a __call metamethod to Rcade. This will allow us to call a table like it’s a function. In this case, the table would be the class. We are using __call to replace the new function

Here is Rcade with a __call metamethod added:

local Rcade = {}

function Rcade:__call(...)
    -- note how self in this case points to the object's class
    local object = {}
    setmetatable(object, self)
   
    if self.__init then
        self.__init(object, ...)
    end

    return object
end

function Rcade.class(newClass)
    newClass.__index = newClass
    return setmetatable(newClass, Rcade)
end

return Rcade

Instead of creating an object by calling class.new()

We will now do it by calling class()



You might be confused about the code inside the __call metamethod.

If you’re confused, I don’t blame you

Here’s a step by step on what it does:

  1. It receives a variable amount of parameters indicated by the ...
  2. It creates a table which is our object
  3. Sets the object’s metatable to be its class.
  4. Looks to see if the object’s class has a function called __init and if it does then it runs it, passing the object and the variable amount of parameters.
  5. Returns the object

But where did this __init function come from? It’s not a metamethod recognized by Lua.
It’s a function you will define inside each class yourself. The __init function is where we set an object’s properties.



Lets create an example class called “Person” again that uses Rcade.

Here’s the basic structure:

local Rcade = require(game.ServerStorage.Rcade) -- change this to where ever your Rcade is located
local Person = Rcade.class{}

function Person:__init(name, age)
    self.name = name
    self.age = age
end

function Person:shout()
    print(self.name .. ": AHHHHHH")
end

return Person

Notice how when I called the class function inside Rcade, I did

Rcade.class{} instead of Rcade.class() - curly brackets vs parenthesis

What’s the difference?

Rcade.class{} is equivalent to Rcade.class({}).

In other words, It passes a table as the first parameter. Just less code to write and easier to read, though either way is fine.



Now let’s actually create an object as an example that it works.

Let’s create a new Script. Lets place the script inside ServerScriptService
with the following code inside:

local Person = require(game.ServerStorage.Person)

local newPerson = Person("Jimmy", 100)
print(newPerson.name)
print(newPerson.age)
newPerson:shout()

This example will print out this in the output bar:

jimmyupdated



Now we need to add inheritance. With inheritance, we can have a class inherit properties and functions of another class.

To help explain what it means, here’s an example:

  • Let’s say we have a class called “Person”
  • Let’s say we have another class called “Student” that inherits from Person.

A Student is a Person, but a Person is not always a Student

A Student should have every property and function that a Person has.

However, a Student may also have properties or functions that a Person doesn’t have.



So how do we add Inheritance?

The first step to do this is by adding a __super property to classes that inherit from another class. (__super refers to the class it inherits from)

Lets create a new class called Student, mimicking the example from before.

Here’s our student class that will inherit from our Person class

local Rcade = require(game.ServerStorage.Rcade)
local Super = require(game.ServerStorage.Person)
local Student = Rcade.class{__super = Super } -- <-- added super class here

function Student:__init(name, age, id)
    self.__super.__init(self, name, age)
    self.id = id
end

return Student

Here’s what’s different from the Person class:

  • Added __super property to the class which points to what it inherits from
  • Inside __init we call the superclass’ __init, passing the object and the Person parameters.
  • Added a new parameter to the __init function called “id” and set it inside the object.
  • No functions were defined under the Student class

With a student object, we can index:

  • student.name, student.age which is inherited from the Person class
  • student.id which is defined inside the Student class

However, we’re not done yet. As of right now, if we have a student object and try to call student:shout(), the script will error out since there is no shout function inside the Student object.

We want our Student class to look inside our Person class to find the shout function.

Since Rcade is the metatable to all classes we create, we need to add an __index metamethod to Rcade.

Here is the metamethod

function Rcade:__index(index)
    -- self is the class
    if rawget(self, "__super") then
        return self.__super[index]
    end
end

When we call __super on classes, This checks to see if our class has a __super property, and if it does it will return the superclass. If not it will return nil.

So the current full Rcade script will look like this:

local Rcade = {}

function Rcade:__call(...)
    -- note how self in this case points to a class
    local object = {}
    setmetatable(object, self)

    if self.__init then
        self.__init(object, ...)
    end

    return object
end

function Rcade:__index(index)
    -- self is the class
    if rawget(self, "__super") then
        return self.__super[index]
    end
end

function Rcade.class(newClass)
    newClass.__index = newClass
    return setmetatable(newClass, Rcade)
end

return Rcade

Now it wont error out when we try Student:shout()

The cool thing with this system is that we can define metamethods for objects directly inside the class, while looking very clean.

If we try to print out a student object, it gives us a string that points to the memory location holding the table.

Useless


  • With the new output bar in Roblox Studio, you can now see what’s inside a table when you print it. However, I still don’t like it.

What if we want to have a custom output when you print an object?

All we have to do is add a __tostring function inside the object’s class.

Here’s an example of the Person class that has a custom print output for Person objects.

local Rcade = require(game.ServerStorage.Rcade)
local Person = Rcade.class{}

function Person:__init(name, age)
    self.name = name
    self.age = age
end

function Person:__tostring()
    return "name: " .. self.name .. " age: " .. self.age
end

function Person:shout()
    print(self.name .. ": AHHHHHH")
end

return Person

Now when we print a Person object, it will print it’s name and age.

Here’s an example of printing a Person object and its output using a script inside ServerScriptService:

local Person = require(game.ServerStorage.Person)

local newPerson = Person("Jimmy", 100)

print(newPerson)

And it outputs:

output1



BUT WAIT


If we try to print out a Student, which inherits from our Person class, it won’t use our tostring metamethod. It will give us that useless memory location string again.

We need a way to have metamethods also be inherited.

First I want to make clear that this section is purely optional. The way I’m going to show you will increase memory usage. I have yet to figure out a way to cleanly do this. However, it works and is not as bad as the first examples of OOP I showed you.



Adding Metamethod Inheritance

To do this, we will need to edit Rcade once more.

We will change Rcade’s class function to this:

function Rcade.class(class)
    if class.__super then
        for key, value in pairs(class.__super) do
            local sub = string.sub(key,1,2)
            if sub == "__" and key ~= "__super" and key ~= "__init" then
                class[key] = value
            end
        end
    end
    class.__index = class
    return setmetatable(class, Rcade)
end

What we added in steps:

  • added a check to see if the class in question had a __super property.
  • If it does have __super, loop through every property the __super class has
  • If the property has “__” as the first two characters (our metamethods) and is not __super and __init, then add it to the class we are creating.

Now our classes inherit metamethods as well!



Listeners

Now for the final aspect of this OOP system. Listeners. What if we want a certain function of a class to fire for a specific event? Like when a player joins, or a character dies.

First let’s add a blank module script to Rcade. I’m going to name it “Utility”

Utility will be a child of Rcade in the Explorer tab, as shown here:

utility

Inside utility we will add three different functions, as well as retrieve the service “RunService” as shown here:

local Utility = {}
local RunService = game:GetService("RunService")

function Utility:eventListener(bindableEvent, method)
    bindableEvent:Connect(function(...)
        self[method](self, ...)
    end)
end

function Utility:remoteListener(remoteEvent, method)
    if RunService:IsServer() then
        remoteEvent.OnServerEvent:Connect(function(...)
            self[method](self, ...)
        end)
    elseif RunService:IsClient() then
        remoteEvent.OnClientEvent:Connect(function(...)
            self[method](self, ...)
        end)
    end
end

function Utility:bindToClose(method)
    game:BindToClose(function()
        self[method](self)
    end)
end

return Utility

I won’t even begin to explain what’s happening here. All you need to know is how to use it at this point.



So let’s use it

First we need to go inside Rcade AGAIN and add a property. So at the top of Rcade, we’ll insert it as such:

local Rcade = {}
Rcade.Utility = require(script.Utility)

Which results in the complete and final version of Rcade:

local Rcade = {}
Rcade.Utility = require(script.Utility)

function Rcade:__call(...)
    -- note how self in this case points to a class
    local object = {}
    setmetatable(object, self)
    if self.__init then
        self.__init(object, ...)
    end
    return object
end

function Rcade:__index(index)
    -- self is the class
    if rawget(self, "__super") then
        return self.__super[index]
    end
end

function Rcade.class(newClass)
    if newClass.__super then
        for key, value in pairs(newClass.__super) do
            local sub = string.sub(key,1,2)
            if sub == "__" and key ~= "__super" and key ~= "__init" then
                newClass[key] = value
            end
        end
    end

    newClass.__index = newClass
    return setmetatable(newClass, Rcade)
end

return Rcade

Now let’s create a new class called “Server”.

It’s going to include functions called onJoin, onLeave, and onClose.

Here’s the basic layout of our Server class:

local Rcade = require(game.ServerStorage.Rcade) 
local Server = Rcade.class{}

function Server:__init()

end

function Server:__tostring()
    return "Server"
end

function Server:onJoin(player)
    print(player.Name .. " joined")
end

function Server:onLeave(player)
    print(player.Name .. " left")
end

function Server:onClose()
    print("The server is closing")
end

return Server

In this example, we will make it so:

  • when a player joins, the onJoin function will fire, with the player being the parameter.
  • When a player leaves, the onLeave function will fire with the player being the parameter
  • When the server requests to shut down, the onClose function will fire

To do this, we will add code to the __init function as such:

function Server:__init()
    Rcade.Utility.eventListener(self, game.Players.PlayerAdded, "onJoin")
    Rcade.Utility.eventListener(self, game.Players.PlayerRemoving, "onLeave")
    Rcade.Utility.bindToClose(self, "onClose")
end

Note how the eventListeners work.

  • The first parameter is self (the object)
  • The second parameter is the event you want to listen to.
  • The third parameter is the name of the method you want to fire represented as a string.

It’s the same for remoteListener

however with bindToClose it only needs self and the method name. (2 parameters)

Now our Server class should do exactly what we planned it to do.



Let’s test it out

Create a script inside ServerScriptService that contains the following code:

local Server = require(game.ServerStorage.Server)
local newServer = Server()

Now start the game.

It should print something when you join, leave and when the server closes now. Here’s what my output prints out when I join and leave in Roblox Studio:

joinleaveclose



Default Values

Sometimes we might not want to input every parameter a class has. Let’s take a look at our Person class once more.

Let’s say we create an object without giving the person an age. The issue here is that if we were to try to have the person shout, it would error since you can’t concatenate nil with a string.

How can we fix this? The normal way would look something like this

local Rcade = require(game.ServerStorage.Rcade) -- change this to where ever your Rcade is located
local Person = Rcade.class{}

function Person:__init(name, age)
    self.name = name or "NO NAME"
    self.age = age or -1
end

function Person:shout()
    print(self.name .. ": AHHHHHH")
end

return Person

Inside the __init function, when a property is set you can add a or to set a default value that only is used when the given parameter is nil.

The issue is that if we nil the value inside the object later, then the value would be nil when we index it.

I recommend a different method, one that works really well with the system we have set up:

When we create a class through Rcade, inside the table we pass to Rcade, you can add default values for your objects. So for our Person class example, the default values could instead be created via the code:

local Rcade = require(game.ServerStorage.Rcade) -- change this to where ever your Rcade is located
local Person = Rcade.class{name = "NO NAME", age = -1}

function Person:__init(name, age)
    self.name = name
    self.age = age 
end

function Person:shout()
    print(self.name .. ": AHHHHHH")
end

return Person

The default values are located at the top of the script where we create the Person class

local Person = Rcade.class{name = "NO NAME", age = -1}

Now if we don’t give an age parameter when creating an object, indexing it will return -1 no matter what unless a name property is added to the object. Even if we do person.age = nil it will still return -1 when indexed.


Garbage Collector

If you add a roblox object as a property of an object, you need to nil the property inside the object in order for the object to be cleaned by the garbage collector.

As an example, if we created an object every time a player joins the game, and the object contains the player as a property, we must nil out the value when the player leaves to stop memory leaks from occurring. I don’t know why. Just nil the property when you’re done with them. I usually add a destroy function to objects to easily clean them up.



Conclusion

And we’re done! This was a very long tutorial, And I know I’m not a good teacher. For those of you who got to the end, here’s a reward (even scrolling here took an effort):



Final Example link:


Rcade Example Kit - Roblox

Take this and edit it however you want! Just make sure you understand what is happening. Leads to a better user friendly experience.

(I didn’t want to put it at the top of the post since people would just get the model and not read the post)


Final Words


I really hope you learned something from this post. This is my first ever full post so I hope everything if formatted correctly and is understandable. If you think you can improve this OOP system, by all means go for it. If there's any issues or errors with the code, or any typos, let me know and I'll fix it up ASAP.

Soon I’ll be creating a plugin that makes it very fast and simple to create a basic template for a class.
I’ll also likely create a tutorial on how to create an obby with this system.


Cheers

97 Likes
04/21/2023: This is bad advice so hiding it from immediate view. Please check out the post above.

I don’t speak for everyone when I say this but I think the Rcade layer is completely unnecessary and ruins readability. Constructors are made to be explicit and some classes may require or desire more than one constructor for comfortability’s sake (Roblox classes make especially large use of this). Plus, it just looks cleaner in your code if you create an object directly from the class with a constructor.

In terms of inheritance, same thing goes - quicker and cleaner to just use two metatables. I don’t know about how performant it is compared to what you’re doing because there aren’t any benchmarks nor have I taken them myself but I prefer simplicity when it comes to code. I wouldn’t need a whole layer to do my work for me, inheritance or otherwise.

local BaseClass = {}
BaseClass.__index = BaseClass

local SubClass = setmetatable({}, BaseClass)
SubClass.__index = SubClass

Four lines worth of code creates a base class and a subclass that inherits from the base class without any kind of handler layer, constructors pending.

18 Likes

I can understand that. However what about this scenario: You want all classes to have the same metatable attached to them. Without this extra layer, you would have to include metatables inside each class, and I think it would be weird if different classes required different metatables / metamethods.

When you add metamethods to Rcade, every class you create will be affected. I guess the question is if you want more metamethods attached to classes or not.

Also, having the same logic written in every class is just going against the DRY principle, even if its simpler or not.

With the performance difference, there definitely is some performance hit, since there is more logic being computed. However, it would be very minimal (though I have not benchmarked it).

I do appreciate your feedback! Any more would be greatly appreciated!

And then I just remembered the metatable could be inside a module script…

Anyways having one thing to point to (Rcade) for anything class related and not having to worry about setting it up yourself in every class is still reassuring.

I’m biased with having an extra layer since it can speed up the work process and keeps things you don’t need to see all the time out of mind.

04/21/2023: This is bad advice so hiding it from immediate view. Please check out the post above.

If I want all my subclasses to have the same base class I explicitly declare that in my code, so I’m fine with doing that. That is what you’re supposed to be doing anyway. No one class should be inheriting from multiple different unrelated classes outside its hierarchy; only traverse downwards.

Merging superclasses and methods together through a single layer is gross and defies a lot of common OOP patterns as well as diminishes readability and clarity. An object should only have upwards inheritance and not be picking out the methods of other unrelated classes. Accessory instances don’t inherit from ValueBase: the same pattern should apply for OOP-in-Lua maintained by developers.

I’m no stranger to the DRY principle but I don’t think you’re understanding its application here. DRY doesn’t say “merge everything into a single unreadable layer”; it just literally means don’t repeat yourself, wherever you can. Sacrificing readability to follow it down to the word literally isn’t representative of the DRY principle, it’s just straight up bad code.

I enforce the DRY principle through the use of ModuleScripts which tie into my application of the single responsibility principle (SRP) as much as possible. If I need to inherit, I borrow from one of those modules but only because the subclass is related to the main class and makes more sense as a separate feature rather than one conjoined with the main feature.

3 Likes

Oh cool! The only thing I knew how to do was:

local Baseplate = game:GetService("Workspace"):WaitForChild("Baseplate")
4 Likes

I might be going completely wrong here but,

I think a good way to think about OOP, is DataStoreObjects.

When working with a datastore module, you’ll have to get the datastore for every call. (Or keep the datastoreobject as info).

In this case, you create your own DataStoreObject which has the redirects and stuff to the functions you need.

With that, you can do :Get() and it will grab info and the function could use it.

Example:

local methods = {
    Get = function(self, key)
        local dataStore = game:GetService("DataStoreService"):GetDataStore(self.DataStore)
        return dataStore:GetAsync(key)
    end;
}

local function GetDataStore(dataStoreName)
    local info = {
        DataStore = dataStoreName
    }
    setmetatable(info, {
        __metatable = true; --lock metatable
        __index = function(_, index)
            return methods[index]
        end
    })
    if false then
        return {
            Get = function() end;
        }
        --Only the best for my users. A if false statement goes through script editor module caching, meaning it will show in the script editor previews.
    end
    return info
end
2 Likes

I thought we were talking about metamethods. Anyways, subclasses inherit from (optionally) one base class. it’s a one way street. It does not inherit anything else, other than setting its metatable to Rcade.

Also Rcade is a module script. that’s just where the metamethods are located and where you create classes and use Utility. Which you said you like using module scripts for DRY. Why not Rcade?

If you need to inherit you do it inside the class as well in my examples. You have to write the inheritance in with __super. It’s not like they magically inherit something else out of nowhere. And they only inherit from one class, this system does not support multiple inheritance (meaning 1 class inheriting from 2 different classes)

Also, yes an extra layer makes it less readable. While it was a point I made in the tutorial, it’s something I sacrificed (a little) to have a better user experience. Though if you follow the tutorial you should very well understand what the code is trying to do. Some things need a learning curve like anything.

I assume that the issues you have pointed out are

  • the extra layer reduces readability
  • the metamethods inside Rcade have an effect on every class created through Rcade which is bad?

However, this has had noticeable results in my development. And I’ve seen other OOP concepts outside of LUA that use a third layer. Not saying that would make this system inherently good. However, I don’t agree with your statement. Imagine the third layer like the class implementations that are built in into other coding languages natively. That’s what I was going for anyway.

If you didn’t want metamethods inherited from one place, then sure, don’t add the extra layer. Though subclasses would not inherit metamethods base classes.

The one thing I never figured out how to do was how to have more than one constructor depending on the types of parameters and the amount of parameters. If you had an example of this it would be great since I could try to implement it into the system.

I do like getting your feedback.

Thanks!

04/21/2023: This is bad advice so hiding it from immediate view. Please check out the post above.

It depends on what you mean by metamethods? When talking OOP using the term metamethods is confusing for anything other than explaining how class-level items for OOP-in-Lua is implemented. I’m specifically talking about classes and inheritance and why another layer is unnecessary.

Rcade might be intended to be a ModuleScript but that doesn’t automatically mean it fits within my vision of DRY/SRP application with modules. I’ve mentioned in my posts already that I prefer to be explicit about the way I write my code and follow common OOP patterns which include forthright declarations of my constructors and inheritance in code.

Turning a constructor into a function with the __call metamethod is a pointless abstraction of a standard new constructor and makes it less obvious what you’re doing in code. Additionally, if your class supports multiple constructors, then you have inconsistency in your code which is bad for readability.

-- With Rcade
local Thing = MyClass(args)
local Thing2 = MyClass.fromSomething(something)

-- Without Rcade
local Thing = MyClass.new(args)
local Thing2 = MyClass.fromSomething(something)

Just so I can clear the air here: is your module intended to be some kind of implementation of a MiddleClass? I know that some developers do in fact follow this pattern but I don’t see the large benefit unless you explicitly provide utility with the module that makes it worth using. This is what I initially see your module as trying to accomplish and that’s nothing like the native implementations other languages have. They too also have explicit declarations, i.e. in Java you extend a class.

For constructors, you simply need to declare them in your class if you want more than one; and if you want one constructor to be overridden based on the amount of arguments given (which you really should not do anyway), you can either swap arguments around via if statements or use varargs to count or pack arguments into something workable. If applicable, new constructors can also just call the main constructor with some arguments autofilled:

local Space2D = {}
Space2D.__index = Space2D

function Space2D.new(xScale, xOffset, yScale, yOffset)
    local object2D = setmetatable({}, Space2D)

    object2D.X = {Scale = xScale, Offset = xOffset}
    object2D.Y = {Scale = yScale, Offset = yOffset}

    return object2D
end

function Space2D.fromScale(xScale, yScale)
    return Space2D.new(xScale, 0, yScale, 0)
end
2 Likes

Yes a middleclass is exactly what I was going for. Never heard the term before, thanks for bringing it to my attention.


I agree that it does make it less obvious. However my thinking was that it would keep class objects from being able to call the new function from the metatables. However I realize you can keep it from being indexed by objects by editing the __index metamethod. Is it wrong to think that objects shouldn’t be able to call constructors themselves? Or is there a benefit in doing so? is it pointless to worry?

I see I have overcomplicated the idea in my head. I appreciate the example.

You have great points. I’ll be working on a way to make it more explicit, and give opportunity to multiple constructors. I’m glad I posted this here since I wouldn’t have been corrected if I hadn’t!

The point is you are meant to abstract this away, yes someone might be able to do cls.new().new().new().new() forever, but they shouldn’t be programming like that anyway, Same for cls.new().__index.__index.__index, but again, why would someone do this.

3 Likes

It seems like the topic was bumped (due to editing, I didn’t remember posting this anyway?)

I know how to simplify this now.

local function GetDataStore(name: string)
    return setmetatable({
        _dataStore = DataStoreService:GetDataStore(name);
        _dataStoreName = name;
    }, {__index = dataStoreMethods})
end

Better yet, use module:GetDataStore() and just redirect the other function to it (i have a __call thing?).

I stopped using if false then statements to help on prediction because I started using Rojo and that is basically a non issue. It actually does help Rojo from my last testing though. But it’s harder to read, but it’s still whatever. But it has not worked sometimes for me when I have systems that are too in deep. Like a class that gets another class inside another class, etc.


Overall, your explanation on everything is really good. I knew that already as of time of reading this, I haven’t gotten over everything, but I can pretty much see that you showed a lot of good points here.

Wow, been almost 2 years since I’ve read my own post on this thread, but since to this day I still notice it bleeds a few likes upon unsuspecting observers, I’d like to give a follow up to this now that I’ve had the chance to work on more serious projects and rack up experience.


Addressing my reply and therefore the original thread: I was wrong in calling the Rcade class layer unnecessary. Very very wrong, and I’ll deep dive into that.

I almost literally use the exact pattern shown for Rcade now when creating classes and it not only helps with making inheritance much easier and simpler but also for consistency among classes because it abstracts creating the class container boilerplate away. vocksel/class from Wally.

The way the class module works is it creates a basic class container (the whole shebang - the table, the ClassName, inheritance support for the class and its metamethods and so on) and you instead write everything that you want happening on construction in an init function. Not as extensive but it gets the necessities out of the way so you don’t repeat yourself constantly.

The rest of the information can be gleaned from the thread itself which describes the use of the pattern; although they are implemented differently, both the class library and Rcade from the thread both achieve the same intended end result of better class creation.

The posts I made in this thread prior were born out of a place of misplaced confidence in what I believed was correct without either actually having tried out the pattern or being familiar with it and knowing what its drawbacks may have been, if there were any. So over 2 years later, I want to clear the air and apologise for misleading information and terrible advice in my posts. There was a whole lot of words to say virtually nothing very useful and said from a place of inexperience.


Addressing my own points directly:

  • Rcade is unnecessary and ruins readability: Flat out false. Readability is not affected by the use of a middleclass or class creation abstraction. You are simply picking a different location to write your object’s properties for construction.

  • Constructors are made to be explicit: init is explicit. This point makes no sense.

  • Some classes may (require or) desire more than one constructor: Possible to do even with an abstracted class. It may look a bit messy though especially if your new is created somewhere else but alternate constructors are essentially supposed to just change how the base constructor is called as opposed to making a whole new unrelated object.

  • Inheritance is quicker and cleaner with two metatables: Not in most cases honestly. Certainly not quicker and debatable on if it’s cleaner. With the class library I’ve been using as linked earlier, inheritance is incredibly easy - I just have to think in terms of writing properties alone.

  • I wouldn’t need a whole layer: This is true. It’s also true that I like to be consistent among my projects. I don’t think there’s a good or bad answer here since different content has different parameters but this isn’t a view I hold any longer. Speedy provided a good follow up on why the layer is good which is addressing metatables. It is also correct that it’s very easy to break DRY.

  • “An object should only have upwards inheritance”: In my second post, all my comments about inheritance are bold ways to say that the only OOP I know is OOP-in-Roblox and know nothing about encapsulation, polymorphism or any tenets of true OOP. This is just a mess of a reply here, and I was also wrong in saying that OP doesn’t understand the application of DRY.


Although my posts already each have replies, I’m going to hide the content and replace them with a message pointing out this reply. I don’t want any more developers being led by bad advice I gave over 2 years ago when they find this thread because it’s not good for their learning.

This is a good thread with good explanations and a pattern worth using. And even if you don’t want to go to this extent, consider giving vocksel’s class a look as well (especially if you’re a Wally user!).

5 Likes

I find it crazy that it has already been 2 years. I must admit Rcade has its issues, with the biggest problem it has is being more complicated than it has to be.

Back then I don’t think I was knowledgeable enough to explain my intentions clearly. However, thanks to your previous posts colbert, I was driven to improve on it while maintaining the codes original intent.

Since then, I’ve made big improvements to the system which led to increased readability and efficiency. I’m thinking of posting the updated system after making a game based on it.

I appreciate the feedback even this long after! I assume after 2 years we’ve both learned a lot more than we did before, if I ever make any new posts, I hope to have your feedback again!

3 Likes

that would throttle because the object is already created

how DO I POST

never imagine a 100 yr old can shout

2 Likes

So why would we use all these self classes and metatables when you can just do this

local Players = game:GetService("Players")
local player = Players.LocalPlayer
print(player.Name, player.AccountAge)

then just setup a function to check if any message set is containing “AH” as the first to letters to detect if its “AH” or “AHHHHH” or “AHHHHHHHHHHHHHHHHH” and then just return the players name, and if we know the name, we know the account age.

That was just an example of how the OOP system works. You can make much more difficult programs that without the OOP system, would be a mess. OOP is really good at organizing your code into concepts (classes and objects).

Also, with the example I wasn’t necessary meaning the Account age, I meant real age. Of course, you would never use these examples in real Roblox games.

2 Likes