Advanced OOP Implementation

As I work on my new neural network library, I needed a proper object-orientated programming (OOP) implementation in order to start. Unsatisfied with the existing libraries, I simply wrote my own. On advice from @ReturnedTrue, I decided to open-source this implementation as it seems to be the most advanced public library for OOP on Roblox. The library is designed to mimic Roblox’s Instance functions as well as Java’s OOP coding style. If you’re a fan of OOP, chances are, you are familiar with Java. If not, I will try to explain everything in detail anyway!

ed65a9668d521d14ee75e7ddfadd0fc2

An example of the supported OOP code from the tutorial.

Why Should You Consider OOP?

OOP is a foundation for many successful modern games, with others using similar concepts such as ECS. Building a game, or any program for that matter, without a concept like OOP, would be a total nightmare, if possible. The reason is that all of those games use objects or entities to work. Roblox, for one, uses OOP; all instances on Roblox are objects. Whenever you try to build a game in any game studio software, you are building it with various objects that have various abilities. No matter what you do, you are bound to be relying on OOP or a concept similar to it anyway.
Trying to build a game without objects or entities is like trying to build a cathedral without carefully cut blocks or bricks!

The difference between Roblox and those other game studios is that we are not inherently able to create our own objects. Creating our own objects would generally make development far easier, and, believe it or not, reduce the amount of code you need to write! Objects group up all the data they use and all the functions they need in a convenient little package, just like Parts, Vector3s, Models, Services, or the Player.

Features

So, what exactly does this library offer that makes it so special? Currently, the library offers:

  • Clean, Java-esque OOP coding without the need of ever using metatables yourself as well as an extremely clean coding style
  • Creation of named classes and easy inheritances
  • Java’s super() function
  • Object uniqueness guaranteed by GUIDs, much like Instances
  • All appropriate metamethods bound to functions of a certain name (example: __add = :add()) by default. This also means that you can inherit metamethods!
  • A set of default functions for all objects like: :toString(), :equals(), :isA(), :GetID(), :GetName(), :SetName(), and :Destroy(). Feel free to add more!
  • A set of functions in the library for easily managing your objects: find(), findByName(), type(), copyTable()
  • The ability to serialize all custom objects and tables into strings, regardless if it is cyclical or uses things like Vector3 or UDim2 (all of which are not supported by JSON without the library)!

Future Features

This module was built as a foundation for my projects, but, if it is actually wanted by the community and there is enough interest in it, I will try to add the following trivial features when I have the time:

  • Type detection that would allow you to set what types each function will accept and a warning system for various other mistakes the user may make, like a cyclical inheritance.
  • Event objects that would behave just like any other events, but for your custom objects. This includes a .Changed event that I found in some libraries, though I’m not sure if it is useful here.
  • Multiple Inheritance would allow you to have a class inherit the properties of more than 1 other class at once (not to be mistaken with inheriting what your parent inherits). This isn’t a base feature because it is frowned upon in many circles as it isn’t always necessary and tends to make code more confusing than it needs to be. Instead, I might choose to implement Java interfaces over multiple inheritance.
  • Anything else that you suggest that may be of use!

Documentation

While @ReturnedTrue is working on a MkDocs-based documentation website, here is the documentation. Before we go on, though, lets clear up some definitions:

Definitions:

  • Base = The name of the library’s require()
  • Class = A Class created with Base.new()
  • CObject = A custom object created with Class:make() or Class:super()
  • PrimObject = A primitive object like UDim, UDim2, Vector2, Vector2int16, Vector3, Vector3int16, or CFrame.
  • self = The metatable/CObject the function is used on.
    With that out of the way, let’s begin.

Base Functions:

Base.new(className - String, parent[OPTIONAL] - Class)
Returns: Class

– ‘className’ is the name of the class to be created. It is highly recommended that it is the same as the name of a module this function is used in for the sake of organization and ease of use.
– ‘parent’ is the class that this new class inherits. All functions that are usable on the parent (and it’s parents) are usable in the new class.
This function creates a new class with the given name and optional ancestor/parent class from whom the new class inherits all functions, if any, and returns it.


Base.newExtends(className - String, parent[OPTIONAL] - Class)
Returns: Class

– ‘className’ is the name of the class to be created. It is highly recommended that it is the same as the name of a module this function is used in for the sake of organization and ease of use.
– ‘parent’ is the class that this new class inherits. All functions that are usable on the parent (and it’s parents) are usable in the new class.
Functionally identical to Base.new() but with a different name for the sake of clarity for those who are used to Java.


