12/08/2018, 15:45

Rails autoloading - cách làm việc

Constant lookup trong Ruby khá đơn giản, một khi bạn đã biết các quy tắc, nhưng không phải lúc nào cũng hoàn toàn trực quan. Khi bạn gọi một constant trong một phạm vi nào đó, constant đó sẽ được tìm kiếm trong: Mỗi mục trong Module.nesting Mỗi mục trong Module.nesting.first.ancestors Mỗi ...

Constant lookup trong Ruby khá đơn giản, một khi bạn đã biết các quy tắc, nhưng không phải lúc nào cũng hoàn toàn trực quan. Khi bạn gọi một constant trong một phạm vi nào đó, constant đó sẽ được tìm kiếm trong:

  1. Mỗi mục trong Module.nesting
  2. Mỗi mục trong Module.nesting.first.ancestors
  3. Mỗi mục trong Object.ancestors nếu Module.nesting.first là nil hoặc một module

Nói một cách khác, việc tìm kiếm đầu tiên bắt đầu từ điểm tham khảo, sau đó trở lên qua chuỗi kế thừa của lớp có chứa (nếu có) hoặc đối tượng khác.

C = "At the top level"

module A
    C = "In A"
end

module A
    module B
        puts Module.nesting # => [A::B, A]
        puts C                          # => "In A"
    end
end

module A::B
    puts Module.nesting # => [A::B]
    puts C                          # => "At the top level"
end

Trong ví dụ đầu tiên, vì A là một thành viên của Module.nesting, nó có thể được tìm kiếm cho hằng số C, cho nên A::C tồn tại, và được trả về. Trong ví dụ thứ 2, A không phải là một thành phần của liên kết, nên ::C được trả về.

Ruby có sẵn tính năng autoload, nó cho phép chương trình xác định địa chỉ file mà hằng số có thể được tìm thấy. Ruby sau đó sẽ load file đó khi hằng số đựợc gọi bởi chương trình. Rails, tuy nhiên, sẽ tự động nạp các hằng số tùy ý trong thời gian chạy, thậm chí cả những tệp không tồn tại khi ứng dụng được bắt đầu. Nó không thể đơn giản sử dụng tính năng autoload tích hợp sẵn của Ruby, vì cần biết cả tên và vị trí tập tin của mỗi hằng liên tục, và Rails không biết những thứ này khi khởi động. Thay vào đó, nó thực hiện hệ thống autoload riêng của nó, tăng cường tra cứu liên tục của Ruby bằng một tập hợp các quy tắc suy luận xác định các tệp nào được mong đợi định nghĩa một tên hằng nhất định. Những thứ có thể được nạp khi hằng số được gọi lần đầu. Nhưng chúng làm việc như thế nào?

Hầu hết những lập trình viên Ruby sẽ quen thuộc với #method_missing, phương thức được gọi khi một tin nhắn đựoc gửi đến nguời nhận không đáp hứng với thông điệp đó. Nó có một đối tác cho constant lookup, Module#const_missing, được gọi khi một tham chiếu đến một hằng không được giải quyết:

module Foo
    def self.const_missing(name)
        puts "In #{self} looking for #{name}..."
        super
    end
end
>  Foo::Bar
In Foo looking for Bar...
NameError: uninitialized constant Foo::Bar

Khi bạn gọi một hằng số, Ruby đầu tiên sẽ tìm kiếm nó theo quy tắc tìm kiếm đã được tích hợp, đã mô tả ở trên. Nếu không có hằng số nào trùng hợp có thể tìm thấy, Module#const_missing sẽ được gọi - trong trường hợp của ví dụ bên trên, cái được gọi là Foo.const_missing("Bar"). Đây là nơi Rails tiếp nhận, sử dụng file lookup convention và kiến thức của nó về các hằng số đã được tải, Rails ghi đè hàm #const_missing để load missing constants mà không cần phải yêu cầu lời gọi require rõ ràng từ lập trình viên.

Ngược lại với autoload của Ruby, đòi hỏi vị trí của autoloaded constant phải đựoc xác định trước, Rails theo một quy ước đơn giản là maps tên hằng số theo tên file. Nesting tương ứng với các thư mục, và các tên hằng viết thường:

MyModule::SomeClass # => my_module/some_class.rb

