หัดเขียนโปรแกรมแบบบ้านๆ ตอนที่ 11 OOP

 บทความก่อนหน้า


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 มีสมาชิกแค่สามตัวคือ classname กับ 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.
  ]]

เทียบกับภาษา Ruby

john.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 ครับ

ความคิดเห็น