Object-Oriented Lua

What is Object-Oriented Programming? 

Object-oriented programming (OOP) is a programming paradigm, structured around the concept of encapsulating your code into objects.

What is an object?

An object is an encapsulation of both state (data) and behavior (functions). These provide an easier way of thinking of the structure of the program, and how each piece of the program interacts with the rest. 

Tenets of OOP

There are four basic tenets of object-oriented programming, which are encapsulation, abstraction, inheritance, and polymorphism.

Encapsulation

Encapsulation is the practice of coupling together components of a program’s data and the functions that manipulate that data. Object-oriented languages also allow for protecting data/methods from manipulation outside the component via the use of private or protected variables. 

Abstraction

Abstraction is the concept of hiding implementation details behind a set of high-level methods used for interacting with the object. In object-oriented languages, this is done via the concept of abstract objects, or objects that provide an interface that can be extended by other objects. Any child object that implements these interfaces can be accessed as if they were the abstract object.

Inheritance

Inheritance allows for the child of an object to inherit the data and methods from the parent object. This is useful for code reusability as there could be common data and methods for an entire sub-section of a program's objects. Using inheritance reduces the amount of code duplication that is necessary.

Polymorphism

Polymorphism is the concept of building objects to share behavior. It allows for the same method in different objects to behave in different ways. In object-oriented languages, this can be done either through method overriding or method overloading. For example, a child object could redefine one of its parent’s methods to provide its custom behavior.

Object-Oriented Lua

While Lua isn’t an object-oriented language, it’s possible to emulate the behavior of objects or classes via the practice of object prototyping. In Lua, we do this by using tables as objects. Tables can hold both data and methods (encapsulation), and their dynamic nature allows for the ability to change their behavior (polymorphism) and to access a table as if it were another table (abstraction). The behavior of inheritance can be easily given via the use of a table’s metatable.

Lua tables

The first place to start is by simply treating Lua tables as objects. This is the most straightforward approach and gives us a similar way of encapsulating our data and behavior as object-oriented languages.

local button = {}

function button.new(control, down_color)
        local inst = {}
         
        ---
        -- Initialize the new module instance
        -- @param control #string - The fully qualified name for the control this represents
        -- @param down_color #number - The RGB color code for the down state color
        function inst:init(control, down_color)
                    self.control = control
                    self.up_color = gre.rgb(0, 255, 0, 255)
                    self.down_color = down_color or gre.rgb(255, 0, 0, 255)
                    self.active = false
                    self.callbacks = {}
        end
         
        ---
        -- Call when the control receives a press event
        function inst:press()
                    gre.set_value(string.format("%s.color", self.control), self.down_color)
                    self:set_active(true)
        end
         
        ---
        -- Call when the control receives a release event
        function inst:release()
                    gre.set_value(string.format("%s.color", self.control), self.up_color)
                    self:set_active(false)
        end
         
        ---
        -- Sets the buttons active state
        -- @param active #boolean - True when active (ie, pressed), false otherwise
        function inst:set_active(active)
                    self.active = active
                    self:notify(self.active)
        end
         
        ---
        -- Registers a callback function for a active state change
        -- @param callback #function - The callback to register
        -- @return #boolean - True if callback successfully registered
        function inst:register(callback)
                    table.insert(self.callbacks, callback)
                return true
        end
         
        ---
        -- Removes a callback function from the list of callbacks
        -- @param callback #function - The callback to remove
        -- @return #boolean - True if callback successfully removed
        function inst:unregister(callback)
                    for i,v in ipairs(self.callbacks) do
                                if (v == callback) then
                                            table.remove(self.callbacks,i)
                                            return true
                                end
                    end
                    return false
        end
         
        ---
        -- Notifies all callbacks for a active state change
        -- @param state #boolean - The new active state
        function inst:notify(state)
                    for _,v in ipairs(self.callbacks) do
                                v(state)
                    end
        end
         
        inst:init(control, down_color)
        return inst
end

return button

 

In this example, our Lua module has a method called ‘new’ that creates a new instance of the module. Each module instance contains its own unique state, and the new function implements the same methods for each instance.

It would be useful to note the use of the ‘:’ operator when naming the functions. For instance: 

function inst:press()

 This tells Lua to prepend a hidden variable named ‘self’ to the function parameters that contain the table the function is stored in, in this case it’s our object instance. It is the equivalent of this:

