Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

หลีกเลี่ยงหลุมที่พบบ่อยเมื่อ Nesting Ruby Modules

โมดูล (และคลาส) ถูกออกแบบมาเพื่อ 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 ในเจตนา หากคุณรู้กรณีไหน บอกมานะ!