Web Analytics

OOP Basics in Lua

Intermediate ~30 min read

Lua doesn't have built-in classes, but its flexible table and metatable system makes it easy to implement Object-Oriented Programming. You can create classes, objects, methods, and all the OOP patterns you need. In this lesson, you'll learn how to build robust OOP systems in Lua using tables and metatables. Let's get started!

Creating a Class

A class in Lua is typically a table with methods:

-- Define the class
local Person = {}
Person.__index = Person

-- Constructor
function Person:new(name, age)
    local self = setmetatable({}, Person)
    self.name = name
    self.age = age
    return self
end

-- Method
function Person:greet()
    print("Hello, I'm " .. self.name)
end

-- Create instances
local alice = Person:new("Alice", 25)
local bob = Person:new("Bob", 30)

alice:greet()  -- Hello, I'm Alice
bob:greet()    -- Hello, I'm Bob
How it works:
  1. Person is a table that serves as the class
  2. Person.__index = Person makes methods available to instances
  3. new() creates an instance with setmetatable
  4. The colon : syntax passes self automatically
Output
Click Run to execute your code

Instance Methods vs Class Methods

Instance Methods

Methods that operate on individual instances:

local Dog = {}
Dog.__index = Dog

function Dog:new(name, breed)
    local self = setmetatable({}, Dog)
    self.name = name
    self.breed = breed
    return self
end

-- Instance method (uses self)
function Dog:bark()
    print(self.name .. " says: Woof!")
end

function Dog:getInfo()
    return self.name .. " is a " .. self.breed
end

local dog = Dog:new("Buddy", "Golden Retriever")
dog:bark()  -- Buddy says: Woof!

Class Methods (Static Methods)

Methods that belong to the class itself:

local Dog = {}
Dog.__index = Dog
Dog.count = 0  -- Class variable

function Dog:new(name, breed)
    local self = setmetatable({}, Dog)
    self.name = name
    self.breed = breed
    Dog.count = Dog.count + 1  -- Increment class variable
    return self
end

-- Class method (uses dot notation)
function Dog.getCount()
    return Dog.count
end

local dog1 = Dog:new("Buddy", "Golden Retriever")
local dog2 = Dog:new("Max", "Labrador")
print("Total dogs:", Dog.getCount())  -- Total dogs: 2
Output
Click Run to execute your code

Encapsulation and Private Variables

Use closures to create private variables:

local function BankAccount(initialBalance)
    -- Private variables
    local balance = initialBalance
    
    -- Public interface
    local self = {}
    
    function self:deposit(amount)
        if amount > 0 then
            balance = balance + amount
            return true
        end
        return false
    end
    
    function self:withdraw(amount)
        if amount > 0 and amount <= balance then
            balance = balance - amount
            return true
        end
        return false
    end
    
    function self:getBalance()
        return balance
    end
    
    return self
end

local account = BankAccount(1000)
account:deposit(500)
print(account:getBalance())  -- 1500
-- print(account.balance)  -- nil (private!)
Best Practice: Use closures for true privacy. Table fields can always be accessed, but closure variables are truly private.

Getters and Setters

Control access to properties:

local Rectangle = {}
Rectangle.__index = Rectangle

function Rectangle:new(width, height)
    local self = setmetatable({}, Rectangle)
    self._width = width
    self._height = height
    return self
end

-- Getter
function Rectangle:getWidth()
    return self._width
end

-- Setter with validation
function Rectangle:setWidth(width)
    if width > 0 then
        self._width = width
    else
        error("Width must be positive")
    end
end

function Rectangle:getArea()
    return self._width * self._height
end

local rect = Rectangle:new(10, 5)
print(rect:getArea())  -- 50
rect:setWidth(20)
print(rect:getArea())  -- 100
Output
Click Run to execute your code

Method Chaining

Return self to enable fluent interfaces:

local StringBuilder = {}
StringBuilder.__index = StringBuilder

function StringBuilder:new()
    local self = setmetatable({}, StringBuilder)
    self.parts = {}
    return self
end

function StringBuilder:append(str)
    table.insert(self.parts, str)
    return self  -- Enable chaining
end

function StringBuilder:prepend(str)
    table.insert(self.parts, 1, str)
    return self  -- Enable chaining
end

function StringBuilder:toString()
    return table.concat(self.parts)
end

-- Chaining in action
local result = StringBuilder:new()
    :append("Hello")
    :append(" ")
    :append("World")
    :prepend(">>> ")
    :toString()

print(result)  -- >>> Hello World

Practical Examples

Shopping Cart

local ShoppingCart = {}
ShoppingCart.__index = ShoppingCart

function ShoppingCart:new()
    local self = setmetatable({}, ShoppingCart)
    self.items = {}
    return self
end

function ShoppingCart:addItem(name, price, quantity)
    table.insert(self.items, {
        name = name,
        price = price,
        quantity = quantity or 1
    })
    return self
end

function ShoppingCart:getTotal()
    local total = 0
    for i, item in ipairs(self.items) do
        total = total + (item.price * item.quantity)
    end
    return total
end

function ShoppingCart:getItemCount()
    return #self.items
end

local cart = ShoppingCart:new()
cart:addItem("Apple", 1.50, 3)
    :addItem("Banana", 0.75, 5)
    :addItem("Orange", 2.00, 2)

print("Total:", cart:getTotal())  -- 12.25
print("Items:", cart:getItemCount())  -- 3

Timer Class

local Timer = {}
Timer.__index = Timer

function Timer:new()
    local self = setmetatable({}, Timer)
    self.startTime = nil
    self.elapsed = 0
    return self
end

function Timer:start()
    self.startTime = os.clock()
    return self
end

function Timer:stop()
    if self.startTime then
        self.elapsed = os.clock() - self.startTime
        self.startTime = nil
    end
    return self
end

function Timer:getElapsed()
    if self.startTime then
        return os.clock() - self.startTime
    end
    return self.elapsed
end

local timer = Timer:new()
timer:start()
-- Do some work...
timer:stop()
print("Elapsed:", timer:getElapsed(), "seconds")
Output
Click Run to execute your code

Practice Exercise

Try these OOP challenges:

Output
Click Run to execute your code

Summary

In this lesson, you learned:

  • Creating classes using tables and metatables
  • Constructors with new() method
  • Instance methods vs class methods
  • Encapsulation with closures for private variables
  • Getters and setters for controlled access
  • Method chaining for fluent interfaces
  • Practical examples: ShoppingCart, Timer

What's Next?

You've learned the basics of OOP in Lua! Next, we'll explore inheritanceโ€”how to create class hierarchies, extend functionality, and implement polymorphism. Let's continue! ๐Ÿš€