คุณคิดว่าอะไรจะเกิดขึ้นเมื่อคุณเรียกเมธอด? Ruby ตัดสินใจอย่างไรว่าจะเรียกเมธอดไหนเมื่อมีอีกเมธอดหนึ่งชื่อเดียวกัน? คุณเคยสงสัยไหมว่าเมธอดนั้นอยู่ที่ไหนหรือมาจากที่ไหน?
Ruby ใช้ "วิธี" หรือ "รูปแบบ" ที่กำหนดไว้เพื่อกำหนดเมธอดที่ถูกต้องที่จะเรียก และเวลาที่ถูกต้องในการคืนค่า "no method error" เราสามารถเรียก "วิธี" นี้ว่า Ruby Method Lookup Path (เส้นทางการค้นหาเมธอดของ Ruby) ในบทเรียนนี้ เราจะเจาะลึกเข้าไปในการค้นหาเมธอดของ Ruby ในที่สุด คุณจะเข้าใจได้ดีว่า Ruby ผ่านลำดับชั้นของออบเจ็กต์อย่างไรเพื่อกำหนดว่าคุณอ้างถึงเมธอดไหน
เพื่อให้เข้าใจอย่างเต็มที่สิ่งที่เราจะเรียน คุณจะต้องมีความเข้าใจพื้นฐานเกี่ยวกับ Ruby แม้เราจะกล่าวถึงสิ่งอย่างเช่นโมดูลและคลาส แต่จะไม่ได้เจาะลึกลึกเข้าไปในสิ่งที่พวกมันทำ เราจะครอบคลุมเพียงความลึกที่จำเป็นเพื่อให้บรรลุเป้าหมายของบทเรียนนี้: แสดงให้คุณเห็นว่า Ruby กำหนดข้อความ (เมธอด) ที่คุณส่งต่อให้ออบเจ็กต์อย่างไร
ภาพรวม
เมื่อคุณเรียกเมธอด เช่น first_person.valid? Ruby ต้องกำหนดสองสิ่ง:
- เมธอด
.valid?ถูกนิยามที่ไหน - มีหลายที่ที่เมธอด
.valid?ถูกนิยามหรือไม่? หากมี ที่ไหนคือตัวที่ถูกต้องสำหรับบริบทนี้
กระบวนการ (หรือเส้นทาง) ที่ Ruby ปฏิบัติตามในการค้นหาคำตอบคือสิ่งที่เราเรียกว่า method lookup (การค้นหาเมธอด) Ruby ต้องค้นหาที่เมธอดถูกสร้างขึ้น เพื่อที่จะสามารถเรียกมันได้ มันต้องค้นหาในสถานที่ต่อไปนี้เพื่อให้แน่ใจว่ามันเรียกเมธอดที่ถูกต้อง:
- เมธอดซิงเกิลตัน (Singleton methods): Ruby ให้วิธีสำหรับออบเจ็กต์ในการนิยามเมธอดของตัวเอง เมธอดเหล่านี้มีให้เฉพาะออบเจ็กต์นั้นเท่านั้น และไม่สามารถเข้าถึงได้โดยอินสแตนซ์ของออบเจ็กต์
- เมธอดในโมดูลที่ผสมผสาน (Mixed-in modules): โมดูลสามารถผสมผสานเข้าไปในคลาสโดยใช้
prepend,include, หรือextendเมื่อสิ่งนี้เกิดขึ้น คลาสจะเข้าถึงเมธอดที่นิยามในโมดูลได้ และ Ruby จะเข้าไปค้นหาในโมดูลสำหรับเมธอดที่ถูกเรียก ยังสำคัญอีกอย่างที่โมดูลอื่นๆ สามารถผสมผสานเข้าไปในโมดูลเริ่มต้นได้ และการค้นหาก็จะก้าวต่อไปในโมดูลเหล่านั้นด้วย - เมธอดอินสแตนซ์ (Instance methods): เมธอดเหล่านี้ถูกนิยามในคลาสและเข้าถึงได้โดยอินสแตนซ์ของคลาสนั้น
- เมธอดหรือโมดูลของคลาสพาเรนต์ (Parent class methods or modules): หากคลาสเป็นลูกของอีกคลาสหนึ่ง Ruby จะค้นหาในคลาสพาเรนต์ การค้นหาจะเข้าไปในเมธอดซิงเกิลตันของคลาสพาเรนต์ โมดูลที่ผสมผสาน และคลาสพาเรนต์ของมัน
- Object, Kernel, และ BasicObject: สถานที่สุดท้ายที่ Ruby ค้นหา เนื่องจากทุกออบเจ็กต์ใน Ruby มีพวกมันเป็นส่วนหนึ่งของอานเซสเตอร์ (ancestors)
คลาสและโมดูล (Classes and Modules)
เมธอดมักถูกเรียกบนออบเจ็กต์ ออบเจ็กต์เหล่านี้ถูกสร้างโดยคลาสบางคลาส ซึ่งอาจเป็นคลาสในตัวของ Ruby หรือคลาสที่สร้างโดยนักพัฒนา
class Human attr_reader :name def initialize(name) @name = name end def hello put "Hello! #{name}" end end เราสามารถเรียกเมธอด hello ที่เราสร้างไว้ข้างบนบนอินสแตนซ์ของคลาส Human ได้ เช่น
john = Human.new("John") john.hello # Output -> Hello John เมธอด hello เป็นเมธอดอินสแตนซ์ นี่คือเหตุผลที่เราจึงเรียกมันบนอินสแตนซ์ของคลาส Human ได้ อาจมีกรณีที่เราไม่ต้องการให้เมธอดถูกเรียกบนอินสแตนซ์ ในกรณีเหล่านี้ เราต้องการเรียกเมธอดบนคลาสเอง เพื่อให้บรรลุเป้าหมายนี้ เราจะต้องสร้างเมธอดคลาส (class method) การนิยามเมธอดคลาสสำหรับคลาสข้างบนจะมีลักษณะดังนี้:
def self.me puts "I am a class method" end เราสามารถเรียกมันได้โดยทำ Human.me เมื่อความซับซ้อนของแอปพลิเคชันของเราเพิ่มขึ้น (จินตนาการว่าเรากำลังสร้างสตาร์ทอัพใหม่) อาจจะมาถึงเวลาที่สองหรือมากกว่าคลาสของเรามีหลายเมธอดที่ทำสิ่งเดียวกัน หากสิ่งนี้เกิดขึ้น หมายความว่าเราต้องเก็บสิ่งต่างๆ ให้แห้ง (DRY) และแน่ใจว่าเราไม่ทำซ้ำตัวเอง ปัญหานี้เกี่ยวกับวิธีที่เราแบ่งปannsฟังก์ชันการทำงานข้ามคลาสเหล่านี้
หากคุณยังไม่เคยใช้โมดูลมาก่อน คุณอาจถูกดึงดูดให้สร้างคลาสใหม่อย่างเคร่งครัดสำหรับเมธอด "ร่วม" เหล่านี้ อย่างไรก็ตาม การทำเช่นนั้นอาจนำไปสู่ผลลัพธ์ที่เป็นลบ โดยเฉพาะเมื่อคุณต้องใช้การสืบทอดหลายแบบ (multiple inheritance) ซึ่ง Ruby ไร้สนับสนุน โมดูลคือวิธีที่ดีที่สุดในการจัดการกรณีนี้ โมดูลคล้ายกับคลาส แต่มีความแตกต่างนิดหน่อย ขอเริ่มจากตัวอย่างของโมดูล:
module Movement def walk puts "I can walk!" end end - การนิยามเริ่มต้นด้วยคีย์เวิร์ด
moduleแทนclass - โมดูลไม่สามารถมีอินสแตนซ์ได้ ดังนั้นคุณไม่สามารถใช้
Movement.newได้
เมธอด (Methods)
เมธอดสามารถมองได้เป็นการกระทำที่จะถูกดำเนินการโดยออบเจ็กต์เฉพาะ หากฉันมีอาร์เรย์อย่าง [2, 3, 4] กำหนดให้ตัวแปรชื่อ numberList เมธอด .push เป็นการกระทำที่อาร์เรย์สามารถดำเนินการได้เพื่อ ใส่ ค่าที่มันรับเข้าไปในอาร์เรย์ ตัวอย่างโค้ดนี้เป็นตัวอย่าง:
john.walk อาจเป็นเรื่องปกติของคุณที่จะพูดว่ "ฉันกำลังเรียกเมธอดของออบเจ็กต์" ซึ่ง john อ้างอิงถึงออบเจ็กต์ที่เป็นอินสแตนซ์ของ Human และ walk เป็นเมธอด อย่างไรก็ตาม นี่ไม่ได้ถูกต้องสมบูรณ์เพราะเมธอดที่อนุมานมักมาจากคลาสของออบเจ็กต์ ซูเปอร์คลาส หรือโมดูลที่ ผสมผสาน (mixed-in) เข้าไป
สำคัญที่จะต้องเพิ่มว่า เป็นไปได้ที่จะนิยามเมธอดบนออบเจ็กต์ แม้แต่ออบเจ็กต์อย่าง john เพราะทุกอย่างใน Ruby เป็นออบเจ็กต์ แม้แต่คลาสที่ใช้สร้างออบเจ็กต์
def john.drip puts "My drip is eternal" end เมธอด drip สามารถเข้าถึงได้เฉพาะโดยออบเจ็กต์ที่กำหนดให้ john drip เป็นเมธอดซิงเกิลตันที่จะพร้อมใช้งานสำหรับออบเจ็กต์ john สำคัญที่จะรู้ว่าไม่มีความแตกต่างระหว่างเมธอดซิงเกิลตันและเมธอดคลาส ตามที่คุณเห็นจากคำตอบ Stack Overflow นี้ เว้นแต่คุณจะอ้างอิงถึงเมธอดที่นิยามบนออบเจ็กต์อย่างในตัวอย่างข้างบน จะไม่ถูกต้องที่จะพูดว่าเมธอดนั้นเป็นของออบเจ็กต์เฉพาะ ในตัวอย่างของเรา เมธอด walk เป็นของโมดูล Movement ในขณะที่เมธอด hello เป็นของคลาส Human ด้วยความเข้าใจนี้ จะง่ายขึ้นที่จะก้าวต่อไป ซึ่งคือ เพื่อกำหนดเมธอดที่แน่นอนที่ถูกเรียกบนออบเจ็กต์ Ruby ต้องตรวจสอบคลาสของออบเจ็กต์ ซูเปอร์คลาส หรือโมดูลที่ถูก ผสมผสาน (mixed) เข้าไปในลำดับชั้นของออบเจ็กต์
การผสมผสานโมดูล (Mixing Modules)
Ruby รองรับการสืบทอดเดียว (single inheritance) เท่านั้น คลาสสามารถสืบทอดจากคลาสเดียวเท่านั้น ทำให้คลาสลูกสามารถสืบทอดพฤติกรรม (เมธอด) ของอีกคลาสหนึ่งได้ สิ่งที่เกิดขึ้นเมื่อคุณมีพฤติกรรมที่ต้องแบ่งปannsข้ามคลาสที่ต่างกัน? ตัวอย่างเช่น เพื่อให้เมธอด walk พร้อมใช้งานสำหรับอินสแตนซ์ของคลาส Human เราสามารถ ผสมผสาน (mix in) โมดูล Movement เข้าไปในคลาส Human ดังนั้น การเขียนใหม่คลาส Human โดยใช้ include จะมีลักษณะดังนี้:
require "movement" # สมมติว่าเรามีโมดูลในไฟล์ชื่อ movement.rb class Human include Movement attr_reader :name def initialize(name) @name = name end def hello put "Hello! #{name}" end end ตอนนี้ เราสามารถเรียกเมธอด walk บนอินสแตนซ์ได้:
john = Human.new("John") john.walk Include
เมื่อคุณใช้คีย์เวิร์ด include เหมือนเราทำข้างบน เมธอดของโมดูลที่รวมเข้าไปจะถูกเพิ่มเข้าไปในคลาสเป็นเมธอดอินสแตนซ์ (instance method) เนื่องจากโมดูลที่ รวมเข้าไป (included) ถูกเพิ่มเข้าไปใน อานเซสเตอร์ (ancestors) ของคลาส Human ทำให้โมดูล Movement มองได้เป็นพาเรนต์ของคลาส Human ตามที่คุณเห็นในตัวอย่างที่เราแสดงข้างบน เราได้เรียกเมธอด walk บนอินสแตนซ์ของคลาส Human
Extend
นอกจาก include Ruby ยังให้เราใช้คีย์เวิร์ด extend นี่ทำให้เมธอดของโมดูลพร้อมใช้งานสำหรับคลาสเป็นเมธอดคลาส (class methods) ซึ่งรู้จักกันในชื่อเมธอดซิงเกิลตัน (singleton methods) ตามที่เราเรียนรู้ก่อนหน้านี้ ดังนั้น หากเรามีโมดูลชื่อ Feeding ที่มีลักษณะดังนี้
module Feeding def food "I make my food :)" end end เราสามารถแบ่งปannsพฤติกรรมนี้ในคลาส Human ของเราโดยการ require มันและเพิ่ม extend Feeding อย่างไรก็ตาม เพื่อใช้งาน มันแทนที่จะเรียกเมธอด food บนอินสแตนซ์ของคลาส เราจะเรียกมันบนคลาสเอง วิธีเดียวกับการเรียกเมธอดคลาส
Human.food Prepend
สิ่งนี้คล้ายกับ include แต่มีความแตกต่างบ้าง ตามที่ระบุในโพสต์นี้;
มันทำงานจริงๆ แล้วเหมือน include ยกเว้นว่าแทนที่จะแทรกโมดูลไว้ระหว่างคลาสและซูเปอร์คลาสในเชน มันจะแทรกมันที่ด้านล่างของเชน ก่อนคลาสเอง
สิ่งนี้หมายความว่าเมื่อเรียกเมธอดบนอินสแตนซ์ของคลาส Ruby จะดูในเมธอดของโมดูลก่อนที่จะดูในคลาส
หากเรามีโมดูลที่นิยามเมธอด hello ซึ่งเราจึงผสมผสานเข้าไปในคลาส Human โดยใช้ prepend Ruby จะเรียกเมธอดที่เรามีในโมดูลแทนที่จะเป็นเมธอดที่เรามีในคลาส
เพื่อเข้าใจอย่างถ่องแท้ว่า prepend ของ Ruby ทำงานอย่างไร ขอแนะนำให้ดูบทความนี้
เส้นทางการค้นหาเมธอด (Method Lookup Path)
สถานที่แรกที่อินเตอร์พรีเตอร์ Ruby มองหาเมื่อพยายามเรียกเมธอดคือเมธอดซิงเกิลตัน ฉันสร้าง repl นี้ไว้ ซึ่งคุณสามารถเล่นกับมันเพื่อดูผลลัพธ์ที่เป็นไปได้
สมมติว่าเรามีชุดโมดูลและคลาสที่มีลักษณะดังนี้:
module One def another puts "From one module" end end module Two def another puts "From two module" end end module Three def another puts "From three module" end end class Creature def another puts "From creature class" end end มาเอาไปผสมผสานเข้าไปในคลาส Human กัน
class Human < Creature prepend Three extend Two include One def another puts "Instance method" end def self.another puts "From Human class singleton" end end นอกจากการผสมผสานโมดูล เรายังมีเมธอดอินสแตนซ์และเมธอดคลาส คุณยังเห็นได้ว่าคลาส Human เป็นซับคลาสของคลาส Creature
การค้นหาครั้งแรก - เมธอดซิงเกิลตัน (Singleton Methods)
เมื่อเรารัน Human.another สิ่งที่ถูกพิมพ์คือ From Human class singleton ซึ่งเป็นสิ่งที่เรามีในเมธอดคลาส หากเราคอมเมนต์เมธอดคลาสทิ้งและรันอีกครั้ง มันจะพิมพ์ From two module ออกมาที่คอนโซล นี่มาจากโมดูลที่เราผสมผสานโดยใช้ extend มันแสดงให้เห็นว่าการค้นหาเริ่มต้นในเมธอดซิงเกิลตัน หากเราลบ (หรือคอมเมนต์) extend Two ทิ้งและรันคำสั่งอีกครั้ง จะโยน method missing error เราได้ข้อผิดพลาดนี้เพราะ Ruby ไม่พบเมธอด another ในเมธอดซิงเกิลตัน
เราจะไปสร้างอินสแตนซ์ของคลาส:
n = Human.new เราจะสร้างเมธอดซิงเกิลตันสำหรับอินสแตนซ์ด้วย:
def n.another puts "From n object" end ตอนนี้ เมื่อเรารัน n.another เวอร์ชันที่ถูกเรียกคือเมธอดซิงเกิลตันที่นิยามบนออบเจ็กต์ n เหตุผลที่ Ruby ไม่ไปเรียกโมดูลที่ผสมผสานโดยใช้ extend ในกรณีนี้ คือเพราะเรากำลังเรียกเมธอดบนอินสแตนซ์ของคลาส สำคัญที่จะรู้ว่าเมธอดซิงเกิลตันมีความเกี่ยวข้องสูงกว่าเมธอดที่เกี่ยวข้องกับโมดูลที่ผสมผสานโดยใช้ extend
การค้นหาครั้งที่สอง - โมดูลที่ผสมผสานโดยใช้ prepend
หากเราคอมเมนต์เมธอดซิงเกิลตันบนออบเจ็กต์ n ทิ้งและรันคำสั่ง เวอร์ชันของเมธอดที่ถูกเรียกคือโมดูลที่เราผสมผสานโดยใช้ prepend เนื่องจากการใช้ prepend แทรกโมดูลก่อนคลาสเอง
การค้นหาครั้งที่สาม - คลาส (The Class)
หากเราคอมเมนต์โมดูล Three ทิ้ง เวอร์ชันของเมธอด another ที่ถูกเรียกคือเมธอดอินสแตนซ์ที่นิยามบนคลาส
การค้นหาครั้งที่สี่ - โมดูลที่ผสมผสานโดยใช้ include
สถานที่ถัดไปที่ Ruby ค้นหาเมธอดคือในโมดูลที่ผสมผสานโดยใช้ include ดังนั้น เมื่อเราคอมเมนต์เมธอดอินสแตนซ์ทิ้ง เวอร์ชันที่เราได้คือโมดูล One
การค้นหาครั้งที่ห้า - คลาสพาเรนต์ (Parent Class)
หากคลาสมีคลาสพาเรนต์ Ruby จะค้นหาในคลาส การค้นหานี้รวมถึงการเข้าไปในโมดูลที่ผสมผสานในคลาสพาเรนต์ หากเรามีเมธอดนิยามในโมดูลที่ผสมผสานในคลาส Creature เมธอดจะถูกเรียก
จุดสิ้นสุดของการค้นหาเมธอด (The End Of The Method Search)
เราสามารถรู้ได้ว่าการค้นหาเมธอดสิ้นสุดที่ไหนโดยการตรวจสอบอานเซสเตอร์ของมัน: เรียก .ancestors บนคลาส การทำเช่นนี้สำหรับคลาส Human จะคืนค่า [Three, Human, One, Creature, Object, Kernel, BasicObject] การค้นหาเมธอดสิ้นสุดที่คลาส BasicObject ซึ่งเป็นคลาสราก (root class) ของ Ruby ทุกออบเจ็กต์ที่เป็นอินสแตนซ์ของบางคลาสมีต้นกำเนิดมาจากคลาส BasicObject
หลังจากการค้นหาเมธอดผ่านคลาสพาเรนต์ที่นักพัฒนานิยาม มันจะไปถึง:
- คลาส
Object - โมดูล
Kernel - คลาส
BasicObject
เมธอด method_missing
หากคุณใช้ Ruby มาสักพัก คุณน่าจะเคยพบ NoMethodError ซึ่งเกิดขึ้นเมื่อคุณพยายามเรียกเมธอดที่ไม่รู้จักบนออบเจ็กต์ สิ่งนี้เกิดขึ้นหลังจากที่ Ruby ผ่านอานเซสเตอร์ของออบเจ็กต์แล้วและไม่พบเมธอดที่ถูกเรียก ข้อความข้อผิดพลาดที่คุณได้รับถูกจัดการโดยเมธอด method_missing ซึ่งนิยามอยู่ในคลาส BasicObject เป็นไปได้ที่จะโอเวอร์ไรด์เมธอดสำหรับออบเจ็กต์ที่คุณกำลังเรียกเมธอด ซึ่งคุณสามารถเรียนรู้ได้โดยตรวจสอบลิงก์นี้
สรุป
ตอนนี้คุณรู้เส้นทางที่ Ruby ใช้ในการค้นหาเมธอดที่ถูกเรียกบนออบเจ็กต์ ด้วยความเข้าใจนี้ คุณควรจะสามารถแก้ไขข้อผิดพลาดที่เกิดขึ้นจากการเรียกเมธอดที่ไม่รู้จักบนออบเจ็กต์ได้อย่างง่ายดาย