---
# System prepended metadata

title: Lua 物件導向設計思路
tags: [Lua]

---

# 前言
Lua 並不是為了物件導向（Object-Oriented）而設計的一種語言，因此僅從原生語法上並不直接支持物件導向的編程，但依然可以透過一些自身的機制實現類似的效果
>[!Tip]物件導向幾大特性：
>封裝 Encapsulation
>繼承 Inheritance
>多型 Polymorphism

# Lua 基本語法特性
## 函數的宣告和調用
Lua 在函數的宣告和調用時，有點號（‧）和冒號（：）兩種寫法，點號是一般預設的用法
```gherkin=
local human = {}
function human.say(content)
    print(content)
end
human.say("Hello")
```
```gherkin=
Hello
```

---

冒號之於點號是一種語法糖關係，默認會有個第一排序的隱藏參數 self，指該對象本身
```gherkin=
local human = {}
function human:say(content)
    print(content)
end
human:say("Hello")
```
```gherkin=
Hello
```
等同於
```gherkin=
local human = {}
function human.say(self, content)
    print(content)
end
human.say(human, "Hello")
```
```gherkin=
Hello
```

---

兩者可以混用，但一旦使用了冒號，該函數就會多個第一排序的隱藏參數，以下有幾個錯誤例子
```gherkin=
local human = {}
function human.say(content)
    print(content)
end
human:say("Hello")
```
```gherkin=
table: 0x556e9a12cf40
```
```gherkin=
local human = {}
function human:say(self, content)
    print(content)
end
human.say("Hello")
```
```gherkin=
nil
```
正確的混用例子
```gherkin=
local human = {}
function human.say(self, content)
    print(content)
end
human:say("Hello")
```
```gherkin=
Hello
```

## 可變參數 arg
Lua 的函數可以接受不固定數量的參數，在函數參數列表中使用 ... 來表示
```gherkin=
local human = {}
function human:say(content, ...)
    local arg = { ... }
    print(content)
    local argContent = ""
    for i, v in ipairs(arg) do
        argContent = argContent .. v
    end
    print(argContent)
end
human:say("Hello", "I'm ", "Human", ".")
```
```gherkin=
Hello
I'm Human.
```

## 元表 metatable
元表是一個對於表（table）的特殊屬性，元表可以設定或改變該表的部分行為
```gherkin=
local human = {}                              -- 元表
local man = {}                                -- 表
setmetatable(man, human)                      -- 將 human 設為 man 的元表
local mt = getmetatable(man)                  -- 返回 man 的元表，即 human
```
在元表中，有許多元方法（Metamethods）可以使用，其中以 ˍˍindex 為本文重點，其作用為當表被訪問時，若該表不存在指定的 key 值，則會往元表中搜尋
其他還有諸如 ˍˍadd、ˍˍsub、ˍˍeq、ˍˍtostring 等等元方法，就先不展開細講了

# 繼承
在 C++ 或 python 等這樣的 OO 語言中，使用 class 語言來創建 object、instance，即每個對象都是特定類別的實例
而在 Lua 中沒有類的概念，也就是說對象是沒有類型的，若要創建多個具有類似行為的對象，則需要透過元表的 ˍˍindex 元方法
將子類 table 的 ˍˍindex 元方法設置為父類 table，如此一來子類被調用自身沒有的屬性或函數時，就會去查找元表，實現類似繼承的效果
```gherkin=
local human = { name = "Human" }
function human:say()
    print("Hello, I'm " .. self.name .. ".")
end

local man = {}
human.__index = human
setmetatable(man, human)

man:say()
```
```gherkin=
Hello, I'm Human.
```
其中上述第 6-8 行可以簡化寫法
```gherkin=
local man = setmetatable({}, { __index = human })
```
:::info 
另外提一個我在 VSCode 開發中的現象，若是不使用簡化寫法，在查找子類所繼承的屬性或函數的實作時，使用移至定義 ( Go to Definition ) 是找不到實作位置的；使用簡化寫法就沒問題，不確定原理，不清楚其他編輯器是怎麼樣
:::

