Intro to OOP

Out-of-the-box thinking is necessary!

by yin_yang

Author Avatar

Disclaimer: OOP is not required in order to create efficient code. In fact, using this improperly would make it even less efficient than before.

Disclaimer 2: This lesson assumes you already have prior knowledge of the Lua language.

What is OOP?

Object-Oriented Programming -- or OOP -- is simply a programming model where all kinds of data (be it Bools, Strings, etc.) is encapsulated into an object. In other words, you are essentially bundling up information under a single unit that can later be manipulated by the object's function operations or methods that you create.

The thing with OOP is that you'll be able to create classes of your own, hosting certain behaviors and function operations which future objects can then extend from or inherit. You can pretty much think of this as something cloning another, with the ability to do exactly what its counterpart is able to do, but with a twist of its own. Here's some code for visualization:

Say a random crayon had magically appeared in our hand. It just so happened to be red, dull, and probably 2 inches after being used so much.

local RedCrayon = {
    Color = "Red",
    Condition = "Dull",
    Length = 2
}

From what we know with our coloring activities in the past, our crayons weren't always red or 2 inches long and dull. We sometimes had blue crayons, purple crayons, and even in the color black. On top of that, some were a lot longer, or even extremely shorter than others, with a mix of sharpness or dullness.

But from just this poor excuse of a red crayon, how can we tell exactly its original shape, color, or condition? It had to have come from somewhere, right? From a default set of data.

local Crayon = {
    Color = "White",
    Condition = "Sharp",
    Length = 5
}

This is a class with default properties that can be inherited by objects (in this case, crayons!).

But take that with a grain of salt, because Lua doesn't actually know what a class is. It is still in and of itself an object, although you should be careful not to declare that as final since other languages out there do make the distinction. In Lua, we're doing what's called prototypal inheritance, where, put simply, objects inherit from objects.

--

Crayons can also color on things when physically contacting another surface.

function Crayon.ColorSomething()
    Crayon.Length = Crayon.Length - 1
    Crayon.Condition = "Dull"
end

But wait! If we have a function or method operating on the table (Crayon) in the global scope, we would end up modifying the default values! If future objects end up inheriting from that same table we accidentally operated on, they would be affected too.

This is where we include a self parameter; to give our objects a selfness to them; to say that, "hey, these are my values, and only I can act on them."

function Crayon.ColorSomething(self)
    self.Length = self.Length - 1
    self.Condition = "Dull"
end

Now, when calling the ColorSomething() method, objects will only be able to manipulate its own lengths and conditions. Not the class's, nor any of the other crayons: only its own.

self is talking about the object that fired the method. If I had a purple crayon, and did:

function PurpleCrayon.ColorSomething(self)

self would be equal to PurpleCrayon.

--

Going back to our red crayon, you can probably already tell that it's been dull ever since we got it. Now if only there was some way to sharpen it, along with some of the other dull crayons laying around. Maybe the Crayon class has something to offer us?

function Crayon.Sharpen(self)
    if (self.Condition == "Dull") then
        self.Condition = "Sharp"
    end
    self.Length = self.Length - 1
end

Yes! That's exactly what we needed! All we need to do now is execute that command from our red crayon.

RedCrayon:Sharpen()
print(RedCrayon.Condition)

It will print "Sharp" in the output. It looks like we did it!

But you're probably wondering: why are the parentheses empty? Isn't there supposed to be an argument to pass as self? Nope. That's how an object's selfness works. The method cannot be used to modify any other object but itself. You can use the colon operator (:) to hide this self paramater, without changing the definition of the function.

function Crayon:Sharpen()
    if (self.Condition == "Dull") then
        self.Condition = "Sharp"
    end
    self.Length = self.Length - 1
end

But that's just syntactical sugar. You're free to use either a dot or colon.

It's good we have all of these behaviors and all, but what good are they if we can't create new crayons or instantiate them?

Instantiation

Before proceeding, it's good to know that, unlike other familiar programming languages like (but is not limited to) Javascript, Lua does not use any built-in constructor functions that identifies and/or defines an object. That's because it isn't an OO language. We are simply mimicking OOP (call it fake OOP if you will) using what's available to us, thanks to Lua's extensibility: Metatables and Metamethods.

Word of advice, it's good to know that Metatables are not different from tables. They are still just ordinary tables, but with data (metadata) that act as source information to describe other data.

Learn more about Metatables and Metamethods here: https://developer.roblox.com/en-us/articles/Metatables

For the sake of introduction, we'll only be focusing on how to create an object using setmetatable() function and __index metamethod as the final part of this lesson.

Here's what a typical instantiation function looks like, using our crayons example:

function Crayon:New(Color)
    local Object = {}

    Object.Color = Color

    self.__index = self
    setmetatable(Object, self)
    return Object
end

setmetatable(Object, self) is basically read as, "The metatable of Object is self." (Using what we learned from earlier, self is equal to Crayon in this case)

And now you're probably wondering, what is __index? Why did you set the __index of self to self?

"__index" in this example, in conjunction with setmetatable() tells the code to look for a value in self IF the value cannot be found in the object itself, represented by table Object. This is how inheritance works.

To elaborate further, I'm sure you've already noticed that only the Color property is being set by the instantiation function. But if we tried printing the Condition property of a new object, it'll output "Sharp", which, surprise surprise, is the same as the default value set in the Crayon table. That's because the code was told to look at the metatable (Crayon) since we never set a Condition value in our new object.

--

In regards to the function itself, you don't actually have to name the method as "New." It could be anything; it's only to identify it as the instantiator. Here are some ways of implementing an instantiation function:

function Crayon:Create(Color)
    local self = setmetatable({}, {
        __index = Crayon
    }

    self.Color = Color

    return self
end

function Crayon._make(self, Color)
    return setmetatable({
        Color = Color
    }, {
        __index = self
    })
end

Main Code

Now you're probably curious on how this would all look in one, big chunk of code.

local Crayon = {
    Color = "White",
    Condition = "Sharp",
    Length = 5
}
Crayon.__index = Crayon

function Crayon:New(Color)
    local Object = {}

    Object.Color = Color

    setmetatable(Object, self)
    return Object
end

function Crayon.ColorSomething(self)
    self.Length = self.Length - 1
    self.Condition = "Dull"
end

function Crayon:Sharpen()
    if (self.Condition == "Dull") then
        self.Condition = "Sharp"
    end
    self.Length = self.Length - 1
end

And that's pretty much it for your Crayon class (or object, if we're being technical.) All you have to do now is create a new object to extend from this class, otherwise all of this is just worthless.

local RedCrayon = Crayon:New("Red")

for i = 1, 3 do
    RedCrayon:ColorSomething()
end

RedCrayon:Sharpen()

print(RedCrayon.Length)

If the Lua gods will it, this snippet of code should print a simple 1 from RedCrayon's Length property.

And that's a wrap!

The best way of practicing OOP is just to apply it. Take it from me, reading documents over and over again will hardly get you anywhere; they're only useful to providing you with what you NEED to know. Play around with the code above, add your own behaviors, or even create an entirely a new class with unique behaviors using what you know so far!

Happy coding!

Taught with <3 by yin_yang

View in-game to comment, award, and more!