Object Oriented Programming
Or should be called Oopsies Oopsies :trollface:
Thank you github copilot (opens in a new tab) for helping me writing this :3
Summary
Object Oriented Programming (OOP) is a programming paradigm that uses objects and their interactions to design and program applications. The main goal of OOP is to bind together the data and the functions that operate on them so that no other part of the code can access this data except that function.
Parts of OOP
What is an Object?
An object is a software bundle of related state and behavior. Software objects are often used to model the real-world objects that you find in everyday life. For example, a dog is an object. It has state (color, name, breed, hungry) and behavior (bark, eat, wag tail). An object stores its state in fields (variables in some programming languages) and exposes its behavior through methods (functions in some programming languages). Methods operate on an object's internal state and serve as the primary mechanism for object-to-object communication.
What is a Class?
A class is a blueprint for the object. A class encapsulates data for the object.
OOP Principles
Abstraction
Abstraction is the process of hiding the implementation details and showing only functionality to the user. In other words, the user will have the information on what the object does instead of how it does it.
Encapsulation
Encapsulation is the wrapping up of data under a single unit. It is the mechanism that binds together code and the data it manipulates. In encapsulation, the variables or data of a class is hidden from any other class and can be accessed only through any member function of own class in which they are declared.
Polymorphism
Polymorphism is the ability of a message to be displayed in more than one form. In simple words, we can define polymorphism as the ability of a variable to take more than one form. Polymorphism is considered as one of the important features of Object Oriented Programming. Polymorphism is used to perform a single action in different ways. Polymorphism is derived from two Greek words: poly and morphs. The word "poly" means many and "morphs" means forms. Thus, polymorphism means many forms.
Inheritance
Inheritance is a way to form new classes using classes that have already been defined. The newly formed classes are called derived classes, the classes that we derive from are called base classes. Important benefits of inheritance are code reusability and reduction of complexity of a program. The derived classes (descendants) inherit the members of the base classes (ancestors).
TLDR
OOP is a programming paradigm that uses objects and their interactions to design and program applications. The main goal of OOP is to bind together the data and the functions that operate on them so that no other part of the code can access this data except that function. An object is a software bundle of related state and behavior. A class is a blueprint for the object. A class encapsulates data for the object. Abstraction is the process of hiding the implementation details and showing only functionality to the user. Encapsulation is the wrapping up of data under a single unit. Polymorphism is the ability of a message to be displayed in more than one form. Inheritance is a way to form new classes using classes that have already been defined.
Did you know You've have used OOP without you knowing it. Every object/instance uses OOP such as changing a part's color or moving their position
Creating your First OOP Module!
Creating a new module script
You would create an ModuleScript inside of a very generic place like ReplicatedStorage. Viewing the ModuleScript would have the pre-inserted code:
local module = {}
return module
Creating a new class
You would rename the defined module = {}
to any class name you desire. aka: myCoolClass = {}
. You should get something like this:
local myCoolClass = {}
return myCoolClass
Using metamethods
Metamethods are require on making a new class. Learn more about metamethods.
My friend Fevo pointed out they are not required to make a class, but the practice of making one without it is not recommended.
local myCoolClass = {}
myCoolClass.__index = myCoolClass
return myCoolClass
Creating a new instance
Creating a new instance is fairly simple, you would write a function thats written like function myCoolClass.new(args,...)
Then you would define the self
variable and set it to setmetatable({}, myCoolClass)
. The reason why we would set it to the myCoolClass
as the metatable is because inside the myCoolClass
table we've defined .__index, so we are using it for the __index metamethod.
You can also just write {__index = myCoolClass}
too if you dont want to write myCoolClass.__index = myCoolClass
at the very top.
local myCoolClass = {}
myCoolClass.__index = myCoolClass
function myCoolClass.new(string)
local self = setmetatable({}, myCoolClass)
self.Text = string
return self
end
return myCoolClass
Making a new method
Methods are a bit different from the constructor functions. The reason is one the method function uses a :
while the constructor uses a .
. The :
is used to define the self
variable. The self
variable is used to define the instance of the class. You can also use self
to define the instance's properties.
local myCoolClass = {}
myCoolClass.__index = myCoolClass
function myCoolClass.new(string)
local self = setmetatable({}, myCoolClass)
self.Text = string
return self
end
function myCoolClass:PrintText()
print(self.Text)
end
return myCoolClass
Using the class
Congratulations! You have successfully created your first OOP module!
Heres a quick example of how to use the class:
local myCoolClass = require(game.ReplicatedStorage.ModuleScript)
local myInstance = myCoolClass.new("Hello World!")
myInstance:PrintText()
This should result in the output of Hello World!
in the Output tab.
Overview
Here's a quick overview of the code that you've written:
local myCoolClass = {}
myCoolClass.__index = myCoolClass
function myCoolClass.new(string)
local self = setmetatable({}, myCoolClass)
self.Text = string
return self
end
function myCoolClass:PrintText()
print(self.Text)
end
return myCoolClass
local myCoolClass = require(game.ReplicatedStorage.ModuleScript)
local myInstance = myCoolClass.new("Hello World!")
myInstance:PrintText()
Conclusion
OOP is a very powerful tool that can be used to make your code more organized and easier to read, and become more friendly to other developers.
Note: You dont need OOP to create games, but it is a very good practice to use OOP since of how it makes life easier when doing things.You Should Ditch the old OOP Idiom
A new object-oriented programming idiom, and why you should ditch the old one! (opens in a new tab) By darmantinjr (opens in a new tab)
You would be asking, "Why in the world would I ditch this OOP idiom you just taught me?". The reason being is that theres a few issues when using the old idiom of OOP.
Issue 1 - .new().new()
.new()
shouldn't be listed as a member of the object that you have already created. It wouldn't make sense of making a new instance of the object that you have already created.
Issue 2 - .__index
.__index
shown alot for type suggestions. Except we always see the .__index
many times which can get annoying.
Issue 3 - No type inference when writing methods of class
When typing the functionality of the class, no type suggestions would be given to you about the object that you are working with. Only the attributes from __index
would be given to you. Everything else in self
would be kept as a secret.
Creating a Better OOP Module
Skipping steps 1 & 2
Redefining the class layout
To solve the first issue we would write this code below
local myCoolClass = {}
myCoolClass.constructors = {} -- Can be .interface
myCoolClass.methods = {} -- Can be .schema
myCoolClass.metatable = { __index = myCoolClass.methods }
We have split the class into three different subcategories.
constructors - This is where all of the constructor functions would be placed in. aka myCoolClass.new()
. This is exposed to the user when they first required the module. No methods would be shown from type suggestions except for the constructor function(s).
methods - This is where all of the methods would be placed in. aka myCoolClass:PrintText()
. This contains all of the .__index
initialized objects such as methods
, common values
, etc
. The constructor in the constructors table would return an object that has access to all of the members of its table. In type suggestions it would show you the methods, values, or etc of that object you've created.
metatable - Using self = setmetatable({}, {__index = myCoolClass.methods})
can cause a performance overhead when youre creating a new metatable on each construct, so defining it on the root of the code would define it once and can be reused multiple times. Fixing the performance overhead, and issue #2
Creating a new constructor
Creating a new constructor is a bit different but not too much to the point its unknown territory. The only difference is that we would be using the constructors
table instead of the myCoolClass
table.
local myCoolClass = {}
myCoolClass.constructors = {}
myCoolClass.methods = {}
myCoolClass.metatable = { __index = myCoolClass.methods }
function myCoolClass.constructors.new(string)
local self = setmetatable({}, myCoolClass.metatable)
self.Text = string
return self
end
This would also fix issue #1 since the methods and constructors are separated into different tables. Now this would give back all the methods that object contains, and the constructor be not infered as part of the object.
Making a new method
You can tell this is going to be different when making a new method
We would be using the .
and not the :
operator when defining the method. Reason being is that we want to explicitly define self
so when using values in self
we would get some type suggestions.
local myCoolClass = {}
myCoolClass.constructors = {}
myCoolClass.methods = {}
myCoolClass.metatable = { __index = myCoolClass.methods }
function myCoolClass.constructors.new(string)
local self = setmetatable({}, myCoolClass.metatable)
self.Text = string
return self
end
function myCoolClass.methods.PrintText(self: myCoolClass)
print(self.Text)
end
Adding a type
We would need to make a type
for the module since its going to yell at you for the code when you did self: myCoolClass. We would need to make a type for the myCoolClass
so that it can be used in the method.
local myCoolClass = {}
myCoolClass.constructors = {}
myCoolClass.methods = {}
myCoolClass.metatable = { __index = myCoolClass.methods }
function myCoolClass.constructors.new(string)
local self = setmetatable({}, myCoolClass.metatable)
self.Text = string
return self
end
function myCoolClass.methods.PrintText(self: myCoolClass)
print(self.Text)
end
type myCoolClass = typeof(myCoolClass.constructors.new(table.unpack(...)))
You would be wondering whats with the table.unpack(...)
Totally not ctrl c and v from the post to explain why This is because you will get a warning in your script editor when you do not explicitly define each individual required argument expected to be passed to the constructor. You can manually define these by hand, or use table.unpack(...)
as a shortcut to handle the error.
Finalizing the module
The last line you are going to add is the return statement. This is where you would return the constructors table so that the user can use the constructors to create new objects. If you just returned myCoolClass
then the user would be able to access the methods table and the metatable table which is not what we want.
local myCoolClass = {}
myCoolClass.constructors = {}
myCoolClass.methods = {}
myCoolClass.metatable = { __index = myCoolClass.methods }
function myCoolClass.constructors.new(string)
local self = setmetatable({}, myCoolClass.metatable)
self.Text = string
return self
end
function myCoolClass.methods.PrintText(self: myCoolClass)
print(self.Text)
end
type myCoolClass = typeof(myCoolClass.constructors.new(table.unpack(...)))
return myCoolClass.constructors
Using the new and improved class
Now that we have created the new and improved class, we can now use it in our code.
You will notice when typing this code is that none of the three issues listed above are present. You will start seeing type suggestions for the constructor, and the methods. You will also get type suggestions for the values that are in the object.
local myCoolClass = require(game.ReplicatedStorage.ModuleScript)
local myObject = myCoolClass.new("Hello World!")
myObject:PrintText()
You should get an expected output of Hello World!
in the output window.
Overview
Heres a quick overview of the code we have written so far.
local myCoolClass = {}
myCoolClass.constructors = {}
myCoolClass.methods = {}
myCoolClass.metatable = { __index = myCoolClass.methods }
function myCoolClass.constructors.new(string)
local self = setmetatable({}, myCoolClass.metatable)
self.Text = string
return self
end
function myCoolClass.methods.PrintText(self: myCoolClass)
print(self.Text)
end
type myCoolClass = typeof(myCoolClass.constructors.new(table.unpack(...)))
return myCoolClass.constructors
local myCoolClass = require(game.ReplicatedStorage.ModuleScript)
local myObject = myCoolClass.new("Hello World!")
myObject:PrintText()
Summary and Conclusion
OOP is one of the most useful methods of programing in Lua. It allows you to create objects that can be used in your code. It also allows you to create a more organized code base.
A crazy good tool to have in your arsenal when you are writing code in Lua. 10/10 would recomend to anyone who does coding in Roblox or off-platform.