Interfaces
Interfaces define a contract that classes must fulfill. They specify method signatures that implementing classes are required to have.
Defining an Interface
Interface methods are just signatures - the function bodies are empty. They document:
- The method name
- Expected arguments
Interface Restrictions
Interfaces can only contain:
- Required methods - must be implemented by classes
- Default methods - optional, with a default implementation
Variables, static methods, and private/protected members are not allowed in interfaces.
Implementing an Interface
Use implements to declare that a class fulfills an interface contract:
If you forget to implement a required method, you get an error at class definition time:
class "Wall" implements "Damageable" {
-- missing takeDamage and getHealth
}
-- Error: class Wall: missing method 'takeDamage' required by interface Damageable
Default Methods
Use default for optional methods that have a default implementation:
interface "Damageable" {
takeDamage = function(self, amount) end; -- required
getHealth = function(self) end; -- required
default {
onDeath = function(self) -- optional
print(self:get_name() .. " died")
end;
};
}
class "Player" implements "Damageable" {
health = 100;
takeDamage = function(self, amount)
self.health = self.health - amount
if self.health <= 0 then
self:onDeath() -- uses default implementation
end
end;
getHealth = function(self)
return self.health
end;
-- onDeath not implemented, uses default from interface
}
class "Boss" implements "Damageable" {
health = 1000;
takeDamage = function(self, amount)
self.health = self.health - amount
end;
getHealth = function(self)
return self.health
end;
onDeath = function(self) -- override default
print("BOSS DEFEATED!")
self:dropLoot()
self.Damageable:onDeath() -- call default implementation
end;
}
You can call the default implementation from an override using self.InterfaceName:method(), similar to calling parent methods in class inheritance.
local damageable = interface("Damageable")
damageable.takeDamage = function(self, amount) end -- required
damageable.getHealth = function(self) end -- required
damageable.default.onDeath = function(self) -- optional
print(self:get_name() .. " died")
end
damageable:register()
local player = class("Player", {implements = "Damageable"})
player.health = 100
function player:takeDamage(amount)
self.health = self.health - amount
if self.health <= 0 then
self:onDeath() -- uses default implementation
end
end
function player:getHealth()
return self.health
end
-- onDeath not implemented, uses default from interface
player:register()
local boss = class("Boss", {implements = "Damageable"})
boss.health = 1000
function boss:takeDamage(amount)
self.health = self.health - amount
end
function boss:getHealth()
return self.health
end
function boss:onDeath() -- override default
print("BOSS DEFEATED!")
self:dropLoot()
end
boss:register()
Multiple Interfaces
A class can implement multiple interfaces:
interface "Damageable" {
takeDamage = function(self, amount) end;
}
interface "Serializable" {
serialize = function(self) end;
deserialize = function(self, data) end;
}
class "Player" implements "Damageable, Serializable" {
takeDamage = function(self, amount) ... end;
serialize = function(self) ... end;
deserialize = function(self, data) ... end;
}
local damageable = interface("Damageable")
damageable.takeDamage = function(self, amount) end
damageable:register()
local serializable = interface("Serializable")
serializable.serialize = function(self) end
serializable.deserialize = function(self, data) end
serializable:register()
local player = class("Player", {implements = "Damageable, Serializable"})
function player:takeDamage(amount) ... end
function player:serialize() ... end
function player:deserialize(data) ... end
player:register()
Interface Inheritance
Interfaces can extend other interfaces:
A class implementing Killable must implement all methods from both Killable and Damageable:
Combining Extends and Implements
Classes can extend other classes AND implement interfaces:
local entity = class("Entity")
entity.x = 0
entity.y = 0
entity:register()
local damageable = interface("Damageable")
damageable.takeDamage = function(self, amount) end
damageable:register()
local player = class("Player", {extends = "Entity", implements = "Damageable"})
player.health = 100
function player:takeDamage(amount)
self.health = self.health - amount
end
player:register()
Checking Interface Implementation
Use instance_of to check if an instance implements an interface:
local player = Player()
local wall = Wall()
player:instance_of(Damageable) -- true
wall:instance_of(Damageable) -- false
-- Also works for inherited interfaces
player:instance_of(Killable) -- true (if Player implements Killable)
player:instance_of(Damageable) -- true (inherited from Killable)
Common Pattern: Safe Interface Calls
Check before calling interface methods on unknown objects:
function damageAllInArea(objects, amount)
for _, obj in pairs(objects) do
if obj:instance_of(Damageable) then
obj:takeDamage(amount)
end
end
end
Interfaces Cannot Be Instantiated
interface "Damageable" {
takeDamage = function(self, amount) end;
}
Damageable.new() -- Error: cannot instantiate interface Damageable
Namespaces
Interfaces work with namespaces like classes:
namespace "game.combat"
local damageable = interface("Damageable")
damageable.takeDamage = function(self, amount) end
damageable:register()
local player = class("Player", {implements = "Damageable"})
function player:takeDamage(amount) ... end
player:register()
-- Access via namespace
local p = game.combat.Player()
p:instance_of(game.combat.Damageable) -- true
Strict Interface Checking
By default, SIMPLOO only checks that implementing methods exist and have the correct type. Enable strictInterfaces for additional validation:
With strict checking enabled, SIMPLOO also verifies:
- Argument count matches the interface signature
- Argument names match the interface signature
- Varargs (
...) are present if the interface requires them
interface "Formatter" {
format = function(self, template, ...) end;
}
-- This fails with strictInterfaces = true:
class "BadFormatter" implements "Formatter" {
format = function(self, str) -- wrong arg name, missing varargs
return str
end;
}
-- Error: class BadFormatter: method 'format' argument 2 is named 'str' but interface Formatter expects 'template'
Note
Strict interface checking requires Lua 5.2+. On Lua 5.1, this setting has no effect.