Unleasing the power of Object-Oriented Programming | OOP

headings:

Basic Understanding

Firstly, what the heck is OOP?

OOP, short for Object-Oriented Programming, is all about organizing your code around objects. Think of objects as these nifty little things that bundle together data and behavior in a nice, neat package.

In OOP, you have classes, which are like blueprints or templates for creating objects. You use a class to define the structure and behavior of an object, and then you can create multiple instances of that class, each with its own unique data.

so, why do we bother with this object stuff? well, it helps us manage complexity and make our code more organized and clean. Instead of dealing with a junk of functions and variables all over the place, we can group related things together in objects. It’s like putting your code into these little boxes that are easier to work with.

and hey, objects can interact with each other too! They can share data, and work together to get things done. It’s like a team effort, where each object plays its part to achieve the goal!

that’s OOP in a nutshell. It’s all about objects, classes, and organizing your code in a way that fits you.

more detailed info & the concepts of OOP

In OOP, objects are instances of classes, which serve as blueprints or templates for creating objects. Objects encapsulate data (attributes or properties) and behavior (methods or functions) into one.

(classes and objects are different)

some key concepts of OOP:

  • Classes
    Classes are user defined data types that define the structure and behavior of objects. A class acts as a blueprint, specifying the attributes and methods that objects of that class. It defines the common properties and behaviors shared by all objects created from it.

  • Objects
    Objects are instances of classes. They represent specific things within a program. Each object has its own unique state, defined by the values of its attribute/properties, and can perform operations or actions through its methods. Objects interact with each other by method calls

  • Inheritance
    Inheritance enables the creation of new classes (derived or child classes) based on existing classes (base or parent classes). The derived classes inherit attributes and methods from their parent class, allowing for code reuse and the establishment of hierarchical relationships. Inheritance promotes the concept of “is-a” relationships, where a derived class is a specialized version of its parent class.

  • Polymorphism
    Polymorphism allows objects of different classes to be treated uniformly, enabling the use of a common interface.

  • Abstraction
    Abstraction provides a way to define interfaces or abstract classes with methods that must be implemented by derived classes.

  • Encapsulation
    Encapsulation is the bundling of data and methods within a class. It allows for data hiding and abstraction, where the internal workings and implementation details of an object are hidden from outside access. Encapsulation helps ensure data integrity and provides a clear interface for interacting with objects.

  • Composition
    Composition is a key concept in object-oriented programming (OOP) that describes a relationship between objects where one object is composed of one or more other objects. It represents a “has-a” relationship, where the composed objects cannot exist independently of the main object and have a strong lifecycle dependency.

Examples

lets create a house class with OOP!!!

local House = {}
local Static = {} -- a separate table for methods

function House.new()
   return setmetatable({
      rooms = {} -- define the "rooms" property
   }, {__index = Static}) -- assign the new "House" object to the Static table (where the methods go)
end

function Static:AddRoom(roomName: string) -- a method to add a new room to the house
   table.insert(self.rooms, roomName) -- self is the house object referenced
end

function Static:PrintRooms() -- another method to print the rooms in the house
   table.foreach(self.rooms, print)
end

return House

you may say, bb-but what is __index and setmetatable!!!

in simple terms, setmetatable is a function in Lua that allows you to associate a metatable with a table. A metatable is essentially a table that defines :sparkles: special behavior :sparkles: and properties for the original table.

when you use setmetatable, you are essentially saying, “hey, this table should have a special metatable that defines how certain operations or lookups should do.”

  • “ok how about __index??”

__index fires when table[index] is indexed, if table[index] is nil. can also be set to a table, in which case that table will be indexed.

in this case, __index is set to Static, which is where methods go, allowing us to access and call the methods

  • “ok cool, but why and what is self”

in Lua, self is a special variable that is used inside methods of an object-oriented Lua class. It refers to the instance of the class itself.

local Person = {}
local Static = {}

function Person.new()
    return setmetatable({
       chores_list = {"do the dishes"}
    }, {__index = Static})
end