---

甚至可以模仿建構函數的方式來創建對象，其中建構函數 ctor 的命名是自訂的，Lua 並沒有提供建構的固定保留字，想用 new 之類的都可以
```gherkin=
local human = { name = "Human" }
function human:ctor()
    function human:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    local o = setmetatable({}, { __index = self })
    return o
end

local man = human:ctor()
man:say()
```
```gherkin=
Hello, I'm Human.
```
建構函數內要帶值也沒問題
```gherkin=
local human = {}
function human:ctor(c)
    self.name = c or "Human"
    
    function human:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    local o = setmetatable({}, { __index = self })
    return o
end

local man = human:ctor("Adam")
man:say()
```
```gherkin=
Hello, I'm Adam.
```

---

但此時若子類有複數個，就會出現以下問題
```gherkin=
local human = {}
function human:ctor(c)
    self.name = c or "Human"
    
    function human:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    local o = setmetatable({}, { __index = self })
    return o
end

local man = human:ctor("Adam")
local woman = human:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Eve.
Hello, I'm Eve.
```
原因是在第 3 行，因為 self 指的就是 human 本身，所以在第 13、14 行建構時，因為子類 man 沒有 name 的屬性，就會根據元表查到父類 human 的 name 進行賦值
而複數的子類依序對單一的父類進行賦值，才會導致最終所有子類的屬性的值都是一樣的，因為說穿了就是都導向了同樣的對象
解決辦法是更改賦值的對象，在 ctor 時提前宣告對象，並使屬性、函數的對象都指向它
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.name = c or "Human"
    
    function o:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    return o
end

local man = human:ctor("Adam")
local woman = human:ctor("Eve")
man:say()
woman:say()

```
```gherkin=
Hello, I'm Adam.
Hello, I'm Eve.
```
另解，也可以在宣告時同時賦予子類相同的屬性，函數也同理
```gherkin=
local human = {}
function human:ctor(c)
    self.name = c or "Human"
    
    function human:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    local o = setmetatable({ name = self.name }, { __index = self })
    return o
end

