หน้าเว็บ

วันพฤหัสบดีที่ 9 ตุลาคม พ.ศ. 2557

Method lookup และ super

ทุ
กครั้งที่ message ถูกส่งไปยังผู้รับ (receiver) Ruby จะทำการตรวจสอบทันทีว่าภายในคลาสต้นสังกัดของอ็อบเจกต์ที่เป็นผู้รับ message นั้นมีเมธอดที่มีชื่อเดียวกันกับชื่อของ message ที่ถูกส่งเข้ามาหรือไม่
กระบวนการค้นหาเมธอดที่มีชื่อเดียวกับ message นั้น หากค้นหากันภายในคลาสของผู้รับแล้วไม่เจอเมธอดที่มีชื่อเดียวกับ message Ruby จะทำการค้นหาต่อไปยัง โมดูลที่คลาสนั้นผนวก (include) เข้ามา รวมถึงคลาสแม่ทุกๆ คลาสที่สืบทอดต่อกันมา เรื่อยไปขึ้นไปจนถึงคลาส BasicObject ซึ่งเป็นคลาสแม่ลำดับบนสุดของทุกๆ คลาส กันเลยทีเดียว 
เรื่องของลำดับขั้นในการค้นหาเมธอดของอ็อบเจกต์เมื่อตัวมันได้รับ message นั้นถือว่ามีความสำคัญไม่น้อย เพราะนอกจากจะช่วยให้เราแยกแยะการทำงานของอ็อบเจกต์และเมธอดได้แล้ว ยังช่วยให้เราเข้าใจถึงการ override ของ Ruby อีกด้วย

สมมติว่าเรากำหนดให้คลาส A, คลาส B และ โมดูล M มีความสัมพันธ์กันตามโค้ดต่อไปนี้

module M
  def print
    puts “this is print method”
  end
end

class A
  include M
end

class B < A
end



จากโค้ดจะเห็นว่าคลาส B ได้รับสืบทอดคุณสมบัติจากคลาส A ซึ่งมีการผนวกเอาโมดูล M เข้ามารวมเอาไว้ในตัวมัน
เราพบว่าอ็อบเจกต์จากคลาส B จะสามารถตอบสนองกับ message ที่มีชื่อว่า print ได้ ดังโค้ดข้างล่างนี้ ถึงแม้ว่าจะไม่มีเมธอดที่ชื่อ print กำหนดอยู่ภายในคลาส B ก็ตาม
b = B.new
b.print    # => "this is print method"

สาเหตุที่เป็นเช่นนี้ก็เพราะ เมื่อมีการส่ง message ไปยังอ็อบเจกต์ใดๆเกิดขั้น Ruby จะพยายามทำการค้นหาเมธอดที่มีชื่อเหมือนกับ message ที่ถูกส่งไปยังอ็อบเจกต์นั้นๆ แล้วทำการประมวลผลหรือเรียกเมธอดนั้นขึ้นมาใช้เมื่อเจอ
จากตัวอย่างข้างต้น การค้นหาเมธอดของ Ruby มีจะทำการตรวจสอบเมธอดที่อยู่ภายในคลาสและโมดูลที่เกี่ยวข้องโดยจะเริ่มตรวจสอบตามลำดับดังต่อไปนี้
1. มีการส่ง message ชื่อ print ไปยังอ็อบเจกต์ b
2. อ็อบเจกต์ b มีสถานะเป็นผู้รับ จึงต้องเริ่มค้นหาเมธอดที่มีชื่อเดียวกับ print โดยเริ่มค้นหาจากภายในคลาสของตัวเองซึ่งก็คือคลาส B ก่อน
3. มีเมธอดชื่อ print อยู่ภายในคลาส B หรือไม่ --> ไม่มี
4. คลาส B มีการผสม (mix-in) โมดูลอื่นเข้ามาหรือไม่ --> ไม่มี
5. คลาส B สืบทอดจากคลาสอื่นหรือไม่ --> มี, คลาส B สืบทอดมาจากคลาส A
6. ทำการตรวจสอบภายในคลาส A ว่ามีเมธอดชื่อ print หรือไม่ --> ไม่มี
7. คลาส A มีการผสมโมดูลอื่นเข้ามาหรือไม่ --> มี, โมดูล M
8. ทำการตรวจสอบภายในโมดูล M ว่ามีเมธอดชื่อ print หรือไม่ --> มี, OK ทำการประมวลผลเมธอดนี้เลย

