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.
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.
Metatables
Metatables allow you to define how operations on tables behave. This is key to implementing inheritance and other OOP features.
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.
-- 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 passingself
as the first parameter
Implementing Inheritance
Inheritance can be implemented by having one class derive from another using metatables.
-- 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 ofAnimal
, which serves as the prototype for theDog
class - When we create
Dog
instances, they have access to bothDog
methods and inheritedAnimal
methods - If a method doesn't exist in the
Dog
instance, Lua looks inDog
'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.
-- 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:
-- 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)