local man = human:ctor("Adam")
local woman = human:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Eve.
```

# 多型
除了單純繼承外，另外重要的概念還有派生，即允許由父類衍生子類，且除了繼承基底類別的行為，子類還可以定義自己獨有的行為，即多型
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.name = c or "Human"
    
    function o:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })
    
    o.say = function(self)
        print("Hello, I'm " .. self.name .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve, nice to meet you.
```
甚至可以直接宣告同名屬性或函數，原理也很簡單，外部在調用子類的屬性或函數時，直接在子類內找到同名，就不會透過元表查找父類了，實現類似覆寫的效果
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.name = c or "Human"
    
    function o:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })
    
    function o:say()
        print("Hello, I'm " .. self.name .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve, nice to meet you.
```

---

若是不想完全覆寫，或是仍想調用父類的屬性或函數，可以在子類利用 getmetatable 來實現
其中 super 的命名是自訂的，Lua 並沒有調用父類的固定保留字，想用 sup 之類的都可以
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.name = c or "Human"
    
    function o:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })
    o.super = setmetatable({}, getmetatable(o))
    
    function o:say()
        self.super:say()
        print("Hello, I'm " .. self.name .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```
或是在父類直接定義好
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.super = setmetatable({}, { __index = o })
    o.name = c or "Human"
    
    function o:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })

    function o:say()
        self.super:say()
        print("Hello, I'm " .. self.name .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Adam, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```

# 封裝
在設計物件導向的程式時，都會希望那些不需要對外公開的屬性或函數進行封裝，避免外部亂調用或賦值，同時也方便開發排查
而一般在 Lua 裡屬性和函數都是預設全域的，可以被外部調用或賦值
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.super = setmetatable({}, { __index = o })
    o.name = c or "Human"

    function o:say()
        print("Hello, I'm " .. self.name .. ".")
    end
    
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })

    function o:say()
        self.super:say()
        print("Hello, I'm " .. self.name .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man.name = "Lilith"
man:say()
woman:say()
```
```gherkin=
Hello, I'm Adam.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```

---

上述 human 的屬性 name 為全域屬性，可以被外部賦值，且僅對子類進行賦值，父類的 super 並未被賦值
這並非預想的情況，所以需要使用 local 區域化，並提供對外讀寫函數
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.super = setmetatable({}, { __index = o })
    local name = c or "Human"
    
    function o:getName()
        return name
    end
    
    function o:setName(value)
        name = value
    end

    function o:say()
        print("Hello, I'm " .. self:getName() .. ".")
    end
    
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })

    function o:say()
        self.super:say()
        print("Hello, I'm " .. self:getName() .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:setName("Lilith")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Lilith.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you. 
```
同理另解，也可以在宣告時同時賦予子類相同的函數
```gherkin=
local human = {}
function human:ctor(c)
    local name = c or "Human"
    
    function human:getName()
        return name
    end
    
    function human:setName(value)
        name = value
    end

    function human:say()
        print("Hello, I'm " .. self:getName() .. ".")
    end
    
    local o = setmetatable({
        getName = self.getName,
        setName = self.setName,
        say = self.say
    }, { __index = self })
    o.super = setmetatable({
        getName = self.getName,
        setName = self.setName,
        say = self.say
    }, { __index = o })
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })

    function o:say()
        self.super:say()
        print("Hello, I'm " .. self:getName() .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:setName("Lilith")
man:say()
woman:say()
```
```gherkin=
Hello, I'm Lilith.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you. 
```

# 額外補充
## 多載
Lua 無法像 C++ 或 Java 一樣支援多載，會出現「後覆蓋前」的情況
```gherkin=
local human = {}
function human:ctor()
    local o = setmetatable({}, { __index = self })
    
    function o:say()
        print("Hello, I'm human.")
    end
    
    function o:say(name)
        name = name or ""
        print("Hello, I'm " .. name .. ".")
    end
    
    return o
end

local man = human:ctor()
man:say()
man:say("Adam")
```
```gherkin=
Hello, I'm .
Hello, I'm Adam.
```
改用可變參數來處理可以達到同樣效果
```gherkin=
local human = {}
function human:ctor()
    local o = setmetatable({}, { __index = self })
    
    function o:say(...)
        local arg = { ... }
        name = arg[1] or ""
        if name == "" then
            print("Hello, I'm human.")
        else
            print("Hello, I'm " .. name .. ".")
        end
    end
    
    return o
end

local man = human:ctor()
man:say()
man:say("Adam")
```
```gherkin=
Hello, I'm human.
Hello, I'm Adam.
```

# 結語
Lua 確實可以透過一些方法和自身的機制實現物件導向設計類似的效果，雖然需要一些學習成本，也要習慣比較特殊的寫法，不過對於需要使用物件導向來開發的開發者來說，也算是個比較折衷的辦法了，~~不然就是換語言~~

最後統整起來的腳本：
```gherkin=
local human = {}
function human:ctor(c)
    local o = setmetatable({}, { __index = self })
    o.super = setmetatable({}, { __index = o })
    local name = c or "Human"
    
    function o:getName()
        return name
    end
    
    function o:setName(value)
        name = value
    end

    function o:say()
        print("Hello, I'm " .. self:getName() .. ".")
    end
    
    return o
end

local humanBeing = {}
function humanBeing:ctor(c)
    local base = human:ctor(c)
    local o = setmetatable({}, { __index = base })

    function o:say()
        self.super:say()
        print("Hello, I'm " .. self:getName() .. ", nice to meet you.")
    end
    
    return o
end

local man = humanBeing:ctor("Adam")
local woman = humanBeing:ctor("Eve")
man:setName("Lilith")
man:say()
woman:say()

```
```gherkin=
Hello, I'm Lilith.
Hello, I'm Lilith, nice to meet you.
Hello, I'm Eve.
Hello, I'm Eve, nice to meet you.
```