จากการกระบวนการค้นหาเมธอดข้างต้น เมธอดที่ Ruby ค้นเจอก็จะถูกนำมาประมวลผลหรือเราอาจมองว่าเมธอดถูกเรียกออกมาใช้งานได้อย่างไม่มีปัญหาก็ได้ แต่ถ้าไม่เจอ Ruby จะทำการเรียกใช้เมธอดพิเศษที่มีชื่อว่า method_missing ขึ้นมาทำงานซึ่งโดยทั่วไปแล้วมันจะแสดงผลลัพธ์ออกมาเป็นข้อผิดพลาด (Error) แล้วหยุดการทำงานของโปรแกรมลง ไอ้เจ้าเมธออดที่ชื่อ method_missing ตัวนี้เป็นกลไกสำคัญที่เราสามารถนำไปประยุกต์ใช้เพื่อควมคุมการทำงานของโปรแกรมได้อย่างสมบูรณ์ ซึ่งคงได้มีโอกาสนำมาลงใน post ต่อๆ ไป

เนื่องจากใน Ruby แทบทุกสิ่งล้วนเป็นอ็อบเจกต์ และคลาสของอ็อบเจกต์เหล่านั้นก็มีการสืบทอดคลาส ผสมโมดูลกันมากมาย คุณอาจจะมีคำถามในใจว่า แล้ว Ruby ต้องค้นหาเมธอดแยอะไหม ถ้า message ที่ส่งเข้าไปให้อ็อบนั้นไม่มีอยู่จริง จริงๆ แล้วมันก็ไม่ได้ซับซ้อนมากขนาดนั้น เพราะเเมื่อลองพิจารณาดูเราจะพบว่าคลาสแม่ที่อยู่ในลำดับบนสุดนั้นคือคลาส BasicObject ซึ่งมีอินสแตนซ์เมธอดอยู่เพียงแค่ 8 เมธอดเท่านั้น ซึ่งไม่ซับซ้อน แต่คลาส BasicObject จะถูกสืบทอดโดยคลาส Object ซึ่งเป็นคลาสที่รวบรวมเมธอดพื้นฐานต่างๆ ที่ Ruby ออกแบบไว้ให้ทุกๆ อ็อบเจกต์ต้องมีร่วมกัน และคลาส Object นี้เองที่เป็นคลาสแม่ของทุกๆ คลาสใน Ruby หากเราสร้างคลาสโดยไม่ได้ระบุว่าจะให้คลาสนั้นสืบทอดจากคลาสใด Ruby จะกำหนดให้คลาสที่เราสร้างขึ้นสืบทอดคลาส Object โดยอัตโนมัติ
เพิ่มเติมตรงนี้นิดนึงว่า คุณสมบัติที่มีในคลาส Object นั้นไม่ได้มาจากอินสแตนซ์เมธอดภายใน Object อย่างเดียว แต่มาจากโมดูล Kernal ที่ถูก Object ผสมเข้าไปด้วย ซึ่งฟังชั่นพื้นฐานต่างๆ ที่เราคุ้นๆ กันอย่าง puts, gets, require, exit อะไรพวกนี้ ก็มาจากเจ้าโมดูล Kernal ตัวนี้แหละ
ถ้าสมมติว่ามี message ชื่อ x ส่งเข้ามาให้กับอ็อบเจกต์ b  Ruby ก็จะวิ่งหาเมธอด x ตามเส้นทางดังต่อไปนี้


ลำดับขั้นของการค้นหาเมธอดเมื่อมีการส่ง message ให้กับอ็อบเจกต์สามารถสรุปออกมาได้โดยเรียงลำดับการค้นหาตามตำแหน่งต่อไปนี้

1. ภายในคลาสของอ็อบเจกต์ที่เป็นผู้รับ message
2. ภายในโมดูลที่ถูกผสมอยู่ในคลาสเดียวกับข้อ 1. หากมีโมดูลผสมอยู่มากกว่า 1 โมดูล จะทำเริ่มค้นหาจากโมดูลที่ระบุเป็นตัวสุดท้ายไล่ขึ้นมา
3. ภายในคลาสแม่ที่คลาสของผู้รับสืบทอดต่อๆมา
4. ภายในโมดูลที่ถูกผสมอยู่ในคลาสแม่จากข้อ 3.
5. ภายในคลาส Object (ถ้าคลาสแม่ที่สืบทอดต่อกันมาถูกตรวจสอบหมดแล้ว)
6. ภายใน Kernal
7. ภายใน BasicObject

