โมดูล (และคลาส) ถูกออกแบบมาเพื่อ nesting โค้ดแบบ ActiveRecord::RecordNotFound พบบ่อยจนเราไม่คิดสองครั้ง แต่ฝังอยู่ในการ implement nesting ของ Ruby — และระบบ autoload ของ Rails — มีกับดักเล็กน้อยที่ทำให้โค้ดล้มเหลวแบบแปลกประหลาด บทความนี้จะอภิบายที่มาของกับดักเหล่านี้และวิธีหลีกเลี่ยง
ค่าคงที่ (Constant) คืออะไร?
บทความนี้เกี่ยวกับโมดูล แต่ต้องเข้าใจค่าคงที่ก่อน ในภาษาอื่น ค่าคงที่ใช้เก็บข้อมูลเล็กน้อย เช่น:
# ค่าคงที่ง่ายๆ
MAX_RETRIES = 5
DEFAULT_LANGUAGE = "en"
แต่ใน Ruby คลาสและโมดูลก็เป็นค่าคงที่เช่นกัน ตัวอย่าง: มีค่าคงที่ 3 ตัวในโมดูลด้านล่าง: ตัวเลข, คลาส, และโมดูลซ้อน เมื่อเข้าถึงคลาสหรือโมดูลซ้อน Ruby หาโดยใช้กฎเดียวกับค่าคงที่ตัวเลขง่ายๆ
module MyModule
MY_FAVORITE_NUMBER = 7
# คลาสเป็นค่าคงที่
class MyClass
end
# โมดูลก็เป็นค่าคงที่
module MyEmbeddedModule
end
end
puts MyModule.constants.inspect
# => [:MY_FAVORITE_NUMBER, :MyClass, :MyEmbeddedModule]
บ่อยครั้ง โมดูลเข้าถึงค่าคงที่ที่กำหนดใน parent ได้ นั่นคือเหตุที่เขียนโค้ดแบบนี้ได้:
module X
MARCO = "polo"
module Y
def self.n
puts MARCO
end
end
end
X::Y.n() # => "polo"
แต่คุณจะผิดหากคิดว่าความสัมพันธ์ parent/child คือสาเหตุที่ Y เข้าถึง MARCO ของ X ได้
ปัญหาที่พบบ่อย
เขียนโค้ดข้างต้นแบบเล็กน้อยต่างกัน เรื่องน่าประหลาดเกิดขึ้น: Y เข้าถึง X::MARCO ไม่ได้ เกิดอะไรขึ้น?
module A
MARCO = "polo"
end
module A::B
def self.n
puts MARCO # => uninitialized constant A::B::MARCO (NameError)
end
end
A::B.n()
มันคือการ "สืบทอด" ค่าคงที่ของ parent โดย child ไม่ได้มาจากความสัมพันธ์ parent/child มันเป็น lexical (lexical scoping) หมายถึงขึ้นกับโครงสร้างโค้ด ไใช่โครงสร้างออบเจกต์ที่โค้ดสร้าง
ตรวจสอบ nesting
หากต้องการเข้าใจลึกขึ้นว่า Ruby ค้นหาค่าคงที่ซ้อนอย่างไร ลองดูฟังก์ชัน Module.nesting
ฟังก์ชันนี้คืนอาร์เรย์ของออบเจกต์ที่เป็น "เส้นทางค้นหา" สำหรับค่าคงที่ในขอบเขตที่กำหนด ลองตรวจ nesting ตัวอย่างก่อนหน้า
ตัวอย่างแรก: nesting คือ [A::B, A] หมายถึงถ้าใช้ค่าคงที่ MARCO Ruby จะหาใน A::B ก่อน แล้วค่อย A
module A
MARCO = "polo"
module B
def self.n
puts Module.nesting.inspect # => [A::B, A]
puts MARCO # => "polo"
end
end
end
ตัวอย่างที่สอง: nesting มีแค่ A::B ไม่มี A แม้ B จะเป็น "ลูก" ของ A แต่การเขียนโค้ดไม่แสดงการซ้อนกัน จึงไม่นับว่า nested สำหรับวัตถุประสงค์นี้
module A
MARCO = "polo"
end
module A::B
def self.n
puts Module.nesting.inspect # => [A::B]
puts MARCO # => uninitialized constant A::B::MARCO (NameError)
end
end
ปัญหา Rails autoload
เคยสังเกตไหมว่าใช้ Rails ไม่ต้อง include ไฟล์? อยากใช้ model แค่เรียกมันก็พอ
เป็นไปได้เพราะ Rails มีระบบ autoload ใช้ Module.const_missing ตรวจจับพยายามอ้างอิงค่าคงที่ที่ยังโหลดไม่ได้ แล้วโหลดไฟล์ที่ควรมีค่าคงนั้น ทำงานได้ส่วนใหญ่ แต่มีข้อต้องระวัง
Rails สมมติว่าโมดูลมี nesting ใหญ่ที่สุด สมมติโมดูล A::B::C จะมี nesting [A::B::C, A::B, A] หากไม่ใช่ จะได้พฤติกรรมไม่คาดคิด
โค้ดด้านล่าง โมดูล B ไม่ควรเข้าถึง A::MARCO ใน Ruby ธรรมดาไม่ได้เพราะ nesting เป็น [A::B] ควรได้ exception แต่ Rails autoload ไม่โยน exception กลับคืน A::MARCO แทน
# a.rb
module A
MARCO = "polo"
end
# a/b.rb
module A::B
def self.n
puts MARCO # => "polo"
end
end
# some_controller.rb
A::B.n()
สรุป?
เรื่องนี้เยอะมาก ผมชอบหลีกเลี่ยงการคิด ถ้าเป็นไปได้ จึงพยายามหลีกเลี่ยงไวยากรณ์โมดูล A::B ไม่คิดว่าจะมีกรณีที่ต้องการจัดการ module nesting ในเจตนา หากคุณรู้กรณีไหน บอกมานะ!