Đối với một constant nhất định, tên tập tin tương ứng này sau đó sẽ được tìm kiếm trong một số đường dẫn autoload, như được xác định bởi tùy chọn cấu hình autoload_paths. Mặc định, Rails sẽ tìm kiếm trong tất cả các thư mục con ngay bên trong thư mục /app, và cá thành phần khác có thể được thêm vào bằng cách sau:

# config/application.rb
module MyApp
    class Application < Rails::Application
        config.autoload_paths << Rails.root.join("lib")
    end
end

Nếu autload_paths được set là ["app/models", "lib"], một constant lookup cho một hằng số User sẽ được tìm trong:

  • app/models/user.rb
  • lib/user.rb Rails sẽ kiểm tra từng thành phần của những địa chỉ đã cho, và nếu có tồn tại, nó sẽ suy đoán tải tập tin, xem vị trí mong đợi của hằng số mới. Nếu nó xuất hiện sau khi file được tải, thuật toán thành công. Nếu không, một lỗi sẽ được raise lên tương tự như sau:
LoadError: Expected app/models/user.rb to define User

Tại thời điểm này, chúng ta chỉ có thể thấy cách một tên hằng số được maps với một tên file. Nhưng như chúng ta biết, một tham chiếu không đổi trong Ruby có thể giải quết cho nhiều hằng số được định nghĩa khác nhau, thay đổi tùy thuộc vào sự phụ thuộc mà cách tham chiếu đó đang được thực hiện. Làm thế nào mà Ruby có thể giải quết được điều này? Câu trả lời là "partially". Giống như Module#const_missing passes không có thông tin phụ thuộc đến nơi nhận, Rails không thể biết được nơi phụ thuộc mà tham chiếu đã được thực hiện, và nó phải làm một giả định. Với bất kì tham chiếu tới hằng số Foo::Bar::Baz, nó sẽ được giả định như sau:

module Foo
    module Bar
        Baz # Module.nesting => [Foo::Bar, Foo]
    end
end

Một giả thuyết khác, nó giả định khả năng nesting tối đa có thể cho một hằng số tham chiếu. Một ví dụ tham chiếu được xử lý chính xác như sau:

Foo::Bar::Baz # Module.nesting => []

module Foo::Bar
    Baz  # Module.nesting => [Foo:Bar]
end

Mặc dù đã có một sự mất mát đáng kể thông tin, nhưng Rails có thêm một số thông tin mà nó có thể sử dụng. Nó biết rằng Ruby đã thất bại trong việc giải quyết tham chiếu hằng số đặc biệt này bằng cách sử dụng tra cứu thông thường của nó, có nghĩa là bất cứ hằng số nào được gọi điều không thể thực sự đựợc nạp. Khi Foo::Bar::Baz được gọi, Rails sẽ cố gắng lần lượt tải các hằng số sau, cho đến khi nó tìm thấy một trong đó đã được nạp:

  • Foo::Bar::Baz
  • Foo::Baz
  • Baz Ngay khi một hằng số Baz đã nạp xong, Rails biết rằng nó không phải là Baz mà nó đang tìm kiếm, và thuật toán raise một NameError.

Phần này, theo tôi nghĩ, là phần khó nhất trong quy trình để hiểu và đó là một trong những hành vi dẫn tới một số hành vi phản trực giác cao, trong đó chúng ta sẽ sớm thấy một ví dụ.

Bây giờ chúng ta có thể xây dựng một phác họa về cách Rails autoloading hoạt động. (Điều này không hoàn toàn nắm bắt được quy trình đầy đủ, nhưng nó là một đại diện đủ tốt cho các mục đích của bài viết này.) Một hằng số không nạp Foo::ar::Baz được tham chiếu, Ruby không giải quết được, và sẽ gọi Foo::Bar.const_missing("Baz") Sau đó Rails sẽ:

  1. Tìm kiếm trong autoload_paths cho foo/bar/baz.rb
  2. Nếu một file trùng khớp được tìm thấy, nó sẽ được tải theo tính toán:
    • Nếu một hằng số chính xác được định nghĩa, nó sẽ đựợc trả về
    • Nếu không, một lỗi sẽ được raise
  3. Nếu không có file trùng khớp được tìm thấy, nó sẽ tìm kiếm thay thế với Foo::Baz, sau đó Baz, trừ khi chúng đã được xác định.
  4. Nếu không có trường hợp nào thoả mãn hằng số cần load, nó sẽ raise một NameError.
0