function Static.doChores(self) -- `Static.doChores(self)` is the same as `Static:doChores()`
   print(self)
   -- output:
   -- {
   --   chores_list = {"do the dishes"}
   --   doChores = function
   -- }
end

think of self as script, script refers to the script its running the code from, and in this case, self refers to the person object


now lets create my dream house!!! which consists of a gaming room and a bedroom

local House = require(...) -- require the house module

local myHouse = House.new() -- creates a new house object with the .new constructor
myHouse:AddRoom("gaming room") -- add a gaming room
myHouse:AddRoom("bedroom") -- add a bedroom
myHouse:PrintRooms() -- (1, gaming room), (2, bedroom)

now, you might be asking, why the heck do i need to do all this? and that i respond with, you don’t, its a :sparkles: style :sparkles:; as i has said, it is an approach to organizing your code around “objects”

Using Inheritance in OOP

i am going to make an Furniture class as the “Base” or “Parent” class

local Furniture = {}
local Static = {}

function Furniture.new(name, material)
   return setmetatable({
      name = name,
      material = material
   }, {__index = Static})
end

function Static:printAttributes()
   for attr, v in self do
       if type(v) == 'function' then continue end
       print(attr, v)
   end
end

return Furniture

now, lets say i want to make a Chair class with a new attribute, but i don’t want to repeat and copy paste the Furniture class code to the Chair class

local Furniture = require(...)

local Chair = {}

function Chair.new(name, material, legs)
    local self = Furniture.new(name, material) -- create a new furniture class
    self.legs = legs -- assign the new attribute/property

    return self
end

return Chair

Interacting with other objects (Association)

lets create a “Planet” class with a method to get the distance between the planet and the other planet

local Planet = {}
local Static = {}

function Planet.new(x, y, z)
    return setmetatable({
        position = Vector3.new(x or 0, y or 0, z or 0) -- create a new vector3 object
    }, {__index = Static})
end

function Static:GetDistanceFromPlanet(otherPlanet)
    local diff = otherPlanet.position - self.position
    local magnitude = diff.Magnitude -- the distance

    return magnitude
end

return Planet
local Planet = require(...)

local planetA = Planet.new(0, 0, 0)
local planetB = Planet.new(100, -10, 100)

local dist = planetA:GetDistanceFromPlanet(planetB)

print(dist)

Using polymorphism and abstraction

-- Abstract base class: Shape
local Shape = {}

function Shape.new()
    error("Shape is an abstract class and cannot be instantiated directly.")
end

return Shape
local Shape = require(...)

-- Derived class: Circle (inherit from Shape)
local Circle = setmetatable({}, {__index = Shape})
local Static = {}

function Circle.new(radius)
   local self = setmetatable({}, {__index = Static})
   self.radius = radius

   return self
end

function Static:calculateArea()
   return math.pi * self.radius * self.radius
end

return Circle
local Shape = require(...)

-- Derived class: Rectangle (inherit from Shape)
local Rectangle = setmetatable({}, {__index = Shape})
local Static = {}

function Rectangle.new(length, width)
   local self = setmetatable({}, {__index = Static})
   self.length = length
   self.width = width

   return self
end

function Static:calculateArea()
   return self.length * self.width
end

return Rectangle

when you try to do Shape.new directly, it will error as programmed

local Rectangle = require(...)
local Circle = require(...)
local Shape = require(...)

local rect = Rectangle.new(10, 10) -- 10 by 10 rectangle
local circle = Circle.new(10) -- 10 radius

-- polymorphic function
function printArea(shape)
   print("Area:", shape:calculateArea())
end

printArea(rect)
printArea(circle)

local shape = Shape.new() -- errors "Shape is an abstract class and cannot be instantiated directly."

Encapsulation

in simple terms, is like putting something inside a box or container and controlling access to it. In programming, encapsulation refers to bundling data and related functionality (methods or functions) together as a single unit, called an object.

