Skip to content

Instantly share code, notes, and snippets.

@bjorn
Created December 15, 2011 18:32
Show Gist options
  • Save bjorn/1482246 to your computer and use it in GitHub Desktop.
Save bjorn/1482246 to your computer and use it in GitHub Desktop.
Lua OOP system
--
-- Object is the base class of anything that follows this OOP mechanism.
-- Usage example:
--
--
-- CREATING A DERIVED CLASS
--
-- local Human = Object:subclass {
-- name = "Anonymous",
-- weight = 0,
-- }
--
-- function Human:say(text)
-- print(self.name .. " says: " .. text)
-- end
--
--
-- INSTANTIATING AN OBJECT
--
-- local jim = Human {
-- name = "Jim Jimmalot"
-- weight = "85"
-- }
--
-- jim:say("Hi!")
--
--
-- Default constructor, called when class is used as a function.
-- It turns 'instance' into an instance of 'class'.
--
local function construct(class, instance)
assert(rawget(class, "__call"), "Trying to intantiate an instance")
setmetatable(instance, class)
instance:init()
return instance
end
--
-- It turns 'subclass' into a subclass of 'class', and prepares it to be
-- used as metatable for instances of 'subclass'.
--
local function subclass(class, subclass)
-- The class is reused as the metatable for subclasses and instances
subclass.__index = subclass
subclass.__call = construct
return setmetatable(subclass, class)
end
--
-- Bootstrap the first class into the hierarchy
--
local Object = subclass({
-- Report an error when trying to access a member that doesn't exist
__index = function(table, key)
error("No such member: " .. tostring(key))
end,
__call = construct,
}, {})
Object.init = function() end
Object.subclass = subclass
--
-- Returns a constructor for this class. This saves some overhead compared to
-- relying on the metatable, and can be used for classes that are frequently
-- instantiated.
--
-- A module can only return the constructor, in which case the class can no
-- longer be subclassed and members are not accessible without creating an
-- instance.
--
function Object:constructor()
local constructor = rawget(self, "_constructor_")
if not constructor then
assert(rawget(self, "__call"), "Instances can't have constructors")
local init = self.init
-- Avoid calling init when it's the empty default
if init == Object.init then
init = nil
end
constructor = function(instance)
setmetatable(instance, self)
if init then
init(instance)
end
return instance
end
self._constructor_ = constructor
end
return constructor
end
--
-- Returns this object when it is an instance of the given class, and nil
-- otherwise.
--
function Object:as(class)
local meta = getmetatable(self)
if type(class) == "table" then
while meta do
if meta == class then
return self
end
meta = getmetatable(meta)
end
elseif type(class) == "function" then
-- Assuming contructor function, so class can't be a subclass
if rawget(meta, "_constructor_") == class then
return self
end
end
return nil
end
--
-- Convenience function to log a message from an instance.
--
function Object:log(...)
print(tostring(self) .. ":", ...)
end
return Object
local Object = require "object"
Object {}
local clock = os.clock
local Fruit = Object:subclass {
weight = 0,
color = "undefined",
}
function Fruit:eat()
self.weight = 0
end
assert(rawget(getmetatable(Fruit), "__call") ~= nil, "Hmm")
-- An apple is a special kind of fruit
local Apple = Fruit:subclass {
weight = 1,
color = "green",
}
function Apple:eat()
-- Most people don't eat the whole apple
self.weight = self.weight / 2.0
print("Ate apple")
end
-- A fruit basket that contains fruit
local FruitBasket = Object:subclass {}
function FruitBasket:init()
self.contents = {}
end
function FruitBasket:add(fruit)
self.contents[#self.contents + 1] = fruit
print("Added fruit with weight", fruit.weight, "and color", fruit.color)
end
-- Eating the fruit basket means eating all the fruit it contains
function FruitBasket:eat()
for k,v in pairs(self.contents) do
v:eat()
end
end
function FruitBasket:isEmpty()
return next(self.contents) == nil
end
function FruitBasket:weight()
local weight = 0
for k,v in pairs(self.contents) do
weight = weight + v.weight
end
return weight
end
assert(rawget(getmetatable(FruitBasket), "__call") ~= nil, "Hmm2")
local fruitBasket = FruitBasket {}
for i=1,10 do
local apple = Apple {}
fruitBasket:add(apple)
end
print("Basket weight is", fruitBasket:weight())
fruitBasket:eat()
print(fruitBasket:isEmpty() and "Basket empty" or "Basket not empty")
print("Basket weight is", fruitBasket:weight())
local fruit = Fruit {}
print("Fruit as Fruit", fruit:as(Fruit))
print("Fruit as Apple", fruit:as(Apple))
fruit = Apple {}
print("Apple as Object", fruit:as(Object))
print("Apple as Fruit", fruit:as(Fruit))
print("Apple as Apple", fruit:as(Apple))
local start
assert(Apple{}:as(Apple))
Apple = Apple:constructor()
assert(Apple{}:as(Apple))
start = clock()
for i=1,1000000 do
local fruit = Apple {}
end
print("Apple creation time:", clock() - start)
Point = Object:subclass {
x = 0,
y = 0,
}
start = clock()
for i=1,1000000 do
local point = Point { x = 10, y = 20 }
end
print("Point creation time:", clock() - start)
Point = Point:constructor()
start = clock()
for i=1,1000000 do
local point = Point { x = 10, y = 20 }
end
print("Point creation time (constructor):", clock() - start)
start = clock()
for i=1,1000000 do
local point = { x = 10, y = 20 }
end
print("Point creation time (raw):", clock() - start)
fruit = Fruit {}
local start = clock()
for i=1,1000000 do
fruit:eat()
end
print("Calling member function time:", clock() - start)
collectgarbage("collect")
print("Memory (base):", collectgarbage("count"))
local points = {}
for i=1,10000 do
points[i] = Point { x = 10, y = 20 }
end
collectgarbage("collect")
print("Memory (points):", collectgarbage("count"))
points = {}
for i=1,10000 do
points[i] = { x = 10, y = 20 }
end
collectgarbage("collect")
print("Memory (raw points):", collectgarbage("count"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment