ColeValleyGirl
ColeValleyGirl

Reputation: 579

Structuring Lua classes

I'm constructing a class in Lua that has a number of groups of related functions within it, but am unsure whether there's a better way to structure it. I currently have to develop for a Lua 5.1 environment but am hopeful that Lua 5.3 will be possible in the near future.

The class will be used in a number of different Lua programs, so I want something I can just drop in as a single chunk of code (the environment I'm programming for means that modules and require aren't and won't be an option).

Ideally I want a black box piece of code (except for the exposed public methods) and not to duplicate code in different classes (to improve maintainability).

What I have at present is (generalised):

 function Fclass()

     --here I declare a bunch of local functions that can be called by any of the public methods

     local function A(parms)
     end

     --Public methods set 1

     --here I declare a bunch of state variables shared by BSelector and GetB

     local BSelector = function()
         A(parmvalues)
         --returns a bunch of iup controls with supporting (complicated) logic 
     end

     local GetB = function()
         --returns the values of the iup controls created in Bselector
     end

     --Public methods set 2

     --here I declare a bunch of state variables shared by DSelector and GetD

     local DSelector = function()
         --returns a bunch of iup controls with supporting (complicated) logic 
     end

     local GetD = function()
         A(parmvalues)
         --returns the value of the iup controls created in Dselector
     end

     return{BSelector =BSelector , GetB =GetB,  DSelector =DSelector , GetD =GetD}
 end

The "B" and "D" groups of methods are totally independent except they both use the local functions "A" etc. (which don't depend on external variables); their state variables ideally should be local to the group.

Is this a sensible structure? Or should I be splitting the "B" and "D" groups into two separate classes and either duplicating the local functions or dropping them in as a separate piece of code? I don't really want to expose the local functions outside the classe(es) because there will inevitably be naming conflicts... Most programs will use all the groups of methods, although there will be some that only use a single group.

Or is there a better way to do this?

I'm invoking them thus:

myB = Fclass()
myD = Fclass()
someresults = myB.Bselector()
otherresults = myD.Dselector()

Updated to add: I'm advised I may not be using the terminology properly and what I'm doing isn't classes. My approach is based on Programming in Lua and was selected because I wanted to keep the state variables for the class? object? private -- not accessible except via the public methods.

Upvotes: 5

Views: 3577

Answers (3)

Nifim
Nifim

Reputation: 5021

You can create 2 up-values for your class functions. the 1st value holds public variables that will be accessed by your class' caller, such as the functions themselves and any caller handled options.

while the 2nd will be for your private values those that are only known and accessible from within the class. You can use this private table to store internal state or other inner workings that will not be exposed to the caller.

function class(first, second)

    local public = {first}
    local _private = {second}

    function _private.A(parms)
        --private function not available outside of class.
    end

    function public:selector() -- public class available to caller
        _private.A(parmvalues) -- calls private class
    end

    function public:get()
        return _private[1]
    end

    return public
end

myB = class('hello', ' world!') --passing in a variable for public, and one for private.
myD = class('hello...', ' world?')

print(myB[1] .. myB:get()) --get a public value, and uses get function to retrieve private value
print(myD[1] .. myD:get())

Additionally if the class functions should never be changed by your user, you can enforce that by changing return public to:

    local meta = {
        __index = public,
        __newindex = function(t, k, v)
                error("this table is read-only")
              end,
        __metatable = false
    }

    return setmetatable({}, meta) -- this make the public table read only

Upvotes: 0

DarkWiiPlayer
DarkWiiPlayer

Reputation: 7064

In your example, it seems you encapsulate the state of your instances through closures, not table values.

While this has the advantage of stronger encapsulation, as upvalues are invisible from the outside without using the debug library, it also comes with the disadvantage that Lua has to close each method for each instance, wasting some more memory (not a lot though).

Another benefit is that when instance variables are implemented as table fields, they need not be declared before the method, as table indexing is string-based, whereas when implemented as closures, the local varaible needs to be known before the function is defined (this also applies to other methods, which in either implementation work the same way as instance variables).

It's more common to store instance variables as table values inside the object, and passing the object as a first argument to the functions. There's even syntactic sugar for this.


There's lots of ways for doing classes in Lua, with many different tradeoffs (some are better at inheritance, while others perform better, etc.)

Since you don't seem to need any inheritance, you can go with a simple factory function, as you're pretty much doing already.

The way I personally like to build such factory functions is:

local object do
   local class = {}
   local meta = {__index=class} -- Object metatable

   function class:print() -- A method of the class
      print("Hello, I am an object and my x is " .. tostring(self.x))
   end

   function object(self) -- The factory function for the Class
      self.x = self.x or 0
      return setmetatable(self, meta)
   end
end

local o = object {
   x = 20
}
o:print()
o.x = 30
o:print()

This has the benefit that, for classes with many methods and many instances, the methods aren't copied into every instance, which saves some memory.

Alternatively, you can do something like this

local object do
   local function object_print(self)
      print("Hello, I am an object and my x is " .. tostring(self.x))
   end

   function object(self)
      self.x = self.x or 0
      self.print = object_print -- Store method directly in the object
      return self
   end
end

Again, this saves a reference to every method in every instance, wasting some memory. The benefit is that you can now think of classes as traits. When you write

person { name = "henry" }

You can think of it as creating a new person with the name Henry, but you can also think of it as creating an object with the name Henry and adding the person trait to it.

Because of this benefit of combining two concepts of OOP into one implementation and not having any pesky inheritance, it's my favourite way of building objects in Lua in most simple cases.


Update

The trait approach also lends itself to defining several classes/traits together:

local person, human do
   -- Some generic method shared by both classes
   local function object_get_name(self)
      return self.name
   end
   -- Person uses this as a method, but human uses
   -- it as a function through an upvalue. Both work,
   -- but have different upsides and downsides.

   -- A method of person
   local function person_say_hi(self)
      print(self:get_name() .. " says hi!")
      -- Calling get_name as a method here
   end

   -- A method of human
   local function human_describe(self)
      print(object_get_name(self) .. ' is a human!')
      -- Calling get_name as an upvalue
   end

   function person(self)
      self.name = self.name or 'A person'
      self.say_hi = person_say_hi
      self.get_name = object_get_name
      -- Needs to be a method because person_say_hi assumes it to be one
      return self
   end

   function human(self)
      self.name = self.name or 'A human'
      self.describe = human_describe
      return self
   end
end

-- Create a new person
local henry = person{ name = "Henry" }
henry:say_hi()

-- Create a new human
local steve = human { name = "Steve" }
steve:describe()

-- Change the way henry gets his name
function henry:get_name()
   return self.name:upper()
end
-- This only affects henry; all other "person" objects keep their old 
henry:say_hi()
-- This only works because say_hi accesses the method

-- Add the person trait to steve
person(steve)
steve:describe() -- Steve is still a human
steve:say_hi() -- Steve is also a person now

Upvotes: 2

Felix
Felix

Reputation: 2396

Some years ago I built myself a superclass for basic OOP functionality in Lua.

Usage:

Person = LuaObject:extend({
    __name = "Person",
    name = "",
    age = 0,
})

-- constructor
function Person:new(name, age)
    Person.__super.new(self)-- calling the super constructor
    self.name = name
    self.age = age
end

function Person:getName()
    return self.name
end

function Person:getAge()
    return self.age
end

Feel free to use it:

--[[
LuaObject for basic OOP in Lua
Lua 5.0
]]
local function newIndexFunction(tbl, name, value)
    if name == "new" and type(value) == "function" then
        local constructor = value
        rawset(tbl, name, function(self, ...)
            local object = self
            if object.__class == nil then
                object = {}
                object.__class = self
                object.__id = string.sub(tostring(object), 8)

                self.__index = self
                setmetatable(object, self)
            end

            constructor(object, unpack(arg))-- Lua 5.0
            -- constructor(object, ...)-- Lua 5.1+
            return object
        end)
    else
        rawset(tbl, name, value)
    end
end

local function toStringFunction(tbl)
    return tbl:toString()
end

LuaObject = {__name = "LuaObject"}
setmetatable(LuaObject, {__newindex = newIndexFunction, __tostring = toStringFunction})

function LuaObject:extend(class)
    class = class or {}

    self.__index = self
    self.__newindex = newIndexFunction
    self.__tostring = toStringFunction

    local constructor = nil
    if class.new ~= nil then
        constructor = class.new
        class.new = nil
    end

    setmetatable(class, self)

    if constructor ~= nil then
        class.new = constructor
    end

    class.__super = self

    return class
end

function LuaObject:new()
end

function LuaObject:getSuperClass()
    return self.__super
end

function LuaObject:getClass()
    return self.__class
end

function LuaObject:toString()
    return string.format("[%s] %s", self.__class.__name, self.__id)
end

function LuaObject:isInstance(value)
    return value ~= nil and type(value) == "table" and getmetatable(value) == self
end

--[[
-- examples
-- basic class
Person = LuaObject:extend({
    __name = "Person",
    name = "",
    age = 0,
})

-- constructor
function Person:new(name, age)
    Person.__super.new(self)-- calling the super constructor
    self.name = name
    self.age = age
end

function Person:getName()
    return self.name
end

function Person:getAge()
    return self.age
end

-- extending classes
Customer = Person:extend({
    __name = "Customer",
    id = 0,
})

function Customer:new(id, name, age)
    Customer.__super.new(self, name, age)
    self.id = id
end

function Customer:getID()
    return self.id
end

-- overriding methods
function Customer:getName()
    -- calling super methods
    local realResult = Customer.__super.getName(self)
    if string.len(realResult) <= 5 then
        return realResult
    else
        return string.sub(realResult, 1, 5)
    end
end

-- testing
local customer1 = Customer:new(1, "rollback", 19)
local customer2 = Customer:new(2, "Kori", -1)

print(assert(customer1:getName() == "rollb", "Overriding of getName failed"))
print(assert(customer2:getName() == "Kori", "Overriding of getName failed"))
print(assert(customer1:getID() == 1, "Error in getID"))
print(assert(customer1:getAge() == 19, "Error in getAge"))
print(customer1)
]]

Upvotes: 0

Related Questions