Base.find(tab - Table, object - CObject)
Returns: Number/String/Boolean

– ‘table’ is the table that is to be searched. It can be an array or a dictionary as long as the CObject being searched for is a VALUE, not a KEY/INDEX.
– ‘object’ is the CObject that you’re trying to find in the table.
This function scans the given table in order to find the given CObject inside. If it finds the object, it will return the key/index that the object is located in the table. Otherwise, it returns false.


Base.findByName(tab - Table, name - String)
Returns: Tuple(Number/String/Boolean, CObject)/Boolean
– ‘table’ is the table that is to be searched. It can be an array or a dictionary as long as the CObject being searched for is a VALUE, not a KEY/INDEX.
– ‘name’ of the CObject according to it’s .NAME variable.
This function scans the given table in order to find a CObject inside that has the given name. If the .NAME of a CObject inside matches the name being searched for, it returns a tuple that is the key location of the CObject and the CObject itself. Otherwise, it returns false.


Base.type(object - Anything)
Returns: String
– ‘object’ is the object to be checked. It can be of any type.
This function will determine the type of the object provided. It works exactly like Roblox’s typeof() except it works with CObjects, returning their .CLASS_NAME if detected. Otherwise, it returns that typeof() would.


Base.copyTable(dict - Table)
Returns: Table
– ‘dict’ is the table to be copied. It can be an array or a dictionary, but cannot be a metatable or a CObject.
This function is a pure utility function that is unrelated to OOP. It is just there if you want to copy a table. Might make it compatible with CObjects in the future, depending on need. Do not try to copy a cyclic table (a table that references itself or a part of itself in a way that causes a loop) or you will crash.


Base.Serialize(object - Table/CObject, returnJson - Boolean, keyList - Table, typeOfList - Number/String, compressPrimObjects - Boolean, ignoreWarnings - Boolean)
Returns: Table/String
– ‘object’ is the table or CObject to serialize.
– ‘returnJson’ is a boolean for whether or not the function should return the serial table or the JSON of the serial table.
– ‘keyList’ is an array of keys that are used for blacklisting or whitelisting certain variables. Use very carefully or don’t use it at all.
– ‘typeOfList’ is a number or string for whether you want to blacklist or whitelist the keys in keyList. 0 or “Blacklist” for a blacklist and 1 or “Whitelist” for a whitelist.
– ‘compressPrimObjects’ is a boolean for whether or not the function should compress any of the primitive objects it finds. WARNING: Due to JSON limitations, this cannot be used if you want a JSON or if you plan to use this in a Datastore!
– ‘ignoreWarnings’ is a boolean for whether or not you want to ignore the non-essential warnings that may come up.
This function will serialize the given table or CObject (no other metatables, only CObjects) and either return a serial table or the JSON of the serial table. The serial table is a table that contains all of the information needed to reconstruct the table or CObject perfectly. Assuming you do not compress, this is very useful for passing the table/CObject through a medium that doesn’t allow for metatables, like the client-server barrier (Remotes), or doesn’t allow for tables in general. Another advantage of this function is that it supports cyclic tables, that is, tables whose references have a cyclical nature, very common in OOP. Due to JSON not inherently supporting cyclic tables, this function makes it possible to convert cyclic tables into text.


Base.SerializePrimitiveObject(obj - PrimObject, compress - Boolean)
Returns: String
– ‘obj’ is the primitive object to be serialized.
– ‘compress’ is a boolean of whether to compress the data inside of the object using string.pack() or not. WARNING: Do not use this if you intend to use this in a JSON or a Datastore! They are not compatible!
This function serializes the given primitive object into a string, compresses it if need be, and returns it. For internal use but you might find this useful in your own ventures as JSON does not inherently support primitive objects.


Base.DeSerialize(serial - String/Table, objectsToUse - Table)
Returns: Table/CObject
– ‘serial’ is a serialized table or string (if converted to JSON) produced by the CObject’s :Serialize() function.
– ‘objectsToUse’ is a dictionary that provides the classes that are needed by the deserializer to reconstruct the CObjects inside. The KEY must be the correct name of the class and the VALUE must be the reference to the class (require() the class if it is in a module). For example, {Class1 = require(script.Parent.Class1)} is valid. If you want to provide a directory/folder that contains some or all of the classes needed, set it under the “_Package” KEY. For example, {_Package = "script.Parent"} is valid.
This function deserializes the given string and classes into the original table or CObject that it once was. If not all of the classes that are needed are provided, the function will error accordingly. For good practice, avoid using this function and instead use the :DeSerialize() function in all classes, it includes the class it is called on as a part of the ‘objectsToUse’ variable automatically. Otherwise, it is identical.


Base.DeSerializePrimitiveObject(serial - String)
Returns: PrimObject
– ‘serial’ is the serialized string version of the primitive object.
This function deserializes a string produced by Base.SerializePrimitiveObject() and returns the primitive object. It automatically detects if it has been compressed. The output object should be identical to the original.

Default Functions For All Classes:

:make()
Returns: CObject
This function will generate a CObject from the class it is used on, self. This is an essential clean CObject that contains only the functions and metamethods from the given class. If the class inherits/extends another class, use :super() instead!


:super(…)
Returns: CObject
– ‘...’ is a tuple of any number of variables that should be passed onto the .new() constructor of the first inherited ancestor class.
This function is identical to :make() except it supports inheritances. The variables given are passed along to the .new() constructor of the first class that self inherits. Assuming you assembled your hierarchy correctly, all inheriting classes will pass on data to the most upper-level class that the other classes inherit from in a chain-like fashion. This is useful in the case where you want the ancestor class to handle some of the variables with its constructor but want to handle others in the current constructor, finishing off each other’s work. This function works exactly the same as super() in Java and is named as such.

Default Variables/Properties For All CObjects:

.GUID = A globally unique identifier used to differentiate CObjects. In order to keep the chance of collision as close to zero as possible, the ID is 2 GUIDs stacked.
.CLASS_NAME = The name of the class the object comes from. Works exactly the same as ClassName for Roblox Instances.
.NAME = The name of the CObject. Useful for organizational purposes. Works exactly the same as Name for Roblox Instances.
.PARENT = A reference to the class that this object’s class inherits. Required for isA().

Default Metamethod Bindings For All CObjects:

Each of the following metamethods is made to be bound to a function in the CObject by the name given. If such a function doesn’t exist and the metamethod is fired, an error occurs. Don’t worry, this doesn’t occur on accident; only when you try to use the metamethod without defining a function for it to use.

__call = :call()
__tostring = :toString()
__add = :add()
__sub = :subtract()
__mul = :multiply()
__div = :divide()
__pow = :power()
__eq = :equals()
__len = :length()

Default Functions For All CObjects:

:toString()
Returns: String
This function simply returns the .NAME property of the CObject. Feel free to replace it with your own.


:equals(object - CObject)
*Returns: Boolean *
– ‘object’ is the CObject that you are comparing to self.
This function compares the .GUID's of the 2 CObjects. If they are the same, it returns true. Otherwise, false.


:isA(className - String)
Returns: Boolean
– ‘className’ is the name of a class that this CObject is or inherits from.
This function works the same way as Roblox’s :IsA() does except it is used for CObjects. If self is from a class whose .CLASS_NAME is className or inherits from a class whose .CLASS_NAME is className somewhere down the line, it returns true. Otherwise, it returns false. Up to 20 inheritances are supported for the sake of sanity.


:GetID()
Returns: String*
This function returns the .GUID of self.


:GetName()
Returns: String
This function returns the .NAME of self.


:SetName(name - String)
Returns: void
This function sets the .NAME of self to the given string name.


:Destroy()
Returns: void
This function properly destroys the CObject. Currently, it just sets the .PARENT variable to nil and sets self to nil. In the future, it may change.


:Serialize(returnJson - Boolean, keyList - Table, typeOfList - Number/String, compressPrimObjects - Boolean, ignoreWarnings - Boolean)
Returns: Table/String
– ‘returnJson’ is a boolean for whether or not the function should return the serial table or the JSON of the serial table.
– ‘keyList’ is an array of keys that are used for blacklisting or whitelisting certain variables. Use very carefully or don’t use it at all.
– ‘typeOfList’ is a number or string for whether you want to blacklist or whitelist the keys in keyList. 0 or “Blacklist” for a blacklist and 1 or “Whitelist” for a whitelist.
– ‘compressPrimObjects’ is a boolean for whether or not the function should compress any of the primitive objects it finds. WARNING: Due to JSON limitations, this cannot be used if you want a JSON or if you plan to use this in a Datastore!
– ‘ignoreWarnings’ is a boolean for whether or not you want to ignore the non-essential warnings that may come up.
This function is identical to Base.Serialize() except it is exclusive to CObjects and doesn’t require the CObject to be provided.

Getting Started

To start using this library, all you need is the module itself, linked below. However, in order to use it effectively, a few other things need to be done.

Module And Kit Links:

https://www.roblox.com/library/5610874937/Advanced-OOP-Implementation-Kit
https://www.roblox.com/library/5610892012/Advanced-OOP-Implementation-v1-0

