Object @ Lima

June 7, 2017 ยท View on GitHub

Template

The code is split and ordered in the following parts:

  1. Create an empty table M which will contain the methods of the class.
  2. Create the metatable MT.
  3. Create the table representing the class. We call it Class in this example, but it should be named like the class.
local M = {}
local MT = { __index = M }
local Class = { methods = M }

Methods

A public method foo is defined like this:

--- Documentation about `foo` in LDoc format.
M.foo = function(self, ...)
    -- do things
end

A private method bar is defined like this:

-- Optional documentation about `bar`.
local function bar(self, ...)
    -- do things
end

A metamethod qux is defined like this:

--- Documentation about `qux` in LDoc format.
MT.__qux = function(self, ...)
    -- do things
end

Class methods, including the constructor, are defined as members of the class table.

The constructor is defined like this:

Class.new = function(...)
    local self = {
        member_a = default_value_member_a,
        -- member_b = nil,
    }
    return setmetatable(self, MT)
end

Example

-- Module for class 'Sum', if possible named 'sum.lua'.

--- Sum is a class to sum two values.
--- (Talk about over-engineering...)
-- @classmod sum

local M = {}
local MT = { __index = M }
local Sum = { methods = M }

--- Set the first value.
-- @tparam number value_a The first value.
M.set_a = function(self, value_a)
    self.value_a = value_a
end

--- Set the second value.
-- @tparam number value_b The second value.
M.set_b = function(self, value_b)
    self.value_b = value_b
end

--- Get sum of first and second value.
-- @treturn number The sum of the first and second values.
-- Can raise an error if the values cannot be summed.
M.get = function(self)
    return self.value_a + self.value_b
end

--- Example of a `__tostring` metamethod.
MT.__tostring = function(self)
    return string.format("sum: %s + %s", self.value_a, self.value_b)
end

--- Create a new sum.
Sum.new = function()
    return setmetatable({}, MT)
end

-- This module's only purpose is to define the class.
return Sum

If the module defined other classes as well, we would return them in a table like this:

return {
    Sum = Sum,
    ...
}

Usage of this module would be (with the first variant):

local Sum = require("sum")

s = Sum.new()

s:set_a(2)
s:set_b(3)
print(s:get())

Destructor

There are no destructors in Lua. You can rely on the gc metamethod which will be called when an object is being collected, but it MUST NOT be used as the primary way to release resources other than memory.

A common example is the case of a class File with a method close called when the object is destroyed. The developer still needs to call close() explicitly, since no RAII mechanism exists to call it silently.

Monkey-patching

Monkey-patching of Lima code is an anti-pattern and is forbidden.

Monkey-patching of non-Lima code should be as limited as possible (if you can write a wrapper instead, do it) and contained in a single module.

Overriding __tostring

In general it is not useful to override __tostring. If you do it, make sure the output is short and fits on a single line.

Inheritance

First, inheritance is a design-pattern which is powerful yet dangerous. Think twice if you need it and prefer to avoid it.

If necessary, inheritance should be explicitly declared. For instance, if B inherits from A, using:

setmetatable(B.methods, { __index = A.methods})

self:method() vs M.method(self)

self:method() gets the method method of the class of the instance self and calls it with self as the instance. M.method(self) gets the method method of class M and then calls it with self as instance.

The two statements do not perform the same if self's class is a child of M's. If this is the case, self:method() will call the method method of the child class and M.method(self) will call the method method of the parent class.

Example

-- A.lua

local M = {}
local MT = { __index = M }
local A = { methods = M }

M.foo = function(self)
    if SUPER then
        return M.foo(self)
    else
        return self:foo()
    end
end

M.bar = function(self)
    print("This is A!")
end

A.new = function()
    return setmetatable({}, MT)
end

return A
-- B.lua

local A = require "A"

local M = setmetatable({}, { __index = A.methods })
local MT = { __index = M }
local B = { methods = M }

M.bar = function(self)
    print("This is B!")
end

B.new = function()
    local self = A.new()
    return setmetatable(self, MT)
end

return B
-- example.lua

local A = require "A"
local B = require "B"

A.new():foo()
B.new():foo()

If SUPER is false, this will print:

This is A!
This is B!

If SUPER is true, this will print:

This is A!
This is A!