Encapsulation helps in:

  1. Data protection: Encapsulated data can be made private or hidden from external code, preventing unauthorized access or modification.

  2. Code organization: Encapsulation allows for better organization of code by grouping related data and methods together, making it easier to understand and maintain.

  3. Code reusability: Encapsulated objects can be reused in different parts of the program or in other programs, promoting code reusability and modularity.

  4. Flexibility: By providing a public interface to interact with the object’s internal data, encapsulation allows you to modify the internal implementation without affecting the code that uses the object.

imagine you have a toy car that requires battery power to drive; encapsulation allows you to hide the internal mechanism of the toy

local ToyCar = {}

function ToyCar.new(color)
   local self = {} -- public variables go here

   -- private variables
   local color = color
   local battery = 100 -- 100% battery

   function self:getColor()
      return color
   end

   function self:charge()
      battery = 100 -- recharge
      print("fully charged!")
   end

   function self:drive()
      if battery > 0 then
         battery -= 10 -- decrease battery by 10%
         print("driving!")
      else
         print("ran out of battery!")
      end
   end

   return self
end

return ToyCar
local ToyCar = require(...)

local car = ToyCar.new(Color3.new(1, 0, 0)) -- red car
print(car.color) -- nil
print(car:getColor()) -- (1, 0, 0)

car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- driving!
car:drive() -- ran out of battery!
car:charge() -- fully charged!
car:drive() -- driving!

Composition

imagine you’re building a car; a car consists of various components like an engine, wheels, seats, and a steering wheel; each component has its own specific functionality.

Composition is like putting together other classes to create a new class

-- Engine class
local Engine = {}
local Static = {}

function Engine.new()
   return setmetatable({}, {__index = Static})
end

function Static:Start()
   print("Engine Started")
end

return Engine
-- Fuel Tank class
local FuelTank = {}
local Static = {}

function FuelTank.new(capacity)
   return setmetatable({capacity = capacity}, {__index = Static})
end

function Static:Refill()
   print("Fuel tank refilled")
end

return FuelTank
local Engine = require(...)
local FuelTank = require(...)

local Car = {}
local Static = {}

function Car.new(fuelCapacity: number)
    return setmetatable({
        engine = Engine.new()
        fuel_tank = FuelTank.new(fuelCapacity)
    }, {__index = Static})
end

function Static:StartCar() 
   self.engine:Start() -- Engine Started
   print("Car started")
end

function Static:RefillFuel()
   self.fuel_tank:Refill() -- Fuel tank refilled
end

return Car

the Car class is composed of an Engine object and a FuelTank object. which represent the independent components with their own functionalities.


More Examples

Inheritance, Abstraction, Polymorphism
-- Abstract class: Animal
local Animal = {}
local Static = {}

function Animal.new(name)
    local self = setmetatable({}, {__index = Static})
    self.name = name

    return self
end

return function(staticTable)
   Static = staticTable

   return Animal
end
local Animal = require(...)

-- Derived class: Dog
local Dog = {}
local Static = {}

function Static:makeSound()
    print("Woof!")
end

-- Inherit from Animal
Dog = Animal(Static)

return Dog
local Animal = require(...)

-- Derived class: Cat
local Cat = {}
local Static = {}

function Static:makeSound()
    print("Meow!")
end

-- Inherit from Animal
Cat = Animal(Static)

return Cat
local Dog = require(...)
local Cat = require(...)

-- polymorphic function
function makeAnimalSound(animal)
   animal:makeSound()
end

local dog = Dog.new()
local cat = Cat.new()

makeAnimalSound(dog) -- Woof!
makeAnimalSound(cat) -- Meow!

Encapsulation
local Car = {}

function Car.new()
    local self = {} -- public variables & methods
    
    -- private variables
    local engineStatus = "Off"

    -- public methods
    function self:startEngine()
        engineStatus = "On"
        print("Engine started.")
    end

    function self:stopEngine()
        engineStatus = "Off"
        print("Engine stopped.")
    end

    local function checkEngineStatus() -- a private method
        return engineStatus
    end

    return self
end

return Car
local Car = require(...)

local myCar = Car.new()

myCar:startEngine() -- Engine started.
myCar:stopEngine() -- Engine stopped.

Composition & another approach to OOP
local Book = {}

function Book.new(title, author)
    local self = {}
    self.title = title
    self.author = author

    return self
end

return Book
local Library = {}

function Library.new()
    local self = {}
    self.books = {}

    function self:addBook(book)
        table.insert(self.books, book)
    end

    function self:displayBooks()
        for _, book in self.books do
            print(`Title: {book.title}. Author: {book.author}`)
        end
    end

    return self
end

return Library
local Book = require(...)
local Library = require(...)

local book1 = Book.new("In Search of Lost Time", "Marcel Proust")
local book2 = Book.new("To Kill a Mockingbird", "Harper Lee")

local library = Library.new()
library:addBook(book1)
library:addBook(book2)

library:displayBooks()

-- Output:
-- Title: In search of Lost Time. Author: F. Scott Fitzgerald
-- Title: To Kill a Mockingbird. Author: Harper Lee

Conclusion

It’s a powerful paradigm used in a lot of programming languages as well as game development. With it, you can design and implement programs in a structured, intuitive way. It’s easier to build and maintain complex programs when you understand OOP principles.

There are actually many ways to do it and use its key concepts. You can think of OOP as a set of tools that help you organize your code and make it more reusable. There are different approaches like class-based inheritance, prototypal inheritance, mixins, and interfaces. Each approach has its own strengths and trade-offs, and it really depends on the programming language, project requirements, and personal preferences.

OOP is all about finding the right balance and using the concepts that fit your needs. So go ahead and explore the different ways to do OOP, and have fun building your code in style!!

i will be continuing this topic further and theres still a lot to discuss

some resources
  • wha??
  • ok
  • huh
  • ok i think i got it
  • hmm

0 voters

if you still have problems understanding what something does, don’t hesitate to contact me

Go back to top

42 Likes

there is a tutorial from 2014 about this…

1 Like

while yes, there is a tutorial from 2014, 2015, etc; but they might not cover all the things about OOP; and i can post my own topic about OOP with the intention of sharing my knowledge

11 Likes

Yeah. If I recall correctly, they covered the basic parts of it, but they never covered the abstract classes.

3 Likes

I think there is a mistake when you defined the :calculateArea() method for the circle, because you defined it in the Circle table instead of the Static table.

While the :calculateArea() method of the rectangle is defined in the Static table.

I assume that the Static table contains the methods of the metatable.

2 Likes

Ah yes, I did not see that when revising, should be fixed now

1 Like

I did not understand the reason why we use the “Static” table. Is it just there to separate .new() functions from self functions? If it is, do we really need that?

3 Likes

yes

no but it’s cleaner

Im writitng this last sentence becease of the devforum chat limit :slight_smile:

2 Likes

I do it personally so that the autocomplete doesn’t recognize .new as a method after making a new object (and yes its cleaner this way, but its your choice whether or not to create their own place separately. in other words, its a style)

2 Likes

i am having a bit of trouble wrapping around my head around the abstraction class, would it need to or be possible to try using it for any kind of methods or variables. just cus im having trouble picturing the usecase in my head

Abstract classes can’t be instantiated, them only serves as a base class.

An Item can be a Equipment, Consumable or Miscellaneous. These last three are abstract classes too, that inherits from the Item class.

Items go to the player Inventory.
So Equipments too, but they can have Equip()
So Consumables too, but they have Consume()
So Miscellaneous too, but they have Use()

A Tomahawk inherits from the Equipment abstract class, but it have the HeavySwing() skill.
While, IronSword have the DoubleStrike() skill.
So, they are: Item.Equipment.Weapon.OneHanded(…)

What if the player tries to equip a Potion at a weapon slot? This can be fixed limiting that slot to only accept objects inherited by the Equipment class.

1 Like

I’ve recently started using zero width spaces to fill the quota which I recommend doing.

1 Like

Thanks this actually super helpful and clears up the way i think about oop structure!