function inst.press(self)

 This is very important as we also want to encapsulate the data for the module in the table instance.

Metatables

There are downsides to the above implementation, namely that the module implementation is defined in the ‘new’ function. This does cause some problems when trying to parse the Lua code to display in the ADVANCED popup for the Lua action in Storyboard Designer. To improve this, we can move the functions out of the new function and associate them with the ‘button’ module.

local button = {}

---
-- Initialize the new module instance
-- @param control #string - The fully qualified name for the control this represents
-- @param down_color #number - The RGB color code for the down state color
function button:init(control, down_color)
        self.control = control
        self.up_color = gre.rgb(0, 255, 0, 255)
        self.down_color = down_color or gre.rgb(255, 0, 0, 255)
        self.active = false
        self.callbacks = {}
end

---
-- Call when the control receives a press event
function button:press()
        gre.set_value(string.format("%s.color", self.control), self.down_color)
        self:set_active(true)
end

---
-- Call when the control receives a release event
function button:release()
        gre.set_value(string.format("%s.color", self.control), self.up_color)
        self:set_active(false)
end

---
-- Sets the buttons active state
-- @param active #boolean - True when active (ie, pressed), false otherwise
function button:set_active(active)
        self.active = active
        self:notify(self.active)
end

---
-- Gets the buttons active state
-- @return #boolean - True when active
function button:get_active()
        return self.active
end

---
-- Registers a callback function for a active state change
-- @param callback #function - The callback to register
-- @return #boolean - True if callback successfully registered
function button:register(callback)
        table.insert(self.callbacks, callback)
        return true
end

---
-- Removes a callback function from the list of callbacks
-- @param callback #function - The callback to remove
-- @return #boolean - True if callback successfully removed
function button:unregister(callback)
        for i,v in ipairs(self.callbacks) do
                    if (v == callback) then
                                table.remove(self.callbacks,i)
                                return true
                    end
        end
        return false
end

---
-- Notifies all callbacks for a active state change
-- @param state #boolean - The new active state
function button:notify(state)
        for _,v in ipairs(self.callbacks) do
                    v(state)
        end
end

function button.new(control, down_color)
        local inst = {}
        inst:init(control, down_color)
        return inst
end

return button

Let’s modify our new implementation using the function ‘setmetatable’. This sets the second table parameter as the metatable of the first table parameter. The metamethod ‘__index’ can either be a function, or a table to index for the missing key.

function button.new(control, down_color)
        local inst = {}
        setmetatable(inst, {__index=button})
        inst:init(control, down_color)
        return inst
end

Now each time we call these functions on each button instance, we search the prototype for our object instance (namely the button module itself) and call the function it finds. This is great, but what if we want to extend our application by introducing a new type of button, namely a toggle button?

For the most part, they are identical, except for the toggle we want our press function to toggle between the up/down states. We could rewrite everything we have above, but instead, let’s use the button module as the base for our toggle module.

local button = require("components.button")

local toggle = {}

function toggle.new(...)
        local inst = button.new(...)
         
        function inst:press()
                    if self:get_active() then
                                gre.set_value(string.format("%s.color", self.control), self.up_color)
                                self:set_active(false)
                    else
                                gre.set_value(string.format("%s.color", self.control), self.down_color)
                                self:set_active(true)
                    end
        end
         
        function inst:release()
        end
         
        return inst
end

return toggle

An easy way to create our new toggle object is to first create a button, and then override the necessary functions, in this case, press() and release(). The rest of the functions are inherited from the button object and we can call them as if they were part of the toggle object. Now it should be noted that we are essentially redefining these functions for each new instance, similar to our first example. Let’s see how we can use setmetatable to provide the same functionality using a prototype for our toggle object.

Class implementation

To start with, let’s create a base object that we can use for creating any following object. This is intended to emulate the behavior of classes in an object-oriented language. It deals with creating our objects and provides functionality to get their prototype class, their parent prototype class, and even a method for comparing classes to see if one is an instance of another.

---
-- @module class.lua
--

local class = {}