Using The Kit And The Library:

Firstly, you should grab the kit instead of the module as it not only gives the module but also a template Package with the template object module and a BaseRedirect module used for redirecting the objects inside the Package to wherever you want to place the library module.
To begin, simply make a copy of the Temp module, open it up, and replace all instances of ‘NAME’ with the name that you choose for the object, which is preferably the name of the module as well.

If this module is inheriting another object, declare that other object at the top, comment out the Base.new() and replace it with the Base.newExtends() line, comment out the :make() line and replace it with the :super() line, and replace the ‘?’ in the Base.newExtends with the object you declared. The code should look like this:

For the sake of organization, make the new object’s parent the object it inherits. In this case, the hierarchy should look like this:

839f2689767430a2dddc92e23fa69ffc

To add variable, under the :make()/:super() line, add obj.nameOfVariable = Something, obviously you can set it anything you wish. For example:

851a03ada691a9fb6731de643f471f49

Now lets add a function that grabs the variable and returns it to the user. To do this, make a new function below the .new() and name it on the lines of nameOfClass:GetSomeVariable(), and have it return the variable in question using self. In this case, I do this:

ed65a9668d521d14ee75e7ddfadd0fc2

This tutorial, though, is quite barebones. However, teaching OOP in its entirety is a job for an online course, not for this post. OOP is quite the science and is a valuable skill for any developer, whether they like to use it or not. If you want to learn about OOP, and, by extension, this library, then you have 3 choices.
If you know Java, just follow the tutorial on W3Schools.
Otherwise, try reading through my entire documentation, doing so may clear up your questions.
If even this isn’t of any help, just ask for it directly below as a comment! I’ll try to answer what I can.

WARNING: To avoid problems, do not use any of the following reserved keys in your dictionaries!
GUID, NAME, CLASS_NAME, PARENT

38 Likes

This is a very useful resource and makes making classes so much simple.

The link of the kit and Main Module are not set to sale.

2 Likes

Woops. Forgot to open them up, sorry about that. All good now.
I also accidentally uploaded the edited Temp object from the tutorial into the kit so if the names inside aren’t “NAME”, just replace the kit with a new one I reuploaded.

1 Like

The module seems cool but most of the “Why Should You Consider OOP” section is wrong.

For starters, games (outside of Roblox, at least) have adapted to using ECS (Entity Component Systems) for a composition over inheritance approach. OOP is just not considered efficient enough for more complex and intensive games.

You also aren’t bound to rely on OOP. There are other paradigms, and they work pretty well too.

Also the part about convenience and grouping isn’t something specific to OOP; it’s what all paradigms aim to work out in some way.

All in all it’s better to advertise OOP as an option, not a definitive need.

9 Likes

I have seen that ECS can be a more preferred option in game development, however, it seems that in terms of implementation, it’s still getting up there. As far as I known, many engines still need to do more work to support ECS. Along with that, OOP is still currently popular amongst many programmers and there will most likely be a transition period out of using OOP for games.

1 Like

Admittedly I didn’t know that there were other paradigms and that OOP is only one of them, but the basic point still stands that they are necessary for game development.
OOP may be old fashioned, but, much like Lua, it is an excellent starting point that needs to be learned until you can tackle something greater like ECS. You can’t learn LSTM neural networks until you master the feedforward neural network.

Therefore, when I speak about the perks of using OOP, imagine that I am speaking of the general perks of using a paradigm like OOP, which I understand are consistent throughout all of them anyway. OOP is just the most popular one that I have heard about, and, as far as I can tell, has the most fans on Roblox.

Thank you for this.
Some of this is beyond me but I can still see the benefits of it.
I also think that the suggested Future Feature of Event objects could be very useful when you for example want to replicate data from the server to the client when something changes and finally update for example a GUI with the updated data.

Perhaps I’m wrong but I think it’s a common struggle for beginner programmers to effectively handle server changes and then send relevant data to the client and also have a cached local version of the data and reuse this across several modules to minimize the use of remote functions. So I think Event objects would be a useful addition to your module to come closer to be able to handle such scenarios.

3 Likes

Very useful and time-saving resource. Allows users to somewhat bypass the work needed to use metatables to implement objects in Roblox with good features (default functions like destroy to solve memory leaks).

I will definitely use it in my project and credit you.

BTW, any chance of uploading the code onto GitHub if we do run into any issues with the Libary?

2 Likes

I will try to upload it to GitHub eventually when I have the time. I’m just currently working on my neural network library which breaks my brain for a while. Good luck in your project!

1 Like