Lua - Object-Oriented Programming
Object-Oriented Programming
Learn advanced object-oriented programming patterns in Lua using metatables, prototype-based inheritance, and modern OOP techniques for building scalable applications.
Estimated time: 35-40 minutes
Learning Objectives
- Implement classes and objects using metatables
- Create inheritance hierarchies with prototype patterns
- Build polymorphic systems with method overriding
- Design advanced OOP patterns and frameworks
- Apply OOP principles to real-world applications
Basic Class Implementation
Lua doesn't have built-in classes, but we can simulate them using tables and metatables.
-- Simple class implementation
local Person = {}
Person.__index = Person
-- Constructor
function Person.new(name, age)
local self = setmetatable({}, Person)
self.name = name or "Unknown"
self.age = age or 0
return self
end
-- Instance methods
function Person:get_name()
return self.name
end
function Person:set_name(name)
self.name = name
end
function Person:get_age()
return self.age
end
function Person:introduce()
return string.format("Hi, I'm %s and I'm %d years old.",
self.name, self.age)
end
function Person:have_birthday()
self.age = self.age + 1
print(self.name .. " is now " .. self.age .. " years old!")
end
-- Class method (static method)
function Person.get_species()
return "Homo sapiens"
end
-- Demo basic class usage
local function demo_basic_class()
print("=== Basic Class Demo ===")
-- Create instances
local alice = Person.new("Alice", 25)
local bob = Person.new("Bob", 30)
local anonymous = Person.new() -- uses defaults
-- Use instance methods
print(alice:introduce())
print(bob:introduce())
print(anonymous:introduce())
-- Modify instances
alice:have_birthday()
bob:set_name("Robert")
print(bob:introduce())
-- Use class method
print("Species:", Person.get_species())
-- Check instance type
print("Alice is a Person:", getmetatable(alice) == Person)
end
demo_basic_class()
Expected Output:
=== Basic Class Demo ===
Hi, I'm Alice and I'm 25 years old.
Hi, I'm Bob and I'm 30 years old.
Hi, I'm Unknown and I'm 0 years old.
Alice is now 26 years old!
Hi, I'm Robert and I'm 30 years old.
Species: Homo sapiens
Alice is a Person: true
Inheritance and Polymorphism
Create inheritance hierarchies with method overriding and polymorphic behavior.
-- Employee class inheriting from Person
local Employee = {}
Employee.__index = Employee
setmetatable(Employee, Person) -- Employee inherits from Person
-- Constructor
function Employee.new(name, age, job_title, salary)
local self = Person.new(name, age) -- call parent constructor
setmetatable(self, Employee)
self.job_title = job_title or "Employee"
self.salary = salary or 0
return self
end
-- Override introduce method (polymorphism)
function Employee:introduce()
return string.format("Hi, I'm %s, %d years old, and I work as a %s.",
self.name, self.age, self.job_title)
end
-- New methods specific to Employee
function Employee:get_salary()
return self.salary
end
function Employee:get_annual_salary()
return self.salary * 12
end
function Employee:give_raise(amount)
self.salary = self.salary + amount
print(string.format("%s received a raise of $%.2f! New salary: $%.2f",
self.name, amount, self.salary))
end
-- Manager class inheriting from Employee
local Manager = {}
Manager.__index = Manager
setmetatable(Manager, Employee) -- Manager inherits from Employee
function Manager.new(name, age, department, salary)
local self = Employee.new(name, age, "Manager", salary)
setmetatable(self, Manager)
self.department = department or "General"
self.subordinates = {}
return self
end
-- Override introduce method
function Manager:introduce()
return string.format("Hi, I'm %s, %d years old, and I manage the %s department.",
self.name, self.age, self.department)
end
-- Manager-specific methods
function Manager:add_subordinate(employee)
table.insert(self.subordinates, employee)
print(string.format("%s now reports to %s", employee.name, self.name))
end
function Manager:get_team_size()
return #self.subordinates
end
function Manager:hold_team_meeting()
print(string.format("%s is holding a team meeting with %d employees:",
self.name, #self.subordinates))
for _, employee in ipairs(self.subordinates) do
print(" - " .. employee.name .. " (" .. employee.job_title .. ")")
end
end
-- Inheritance demo
local function demo_inheritance()
print("\n=== Inheritance and Polymorphism Demo ===")
-- Create instances of different classes
local john = Person.new("John", 45)
local sarah = Employee.new("Sarah", 28, "Developer", 5000)
local mike = Manager.new("Mike", 35, "Engineering", 8000)
-- Polymorphic behavior - same method, different implementations
local people = {john, sarah, mike}
print("Introductions:")
for _, person in ipairs(people) do
print(" " .. person:introduce())
end
-- Employee-specific functionality
print("\nEmployee operations:")
sarah:give_raise(500)
print("Sarah's annual salary: $" .. sarah:get_annual_salary())
-- Manager-specific functionality
print("\nManager operations:")
mike:add_subordinate(sarah)
local dev2 = Employee.new("Tom", 26, "Junior Developer", 3500)
mike:add_subordinate(dev2)
mike:hold_team_meeting()
print("Mike's team size:", mike:get_team_size())
-- Check inheritance chain
print("\nInheritance checking:")
print("Sarah is Employee:", getmetatable(sarah) == Employee)
print("Mike is Manager:", getmetatable(mike) == Manager)
-- Method resolution - can call parent methods
mike:have_birthday() -- From Person class
print("Mike after birthday:", mike:get_age())
end
demo_inheritance()
Expected Output:
=== Inheritance and Polymorphism Demo ===
Introductions:
Hi, I'm John and I'm 45 years old.
Hi, I'm Sarah, 28 years old, and I work as a Developer.
Hi, I'm Mike, 35 years old, and I manage the Engineering department.
Employee operations:
Sarah received a raise of $500.00! New salary: $5500.00
Sarah's annual salary: $66000
Manager operations:
Sarah now reports to Mike
Tom now reports to Mike
Mike is holding a team meeting with 2 employees:
- Sarah (Developer)
- Tom (Junior Developer)
Mike's team size: 2
Inheritance checking:
Sarah is Employee: true
Mike is Manager: true
Mike is now 36 years old!
Mike after birthday: 36
Advanced OOP Patterns
Implement sophisticated patterns like mixins, composition, and property systems.
-- Mixin pattern for shared behavior
local Serializable = {}
function Serializable:to_json()
local function serialize_value(value)
local value_type = type(value)
if value_type == "string" then
return '"' .. value:gsub('"', '\\"') .. '"'
elseif value_type == "number" or value_type == "boolean" then
return tostring(value)
elseif value_type == "table" then
local parts = {}
for k, v in pairs(value) do
if type(k) == "string" and not k:match("^__") then -- skip metamethods
table.insert(parts, '"' .. k .. '":' .. serialize_value(v))
end
end
return "{" .. table.concat(parts, ",") .. "}"
else
return "null"
end
end
return serialize_value(self)
end
function Serializable:from_json(json_str)
-- Simplified JSON parser (for demo purposes)
-- In real code, use proper JSON library
local data = load("return " .. json_str:gsub('"([^"]+)":', '["%1"]='))()
for k, v in pairs(data) do
self[k] = v
end
return self
end
-- Timestampable mixin
local Timestampable = {}
function Timestampable:set_created()
self.created_at = os.time()
end
function Timestampable:set_updated()
self.updated_at = os.time()
end
function Timestampable:get_age()
if self.created_at then
return os.time() - self.created_at
end
return 0
end
-- Function to add mixin to class
local function add_mixin(class, mixin)
for name, method in pairs(mixin) do
if type(method) == "function" then
class[name] = method
end
end
end
-- Product class with mixins
local Product = {}
Product.__index = Product
function Product.new(name, price, category)
local self = setmetatable({}, Product)
self.name = name or "Unknown Product"
self.price = price or 0
self.category = category or "General"
self:set_created() -- from Timestampable mixin
return self
end
function Product:get_info()
return string.format("%s - $%.2f (%s)", self.name, self.price, self.category)
end
function Product:apply_discount(percentage)
local discount = self.price * (percentage / 100)
self.price = self.price - discount
self:set_updated() -- from Timestampable mixin
return discount
end
-- Add mixins to Product class
add_mixin(Product, Serializable)
add_mixin(Product, Timestampable)
-- Property system implementation
local function create_property(getter, setter, validator)
return {
get = getter,
set = setter,
validate = validator
}
end
-- Advanced class with property system
local BankAccount = {}
BankAccount.__index = BankAccount
function BankAccount.new(account_number, initial_balance)
local self = setmetatable({}, BankAccount)
self._account_number = account_number
self._balance = initial_balance or 0
self._transaction_history = {}
self:set_created()
return self
end
-- Property definitions
BankAccount.properties = {
balance = create_property(
function(self) return self._balance end,
function(self, value)
if value < 0 then
error("Balance cannot be negative")
end
self._balance = value
end,
function(value) return type(value) == "number" and value >= 0 end
),
account_number = create_property(
function(self) return self._account_number end,
function(self, value)
error("Account number is read-only")
end,
function(value) return type(value) == "string" end
)
}
-- Property access metamethods
function BankAccount:__index(key)
local property = self.properties and self.properties[key]
if property and property.get then
return property.get(self)
end
return BankAccount[key]
end
function BankAccount:__newindex(key, value)
local property = self.properties and self.properties[key]
if property then
if property.validate and not property.validate(value) then
error("Invalid value for property " .. key)
end
if property.set then
property.set(self, value)
else
error("Property " .. key .. " is read-only")
end
else
rawset(self, key, value)
end
end
-- Account methods
function BankAccount:deposit(amount)
if amount <= 0 then
error("Deposit amount must be positive")
end
self._balance = self._balance + amount
table.insert(self._transaction_history, {
type = "deposit",
amount = amount,
timestamp = os.time(),
balance_after = self._balance
})
return self._balance
end
function BankAccount:withdraw(amount)
if amount <= 0 then
error("Withdrawal amount must be positive")
end
if amount > self._balance then
error("Insufficient funds")
end
self._balance = self._balance - amount
table.insert(self._transaction_history, {
type = "withdrawal",
amount = amount,
timestamp = os.time(),
balance_after = self._balance
})
return self._balance
end
function BankAccount:get_statement()
local statement = {
account_number = self._account_number,
current_balance = self._balance,
transactions = {}
}
for _, transaction in ipairs(self._transaction_history) do
table.insert(statement.transactions, {
type = transaction.type,
amount = transaction.amount,
date = os.date("%Y-%m-%d %H:%M:%S", transaction.timestamp),
balance_after = transaction.balance_after
})
end
return statement
end
-- Add mixins
add_mixin(BankAccount, Serializable)
add_mixin(BankAccount, Timestampable)
-- Demo advanced patterns
local function demo_advanced_patterns()
print("\n=== Advanced OOP Patterns Demo ===")
-- Mixin demo
print("--- Mixin Pattern ---")
local laptop = Product.new("Gaming Laptop", 1200, "Electronics")
print("Product info:", laptop:get_info())
local discount = laptop:apply_discount(10)
print("Applied 10% discount: $" .. string.format("%.2f", discount))
print("New price:", laptop:get_info())
-- Serialization mixin
local json = laptop:to_json()
print("JSON representation:", json)
-- Property system demo
print("\n--- Property System ---")
local account = BankAccount.new("ACC-12345", 1000)
-- Property access
print("Account number:", account.account_number)
print("Initial balance:", account.balance)
-- Account operations
account:deposit(500)
print("After deposit: $" .. account.balance)
account:withdraw(200)
print("After withdrawal: $" .. account.balance)
-- Try to modify read-only property (will error)
local success, err = pcall(function()
account.account_number = "HACKED"
end)
if not success then
print("Property protection worked:", err)
end
-- Try to set negative balance (will error)
success, err = pcall(function()
account.balance = -100
end)
if not success then
print("Validation worked:", err)
end
-- Account statement
local statement = account:get_statement()
print("\nAccount Statement:")
print("Account:", statement.account_number)
print("Current Balance: $" .. statement.current_balance)
print("Transaction History:")
for _, tx in ipairs(statement.transactions) do
print(string.format(" %s: %s $%.2f (Balance: $%.2f)",
tx.date, tx.type, tx.amount, tx.balance_after))
end
end
demo_advanced_patterns()
Expected Output:
=== Advanced OOP Patterns Demo ===
--- Mixin Pattern ---
Product info: Gaming Laptop - $1200.00 (Electronics)
Applied 10% discount: $120.00
New price: Gaming Laptop - $1080.00 (Electronics)
JSON representation: {"name":"Gaming Laptop","price":1080,"category":"Electronics","created_at":1642265445}
--- Property System ---
Account number: ACC-12345
Initial balance: 1000
After deposit: $1500
After withdrawal: $1300
Property protection worked: Account number is read-only
Validation worked: Balance cannot be negative
Account Statement:
Account: ACC-12345
Current Balance: $1300
Transaction History:
2024-01-15 14:30:45: deposit $500.00 (Balance: $1500.00)
2024-01-15 14:30:45: withdrawal $200.00 (Balance: $1300.00)
Design Patterns in Lua OOP
Implement common design patterns using Lua's OOP capabilities.
-- Observer Pattern
local Observable = {}
Observable.__index = Observable
function Observable.new()
local self = setmetatable({}, Observable)
self.observers = {}
return self
end
function Observable:add_observer(observer)
table.insert(self.observers, observer)
end
function Observable:remove_observer(observer)
for i, obs in ipairs(self.observers) do
if obs == observer then
table.remove(self.observers, i)
break
end
end
end
function Observable:notify_observers(event, data)
for _, observer in ipairs(self.observers) do
if observer.on_notify then
observer:on_notify(event, data)
end
end
end
-- Stock price tracker using Observer pattern
local StockTracker = {}
StockTracker.__index = StockTracker
setmetatable(StockTracker, Observable)
function StockTracker.new(symbol)
local self = Observable.new()
setmetatable(self, StockTracker)
self.symbol = symbol
self.price = 0
self.history = {}
return self
end
function StockTracker:update_price(new_price)
local old_price = self.price
self.price = new_price
table.insert(self.history, {
price = new_price,
timestamp = os.time()
})
-- Notify observers
self:notify_observers("price_change", {
symbol = self.symbol,
old_price = old_price,
new_price = new_price,
change = new_price - old_price
})
end
-- Observer implementations
local EmailAlert = {}
EmailAlert.__index = EmailAlert
function EmailAlert.new(email, threshold)
local self = setmetatable({}, EmailAlert)
self.email = email
self.threshold = threshold or 0
return self
end
function EmailAlert:on_notify(event, data)
if event == "price_change" and math.abs(data.change) >= self.threshold then
print(string.format("EMAIL ALERT to %s: %s price changed by $%.2f to $%.2f",
self.email, data.symbol, data.change, data.new_price))
end
end
local Dashboard = {}
Dashboard.__index = Dashboard
function Dashboard.new()
local self = setmetatable({}, Dashboard)
self.stocks = {}
return self
end
function Dashboard:on_notify(event, data)
if event == "price_change" then
self.stocks[data.symbol] = data.new_price
print(string.format("DASHBOARD UPDATE: %s = $%.2f (%.2f%%)",
data.symbol, data.new_price,
data.old_price > 0 and (data.change/data.old_price)*100 or 0))
end
end
-- Strategy Pattern for different trading strategies
local TradingStrategy = {}
-- Conservative strategy
local ConservativeStrategy = {}
ConservativeStrategy.__index = ConservativeStrategy
function ConservativeStrategy.new()
return setmetatable({}, ConservativeStrategy)
end
function ConservativeStrategy:should_buy(price, moving_average)
return price < moving_average * 0.95 -- Buy when 5% below average
end
function ConservativeStrategy:should_sell(price, moving_average)
return price > moving_average * 1.10 -- Sell when 10% above average
end
-- Aggressive strategy
local AggressiveStrategy = {}
AggressiveStrategy.__index = AggressiveStrategy
function AggressiveStrategy.new()
return setmetatable({}, AggressiveStrategy)
end
function AggressiveStrategy:should_buy(price, moving_average)
return price < moving_average * 0.98 -- Buy when 2% below average
end
function AggressiveStrategy:should_sell(price, moving_average)
return price > moving_average * 1.05 -- Sell when 5% above average
end
-- Trading bot using Strategy pattern
local TradingBot = {}
TradingBot.__index = TradingBot
function TradingBot.new(name, strategy, initial_cash)
local self = setmetatable({}, TradingBot)
self.name = name
self.strategy = strategy
self.cash = initial_cash or 10000
self.portfolio = {}
self.price_history = {}
return self
end
function TradingBot:on_notify(event, data)
if event == "price_change" then
local symbol = data.symbol
local price = data.new_price
-- Update price history
if not self.price_history[symbol] then
self.price_history[symbol] = {}
end
table.insert(self.price_history[symbol], price)
-- Keep only last 10 prices for moving average
if #self.price_history[symbol] > 10 then
table.remove(self.price_history[symbol], 1)
end
-- Calculate moving average
local sum = 0
for _, p in ipairs(self.price_history[symbol]) do
sum = sum + p
end
local moving_average = sum / #self.price_history[symbol]
-- Make trading decisions
local shares_owned = self.portfolio[symbol] or 0
if self.strategy:should_buy(price, moving_average) and self.cash >= price then
local shares_to_buy = math.floor(self.cash / (price * 2)) -- Conservative position sizing
if shares_to_buy > 0 then
self.cash = self.cash - (shares_to_buy * price)
self.portfolio[symbol] = shares_owned + shares_to_buy
print(string.format("%s BUY: %d shares of %s at $%.2f",
self.name, shares_to_buy, symbol, price))
end
elseif self.strategy:should_sell(price, moving_average) and shares_owned > 0 then
local shares_to_sell = math.floor(shares_owned / 2) -- Sell half
if shares_to_sell > 0 then
self.cash = self.cash + (shares_to_sell * price)
self.portfolio[symbol] = shares_owned - shares_to_sell
print(string.format("%s SELL: %d shares of %s at $%.2f",
self.name, shares_to_sell, symbol, price))
end
end
end
end
function TradingBot:get_portfolio_value(current_prices)
local total_value = self.cash
for symbol, shares in pairs(self.portfolio) do
if current_prices[symbol] then
total_value = total_value + (shares * current_prices[symbol])
end
end
return total_value
end
-- Demo design patterns
local function demo_design_patterns()
print("\n=== Design Patterns Demo ===")
-- Create stock tracker
local apple_stock = StockTracker.new("AAPL")
-- Create observers
local email_alert = EmailAlert.new("[email protected]", 5.0)
local dashboard = Dashboard.new()
local conservative_bot = TradingBot.new("ConservativeBot", ConservativeStrategy.new(), 10000)
local aggressive_bot = TradingBot.new("AggressiveBot", AggressiveStrategy.new(), 10000)
-- Register observers
apple_stock:add_observer(email_alert)
apple_stock:add_observer(dashboard)
apple_stock:add_observer(conservative_bot)
apple_stock:add_observer(aggressive_bot)
-- Simulate price changes
local prices = {150, 145, 140, 155, 160, 165, 158, 152, 148, 162, 170}
print("Simulating AAPL price changes:")
for _, price in ipairs(prices) do
apple_stock:update_price(price)
-- Brief pause for readability
print("---")
end
-- Show final portfolio values
local current_prices = {AAPL = 170}
print(string.format("\nFinal Portfolio Values:"))
print(string.format("Conservative Bot: $%.2f",
conservative_bot:get_portfolio_value(current_prices)))
print(string.format("Aggressive Bot: $%.2f",
aggressive_bot:get_portfolio_value(current_prices)))
end
demo_design_patterns()
Expected Output:
=== Design Patterns Demo ===
Simulating AAPL price changes:
DASHBOARD UPDATE: AAPL = $150.00 (0.00%)
---
DASHBOARD UPDATE: AAPL = $145.00 (-3.33%)
---
DASHBOARD UPDATE: AAPL = $140.00 (-3.45%)
ConservativeBot BUY: 35 shares of AAPL at $140.00
AggressiveBot BUY: 35 shares of AAPL at $140.00
DASHBOARD UPDATE: AAPL = $155.00 (10.71%)
---
DASHBOARD UPDATE: AAPL = $160.00 (3.23%)
ConservativeBot SELL: 17 shares of AAPL at $160.00
---
...
Final Portfolio Values:
Conservative Bot: $10847.50
Aggressive Bot: $10925.00
Common Pitfalls
OOP Best Practices in Lua
- Memory management: Be careful with circular references in object hierarchies
- Method calls: Use colon syntax
:
for methods that needself
- Inheritance depth: Avoid deep inheritance chains that can impact performance
- Metatable sharing: Don't share metatables between unrelated objects
- Constructor patterns: Always return the new instance from constructors
Checks for Understanding
- What's the difference between
.
and:
method calls? - How does inheritance work with metatables in Lua?
- What are the benefits of using mixins over inheritance?
- How can you implement private/protected members in Lua classes?
Show answers
:
automatically passesself
as first parameter,.
doesn't- Set the class's metatable to the parent class for method resolution
- Mixins provide multiple inheritance and composition without complex hierarchies
- Use closures or naming conventions (underscore prefix) for encapsulation
Exercises
- Create a game engine with Entity-Component-System architecture
- Implement a plugin system using dynamic class loading
- Build a form validation framework with chainable rules