---
-- Creates a new object prototype by extending from the base object.
-- The new object prototype implements the new, init, class, super, and instanceOf functions
-- @param _ #table - Not used
-- @param base #table - The base object to extend for this new object prototype
-- @return #table the new object prototype
local function extend(_, base)
        local new_class = {}
        local class_mt = {__index = new_class}

        if base then
                    setmetatable(new_class, {__index=base})
        end
         
        ---
        -- Instantiates a new object
        -- @param ... - A list of arguments
        -- @return #table - The newly instantiated object
        function new_class:new(...)
        local inst = {}
        setmetatable(inst, class_mt)
        inst:init(...)
        return inst
        end

        ---
        -- Initializes the data for the new object. This function must be implemented by a subclass.
        -- @param ... - A list of arguments
        function new_class:init(...)
                    error("init must be implemented")
        end

        ---
        -- Get the current class prototype
        -- @return #table - Class prototype
        function new_class:class()
                    return new_class
        end

        ---
        -- Get the parent class prototype
        -- @return #table - Parent class prototype
        function new_class:super()
                    return base
        end

        ---
        -- Check if object is an instance of another class
        -- @param klass #table - A class to check against
        -- @return #boolean - True if object is an instance of klass
        function new_class:instanceOf(klass)
                    local is_a = false
                    local cur = new_class
                    while (cur and not is_a) do
                                if (cur == klass) then
                                            is_a = true
                                else
                                            cur = cur:super()
                                end
                    end

                    return is_a
        end

        return new_class
end

-- allow the class object to be used as a function
setmetatable(class, {__call=extend})

return class

This module is essentially a function for building new object prototypes that allow for easy inheritance from a parent object prototype. Any functions defined in the parent prototype object are available in this newly created object prototype. One thing to note is the use of __call for the ‘class’ module itself. This allows us to use this module as if it were a function:

local class = require('utils.class')
local button = class()

In this case, the __call metamethod would be called, which in turn would call the local function extend(). This is intended to hide the implementation of the extend function and provide a cleaner way of defining new object prototypes. Let’s use this to modify our button object previously used.

local class = require('utils.class')

---
-- @module button
local button = class()

---
-- Initialize the new module instance
-- @param control #string - The fully qualified name for the control this represents
-- @param down_color #number - The RGB color code for the down state color
function button:init(control, down_color)
        self.control = control
        self.up_color = gre.rgb(0, 255, 0, 255)
        self.down_color = down_color or gre.rgb(255, 0, 0, 255)
        self.active = false
        self.callbacks = {}
end

---
-- Call when the control receives a press event
function button:press()
        gre.set_value(string.format("%s.color", self.control), self.down_color)
        self:set_active(true)
end

---
-- Call when the control receives a release event
function button:release()
        gre.set_value(string.format("%s.color", self.control), self.up_color)
        self:set_active(false)
end

---
-- Sets the buttons active state
-- @param active #boolean - True when active (ie, pressed), false otherwise
function button:set_active(active)
        self.active = active
        self:notify(self.active)
end

---
-- Gets the buttons active state
-- @return #boolean - True when active
function button:get_active()
        return self.active
end

---
-- Registers a callback function for a active state change
-- @param callback #function - The callback to register
-- @return #boolean - True if callback successfully registered
function button:register(callback)
        table.insert(self.callbacks, callback)
        return true
end

---
-- Removes a callback function from the list of callbacks
-- @param callback #function - The callback to remove
-- @return #boolean - True if callback successfully removed
function button:unregister(callback)
        for i,v in ipairs(self.callbacks) do
                    if (v == callback) then
                                table.remove(self.callbacks,i)
                                return true
                    end
        end
        return false
end

---
-- Notifies all callbacks for a active state change
-- @param state #boolean - The new active state
function button:notify(state)
        for _,v in ipairs(self.callbacks) do
                    v(state)
        end
end

return button

Since our class prototype provides the function of instantiating new objects, we can remove any of our previous boilerplate code. Now the button module only needs to contain the functions it implements. We can simplify the toggle module in a similar way as well.

local class = require('utils.class')
local button = require('components.button')

---
-- @module toggle
local toggle = class(button)

---
-- Call when the control receives a press event
function toggle:press()
        if self:get_active() then
                    gre.set_value(string.format("%s.color", self.control), self.up_color)
                    self:set_active(false)
        else
                    gre.set_value(string.format("%s.color", self.control), self.down_color)
                    self:set_active(true)
        end
end

---
-- Call when the control receives a release event
function toggle:release()
        -- do nothing
end

return toggle

 

 

 

Was this article helpful?
0 out of 0 found this helpful