เราสามารถตั้งชื่อเมธอดชื่อเดียวกันโดยกำหนดไว้ในคลาสคนละคลาสหรือคนละโมดูล ทีนี้หากคลาสหรือโมดูลเหล่านั้นมีความสัมพันธ์ต่อกันไม่ว่าจะเป็นจากการสืบทอดคลาสหรือจากการผสมโมดูลก็ตาม สิ่งที่เราต้องทำความเข้าใจก็คือถ้าอ็อบเจกต์ได้รับ message เป็นชื่อของเมธอดที่มีชื่อซ้ำกันอยู่ในคลาสหรือโมดูล เมธอดตัวไหนกันแน่ที่จะถูกเรียกใช้ ? แล้ว Ruby มีกระบวนการจัดลำดับการเรียกใช่อย่างไร ?
คำตอบก็คือ ให้ดูตามลำดับการค้นหาเมธอดตามที่ได้พูดถึงไว้ข้างต้น โดยเมธอดที่ถูกพบก่อนก็จะเป็นเมธอดที่ถูกเรียกใช้
ลองดูตัวอย่างจากโค้ดต่อไปนี้

module M
  def test
    puts “method test in M”
  end
end

class A
  include M
  def test
    puts “method test in A”
  end
end

a = A.new
a.test    # => "method test in A"

จากตัวอย่างข้างต้น มีการกำหนดเมธอด test ไว้ในคลาส A และยังได้ผสมโมดูล M เข้าไปในคลาส A ด้วย โดยภายในโมดูล M ก็มีเมธอดชื่อ test กำหนดเอาไว้เช่นเดียวกัน
เมื่อมีการส่ง message ชื่อ test ให้กับ a กระบวนการค้นหาเมธอดก็ต้องดำเนินไปตามที่เราได้สรุปกันเอาไว้ก่อนหน้าคือ เริ่มหาเมธอดจากภายในคลาสของผู้รับก่อน ซึ่งจากตัวอย่างนี้เราเจอเมธอดชื่อ test เลย ถือว่าการค้นหาเสร็จสิ้น เมธอดที่ถูกเรียกใช้จึงเป็น เมธอด test ที่อยู่ในคลาส A
อย่างไรก็ตามหากเราต้องการเจาะจงให้เมธอด test ที่อยู่ในโมดูล M เป็นเมธอดที่ถูกเรียกมาใช้งานก็สามารถทำได้โดยใช้คีย์เวิร์ด super
เราลองแก้ไขเมธอด test ในคลาส A โดยใส่ super ลงไป แล้วทดลองรันโค้ดดูอีกครั้ง

class A
  include M
  
  def test
    puts “method test in A”
    super
  end
end

a = A.new
a.test    
# => “method test in A”
# => “method test in M”

จากโค้ดข้างต้นนั้นเมธอด test ภายในคลาส A ยังคงถูกเรียกใช้ก่อนเพราะเป็นเมธอดที่ถูกค้นพบก่อน แต่เมื่อทำการรันโค้ดภายในเมธอด test ดังกล่าวแล้ว ปรากฏว่าเราเจอคีย์เวิร์ด super ซึ่งคีย์เวิร์ดนี้เปรียบเสมือนการส่งคำสั่งให้ Ruby ทำการค้นหาเมธอดที่มีชื่อเดียวกับเมธอดที่เจ้าคีย์เวิร์ด super นี้อาศัยอยู่ โดยเริ่มจากโมดูลที่มีการผสมเข้ามาในคลาสและจากคลาสแม่หากสืบทอดคลาส จากตัวอย่างดังกล่าว Ruby จึงต้องทำการค้นหาเมธอดชื่อ test ภายในโมดูล M ซึ่งเมื่อเจอ เมธอด test ในโมดูลจึงถูกเรียกใช้ด้วย ดังนั้นผลลัพธ์ที่ได้จึงเป็นข้อความของเมธอด test ที่เกิดจากคลาส A แล้วตามด้วยข้อความที่เกิดจาก super ใช้ให้ Ruby ไปหาเมธอด test จนเจอในโมดูล M นั่นเอง

ในกรณีที่เป็นการสืบทอดคลาสแล้วมีเมธอดที่มีชื่อเหมือนกันอยู่ในคลาสแม่และคลาสลูก เมธอดที่จะถูกเรียกใช้จะยังคงเป็นไปตามกฏของลำดับขั้นของการค้นหาเมธอด และเราก็สามารถใช้ super เพื่อระบุให้ Ruby เรียกหาเมธอดที่มีชื่อเดียวกันจากคลาสแม่ได้เช่นเดียวกัน

class A
  def test
    puts "test in class A"
  end
end

class B < A
  def test
    super
    puts "test in class B"
  end
end

b = B.new
b.test  
# => test in class A
# => test in class B

1 ความคิดเห็น: