บทความก่อนหน้า
OOP
OOP ( Object Oriented Programming ) คืออะไร
การเขียนโปรแกรมเชิงวัตถุเป็นกระบวนทัศน์หนึ่งของการเขียนโปรแกรม ( Programming paradigm ) ที่เคยกล่าวถึงบ้างแล้วในตอนแรกของบทความตอนแรกของบทความ โดยแนวคิดนี้จะแบ่งโปรแกรมที่เราเขียนออกเป็นโปรแกรมเล็กๆ ที่เราเรียกว่าวัตถุ ( object ) หลายๆ ตัวประกอบกันอย่างหลวมๆ ถึงจุดนี้อาจจะดูใกล้เคียงกับกระบวนทัศน์แบบ Procedural programming ที่แบ่งการทำงานเป็นโปรแกรมย่อยหรือฟังก์ชันต่างๆ แต่ฟังก์ชันนั้นมีแต่กระบวนการทำงานเมื่อเรียกทำงานแต่ละครั้งก็จะเป็นการเริ่มทำงานใหม่ไม่มีการเก็บค่าไว้ ( ยกเว้น closure ) ในขณะที่วัตถุนั้นเป็นตัวแปรที่เก็บข้อมูลแบบโครงสร้างที่ประกอบไปด้วยตัวแปรเก็บข้อมูลหรือคุณสมบัติ ( property ) และฟังก์ชันหรือกระบวนการทำงาน ( method ) อยู่ในตัวเหมือนเป็นโปรแกรมสมบูรณ์เล็กๆ ตัวหนึ่งและสามารถเรียกใช้ได้จนกว่าวัตถุนั้นจะถูกทำลาย ก่อนจะไปต่อกันที่ OOP ในภาษา Lua ขอให้ดูตัวอย่างต่อไปนี้ เป็นตัวอย่างภาษา Ruby ซึ่งเป็นภาษาที่มีกระบวนทัศน์เชิงวัตถุอย่างแท้จริง โดยก่อนที่จะมาดูโค้ดกันมาทำความรู้จักความต่างของ syntax บางส่วนระหว่าง Lua กับ Ruby กันเล็กน้อย
- การสร้างฟังก์ชันใน Ruby ใช้ def แทนคำว่า function ใน Lua
- ในฟังก์ชันถ้าไม่ใส่ return จะคืนค่า expression สุดท้าย
- การเรียกใช้ฟังก์ชันจะใส่วงเล็บหรือไม่ก็ได้
- comment ใช้ # แทน -- ใน Lua
- hash คล้าย table แบบคู่ลำดับใน Lua มีรูปแบบเป็น {"key" => value , ...} หรือ {:key => value, ...}
- print ใน Ruby ไม่ขึ้นบรรทัดใหม่ถ้าให้เหมือนใน Lua ใช้ puts
- Ruby สามารถแทรกค่าตัวแปรลงในข้อความได้เลยโดยใช้ #{ชื่อตัวแปร} ใส่ลงในข้อความ
นี่เป็นตัวอย่างบางส่วนเท่านั้นเพื่อช่วยให้ทำความเข้าใจกับโค้ดได้ดีขึ้นเพราะตัวภาษา Ruby นั้นมีความซับซ้อนกว่า Lua มากหากสนใจสามารถหาข้อมูลเพิ่มในอินเทอร์เน็ทได้ไม่ยากเพราะมีแหล่งข้อมูลอยู่มากพอสมควร
ตัวอย่างภาษา Ruby
## แบบ Procedural
# ฟังก์ชัน say
def say(person, word)
"#{person} say: #{word}" # return ผลลัพธ์
end
# โปรแกรมหลัก
JohnSay = say "John", "Hi." # ใส่ค่าจากฟังก์ชันเข้าตัวแปร "JohnSay"
puts JohnSay # พิมพ์ค่าตัวแปร "JohnSay"
JaneSay = say "Jean", "Hi Jone." # ใส่ค่าจากฟังก์ชันเข้าตัวแปร "JaneSay"
puts JaneSay # พิมพ์ค่าตัวแปร "JaneSay"
puts say "Jane", "Hello." # พิมพ์ผลัพธ์จากฟังก์ชัน "say"
john = {"name"=>"John", "word"=>""} # ตัวแปรแบบ hash "john"
jane = {"word"=>"Hi #{john["word"]}."} # ตัวแปรแบบ hash "jane"
john["word"] = "Bye." # แก้ค่า "word" ในตัวแปร "john"
puts say john["name"], jane["word"]
# ผลลัพธ์
# John say: Hi.
# Jean say: Hi Jone.
# Jane say: Hello.
# John say: Hi Bye..
## แบบ Object oriented
# แม่แบบสร้าง object
class Person # ชื่อแม่แบบ
def initialize(n, w) # กำหนดค่าเริ่มต้นจาก Person.new(n, w)
@name = n # รับค่า "n" ไปใส่ property "name"
@word = w # รับค่า "w" ไปใส่ property "word"
end
def name # method "name"
@name # return ค่า property "name"
end
def say(w=nil) # method "say" กำหนดค่าตั้งต้นพารามิเตอร์ w เป็น nil
if w # ถ้ามีการใส่ค่า "w" ให้ไปเก็บใน "word"
@word = w
end
"#{@name} say: #{@word}" # return ค่าผลลัพธ์ของ method
end
end
# โปรแกรมหลัก
john = Person.new "John", "Hi." # สร้างวัตถุ "john" จากแม่แบบ "Person"
jane = Person.new "Jane", "Hi #{john.name}." # สร้างวัตถุ "jane" จากแม่แบบ "Person"
puts john.say # เรียก method "say" ของ object "john"
puts john.say "Hi #{jane.name}." # เรียก method "say" ของ object "john" แบบใส่อาร์กิวเมนท์
puts jane.say # เรียก method "say" ของ object "jane"
puts jane.name # เรียก method "name" ไม่ใช่ property "name"
# ผลลัพธ์
# John say: Hi.
# John say: Hi Jane.
# Jane say: Hi John.
# Jane
จากตัวอย่างข้างต้นจะมีแม่แบบของวัตถุหรือคลาส ( class ) ชื่อ Person ซึ่งจะมี property สองตัวชื่อ name กับ word และมี method หนึ่งตัวชื่อ say() โปรแกรมจะใช้ Person สร้างวัตถุ ( object หรือ instance ของ class ) ออกมาสองตัวได้แก่ john และ jane จะเห็นได้ว่าวัตถุแต่ละตัวนั้นจะมีข้อมูลเป็นของตัวเองทำให้เราจัดการกับข้อมูลแต่ละส่วนได้ง่ายขึ้นแทนที่จะเอาข้อมูลไปเก็บรวมกันไว้ข้างนอก และยังช่วยป้องกันข้อผิดพลาดที่เกิดจากโปรแกรมส่วนอื่นเข้าไปแก้ไขข้อมูลโดยไม่ตั้งใจ
นอกจากนี้ OOP ยังประกอบด้วยหลักการสำคัญ 4 อย่างได้แก่
- Abstraction คือการลดความซับซ้อน เป็นเรื่องของการออกแบบโปรแกรมคือใช้ความเรียบง่ายมาแสดงสิ่งที่ซับซ้อนโดยทั่วไปก็คือการออกแบบ CLASS หรือ OBJECT ว่าจะมีคลาสอะไรบ้างแต่ละตัวหน้าตาอย่างไรมีส่วนประกอบอะไรบ้างเช่นคลาส "เส้นตรง" มีคุณสมบัติได้แก่ "จุดเริ่มต้น", "จุดสิ้นสุด" และมีกระบวนการได้แก่ "หาความยาวเส้นตรง" เป็นต้นเอาการทำงานที่ซับซ้อนซ่อนไว้ในคลาส ที่เหลือจะเป็นการเรียกใช้คลาสโดยโปรแกรมที่เรียกใช้คลาสไม่จำเป็นต้องรู้รายละเอียดภายในคลาสแค่รู้ว่ามี method อะไรให้เรียกใช้
การออกแบบเป็นขั้นตอนที่สำคัญถึงเราจะใช้ภาษาที่เป็น OOP เต็มรูปแบบเขียนโค้ดเป็นคลาสต่างๆ แต่ออกแบบคลาสไม่ดีเช่นรวมทุกอย่างไว้ในคลาสเดียวหรือมีความซ้้ำซ้อนของคลาสก็ไม่ถือว่าเป็นการเขียนโปรแกรมเชิงวัตถุที่แท้จริง โดยแต่ละคลาสไม่ควรมีความซ้ำซ้อนกัน แยกจัดหมวดหมู่ให้ดีเพื่อให้โค้ดสะอาดดูแลรักษาง่ายซึ่งเราจะยังไม่ไปลงรายละเอียดในส่วนนี้ครับ - Encapsulation คือการห่อหุ้มเป็นการจำกัดการเข้าถึงของ property ของวัตถุนั้นโดยตรงจากภายนอก เพื่อป้องกันการเกิดข้อผิดพลาดจากการเปลี่ยนแปลงค่าโดยต้องทำผ่าน method ของวัตถุเท่านั้น เช่นคลาส "สมุดบัญชี" มีคุณสมบัติ "เงินในบัญชี" ถ้าไม่มีการป้องกันการเข้าถึงแล้วมีการใส่ค่าเงินเป็นลบอาจทำให้เกิดผิดพลาดได้แต่ถ้าทำผ่านกระบวนการ "ปรับค่าเงินในบัญชี" เราจะสามารถใส่การตรวจสอบจำนวนลบใน method นี้ได้ ในภาษาที่ออกแบบเป็น OOP จะแบ่งการเข้าถึงเป็นสองถึงสามระดับคือ
- private สามารถเข้าถึงได้เฉพาะ method ในคลาสเท่านั้นโดยทั่วไปจะใช้กับ property
- protected สามารถเข้าถึงได้จากตัวเองและคลาสลูก ( ดูที่ inheritance )
- public สามารถเข้าถึงจากภายนอกได้ โดยทั่วไปจะใช้กับ method
- Inheritance คือการสืบทอดเป็นการส่งต่อคุณลักษณะของ คลาสแม่ ไปยัง คลาสลูก ในการออกแบบนั้นบางทีเราอาจจะมีวัตถุที่มีความคล้ายกันแต่มีรายละเอียดต่างกันเพื่อใช้ในคนละส่วนการออกแบบเป็นแต่ละคลาสแยกกันจะทำให้โค้ดซ้ำซ้อน เราสามารถหาจุดร่วมของแต่ละคลาสเพื่อสร้างคลาสแม่ได้เช่นคลาส "นักดาบ" และ "นักธนู" มีคุณสมบัติ "ค่าสถานะ", "อาวุธ" แต่นักดาบมี "เกราะ" และนักธนูมี "ลูกธนู" เราสามารถสร้างคลาส "ตัวละคร" ที่มีคุณสมบัติแค่ "ค่าสถานะ" กับ "อาวุธ" แล้วเราให้ "นักดาบ" และ "นักธนู" เป็นคลาสลูกของ "ตัวละคร" ที่มีแค่คุณสมบัติเฉพาะของตัวเองได้โดยที่คลาสลูกจะได้รับคุณสมบัติและกระบวนการของคลาสแม่มาด้วย หากในอนาคตเราอยากจะเพิ่มคลาส "จอมเวทย์" เราก็สามารถสืบทอดจากคลาส "ตัวละคร" ได้เลยโดยไม่ต้องเขียนใหม่ทั้งหมด
- Polymorphism คือการเปลี่ยนรูปหมายถึงสิ่งที่ต่างกันแต่ใช้ชื่อเหมือนกัน เช่นในคลาส "รูปทรง" มีคลาสลูก "สี่เหลี่ยม" และ "วงกลม" ซึ่งมีกระบวนการ "หาพื้นที่" เหมือนกันแต่เป็นของวัตถุคนละตัวกันมีการทำงานต่างกันเราเรียกว่า method overriding คือการเขียนทับ method ที่สืบทอดมาจากคลาสแม่ที่เราอาจทำเป็นโครงไว้ ( abstract method ) ในภาษาที่เป็น static type จะสามารถมีวัตถุจากคลาสลูกใส่ในตัวแปรที่เป็นประเภทของคลาสแม่ได้ หรือการที่มี method หลายตัวชื่อเดียวกันแต่รับพารามิเตอร์ต่างกันเราเรียกว่า method overloading ก็เป็นแบบหนึ่งของ polymorphism เช่นกัน
ภาษา Lua กับ OOP
อ่านมาตั้งนาน Lua อยู่ไหนทำไมต้องยกตัวอย่าง Ruby ยังไม่เคยเรียนแล้วจะรู้เรื่องเหรอ คิดว่าเรียนกันมาหลายตอนน่าจะพอรู้พื้นฐานกันพอสมควรแล้วอ่าน comment ตามไปน่าจะพอทำความเข้าใจได้แต่ถ้ายังงงก็ไม่เป็นไรครับข้ามตรงส่วนโค้ดไปก็ได้ ส่วนที่ยก Ruby มาให้ดูก่อนก็เพราะภาษา Lua นั้นไม่ได้ออกแบบมาให้เป็น OOP ครับจบแล้วเจอกันตอนหน้า... เอ๊ยยังสิถึง Lua จะไม่ได้ออกแบบด้วยกระบวนทัศน์การโปรแกรมเชิงวัตถุโดยตรงแต่เราสามารถประยุกต์ใช้คุณสมบัติต่างๆ ของภาษา Lua เช่น table, first-class function, metatable, closure เป็นต้นเพื่อจำลองลักษณะของการโปรแกรมเชิงวัตถุได้ โดยภาษา Lua เราสามารถเขียนโปรแกรมเชิงวัตถุได้สองวิธีคือแบบ table และแบบ closure
Table based:
ย้อนกลับไปในบทความ มารั่วกันต่อ#4 ที่เราได้กล่าวถึงข้อมูลประเภท table และการใช้ table เป็น object มาแล้วมาขอทบทวนกันตามตัวอย่างข้างล่างโดยอ้างอิงจากโค้ด Ruby ข้างบน
-- Object Person
local Person = {}
function Person:say(w) -- method "say()" ( Person:say(w) เท่ากับ Person.say(self, w) )
if w then self.word = w end
return self.name.." say: "..self.word
end
function Person.new(n,w) -- method new() สร้าง object ใหม่
local obj = {class="Person", name=n, word=w} -- รับค่า "n", "w" ไปใส่ property "name" และ "word"
Person.__index = Person -- ให้ "Person" มี metamethod "__index" ชี้ไปที่ตัวเอง
setmetatable(obj, Person) -- ให้ "Person" เป็น metatable ของ object ใหม่
return obj
end
-- Object Employee สืบทอดจาก Person
local Employee = {}
setmetatable(Employee, {__index=Person}) -- ให้ metatable ของ Employee มี "__index" ชี้ไปที่ "Person"
function Employee:setsalary(s) -- method เฉพาะของ "Employee"
if type(s) == "number" then
self.salary = s
end
end
function Employee.new(n, w, s) -- override method "new()"
local obj = {name=n, word=w, salary=s} -- รับค่า "n", "w", "s" ไปใส่ property "name", "word" และ "salary"
obj.class = "Employee"
setmetatable(obj, {__index=Employee}) -- ให้ metatable ของ object ใหม่มี "__index" ชี้ไปที่ "Employee"
return obj
end
-- โปรแกรมหลัก
local john = Person.new("John", "Hi.") -- object "john" เป็น instance ของ "Person"
local jane = Person.new("Jane", "Hi "..john.name)
print(john:say())
print(jane:say())
print(john:say("Bye."))
print(jane.class)
local jack = Employee.new("Jack", "Hello.", 200) -- object "jack" เป็น instance ของ "Employee"
print(jack:say()) -- เรียกใช้ method "say()" ของคลาสแม่ ( Person )
print(jack.salary)
print(jack.class)
-- ผลลัพธ์
--[[
John say: Hi.
Jane say: Hi John
John say: Bye.
Person
Jack say: Hello.
200
Employee
]]
เราใช้ table ซึ่งเป็นประเภทข้อมูลแบบโครงสร้างและสามารถมีสมาชิกแบบคู่ลำดับ ( key=value ) มาเป็นโครงของวัตถุ และใช้สมาชิกที่เป็น function มาเป็น method
ส่วน " : " เป็น syntactic sugar ในภาษา Lua ในกรณีของ การสร้างฟังก์ชัน จะหมายถึงมีพารามิเตอร์ตัวแรกเป็น self และถูกซ่อนไว้เมื่อมี การเรียกใช้ฟังก์ชัน จะหมายถึงการส่งค่าตัวเองไปเป็นอาร์กิวเมนท์ที่ถูกซ่อนไว้
Foo = {}
function Foo:bar() --> function Foo.bar(self)
...code --> ...code
end --> end
Foo:bar() --> Foo.bar(Foo)
การผูก metatable ทำให้เมื่อมีการเรียกถึงสมาชิกที่ไม่มีใน table หลักมันจะไปค้นว่า metamethod __index ชี้ไปที่ไหนแล้วไปค้นหาสมาชิกจากตำแหน่งที่อ้างอิง เช่นจากตัวอย่างตัวแปร john มีสมาชิกแค่สามตัวคือ class, name กับ word เมื่อมีการเรียก john:say() ซึ่งไม่มีอยู่ใน john ก็จะไปดู __index ใน metatable ที่ผูกไว้พบว่าชี้ไปที่ table Person ก็ไปหาสมาชิก say ที่ Person แทนวิธีนี้ทำให้ object ใหม่มี method เหมือนกับต้นฉบับและยังใช้ในการสืบทอดด้วยทำให้ Employee สามารถใช้ method ของ Person ได้เช่นกัน
Person => {__index=Person, say=<function: ...>, new=<function: ...>} john => {class="Person", name="John", word="Bye."} --> Person.__index --> Person Employee => {setsalary=<function: ...>, new=<function: ...>} --> {__index=Person} --> Person jack => {class="Employee", name="Jack", word="Hello.", salary=200} --> {__index=Employee} --> Employee --> {__index=Person} --> Person
แต่วิธีนี้ยังขาดคุณลักษณะ encapsulation ของ OOP ที่ยังสามารถเข้าถึงคุณสมบัติของวัตถุได้โดยตรงอยู่เช่น
john.name = "Jack" -- แก้ค่าใน "name" โดยตรง john.word = "Hello." -- แก้ค่าใน "word" โดยตรง print(john:say()) print(john.word) -- เรียกดูค่าใน "word" โดยตรง -- ผลลัพธ์ --[[ Jack say: Hello. Hello. ]]
เทียบกับภาษา Rubyjohn.name = "Jack" # พยายามแก้ไขค่าใน "name" # ผลลัพธ์ error # # testclass.rb:XX:in `<main>': undefined method `name=' for #<Person:0x0000000249b7e8 @name="John", @word="Hi Jane"> (NoMethodError) # Did you mean? name # puts john.word # พยายาเรียกดูค่าใน "word" # ผลลัพธ์ error # # testclass.rb:XX:in `<main>': undefined method `word' for #<Person:0x0000000249b7e8 @name="John", @word="Hi Jane"> (NoMethodError) #
ซึ่งเราสามารถใช้ metamethod __index และ __newindex มาแก้ปัญหาตรงนี้ได้
__index ใช้เมื่อมีการเรียกดูค่าใน table ถ้าไม่เจอสามชิกที่มีคีย์ตรงกับที่ระบุและมีการผูก metatable ไว้จะไปเรียก __index ที่อยู่ใน metatable ที่ผูกไว้เราสามารถกำหนดค่าเป็น table ที่ต้องการหรือกำหนดค่าเป็นฟังก์ชันที่มีพารามิเตอร์สองตัวคือ table และ key
__newindex ใช้เมื่อมีการกำหนดค่าใน table และมีการผูก metatable ไว้จะไปเรียก __newindex ที่อยู่ใน metatable ที่ผูกไว้เราสามารถกำหนดค่าเป็นฟังก์ชันที่มีพารามิเตอร์สามตัวคือ table, key และ value
การอ้างถึง table ในฟังก์ชันข้างต้นจะใช้ฟังก์ชันเฉพาะคือ rawget(table, key) และ rawset(table, key, value) แทนการใช้ table[key] และ table[key] = value เพื่อป้องกันการเรียกตัวเองซ้ำแต่ถ้าอ้างถึง table อื่นที่ไม่ใช่พารามิเตอร์ที่รับมาก็สามารถใช้การเรียกแบบปกติได้เช่น othertable[key] และ othertable[key] = value
rawget() และ rawset() มีอธิบายไว้ในตอนที่ 5 เป็นฟังก์ชันที่เข้าถึงสมาชิกที่มีอยู่จริงใน table นั้นโดยไม่มีการไปเรียก __index และ __newindex
-- Object Person
local Person = {class="Person"}
function Person:say(w) -- method "say()"
if w then self.word = w end
return self:getname().." say: "..self.word
end
function Person:new(data) -- method new() สร้าง object ใหม่
local mt = {class=Person.class} -- matatable "mt" และเอาไว้เก็บสมาชิกที่เป็น private
local tmp = getmetatable(self)
if tmp then -- ถ้าวัตถุต้นฉบับมี metatable ให้เอาสมาชิกใน metatable มาใส่ใน "mt"
for k, v in pairs(tmp) do
mt[k] = v
end
end
if type(data) == "table" then
mt.class = data.class or mt.class -- รับค่า "class" ใน "data" ไปใส่ property "class"
mt.name = data.name or mt.name -- รับค่า "name" ใน "data" ไปใส่ property "name"
mt.word = data.word or mt.word -- รับค่า "word" ใน "data" ไปใส่ property "word"
end
Person.__index = Person -- ให้ "Person" มี metamethod "__index" ชี้ไปที่ตัวเอง
-- ให้ "word" เป็น public property สามารถเรียกดูค่าได้โดยตรง
mt.__index = function(tbl, key) -- ให้ "mt" มี metamethod "__index" เป็นฟังก์ชันตรวจถ้าไม่ใช่คีย์ที่กำหนด ( method หรือ property ที่กำหนดให้เป็น public ) ให้ error
-- ถ้าคีย์ไม่ใช่ property "word" หรือเป็น method จะไม่สามารถเข้าถึงได้
if type(mt[key]) ~= "function" and key ~= "word" then
error("ไม่มี method '"..key.."' ใน object '"..(mt.name or mt.class).."'")
end
return mt[key] -- คืนค่าสมาชิกที่เป็น public
end
mt.__newindex = function(tbl, key, val) -- metamethod "__newindex" เป็นฟังก์ชันตรวจการแก้ไขค่าในคีย์ถ้าไม่ใช่คีย์ที่กำหนดให้แสดงข้อผิดพลาด
-- method เดิมไม่สามารถเปลี่ยนประเภทข้อมูลได้ ( แต่ override method ได้ )
if type(mt[key]) == "function" and type(val) ~= "function" then
error("ไม่สามารถกำหนดค่าให้ method '"..key.."'")
end
if key == "name" then -- ให้ "name" เป็น private property ไม่สามารถแก้ไขค่าได้โดยตรง
error("ไม่มี method '"..key.."' ใน object '"..(mt.name or mt.class).."'")
end
mt[key] = val -- เปลี่ยนค่าของคีย์ใน "mt"
end
setmetatable(mt, Person) -- ให้ "Person" เป็น metatable ของ "mt"
local obj = {}
setmetatable(obj, mt) -- ให้ "mt" เป็น metatable ของ object ใหม่
return obj
end
function Person:getname() -- getter method สำหรับเข้าถึงค่าของ "name"
local mt = getmetatable(self)
return mt.name
end
-- โปรแกรมหลัก
local john = Person:new{name="John", word="Hi."}
local jane = Person:new{name="Jane", word="Hi "..john:getname()}
print(john:say())
print(jane:say())
print(john:say("Bye."))
print(jane:getname()) -- เรียกดูค่า private property "name" ผ่าน method "getname()"
print(jane.word) -- เรียกดูค่า public property "word" ได้โดยตรง
print(john.name) -- ไม่สามารถเรียกดู private property "name" โดยตรงได้เกิด error
-- ผลลัพธ์
--[[
John say: Hi.
Jane say: Hi John
John say: Bye.
Jane
Hi John
lua: person.lua:XX: ไม่มี method 'name' ใน object 'John'
stack traceback:
....
]]
วิธีการใช้ table กับ metatable แบบนี้จะไม่มีคลาสเหมือนในภาษาอื่นที่รองรับ OOP แต่จะสร้างเป็นวัตถุต้นแบบ ( prototype object ) ดังนั้น "john" และ "jane" นอกจากจะเป็นวัตถุชนิดเดียวกับวัตถุต้นแบบ "Person" แล้วยังถือว่าเป็นวัตถุที่สืบทอดมาจากวัตถุต้นแบบด้วย ทำให้เราสามารถทำการ overriding ในวัตถุใหม่ได้เลย
-- Object Employee
local Employee = Person:new{class="Employee"}
function Employee:setsalary(s) -- method เฉพาะของ "Employee"
if type(s) == "number" and s >= 0 then
local mt = getmetatable(self)
mt.salary = s -- property เฉพาะของ "Employee"
end
end
function Employee:getsalary() -- method เฉพาะของ "Employee"
local mt = getmetatable(self)
return mt.salary or 0
end
function Employee:say(s) -- override method "say()"
self:setsalary(s)
return self.word..", I'm "..self:getname().."\nMy salary is "..self:getsalary().."."
end
-- โปรแกรมหลัก
local jack = Employee:new{name="Jack", word="Yo!"}
print(jack:say())
jack:setsalary(5000)
print(jack:say())
-- ผลลัพธ์
--[[
Yo!, I'm Jack
My salary is 0.
Yo!, I'm Jack
My salary is 5000.
]]
การเขียนแบบ Table based มีข้อดีคือใช้หน่วยความจำน้อยเพราะใช้การผูก metatable ดังนั้นวัตถุแต่ละตัวจะใช้ method ร่วมกันกับวัตถุต้นแบบแต่จะทำให้การเรียก method ที่ผูกไว้ช้ากว่า method ที่อยู่ในวัตถุนั้นเล็กน้อย และการผูก metatable ซ้อนหลายชั้นจะทำให้โค้ดดูซับซ้อนเช่นตัวอย่างโค้ดข้างบนที่ทำ encapsulation ( แต่ถ้าไม่ต้องคำนึงในส่วนนี้ก็สามารถเขียนโค้ดได้กระชับและทำการสืบทอดได้ง่ายขึ้น ) เราอาจทำโค้ดให้ง่ายขึ้นเก็บสมาชิกที่เป็น private ไว้ใน property พิเศษที่เราสร้างขึ้นเป็น table ย่อยอยู่ข้างในเช่น obj = {_pvp={สมาชิก private}, สมาชิก public} แล้วใช้ __index ใน metatable ชี้ไปที่ _pvp อีกทีแต่กรณีนี้ถ้าเรารู้ว่ามี _pvp อยู่ก็สามารถเข้าถึงสามชิกได้โดยตรงอยู่ดีแต่ก็ทำให้โค้ดเรียบง่ายขึ้นและช่วยลดข้อผิดพลาดจากการเข้าถึงสมาชิกจากภายนอกโดยไม่ตั้งใจ
-- Object Person
local Person = {cname="Person"}
function Person:say(w) -- method "say()"
if w then self.word = w end
return self:getname().." say: "..self.word
end
function Person:new(n, w) -- method "new()" สร้าง object ใหม่
local obj = {}
obj._private = {class=Person} -- ให้ "_private" เก็บ private property
-- private property "class" ชี้ไปที่ตำแหน่งของ Person
obj._private.name = n or "" -- รับค่า "n" ไปใส่ private property "name"
obj.word = w or "" -- รับค่า "w" ไปใส่ public property "word"
local mt = {} -- สร้าง metatable "mt"
mt.__index = function(tbl, key) -- ถ้าไม่มีสมาชิกใน "tbl" ( object ที่สร้าง )
val = rawget(tbl._private.class, key) -- ให้ไปดูที่ "tbl._private.class"
if not val and tbl._private.baseclass then
val = rawget(tbl._private.baseclass, key) -- ถ้าไม่มีให้ไปดูที่ "tbl._private.baseclass"
end
if not val then -- ถ้าไม่มีให้ error
error("ไม่มี method '"..key.."' ใน object '"..tbl._private.class.cname.."'")
end
return val
end
mt.__newindex = function(tbl, key, val) -- metamethod "__newindex" เป็นฟังก์ชันตรวจการแก้ไขค่าในคีย์ถ้าไม่ใช่คีย์ที่กำหนดให้ error
-- method เดิมไม่สามารถเปลี่ยนประเภทข้อมูลได้
local tmpval = rawget(tbl, key) or rawget(self, key)
if not tmpval then -- ถ้าไม่มี public property หรือ public method ให้ error
error("ไม่มี method '"..key.."' ใน object '"..tbl._private.class.cname.."'")
end
if type(tmpval) ~= type(val) then -- ถ้าประเภทข้อมูลในคีย์เดิมไม่ตรงกับข้อมูลใหม่ให้ error
error("ไม่สามารถกำหนดค่าให้ method '"..key.."'")
end
rawset(tbl, key, val)
end
setmetatable(obj, mt) -- ให้ "mt" เป็น metatable ของ object ใหม่
return obj
end
function Person:getname() -- getter method สำหรับเข้าถึงค่าของ "name"
return self._private.name
end
-- โปรแกรมหลัก
local john = Person:new("John", "Hi.")
local jane = Person:new("Jane", "Hi "..john._private.name)
print(john:say())
print(jane:say())
print(john:say("Bye."))
print(jane:getname()) -- เรียกดูค่า private property "name" ผ่าน method "getname()"
print(jane.word) -- เรียกดูค่า public property "word" ได้โดยตรง
print(john.name) -- ไม่สามารถเรียกดู private property "name" โดยตรงได้เกิด error
-- ผลลัพธ์
--[[
John say: Hi.
Jane say: Hi John
John say: Bye.
Jane
Hi John
lua: person.lua:XX: ไม่มี method 'name' ใน object 'John'
stack traceback:
....
]]
-- Object Employee
local Employee = {cname="Employee"}
function Employee:setsalary(s) -- method เฉพาะของ Employee
if type(s) == "number" and s >= 0 then
self._private.salary = s
end
end
function Employee:new(n, w, s) -- override method สร้าง object ใหม่สำหรับ "Employee"
local obj = Person:new(n, w) -- ให้ object ใหม่สืบทอดจาก "Person"
obj._private.class = self -- ให้ private property "class" ชี้ไปที่ "Employee"
obj._private.baseclass = Person -- ให้ private property "baseclass" ชี้ไปที่ "Person"
obj._private.salary = tonumber(s) or 0 -- เพิ่ม private property "salary" ค่าเริ่มต้นเป็น 0
return obj
end
Employee.say = function(self, s)
if s then self._private.salary = s end
return self.word..", I'm "..self._private.name.."\nMy salary is "..self._private.salary.."."
end
-- โปรแกรมหลัก
local jack = Employee:new("Jack", "Yo!")
print(jack:say())
jack:setsalary(5000)
print(jack:say())
print(jack._private.class.cname) -- ยังสามารถเข้าถึง private property ได้โดยตรงถ้ารู้ว่าเก็บซ่อนไว้ใน property "_private" อีกที
print(jack._private.baseclass.cname)
-- ผลลัพธ์
--[[
Yo!, I'm Jack
My salary is 0.
Yo!, I'm Jack
My salary is 5000.
Employee
Person
]]
Closure based:
จะใช้ closure ร่วมกับ table โดยใช้คุณสมบัติในการเข้าถึง upvalue ของ closure ในการทำให้ตัวแปรหรือฟังก์ชันที่เป็น local ไม่สามารถเข้าถึงจากภายนอกได้ดังนี้
-- Class Person
local Person = {} -- สร้างแม่แบบ "Person"
function Person.new(n, w) -- ฟังก์ชันสร้าง object จากแม่แบบ
local obj = {} -- เป็น object ที่จะสร้าง
local class = "Person" -- ให้ property "class" เป็นแบบ private
local name = n -- ให้ property "name" เป็นแบบ private
obj.word = w -- ให้ property "word" เป็นแบบ public
local function abc() -- ตัวอย่าง method แบบ private
--
end
function obj.getclass() -- ให้ method "getclass()" เป็นแบบ public
return class
end
function obj.getname() -- ให้ method "getname()" เป็นแบบ public
abc() -- ตัวอย่างเรียกใช้ private method ผ่าน public method
return name
end
function obj.say(w) -- ให้ method "say()" เป็นแบบ public
if w then obj.word = w end
return name.." say: "..obj.word
end
return obj
end
-- โปรแกรมหลัก
local john = Person.new("John", "Hi.")
local jane = Person.new("Jane", "Hi "..john.getname())
print(john.say())
print(jane.say())
print(john.say("Bye."))
print(john.word)
print(john.name) -- ไม่สามารถเรียกดู private property ได้
-- ผลลัพธ์
--[[
John say: Hi.
Jane say: Hi John
John say: Bye.
Bye.
nil
]]
จะเห็นได้ว่าโค้ดดูสะอาดเข้าใจได้ง่ายกว่า และใกล้เคียงกับระบบคลาสมากกว่าแบบ Table based แต่ก็จะใช้หน่วยความจำมากขึ้้นเพราะวัตถุแต่ละตัวจะมี method เป็นของตัวเอง และจุดสำคัญคือการเรียกใช้ method สามารถเรียกโดยใช้จุดแบบ property ได้เลยเพราะไม่ได้ใช้ method ร่วมกับตัวต้นแบบจึงไม่ต้องส่งการอ้างถึงวัตถุไปที่พารามิเตอร์ self แต่เรียกใช้ตัวเองโดยตรงได้เลย ส่วนการสืบทอดนั้นสามารถเขียนได้ดังนี้
local Employee = {}
function Employee.new(n, w, s)
local obj = Person.new(n, w)
local class = "Employee"
local salary = tonumber(s) or 0
obj.getbaseclass = obj.getclass -- method "getclass()" ของ "Person"
function obj.getclass() -- override method "getclass()"
return class
end
function obj.setsalary(s) -- method เฉพาะของ "Employee"
if type(s) == "number" then
salary = s
end
end
function obj.getsalary() -- method เฉพาะของ "Employee"
return salary
end
function obj.say(s) -- override method "say()"
obj.setsalary(s)
return obj.word..", I'm "..obj.getname().."\nMy salary is "..salary.."."
end
return obj
end
-- โปรแกรมหลัก
local jack = Employee.new("Jack", "Hi.", 20)
print(jack.say())
jack.setsalary(5000)
print(jack.say())
print(jack.getclass()) -- เรียก method "getclass()" ของ "Employee"
print(jack.getbaseclass()) -- เรียก method "getclass()" ของ "Person"
-- ผลลัพธ์
--[[
Hi., I'm Jack
My salary is 20.
Hi., I'm Jack
My salary is 5000.
Employee
Person
]]
Polymorphism ใน Lua:
เนื่องจาก Lua เป็นภาษาแบบ Dynamic type คือตัวแปรจะเป็นข้อมูลประเภทไหนก็ได้ดังนั้นจึงไม่มีปัญหาในการรับค่าเป็นวัตถุต่างชนิดกันตราบใดที่มีต้นแบบร่วมกันและมี method เหมือนกันก็เรียกใช้ได้เลยส่วนเรื่อง method overloading นั้น Lua ไม่สนับสนุนส่วนนี้แต่เราสามารถใช้ variadic argument มาจัดการได้
--ตัวอย่างการจัดการกับวัตถุต่างชนิดกัน
local persons = {john, jane, jack}
for i, v in ipairs(persons) do
print(i, v.getname(), v.getclass())
end
-- ผลลัพธ์
--[[
1 John Person
2 Jane Person
3 Jack Employee
]]
--ตัวอย่าง mathod overloading
function sum(...)
local s = 0
for i = 1, select("#", ...) do
s = s + select(i, ...)
end
print(s)
end
sum(1, 2)
sum(3, 4, 5)
print(john.say())
print(john.say("Ha Ha."))
-- ผลลัพธ์
--[[
3
12
John say: Bye.
John say: Ha Ha.
]]
ตอนต่อไปมารู้จักการสร้างและเรียกใช้ Module ครับ
ความคิดเห็น
แสดงความคิดเห็น