Object-Oriented Programming in Lua

Learn how to implement object-oriented programming patterns in Lua

OOP Concepts in Lua

Lua doesn't have built-in object-oriented programming features like classes and inheritance. However, using tables and metatables, we can implement OOP patterns that provide similar functionality.

1

Objects as Tables

In Lua, we use tables to represent objects. Fields of the table store the object's state (properties), and functions stored in the table become methods.

2

Metatables

Metatables allow you to define how operations on tables behave. This is key to implementing inheritance and other OOP features.

3

The __index Metamethod

The __index metamethod is used to look up properties that don't exist in the object itself, providing a way to implement inheritance.

Creating a Basic Class

Let's create a simple "class" in Lua by defining a table with methods and a constructor.

rectangle.lua
-- Define a "class" for rectangles
local Rectangle = {width = 0, height = 0}

-- Class method for creating a new instance
function Rectangle:new(width, height)
  local instance = {}
  instance.width = width or 0
  instance.height = height or 0
  
  -- Set the metatable of the instance to the class
  setmetatable(instance, self)
  self.__index = self
  
  return instance
end

-- Method to calculate area
function Rectangle:getArea()
  return self.width * self.height
end

-- Method to calculate perimeter
function Rectangle:getPerimeter()
  return 2 * (self.width + self.height)
end

-- Method to resize the rectangle
function Rectangle:resize(factor)
  self.width = self.width * factor
  self.height = self.height * factor
end

-- Create an instance
local rect = Rectangle:new(10, 5)

-- Use the methods
print("Area:", rect:getArea())  -- 50
print("Perimeter:", rect:getPerimeter())  -- 30

-- Resize and print new area
rect:resize(2)
print("New area after resize:", rect:getArea())  -- 200

Understanding the Code

  • The :new() function creates and returns a new instance
  • We set the metatable of the instance to point to the class itself
  • The __index metamethod allows instances to find methods in the class
  • The colon syntax Rectangle:new() is syntactic sugar for passing self as the first parameter

Implementing Inheritance

Inheritance can be implemented by having one class derive from another using metatables.

inheritance.lua
-- Base class: Animal
local Animal = {}

function Animal:new(name)
  local instance = {name = name or "Unknown"}
  setmetatable(instance, self)
  self.__index = self
  return instance
end

function Animal:makeSound()
  return "Unknown sound"
end

function Animal:getName()
  return self.name
end

-- Derived class: Dog
local Dog = Animal:new() -- Create base for Dog class

-- Override the new method for the Dog class
function Dog:new(name, breed)
  -- Call the parent constructor
  local instance = Animal.new(self, name)
  instance.breed = breed or "Unknown breed"
  return instance
end

-- Override the makeSound method
function Dog:makeSound()
  return "Woof!"
end

-- Add a new method specific to Dog
function Dog:getBreed()
  return self.breed
end

-- Create instances
local animal = Animal:new("Generic Animal")
local dog = Dog:new("Rex", "Golden Retriever")

-- Use methods
print(animal:getName(), "says:", animal:makeSound())
print(dog:getName(), "says:", dog:makeSound())
print(dog:getName(), "is a:", dog:getBreed())

How Inheritance Works

  • We create Dog as an instance of Animal, which serves as the prototype for the Dog class
  • When we create Dog instances, they have access to both Dog methods and inherited Animal methods
  • If a method doesn't exist in the Dog instance, Lua looks in Dog's metatable thanks to __index
  • Method overriding happens naturally by redefining methods in the derived class

Private Members

Lua doesn't have built-in private fields, but we can implement a pattern to create private members using closures.

private_members.lua
-- Create a Counter class with private state
function Counter(initialValue)
  -- Private variables (not accessible from outside)
  local count = initialValue or 0
  
  -- Return the object with public methods only
  return {
    increment = function(amount)
      count = count + (amount or 1)
    end,
    
    decrement = function(amount)
      count = count - (amount or 1)
    end,
    
    getValue = function()
      return count
    end
  }
end

-- Create a counter instance
local counter = Counter(10)

-- Use the public methods
print("Initial value:", counter:getValue())  -- 10

counter:increment(5)
print("After increment:", counter:getValue())  -- 15

counter:decrement(2)
print("After decrement:", counter:getValue())  -- 13

-- We cannot access the private count variable directly
-- print(counter.count)  -- This would be nil

How Private Members Work

  • We use a factory function (Counter) instead of a class
  • Private variables are declared within the function's scope but outside the returned object
  • Methods in the returned object form closures over these private variables
  • The outside world can only interact with the object through its public methods

Practical Example: Game Entity

Let's implement a game entity system using OOP principles in Lua:

game_entity.lua
-- Base Entity class
local Entity = {}

function Entity:new(x, y, name)
  local instance = {
    x = x or 0,
    y = y or 0,
    name = name or "Entity",
    alive = true
  }
  
  setmetatable(instance, self)
  self.__index = self
  return instance
end

function Entity:move(dx, dy)
  self.x = self.x + (dx or 0)
  self.y = self.y + (dy or 0)
  return self
end

function Entity:getPosition()
  return self.x, self.y
end

function Entity:getName()
  return self.name
end

-- Player class, inherits from Entity
local Player = Entity:new()

function Player:new(x, y, name, health)
  local instance = Entity.new(self, x, y, name)
  instance.health = health or 100
  instance.maxHealth = instance.health
  instance.score = 0
  return instance
end

function Player:takeDamage(amount)
  self.health = math.max(0, self.health - amount)
  if self.health <= 0 then
    self.alive = false
  end
  return self
end

function Player:heal(amount)
  self.health = math.min(self.maxHealth, self.health + amount)
  return self
end

function Player:addScore(points)
  self.score = self.score + points
  return self
end

-- Enemy class, also inherits from Entity
local Enemy = Entity:new()

function Enemy:new(x, y, name, power)
  local instance = Entity.new(self, x, y, name)
  instance.power = power or 10
  return instance
end

function Enemy:attack(player)
  if player and self.alive and player.alive then
    player:takeDamage(self.power)
    return true
  end
  return false
end

-- Create instances
local player = Player:new(0, 0, "Hero", 100)
local enemy = Enemy:new(10, 5, "Goblin", 15)

-- Game logic
print(player:getName(), "starts at position", player:getPosition())
player:move(5, 5)
print(player:getName(), "moves to position", player:getPosition())

print(enemy:getName(), "attacks", player:getName())
enemy:attack(player)
print(player:getName(), "health is now", player.health)

player:addScore(50)
print(player:getName(), "score:", player.score)