Code Loaders ใน Ruby - เข้าใจ Zeitwerk
ด้วย Zeitwerk คุณสามารถ Streamline การเขียนโปรแกรมได้ โดยรู้ว่า Classes และ Modules พร้อมใช้งานทุกที่
Code Loaders คืออะไร?
Code Loaders ให้ Developers กำหนด classes และ modules แยกไฟล์และโฟลเดอร์ และใช้ได้ทั่ว Codebase โดยไม่ต้อง require โดยตรง Rails เป็นตัวอย่าง Software ที่ใช้ Code Loaders การเขียน Rails ไม่ต้อง require Models ก่อนใช้ใน Controllers จริงๆ ใน Rails 6 ทุกอย่างใน Directory app ถูก Auto-load ตอน Boot App ยกเว้นบาง Exceptions
คิดง่ายว่า Code Loading คือแค่เรียก require แต่มันไม่ได้แค่นั้น Code Loading แยกได้ 3 ส่วน:
- Auto Loading: โค้ดโหลด On-the-fly ตอนต้องการ เช่น Rails รัน
rails sไม่โหลด Models/Controllers ทั้งหมด แตตอน Hit ModelUserครั้งแรก Auto Loading Mechanism หาและใช้ Model นี้ได้ นี่คือ Auto Loading ใน Action มีข้อดีสำหรับ Dev Environment: App และrails consoleStartup เร็วขึ้นRails.config.autoload_pathควบคุม Paths ที่ Auto-load - Eager Loading: โค้ดโหลดเข้า Memory ตอน App Startup ไม่รอ Constants ถูกเรียกก่อน Require ใน Rails Code Eager Load ใน Production Auto-loading ใน Production จะทำ Response Times ช้า เพราะแต่ละ Constant ต้อง Require On-the-fly
Rails.config.eager_load_pathsควบคุม Paths ที่ Eager Load - Reloading: Code Loader Watch ไฟล์ใน
autoload_pathตลอดเวลา Reload Files เมื่อสังเกต Changes ใน Rails มีประโยชน์ใน Development รันrails sแล้วแก้ไขพร้อมกัน ไม่ต้อง Restart Server นี่คือ Reloading ใน Action
เห็นได้ว่าส่วนใหญ่ Concepts พัฒนาและอยู่ใน Rails Zeitwerk เปลี่ยนสิ่งนี้! Zeitwerk ให้เรานำ Code Loading Action ไปใช้ใน Ruby Project ใดๆ ได้
Zeitwerk คืออะไร?
Zeitwerk เป็น Efficient และ Thread-safe Code Loader สำหรับ Ruby ใช้ได้ใน Ruby Project ใดๆ รวม Web Frameworks (Rails, Hanami, Sinatra), CLI Tools, และ Gems ด้วยมัน คุณ Streamline Programming ได้ โดยรู้ว่า Classes และ Modules พร้อมใช้งานทุกที่ แบบดั้งเดิม Rails และ บาง Gems มี Built-in Code Loaders สำหรับ Functionality นี้ แต่ Zeitwerk Extract Concepts ออกมาเป็น Gem ให้ Rubyists นำไป Apply ใน Projects ของตนเอง
ติดตั้ง Zeitwerk
First things first ติดตั้ง Gem:
gem install zeitwerk
# OR ใน Gemfile
gem 'zeitwerk', '~> 2.4.0'
Configuring Zeitwerk
เริ่มจาก Basics:
require 'zeitwerk'
loader = Zeitwerk::Loader.new
...
loader.setup
Code ข้างบน Instantiate Loader Instance และเรียก setup หลัง Call setup Loaders พร้อม Load Code แตก่อนหน้านั้น Configurations ทั้งหมดบน loader ควรครบแล้ว ในบทความนี้ครอบคลุม Configurations บน loader และ Conventions สำหรับ Structure Code
- File Structure: Zeitwerk ต้องการ File/Directory Names ตรงกับ Modules/Classes ที่ Define ตัวอย่าง:
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
- Root Namespaces: Root Namespaces คือ Directories ที่
Zeitwerkหา Code ได้ เมื่อmodulesและclassesถูก Reference Zeitwerk รู้ว่า Search Root Namespaces ด้วย Matching File Name ตัวอย่าง:
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("app/models")
loader.push_dir("app/controllers")
# matches as follows
app/models/user.rb -> User
app/controllers/admin/users_controller.rb -> Admin::UsersController
มี 2 วิธีหลัก Define Root Namespace สำหรับ 2 Use Cases วิธี Default:
// init.rb
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/bar")
...
loader.setup
// bar/foo.rb
class Foo; end
หมายถึง Class Foo อ้างอิงได้โดยไม่ต้อง Bar::Foo เพราะ bar Directory เป็น Root Namespace วิธีที่สอง Explicit Namespace ใน push_dir:
// init.rb
require 'zeitwerk'
module Bar
end
loader = Zeitwerk::Loader.new
loader.push_dir("#{__dir__}/src", namespace: Bar)
loader.setup
// src/foo.rb
class Bar::Foo; end
สังเกตจุดสำคัญ:
- Module
BarDefined แล้วก่อนใช้ในpush_dirถ้า Module เป็น Third-party แค่ Require ก็ Define ได้ก่อนใช้ในpush_dir push_dirระบุ NamespaceBarชัดเจน- File
src/foo.rbDefineBar::Fooไม่ใช่Fooและไม่ต้องสร้าง Directorysrc/bar/foo.rb
- Independent Code Loader: ตาม Design Zeitwerk ให้แต่ละ Project/App Dependency จัดการ Project Tree ของตัวเอง Code Loading Mechanism ของแต่ละ Dependency จัดการโดย Dependency นั้นเอง เช่น Rails 6 Zeitwerk Handle Code Loading สำหรับ Rails App และให้แต่ละ Gem Dependency จัดการ Tree ของตัวเองแยกกัน Overlapping Files ระหว่าง Multiple Code Loaders เป็น Error Condition
- Autoloading: ด้วย Setup ข้างต้น หลัง Call
setupClasses และ Modules ทั้งหมด Available On-demand - Reloading: เพื่อ Enable Reloading
loaderต้อง Config ให้ชัดเจน ตัวอย่าง:
loader = Zeitwerk::Loader.new
...
loader.enable_reloading # ต้อง Opt-in ก่อน setup
loader.setup
...
loader.reload
loader.reload Reload Project Tree On-the-fly Changes ใหม่มองเห็นทันที แต่ยังต้องการ Surrounding Mechanism Detect File System Changes และ Call loader.reload Simple Version:
require 'filewatcher'
loader = Zeitwerk::Loader.new
...
loader.enable_reloading
loader.setup
...
my_filewatcher = Filewatcher.new('lib/')
Thread.new(my_filewatcher) {|fw| fw.watch {|filename| loader.reload } }
ใช้ Zeitwerk ใน Rails
Zeitwerk Enabled By Default ใน Rails 6.0 แต่ Opt-out ได้ ใช้ Rails classic Code Loader
# config/application.rb
config.load_defaults "6.0"
config.autoloader = :classic
ใช้ Zeitwerk ใน Gems
Zeitwerk ให้ Convenience Method สำหรับ Gems ถ้าใช้ Standard Gem Structure (lib/special_gem) ใช้ได้แบบนี้:
# lib/special_gem.rb
require 'zeitwerk'
module SpecialGem
end
loader = Zeitwerk::Loader.for_gem
loader.setup
ด้วย Standard Gem Structure for_gem Call เพิ่ม lib Directory เป็น Root Namespace ทำให้ Code ทุกตัวใน lib หาได้อัตโนมัติ
ดู Gems ที่ใช้ Zeitwerk เพิ่มแรงบันดาลใจ:
- Karafka
- Jets
References
Rails autoloading — how it works, and when it doesn't
Zeitwerk