Deep breath
What is OOP
Object-oriented programming, as its name implies, is a paradigm (a way to write and think about your code), which is oriented around an object.
What is an abstraction
Before getting into anything, I want to quickly explain an abstraction. Abstraction is simply hiding implementation details from something. A good example is the Roblox API. It is an abstraction over a bunch of C++ code. Instead of working and interacting with C++ code directly it is made much more simple. Abstractions are extremely valuable since they let you focus on solving a problem elegantly. With good abstractions, you’ll probably never need to know the implementation details.
What is OOP [2]
The base concept of OOP, is, well, the object. An object is just like an object in the real world. They can have data (aka attributes), and “behaviors” (methods which perform on the object).
Let’s use a pizza as an example as an object. A couple of attributes it might have is:
And for methods it might have
(couldn’t think of any other lol)
When you define a function on an object like Pizza:Eat()
that is called a method.
OOP’s purpose is to introduce objects as an abstraction level. We don’t care about how we eat the pizza we just want it to be eaten! OOP focuses on the what, not the how.
What are classes
Most object-oriented languages (like C#) have the concept of a class. Think of a class as a template, blueprint, whatever, for creating objects. You could use the Pizza
class to create pizzas, or even a Car
class (like your example) for cars.
When defining the class we specify the attributes and methods the object will have. When creating objects of a class we say it is an instance of the class. So a pizza could be an instance of the class Pizza
.
Your code could look like this:
local Pizza = { }
function Pizza:Eat()
-- Eat the pizza
end
Pretty epic, no? But, how do we actually make pizzas? That is where the constructor comes in. A constructor defines how we create an instance of a class, and returns it.
So your constructor could look something like
local Pizza = { }
Pizza.__index = Pizza
function Pizza.new(flavor, slice_count)
local this = { }
this.Flavor = flavor
this.Slices = slice_count
setmetatable(this, Pizza)
return this
end
function Pizza:Eat()
-- eat the pizza
end
From here you would call the constructor Pizza.new()
to create a new pizza. The constructor takes some arguments, them being the flavor and how many slices it has.
local new_pizza = Pizza.new("Pepperoni", 8)
print(new_pizza.Slices)
print(new_pizza.Flavor)
You might be asking what I am doing with metatables and metamethods. You should already know about them. You don’t need them for OOP but a metatable approach is more simple. We use __index
so we can define the methods outside of the constructor and keep them in 1 place rather than making new methods. And each instance of the pizza has the same metatable.
The : (colon) notation
function a:b(...)
end
is syntactic sugar (a nicer way to write something) for
function a.b(self, ...)
end
self
is an implicit parameter that is passed when you use the :
notation to define functions. An example to show this can be that workspace.Building:Clone()
is sugar for workspace.Building.Clone(workspace.Building)
. You could pass any argument as the first one and it would be treated as self
, i.e game.Destroy(workspace.Baseplate)
would destroy the baseplate.
So this means that
function Pizza:Eat()
end
is the same as
function Pizza.Eat(self)
end
This syntax is very convenient as it allows to define methods. The .new()
constructor does not use the :
notation and is separate because it isn’t actually a method of the class.
What is inheritance
You can also have your classes inherit from other ones. For instance our Pizza
class might inherit from a FastFood
class. Your Car
class might inherit from a Vehicle
class. A class’ inheritor extends the class (Pizza
extends the FastFood
class), and the extended class is the superclass of the inheritor (aka subclass).
Inheritance can be implemented with metatables. We can use the __index
metamethod to do just that.
Here might be the FastFood
class:
local FastFood = { }
FastFood.__index = FastFood
function FastFood.new()
local this = { }
setmetatable(this, FastFood)
return this
end
function FastFood:ThrowAway()
-- throw the food away
end
Our pizza class constructor, remastered:
local Pizza = { }
Pizza.__index = Pizza
setmetatable(Pizza, { __index = FastFood })
function Pizza.new(flavor, slice_count)
local this = { }
this.Flavor = flavor
this.Slices = slice_count
setmetatable(this, Pizza)
return this
end
local new_pizza = Pizza.new("Pepperoni", 8)
new_pizza:ThrowAway() -- you didn't like it :(
Remember again:
new_pizza:ThrowAway()
is the same as
new_pizza.ThrowAway(new_pizza)
new_pizza
is that implicit self
we were talking about earlier.
You’ve been using it all along!
You probably already knew this, but I’ma say it again. You’ve been creating new instances of the Instance
class via Instance.new()
. You’ve been checking if Instance:IsA("BasePart")
to see if you’re working with an inheritor of BasePart
, the superclass of Part
, MeshPart
, etc. You’ve been cloning these parts with the Instance:Clone()
method (superclasses).
Should I use it?
I personally don’t. But other developers find that it allows the writing of better code. You don’t have to use the paradigm, and I (and many others) are able to write decent code without it.
You should at least try it, which it looks like you already have.
More on OOP
Yes, there is more. I only scratched the surface of OOP. There is more that you should definitely look into. I only talked about abstraction and inheritance, “pillars” of OOP.
- Encapsulation (joining data into 1 unit; a class)
- Polymorphism (where something can be multiple things)
- Getters and setters
- Access modifiers
- Prototype-based OOP rather than class-based (JavaScript uses the former, you can do it in Lua too)
And more.