Các mô-đun (và các lớp) có nghĩa là được lồng vào nhau. Các đoạn mã như ActiveRecord::RecordNotFound
phổ biến đến mức chúng tôi không nghĩ lại về chúng. Nhưng bị chôn vùi trong quá trình thực hiện lồng ghép của Ruby - và hệ thống tự động tải của Rails - là một vài cái bẫy có thể khiến mã của bạn bị lỗi theo những cách kỳ lạ và tuyệt vời. Trong bài đăng này, chúng ta sẽ thảo luận về nguồn gốc của những cái bẫy này và cách bạn có thể tránh chúng.
Hằng số là gì?
Bài đăng này là về các mô-đun, nhưng để hiểu chúng, chúng ta cần hiểu các hằng số. Trong hầu hết các ngôn ngữ, hằng số chỉ được sử dụng để lưu trữ các bit dữ liệu nhỏ, như trong ví dụ dưới đây:
# These are simple constants
MAX_RETRIES = 5
DEFAULT_LANGUAGE = "en"
Nhưng trong Ruby, các lớp và mô-đun cũng là các hằng số. Tôi đã viết một ví dụ nhỏ để chứng minh điều này. Có ba hằng số trong mô-đun dưới đây:một số, một lớp và một mô-đun lồng nhau. Khi bạn truy cập vào một lớp hoặc mô-đun lồng nhau, ruby sẽ tìm thấy nó bằng cách sử dụng các quy tắc tương tự mà nó sẽ sử dụng với một hằng số đơn giản.
module MyModule
MY_FAVORITE_NUMBER = 7
# Classes are constants
class MyClass
end
# So are modules
module MyEmbeddedModule
end
end
puts MyModule.constants.inspect # => [:MY_FAVORITE_NUMBER, :MyClass, :MyEmbeddedModule]
Thông thường, các mô-đun có quyền truy cập vào các hằng số được xác định trong cha mẹ của chúng. Đó là lý do tại sao bạn có thể viết mã như thế này:
module X
MARCO = "polo"
module Y
def self.n
puts MARCO
end
end
end
X::Y.n() # => "polo"
Nhưng bạn đã nhầm nếu bạn nghĩ rằng mối quan hệ cha mẹ / con cái này là thứ cho phép Y truy cập vào MARCO không đổi của X.
Một vấn đề chung
Nếu chúng ta viết lại đoạn mã trên theo một cách hơi khác, một điều gì đó đáng ngạc nhiên sẽ xảy ra. Y không thể truy cập X ::MARCO nữa. Chuyện quái gì đang xảy ra ở đây?
module A
MARCO = "polo"
end
module A::B
def self.n
puts MARCO # => uninitialized constant A::B::MARCO (NameError)
end
end
A::B.n()
Nó chỉ ra rằng sự "kế thừa" các hằng số của cha mẹ bởi con không phải do mối quan hệ cha / con. Nó là từ vựng. Điều đó có nghĩa là nó dựa trên cấu trúc mã của bạn, không dựa trên cấu trúc của các đối tượng mà mã của bạn đang xây dựng.
Kiểm tra việc làm tổ
Nếu bạn muốn hiểu sâu hơn về cách Ruby tìm kiếm các hằng số lồng nhau, bạn nên xem Module.nesting
chức năng.
Hàm này trả về một mảng các đối tượng tạo nên "đường dẫn tìm kiếm" cho các hằng số trong một phạm vi nhất định. Hãy kiểm tra lồng ghép cho các ví dụ trước của chúng tôi.
Trong ví dụ đầu tiên của chúng tôi, chúng tôi thấy rằng lồng nhau là [A::B, A]
. Điều này có nghĩa là nếu chúng ta sử dụng hằng số MARCO, Ruby sẽ tìm kiếm nó đầu tiên trong A::B
, và sau đó trong A
.
module A
MARCO = "polo"
module B
def self.n
puts Module.nesting.inspect # => [A::B, A]
puts MARCO # => "polo"
end
end
end
Trong ví dụ thứ hai, chúng ta thấy rằng việc lồng chỉ bao gồm A::B
, không phải A
. Mặc dù B là "con" của A
, cách tôi đã viết mã không hiển thị chúng là lồng nhau, vì vậy vì mục đích này, chúng cũng có thể không như vậy.
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
Các biến chứng tự động tải của Rails
Bạn đã bao giờ nhận thấy rằng bạn không cần phải bao gồm các tệp khi bạn sử dụng Rails? Nếu bạn muốn sử dụng một mô hình, bạn chỉ cần sử dụng nó.
Điều này có thể thực hiện được vì Rails thực hiện một hệ thống tự động nạp. Nó sử dụng Module.const_missing
để phát hiện khi bạn cố gắng tham chiếu đến một hằng số chưa được tải. Sau đó, nó tải các tệp mà nó tin rằng nên chứa hằng số. Điều này hầu hết đều hoạt động, nhưng có một điểm khó khăn.
Rails giả định rằng một mô-đun luôn có khả năng lồng ghép lớn nhất. Nó giả định rằng mô-đun A ::B ::C sẽ có một tổ hợp [A::B::C, A::B, A]
. Nếu không, bạn sẽ nhận được hành vi không mong muốn.
Trong đoạn mã bên dưới, mô-đun B sẽ không thể truy cập vào A::MARCO
. Trong Ruby bình thường, nó sẽ không thể làm được vì lồng của nó là [A ::B]. Vì vậy, bạn nên nhận được một ngoại lệ. Nhưng tính năng autoload của Rails không đưa ra một ngoại lệ. Thay vào đó, nó trả về 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()
Kết luận?
Tất cả những điều này là rất nhiều để suy nghĩ về. Tôi muốn tránh suy nghĩ nếu có thể, vì vậy tôi cố gắng tránh xa mô-đun A::B
cú pháp. Tôi không thể nghĩ ra trường hợp mà tôi cố tình muốn thao tác lồng ghép mô-đun. Nếu bạn biết về bất kỳ điều gì, tôi rất muốn nghe về họ!